UNPKG

@trycourier/courier-react-native

Version:

Inbox, Push Notifications, and Preferences for React Native

568 lines (567 loc) 25.7 kB
import { Platform, } from 'react-native'; // Imports import { CourierInboxListener } from './models/CourierInboxListener'; import { CourierPushListener } from './models/CourierPushListener'; import { CourierAuthenticationListener } from './models/CourierAuthenticationListener'; import { Modules } from './Modules'; import Broadcaster from './Broadcaster'; import { CourierClient } from './client/CourierClient'; import { Events, CourierUtils } from './utils'; export { CourierClient } from './client/CourierClient'; export { BrandClient } from './client/BrandClient'; // Exports export { CourierInboxView } from './views/CourierInboxView'; export { CourierPreferencesView } from './views/CourierPreferencesView'; export { CourierInboxListener } from './models/CourierInboxListener'; export { CourierPushListener } from './models/CourierPushListener'; export { CourierAuthenticationListener } from './models/CourierAuthenticationListener'; export { CourierUserPreferencesChannel } from './models/CourierUserPreferences'; export { CourierUserPreferencesStatus } from './models/CourierUserPreferences'; export { CourierTrackingEvent } from './models/CourierTrackingEvent'; export { CourierPushProvider } from './models/CourierPushProvider'; export { CourierUtils } from './utils'; class Courier { // Singleton static _sharedInstance; // Listeners authenticationListeners = new Map(); inboxListeners = new Map(); pushListeners = new Map(); // Broadcasting systemBroadcaster = new Broadcaster(Modules.System); sharedBroadcaster = new Broadcaster(Modules.Shared); pushNotificationClickedEmitter; pushNotificationDeliveredEmitter; constructor() { // Sets the initial SDK values // will show all foreground notification styles in iOS Courier.setIOSForegroundPresentationOptions({ options: ['sound', 'badge', 'list', 'banner'] }); // Attach the push notification listeners this.attachPushNotificationListeners(); } // Returns the public shared instance static get shared() { if (!this._sharedInstance) { this._sharedInstance = new Courier(); } return this._sharedInstance; } // Debugging isDebugging = __DEV__; // Show a log to the console static log(message) { if (Courier.shared.isDebugging) { console.log(message); } } // System (Static) async attachPushNotificationListeners() { // Remove existing listeners // Only allows one subscription to be active this.pushNotificationClickedEmitter?.remove(); this.pushNotificationDeliveredEmitter?.remove(); // When a push notification is clicked this.pushNotificationClickedEmitter = await this.systemBroadcaster.addListener(Events.Push.CLICKED, (event) => { try { const message = JSON.parse(event); this.pushListeners.forEach(listener => { if (listener.onPushNotificationClicked) { listener.onPushNotificationClicked(message); } }); } catch (error) { Courier.log(`Error parsing push notification clicked event: ${error}`); } }); // When a push notification is delivered this.pushNotificationDeliveredEmitter = await this.systemBroadcaster.addListener(Events.Push.DELIVERED, (event) => { try { const message = JSON.parse(event); this.pushListeners.forEach(listener => { if (listener.onPushNotificationDelivered) { listener.onPushNotificationDelivered(message); } }); } catch (error) { Courier.log(`Error parsing push notification delivered event: ${error}`); } }); } /** * Sets the iOS foreground presentation options for push notifications. * This method only works on iOS devices. * @param props An object containing an array of iOSForegroundPresentationOptions. * @returns A string indicating the result of the operation. Returns 'unsupported' on non-iOS platforms. */ static setIOSForegroundPresentationOptions(props) { // Only works on iOS if (Platform.OS !== 'ios') return 'unsupported'; const normalizedParams = Array.from(new Set(props.options)); return Modules.System.setIOSForegroundPresentationOptions({ options: normalizedParams, }); } /** * Retrieves the current notification permission status. * @returns A Promise that resolves to a string representing the current notification permission status. */ static async getNotificationPermissionStatus() { return await Modules.System.getNotificationPermissionStatus(); } /** * Requests permission to send push notifications to the user. * @returns A Promise that resolves to a string indicating the result of the permission request. */ static async requestNotificationPermission() { return await Modules.System.requestNotificationPermission(); } /** * Opens the settings page for the current app. * This can be used to direct users to enable notifications if they've previously denied permission. */ static openSettingsForApp() { Modules.System.openSettingsForApp(); } // Client /** * Gets the current CourierClient instance. * @returns {Promise<CourierClient | undefined>} The current CourierClient instance, or undefined if not initialized. */ async getClient() { const client = await Modules.Shared.getClient() ?? undefined; if (!client) { return undefined; } const clientObj = JSON.parse(client); return new CourierClient({ userId: clientObj.userId, showLogs: clientObj.showLogs, jwt: clientObj.jwt, clientKey: clientObj.clientKey, connectionId: clientObj.connectionId, tenantId: clientObj.tenantId, }); } // Authentication /** * Gets the current user ID. * @returns {Promise<string | undefined>} The current user ID, or undefined if not set. */ async getUserId() { return await Modules.Shared.getUserId() ?? undefined; } /** * Gets the current tenant ID. * @returns {Promise<string | undefined>} The current tenant ID, or undefined if not set. */ async getTenantId() { return await Modules.Shared.getTenantId() ?? undefined; } /** * Checks if a user is currently signed in. * @returns {Promise<boolean>} True if a user is signed in, false otherwise. */ async isUserSignedIn() { const isSignedIn = await Modules.Shared.getIsUserSignedIn() ?? 'false'; return isSignedIn.toLowerCase() === 'true'; } /** * Signs out the current user. * @returns {Promise<void>} A promise that resolves when the sign out process is complete. */ async signOut() { return await Modules.Shared.signOut(); } /** * Signs in a user with the provided credentials. * @param {Object} props - The sign-in properties. * @param {string} props.accessToken - The access token for authentication. * @param {string} [props.clientKey] - The client key (optional). * @param {string} props.userId - The user ID. * @param {string} [props.tenantId] - The tenant ID (optional). * @param {boolean} [props.showLogs] - Whether to show debug logs (defaults to __DEV__). * @returns {Promise<void>} A promise that resolves when the sign-in process is complete. */ async signIn(props) { this.isDebugging = props.showLogs ?? __DEV__; return await Modules.Shared.signIn(props.accessToken, props.clientKey ?? null, props.userId, props.tenantId ?? null, this.isDebugging); } /** * Adds an authentication listener to monitor user changes. * @param {Object} props - The listener properties. * @param {function} props.onUserChanged - Callback function triggered when the user changes. * @returns {CourierAuthenticationListener} The created authentication listener. */ async addAuthenticationListener(props) { // Create a listener const listenerId = `authentication_${CourierUtils.generateUUID()}`; // Attach the listener const listener = new CourierAuthenticationListener(listenerId); listener.onUserChanged = await this.sharedBroadcaster.addListener(listenerId, (event) => props.onUserChanged(event)); const id = await Modules.Shared.addAuthenticationListener(listenerId); this.authenticationListeners.set(id, listener); return listener; } /** * Removes a specific authentication listener. * @param {Object} props - The removal properties. * @param {string} props.listenerId - The ID of the listener to remove. * @returns {Promise<string>} A promise that resolves to the ID of the removed listener. */ async removeAuthenticationListener(props) { // Remove the native listener await Modules.Shared.removeAuthenticationListener(props.listenerId); // Remove the listener if (this.authenticationListeners.has(props.listenerId)) { const listener = this.authenticationListeners.get(props.listenerId); listener?.onUserChanged?.remove(); this.authenticationListeners.delete(props.listenerId); } return props.listenerId; } /** * Removes all authentication listeners. * This method clears all registered authentication listeners, both native and JavaScript. */ async removeAllAuthenticationListeners() { // Remove all native listeners await Modules.Shared.removeAllAuthenticationListeners(); // Iterate through all authentication listeners this.authenticationListeners.forEach((listener) => { listener.onUserChanged?.remove(); }); // Clear the map of authentication listeners this.authenticationListeners.clear(); } // Push /** * Retrieves all push notification tokens. * @returns {Promise<Map<string, string>>} A promise that resolves to a Map of provider keys to tokens. */ async getAllTokens() { const tokensObject = await Modules.Shared.getAllTokens(); const tokensMap = new Map(); for (const [key, value] of Object.entries(tokensObject)) { tokensMap.set(key, value); } return tokensMap; } /** * Retrieves the push notification token for a specific key. * @param {Object} props - The properties object. * @param {string} props.key - The key associated with the token. * @returns {Promise<string | undefined>} A promise that resolves to the token or undefined if not found. */ async getToken(props) { return await Modules.Shared.getToken(props.key); } /** * Retrieves the push notification token for a specific provider. * @param {Object} props - The properties object. * @param {CourierPushProvider} props.provider - The push notification provider. * @returns {Promise<string | undefined>} A promise that resolves to the token or undefined if not found. */ async getTokenForProvider(props) { return await Modules.Shared.getToken(props.provider); } /** * Sets the push notification token for a specific key. * @param {Object} props - The properties object. * @param {string} props.key - The key to associate with the token. * @param {string} props.token - The push notification token. * @returns {Promise<void>} A promise that resolves when the token is set. */ async setToken(props) { return await Modules.Shared.setToken(props.key, props.token); } /** * Sets the push notification token for a specific provider. * @param {Object} props - The properties object. * @param {CourierPushProvider} props.provider - The push notification provider. * @param {string} props.token - The push notification token. * @returns {Promise<void>} A promise that resolves when the token is set. */ async setTokenForProvider(props) { return await Modules.Shared.setToken(props.provider, props.token); } /** * Adds a push notification listener. * @param {Object} props - The properties object. * @param {function} [props.onPushNotificationClicked] - Callback function triggered when a push notification is clicked. * @param {function} [props.onPushNotificationDelivered] - Callback function triggered when a push notification is delivered. * @returns {CourierPushListener} The created push notification listener. */ addPushNotificationListener(props) { const listenerId = `push_${CourierUtils.generateUUID()}`; const pushListener = new CourierPushListener(listenerId, props.onPushNotificationClicked, props.onPushNotificationDelivered); // Cache the listener this.pushListeners.set(listenerId, pushListener); // When listener is registered // Attempt to fetch the last message that was clicked // This is needed for when the app is killed and the // user launched the app by clicking on a notifications Modules.System.registerPushNotificationClickedOnKilledState(); return pushListener; } /** * Removes a specific push notification listener. * @param {Object} props - The properties object. * @param {string} props.listenerId - The ID of the listener to remove. * @returns {string} The ID of the removed listener. */ async removePushNotificationListener(props) { if (this.pushListeners.has(props.listenerId)) { this.pushListeners.delete(props.listenerId); } return props.listenerId; } /** * Removes all push notification listeners. */ async removeAllPushNotificationListeners() { this.pushListeners.forEach((listener) => { listener.remove(); }); this.pushListeners.clear(); } // Inbox /** * Gets the current pagination limit for inbox messages. * @returns {Promise<number>} A promise that resolves with the current pagination limit. */ async getInboxPaginationLimit() { return await Modules.Shared.getInboxPaginationLimit(); } /** * Sets the pagination limit for inbox messages. * @param {number} limit - The new pagination limit to set. * @returns {Promise<void>} A promise that resolves when the limit is set. */ async setInboxPaginationLimit(limit) { await Modules.Shared.setInboxPaginationLimit(limit); } /** * Opens a specific message in the inbox. * @param {Object} props - The properties object. * @param {string} props.messageId - The ID of the message to open. * @returns {Promise<void>} A promise that resolves when the message is opened. */ async openMessage(props) { return await Modules.Shared.openMessage(props.messageId); } /** * Registers a click event for a specific message in the inbox. * @param {Object} props - The properties object. * @param {string} props.messageId - The ID of the message that was clicked. * @returns {Promise<void>} A promise that resolves when the click is registered. */ async clickMessage(props) { return await Modules.Shared.clickMessage(props.messageId); } /** * Marks a specific message as read in the inbox. * @param {Object} props - The properties object. * @param {string} props.messageId - The ID of the message to mark as read. * @returns {Promise<void>} A promise that resolves when the message is marked as read. */ async readMessage(props) { return await Modules.Shared.readMessage(props.messageId); } /** * Marks a specific message as unread in the inbox. * @param {Object} props - The properties object. * @param {string} props.messageId - The ID of the message to mark as unread. * @returns {Promise<void>} A promise that resolves when the message is marked as unread. */ async unreadMessage(props) { return await Modules.Shared.unreadMessage(props.messageId); } /** * Archives a specific message in the inbox. * @param {Object} props - The properties object. * @param {string} props.messageId - The ID of the message to archive. * @returns {Promise<void>} A promise that resolves when the message is archived. */ async archiveMessage(props) { return await Modules.Shared.archiveMessage(props.messageId); } /** * Marks all messages in the inbox as read. * @returns {Promise<void>} A promise that resolves when all messages are marked as read. */ async readAllInboxMessages() { return await Modules.Shared.readAllInboxMessages(); } /** * Adds a listener for inbox changes. * @param {Object} props - The properties object. * @param {Function} [props.onInitialLoad] - Callback function called when the inbox is initially loaded. * @param {Function} [props.onError] - Callback function called when an error occurs. Receives the error message as a parameter. * @param {Function} [props.onUnreadCountChanged] - Callback function called when the unread count changes. * @param {Function} [props.onFeedChanged] - Callback function called when the feed changes. * @param {Function} [props.onArchiveChanged] - Callback function called when the archive changes. * @param {Function} [props.onPageAdded] - Callback function called when a new page is added. * @param {Function} [props.onMessageChanged] - Callback function called when a message changes. * @param {Function} [props.onMessageAdded] - Callback function called when a new message is added. * @param {Function} [props.onMessageRemoved] - Callback function called when a message is removed. * @returns {CourierInboxListener} A listener object that can be used to remove the listener later. */ async addInboxListener(props) { const listenerId = `inbox_${CourierUtils.generateUUID()}`; const listenerIds = { loading: `inbox_loading_${CourierUtils.generateUUID()}`, error: `inbox_error_${CourierUtils.generateUUID()}`, unreadCount: `inbox_unread_count_${CourierUtils.generateUUID()}`, feed: `inbox_feed_${CourierUtils.generateUUID()}`, archive: `inbox_archive_${CourierUtils.generateUUID()}`, pageAdded: `inbox_page_added_${CourierUtils.generateUUID()}`, messageChanged: `inbox_message_changed_${CourierUtils.generateUUID()}`, messageAdded: `inbox_message_added_${CourierUtils.generateUUID()}`, messageRemoved: `inbox_message_removed_${CourierUtils.generateUUID()}` }; // Create the initial listeners const listener = new CourierInboxListener(listenerId); listener.onInitialLoad = await this.sharedBroadcaster.addListener(listenerIds.loading, (event) => { props.onInitialLoad?.(event); }); listener.onError = await this.sharedBroadcaster.addListener(listenerIds.error, (event) => { props.onError?.(event); }); listener.onUnreadCountChanged = await this.sharedBroadcaster.addListener(listenerIds.unreadCount, (event) => { props.onUnreadCountChanged?.(event); }); listener.onFeedChanged = await this.sharedBroadcaster.addListener(listenerIds.feed, (event) => { const convertedMessages = this.convertMessages(event.messages); const messageSet = { messages: convertedMessages, totalMessageCount: event.totalMessageCount, canPaginate: event.canPaginate }; props.onFeedChanged?.(messageSet); }); listener.onArchiveChanged = await this.sharedBroadcaster.addListener(listenerIds.archive, (event) => { const convertedMessages = this.convertMessages(event.messages); const messageSet = { messages: convertedMessages, totalMessageCount: event.totalMessageCount, canPaginate: event.canPaginate }; props.onArchiveChanged?.(messageSet); }); listener.onPageAdded = await this.sharedBroadcaster.addListener(listenerIds.pageAdded, (event) => { const convertedMessages = this.convertMessages(event.messages); const messageSet = { messages: convertedMessages, totalMessageCount: event.totalMessageCount, canPaginate: event.canPaginate }; props.onPageAdded?.(event.feed === 'archived' ? 'archived' : 'feed', messageSet); }); listener.onMessageChanged = await this.sharedBroadcaster.addListener(listenerIds.messageChanged, (event) => { const convertedMessage = this.convertMessage(event.message); props.onMessageChanged?.(event.feed === 'archived' ? 'archived' : 'feed', event.index, convertedMessage); }); listener.onMessageAdded = await this.sharedBroadcaster.addListener(listenerIds.messageAdded, (event) => { const convertedMessage = this.convertMessage(event.message); props.onMessageAdded?.(event.feed === 'archived' ? 'archived' : 'feed', event.index, convertedMessage); }); listener.onMessageRemoved = await this.sharedBroadcaster.addListener(listenerIds.messageRemoved, (event) => { const convertedMessage = this.convertMessage(event.message); props.onMessageRemoved?.(event.feed === 'archived' ? 'archived' : 'feed', event.index, convertedMessage); }); // Attach listener to native code const id = await Modules.Shared.addInboxListener(listenerId, listenerIds.loading, listenerIds.error, listenerIds.unreadCount, listenerIds.feed, listenerIds.archive, listenerIds.pageAdded, listenerIds.messageChanged, listenerIds.messageAdded, listenerIds.messageRemoved); // Add listener to manager this.inboxListeners.set(id, listener); return listener; } convertMessages(messages) { return messages.map(jsonString => { try { return JSON.parse(jsonString); } catch (error) { Courier.log(`Error parsing message: ${error}`); return null; } }).filter((message) => message !== null); } convertMessage(message) { try { return JSON.parse(message); } catch (error) { Courier.log(`Error parsing message: ${error}`); throw error; } } /** * Removes a specific inbox listener. * @param {Object} props - The properties object. * @param {string} props.listenerId - The ID of the listener to remove. * @returns {string} The ID of the removed listener. */ async removeInboxListener(props) { // Call native code await Modules.Shared.removeInboxListener(props.listenerId); // Remove the listener if (this.inboxListeners.has(props.listenerId)) { // Remove emitters const listener = this.inboxListeners.get(props.listenerId); listener?.onInitialLoad?.remove(); listener?.onError?.remove(); listener?.onUnreadCountChanged?.remove(); listener?.onFeedChanged?.remove(); listener?.onArchiveChanged?.remove(); listener?.onPageAdded?.remove(); listener?.onMessageChanged?.remove(); listener?.onMessageAdded?.remove(); listener?.onMessageRemoved?.remove(); // Remove the listener this.inboxListeners.delete(props.listenerId); } return props.listenerId; } /** * Removes all inbox listeners. */ async removeAllInboxListeners() { // Call native code await Modules.Shared.removeAllInboxListeners(); // Remove all items from inboxListeners this.inboxListeners.forEach((listener) => { listener?.onInitialLoad?.remove(); listener?.onError?.remove(); listener?.onUnreadCountChanged?.remove(); listener?.onFeedChanged?.remove(); listener?.onArchiveChanged?.remove(); listener?.onPageAdded?.remove(); listener?.onMessageChanged?.remove(); listener?.onMessageAdded?.remove(); listener?.onMessageRemoved?.remove(); }); this.inboxListeners.clear(); } /** * Refreshes the inbox. * Useful for pull-to-refresh functionality. * @returns {Promise<void>} A promise that resolves when the inbox is refreshed. */ async refreshInbox() { return await Modules.Shared.refreshInbox(); } /** * Fetches the next page of inbox messages. * @returns {Promise<InboxMessage[]>} A promise that resolves with an array of fetched inbox messages. */ async fetchNextPageOfMessages(props) { const messages = await Modules.Shared.fetchNextPageOfMessages(props.inboxMessageFeed); return messages.map((message) => JSON.parse(message)); } } export default Courier;