@trycourier/courier-react-native
Version:
Inbox, Push Notifications, and Preferences for React Native
568 lines (567 loc) • 25.7 kB
JavaScript
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;