diff --git a/index.js b/index.ts similarity index 58% rename from index.js rename to index.ts index c1485a9..96da09f 100644 --- a/index.js +++ b/index.ts @@ -3,40 +3,253 @@ import { Platform, NativeEventEmitter, DeviceEventEmitter, + EmitterSubscription, } from 'react-native'; -const ChannelModule = NativeModules.RNChannelIO; +export type ChannelButtonIconType = + | 'channel' + | 'chatBubbleFilled' + | 'chatProgressFilled' + | 'chatQuestionFilled' + | 'chatLightningFilled' + | 'chatBubbleAltFilled' + | 'smsFilled' + | 'commentFilled' + | 'sendForwardFilled' + | 'helpFilled' + | 'chatProgress' + | 'chatQuestion' + | 'chatBubbleAlt' + | 'sms' + | 'comment' + | 'sendForward' + | 'communication' + | 'headset' + +export interface ChannelButtonOption { + xMargin?: number; + yMargin?: number; + position?: 'left' | 'right'; + icon?: ChannelButtonIconType; +} + +export interface BubbleOption { + position?: 'top' | 'bottom'; + yMargin?: number; +} + +export interface Profile extends Record {} + +export type Language = 'ko' | 'ja' | 'en'; + +export type Appearance = 'system' | 'light' | 'dark'; + +export interface User { + id: string + memberId?: string + name?: string + avatarUrl?: string + profile?: Profile + alert: number + unread: number + tags?: string[] + language: Language + unsubscribeEmail?: boolean + unsubscribeTexting?: boolean +} + +export interface PopupData { + chatId: string + avatarUrl: string + name: string + message: string +} + +export interface UserData { + language: Language + tags?: string[] + profile?: Profile + profileOnce?: Record + unsubscribeEmail: boolean + unsubscribeTexting: boolean +} + +export interface BootConfig { + pluginKey: string; + memberId?: string; + memberHash?: string; + profile?: Profile; + language?: Language; + unsubscribeEmail?: boolean; + unsubscribeTexting?: boolean; + trackDefaultEvent?: boolean; + hidePopup?: boolean; + channelButtonOption?: ChannelButtonOption; + bubbleOption?: BubbleOption; + appearance?: Appearance; +} + +interface BootSuccess { + status: 'SUCCESS'; + user: User; +} + +interface BootError { + status: + | 'NOT_INITIALIZED' + | 'NETWORK_TIMEOUT' + | 'NOT_AVAILABLE_VERSION' + | 'SERVICE_UNDER_CONSTRUCTION' + | 'REQUIRE_PAYMENT' + | 'ACCESS_DENIED' + | 'UNKNOWN_ERROR'; + user: undefined; +} + +type BootResult = BootSuccess | BootError + +interface UpdateUserResult { + error: string; + user: User; +} + +interface TagsResult { + error: string; + user: User; +} + +interface AddTagsResult extends TagsResult {} + +interface RemoveTagsResult extends TagsResult {} + +interface ChannelModuleType { + Event: { + ON_PRE_URL_CLICKED: string; + ON_URL_CLICKED: string; + ON_BADGE_CHANGED: string; + ON_POPUP_DATA_RECEIVED: string; + ON_FOLLOW_UP_CHANGED: string; + ON_PUSH_NOTIFICATION_CLICKED: string; + ON_SHOW_MESSENGER: string; + ON_HIDE_MESSENGER: string; + ON_CHAT_CREATED: string; + }; + boot: (bootConfig: BootConfig) => Promise; + sleep: () => void; + shutdown: () => void; + showChannelButton: () => void; + hideChannelButton: () => void; + showMessenger: () => void; + hideMessenger: () => void; + openChat: (chatId?: string | null, payload?: string | null) => void; + openWorkflow: (workflowId?: string) => void; + track: (eventName: string, properties?: Record) => void; + updateUser: (userData: UserData) => Promise; + addTags: (tags: string[]) => Promise; + removeTags: (tags: string[]) => Promise; + isBooted: () => Promise; + setDebugMode: (enable: boolean) => void; + initPushToken: (token: string) => void; + isChannelPushNotification: (userInfo: Record) => Promise; + receivePushNotification: (userInfo: Record) => Promise; + hasStoredPushNotification: () => Promise; + openStoredPushNotification: () => void; + setPage: (page?: string | null, profile?: Profile) => void; + resetPage: () => void; + setAppearance: (appearance: Appearance) => void; + hidePopup: () => void; + handleUrlClicked: (url: string) => void; + notifyPushNotificationClickSubscriberExistence: (exists: boolean) => void; + performDefaultPushNotificationClickAction: (userId: string, chatId: string) => void; +} + +interface RNChannelIO extends Pick { + show: (animated: boolean) => void; + hide: (animated: boolean) => void; + open: (animated: boolean) => void; + close: (animated: boolean) => void; + handlePushNotification: (userInfo: Record) => Promise; + onChangeBadge: (cb?: (unread: number, alert: number) => void) => void; + onBadgeChanged: (cb?: (unread: number, alert: number) => void) => void; + onReceivePush: (cb?: (popup: PopupData) => void) => void; + onPopupDataReceived: (cb?: (popup: PopupData) => void) => void; + onClickChatLink: (handle: boolean, cb?: (url: string) => void) => void; + onUrlClicked: (cb?: (url: string, next: () => void) => void) => void; + onChangeProfile: (cb?: () => void) => void; + onProfileChanged: (cb?: (data: Record) => void) => void; + onFollowUpChanged: (cb?: (data: Record) => void) => void; + onPushNotificationClicked: (cb?: (chatId: string, next: () => void) => void) => void; + willShowMessenger: (cb?: () => void) => void; + onShowMessenger: (cb?: () => void) => void; + willHideMessenger: (cb?: () => void) => void; + onHideMessenger: (cb?: () => void) => void; + onChatCreated: (cb?: (chatId: string) => void) => void; +} + +type Subscriber = EmitterSubscription | null | ((data: { url: string }) => void) + +const ChannelModule = NativeModules.RNChannelIO as ChannelModuleType; const ChannelEventEmitter = Platform.select({ ios: new NativeEventEmitter(NativeModules.RNChannelIO), android: DeviceEventEmitter, }); -var subscribers = {}; +const subscribers: { + [key: string]: Subscriber; +} = {} -const replaceSubscriber = (type, newSubscriber) => { +const replaceSubscriber = (type: string, newSubscriber?: Subscriber) => { const oldSubscriber = subscribers[type]; - if (oldSubscriber && typeof oldSubscriber.remove === 'function') { + if (oldSubscriber && 'remove' in oldSubscriber && typeof oldSubscriber.remove === 'function') { oldSubscriber.remove(); } - subscribers[type] = newSubscriber; + if (newSubscriber) { + subscribers[type] = newSubscriber; + } else { + delete subscribers[type]; + } } -const hasSubscriber = (type) => { +const hasSubscriber = (type: string) => { return !!subscribers[type] } -ChannelEventEmitter.addListener(ChannelModule.Event.ON_PRE_URL_CLICKED, (data) => { +ChannelEventEmitter?.addListener(ChannelModule.Event.ON_PRE_URL_CLICKED, (data: { url: string }) => { if (!hasSubscriber(ChannelModule.Event.ON_URL_CLICKED)) { ChannelModule.handleUrlClicked(data.url); } else { const subscriber = subscribers[ChannelModule.Event.ON_URL_CLICKED]; if (typeof subscriber === 'function') { - subscriber(data); + (subscriber as (data: { url: string }) => void)(data); } } }); -export const ChannelIO = { +export const ChannelIO: RNChannelIO = { /** * Boot `ChannelIO` @@ -44,7 +257,7 @@ export const ChannelIO = { * @param bootConfig BootConfig object contains information for booting * @returns A promise that returns status and guest info */ - boot: async (bootConfig) => { + boot: async (bootConfig: BootConfig) => { return ChannelModule.boot(bootConfig); }, @@ -67,7 +280,7 @@ export const ChannelIO = { * Show `ChannelIO` button * @param {Boolean} aniamted Animate the launcher if true */ - show: (animated) => { + show: (animated: boolean) => { console.log('ChannelIO', 'ChannelIO.show(animated) is deprecated. Please use ChannelIO.showChannelButton()') ChannelModule.showChannelButton() }, @@ -81,7 +294,7 @@ export const ChannelIO = { * Hide `ChannelIO` button * @param {Boolean} aniamted Animate the launcher if true */ - hide: (animated) => { + hide: (animated: boolean) => { console.log('ChannelIO', 'ChannelIO.hide(animated) is deprecated. Please use ChannelIO.hideChannelButton()') ChannelModule.hideChannelButton() }, @@ -95,7 +308,7 @@ export const ChannelIO = { * Open `ChannelIO` messenger * @param {Boolean} aniamted Animate messenger if true */ - open: (animated) => { + open: (animated: boolean) => { console.log('ChannelIO', 'ChannelIO.open(animated) is deprecated. Please use ChannelIO.showMessenger()') ChannelModule.showMessenger() }, @@ -109,7 +322,7 @@ export const ChannelIO = { * Close `ChannelIO` messenger * @param {Boolean} Animate messenger if true */ - close: (animated) => { + close: (animated: boolean) => { console.log('ChannelIO', 'ChannelIO.close(animated) is deprecated. Please use ChannelIO.hideMessenger()') ChannelModule.hideMessenger() }, @@ -123,7 +336,7 @@ export const ChannelIO = { * @param {String} chatId user chat id * @param {String} payload auto fill message */ - openChat: (chatId, payload) => { + openChat: (chatId?: string | null, payload?: string | null) => { if (typeof payload === 'string') { ChannelModule.openChat(chatId, payload); } else { @@ -140,7 +353,7 @@ export const ChannelIO = { * - If you don't pass workflowId, no action is taken. * @param {String} workflowId The ID of workflow to start with. An error page will be shown if such workflow does not exist. */ - openWorkflow: (workflowId) => { + openWorkflow: (workflowId?: string) => { ChannelModule.openWorkflow(workflowId); }, @@ -149,14 +362,14 @@ export const ChannelIO = { * @param {String} eventName event name * @param {Object} properties a json object contains information */ - track: (eventName, properties) => ChannelModule.track(eventName, properties), + track: (eventName: string, properties?: Record) => ChannelModule.track(eventName, properties), /** * Update user * @param userData userData object contains user data information for updating * @returns A promise that returns exception or user info */ - updateUser: async (userData) => { + updateUser: async (userData: UserData) => { return ChannelModule.updateUser(userData) }, @@ -165,7 +378,7 @@ export const ChannelIO = { * @param tags tags object contains tags information for adding * @returns A promise that returns exception or user data */ - addTags: async (tags) => { + addTags: async (tags: string[]) => { return ChannelModule.addTags(tags) }, @@ -174,7 +387,7 @@ export const ChannelIO = { * @param tags tags object contains tags information for removing * @returns A promise that returns exception or user data */ - removeTags: async (tags) => { + removeTags: async (tags: string[]) => { return ChannelModule.removeTags(tags) }, @@ -190,27 +403,27 @@ export const ChannelIO = { * Set debug mode for native module * @param {Boolean} enable True if you want to set debug mode, otherwise false */ - setDebugMode: (enable) => ChannelModule.setDebugMode(enable), + setDebugMode: (enable: boolean) => ChannelModule.setDebugMode(enable), /** * Initialize push token * @param {String} token a push token */ - initPushToken: (token) => ChannelModule.initPushToken(token), + initPushToken: (token: string) => ChannelModule.initPushToken(token), /** * Check whether a push data is for channel * @param {Object} userInfo userInfo part from push data * @returns {Boolean} true if the userInfo indicates `ChannelIO'`s push, otherwise false */ - isChannelPushNotification: async (userInfo) => ChannelModule.isChannelPushNotification(userInfo), + isChannelPushNotification: async (userInfo: Record) => ChannelModule.isChannelPushNotification(userInfo), /** * @deprecated * Handle `ChannelIO` push notification * @param {Object} userInfo userInfo part from push data */ - handlePushNotification: async (userInfo) => { + handlePushNotification: async (userInfo: Record) => { console.log('ChannelIO', 'ChannelIO.handlePushNotification(userInfo) is deprecated. Please use ChannelIO.receivePushNotification(userInfo)') ChannelModule.receivePushNotification(userInfo) }, @@ -218,7 +431,7 @@ export const ChannelIO = { * Receive `ChannelIO` push notification * @param {Object} userInfo userInfo part from push data */ - receivePushNotification: async (userInfo) => ChannelModule.receivePushNotification(userInfo), + receivePushNotification: async (userInfo: Record) => ChannelModule.receivePushNotification(userInfo), /** * Check whether a push data has stored @@ -238,7 +451,7 @@ export const ChannelIO = { * - When nil is assigned to a specific field within the profile object, only the value of that field is cleared. * - The user chat profile value is applied when a user chat is created. */ - setPage: (page, profile) => { + setPage: (page?: string | null, profile?: Profile) => { if (typeof page === "string") { ChannelModule.setPage(page, profile) } else if (page === null || page === undefined) { @@ -257,7 +470,7 @@ export const ChannelIO = { * Sets the appearance of the SDK. * @param {String} appearance system | light | dark */ - setAppearance: (appearance) => { + setAppearance: (appearance: Appearance) => { if (typeof appearance === "string") { ChannelModule.setAppearance(appearance) } else { @@ -275,10 +488,10 @@ export const ChannelIO = { * Event listener that triggers when badge count has been changed * @param {Function} cb a callback function that takes a integer badge count as parameter */ - onChangeBadge: (cb) => { + onChangeBadge: (cb?: (unread: number, alert: number) => void) => { console.log('ChannelIO', 'ChannelIO.onChangeBadge(cb) is deprecated. Please use ChannelIO.onBadgeChanged(cb)') if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_BADGE_CHANGED, (data) => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_BADGE_CHANGED, (data) => { cb(data.unread, data.alert); }); replaceSubscriber(ChannelModule.Event.ON_BADGE_CHANGED, subscription); @@ -291,9 +504,9 @@ export const ChannelIO = { * Event listener that triggers when badge count has been changed * @param {Function} cb a callback function that takes a integer badge count as parameter */ - onBadgeChanged: (cb) => { + onBadgeChanged: (cb?: (unread: number, alert: number) => void) => { if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_BADGE_CHANGED, (data) => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_BADGE_CHANGED, (data) => { cb(data.unread, data.alert); }); replaceSubscriber(ChannelModule.Event.ON_BADGE_CHANGED, subscription); @@ -307,10 +520,10 @@ export const ChannelIO = { * Event listener that triggers when in-app popup has been arrived * @param {Function} cb a callback function that takes a object popup data as parameter */ - onReceivePush: (cb) => { + onReceivePush: (cb?: (popup: PopupData) => void) => { console.log('ChannelIO', 'ChannelIO.onReceivePush(cb) is deprecated. Please use ChannelIO.onPopupDataReceived(cb)') if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_POPUP_DATA_RECEIVED, (data) => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_POPUP_DATA_RECEIVED, (data) => { cb(data.popup); }); replaceSubscriber(ChannelModule.Event.ON_POPUP_DATA_RECEIVED, subscription); @@ -322,9 +535,9 @@ export const ChannelIO = { * Event listener that triggers when in-app popup has been arrived * @param {Function} cb a callback function that takes a object popup data as parameter */ - onPopupDataReceived: (cb) => { + onPopupDataReceived: (cb?: (popup: PopupData) => void) => { if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_POPUP_DATA_RECEIVED, (data) => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_POPUP_DATA_RECEIVED, (data) => { cb(data.popup); }); replaceSubscriber(ChannelModule.Event.ON_POPUP_DATA_RECEIVED, subscription); @@ -339,7 +552,7 @@ export const ChannelIO = { * @param {Boolean} handle True if you want to handle a link, otherwise false * @param {Function} cb a callback function that takes a string url as parameter */ - onClickChatLink: (handle, cb) => { + onClickChatLink: (handle: boolean, cb?: (url: string) => void) => { console.log('ChannelIO', 'ChannelIO.onClickChatLink(handle, cb) is deprecated. Please use ChannelIO.onUrlClicked(cb)') if (cb) { replaceSubscriber(ChannelModule.Event.ON_URL_CLICKED, (data) => { @@ -356,7 +569,7 @@ export const ChannelIO = { * Event listener that triggers when a url has been clicked by a user * @param {Function} cb a callback function that takes a string url as parameter */ - onUrlClicked: (cb) => { + onUrlClicked: (cb?: (url: string, next: () => void) => void) => { if (cb) { replaceSubscriber(ChannelModule.Event.ON_URL_CLICKED, (data) => { const next = () => { @@ -373,7 +586,7 @@ export const ChannelIO = { * @deprecated * 'onChangeProfile' is deprecated. Please use 'onFollowUpChanged'. */ - onChangeProfile: (cb) => { + onChangeProfile: (cb?: () => void) => { console.warn("'onChangeProfile' is deprecated. Please use 'onFollowUpChanged'.") }, @@ -381,7 +594,7 @@ export const ChannelIO = { * @deprecated * 'onProfileChanged' is deprecated. Please use 'onFollowUpChanged'. */ - onProfileChanged: (cb) => { + onProfileChanged: (cb?: (data: Record) => void) => { console.warn("'onProfileChanged' is deprecated. Please use 'onFollowUpChanged'.") }, @@ -389,9 +602,9 @@ export const ChannelIO = { * Event listener that triggers when guest profile is updated * @param {Function} cb a callback function that takes a map */ - onFollowUpChanged: (cb) => { + onFollowUpChanged: (cb?: (data: Record) => void) => { if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_FOLLOW_UP_CHANGED, (data) => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_FOLLOW_UP_CHANGED, (data) => { cb(data); }); replaceSubscriber(ChannelModule.Event.ON_FOLLOW_UP_CHANGED, subscription); @@ -407,11 +620,11 @@ export const ChannelIO = { * * @param {Function} cb a callback function */ - onPushNotificationClicked: (cb) => { + onPushNotificationClicked: (cb?: (chatId: string, next: () => void) => void) => { if (Platform.OS !== 'android') { return } if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_PUSH_NOTIFICATION_CLICKED, data => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_PUSH_NOTIFICATION_CLICKED, data => { const next = () => ChannelModule.performDefaultPushNotificationClickAction(data.userId, data.chatId); cb(data.chatId, next); @@ -430,10 +643,10 @@ export const ChannelIO = { * Event listener that triggers when `ChannelIO` messenger is about to display * @param {Function} cb a callback function */ - willShowMessenger: (cb) => { + willShowMessenger: (cb?: () => void) => { console.log('ChannelIO', 'ChannelIO.willShowMessenger(cb) is deprecated. Please use ChannelIO.onShowMessenger(cb)') if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_SHOW_MESSENGER, cb); + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_SHOW_MESSENGER, cb); replaceSubscriber(ChannelModule.Event.ON_SHOW_MESSENGER, subscription); } else { replaceSubscriber(ChannelModule.Event.ON_SHOW_MESSENGER, null); @@ -443,9 +656,9 @@ export const ChannelIO = { * Event listener that triggers when `ChannelIO` messenger is about to display * @param {Function} cb a callback function */ - onShowMessenger: (cb) => { + onShowMessenger: (cb?: () => void) => { if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_SHOW_MESSENGER, cb); + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_SHOW_MESSENGER, cb); replaceSubscriber(ChannelModule.Event.ON_SHOW_MESSENGER, subscription); } else { replaceSubscriber(ChannelModule.Event.ON_SHOW_MESSENGER, null); @@ -457,10 +670,10 @@ export const ChannelIO = { * Event listener that triggers when `ChannelIO` messenger is about to dismiss * @param {Function} cb a callback function */ - willHideMessenger: (cb) => { + willHideMessenger: (cb?: () => void) => { console.log('ChannelIO', 'ChannelIO.willHideMessenger(cb) is deprecated. Please use ChannelIO.onHideMessenger(cb)') if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_HIDE_MESSENGER, cb); + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_HIDE_MESSENGER, cb); replaceSubscriber(ChannelModule.Event.ON_HIDE_MESSENGER, subscription); } else { replaceSubscriber(ChannelModule.Event.ON_HIDE_MESSENGER, null); @@ -470,9 +683,9 @@ export const ChannelIO = { * Event listener that triggers when `ChannelIO` messenger is about to dismiss * @param {Function} cb a callback function */ - onHideMessenger: (cb) => { + onHideMessenger: (cb?: () => void) => { if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_HIDE_MESSENGER, cb); + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_HIDE_MESSENGER, cb); replaceSubscriber(ChannelModule.Event.ON_HIDE_MESSENGER, subscription); } else { replaceSubscriber(ChannelModule.Event.ON_HIDE_MESSENGER, null); @@ -483,9 +696,9 @@ export const ChannelIO = { * Event listener that triggers when a chat has been created by a user * @param {Function} cb a callback function that takes a string chat id as parameter */ - onChatCreated: (cb) => { + onChatCreated: (cb?: (chatId: string) => void) => { if (cb) { - const subscription = ChannelEventEmitter.addListener(ChannelModule.Event.ON_CHAT_CREATED, (data) => { + const subscription = ChannelEventEmitter?.addListener(ChannelModule.Event.ON_CHAT_CREATED, (data) => { cb(data.chatId); }); replaceSubscriber(ChannelModule.Event.ON_CHAT_CREATED, subscription);