diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index fdb156c21..ad574cdfb 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -200,6 +200,14 @@ export const IterableAppProvider: FunctionComponent< config.enableEmbeddedMessaging = true; + config.onEmbeddedMessageUpdate = () => { + console.log('onEmbeddedMessageUpdate'); + }; + + config.onEmbeddedMessagingDisabled = () => { + console.log('onEmbeddedMessagingDisabled'); + }; + config.inAppHandler = () => IterableInAppShowResponse.show; if ( diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 131719043..2c6edda6a 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -13,6 +13,7 @@ @protocol IterableInAppDelegate; @protocol IterableCustomActionDelegate; @protocol IterableAuthDelegate; @protocol IterableURLDelegate; +@protocol IterableEmbeddedUpdateDelegate; typedef NS_ENUM(NSInteger, InAppShowResponse) { show = 0, skip = 1, diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index 16655aabf..38e8e1ca0 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -33,6 +33,8 @@ import React case receivedIterableInboxChanged case handleAuthSuccessCalled case handleAuthFailureCalled + case handleEmbeddedMessageUpdateCalled + case handleEmbeddedMessagingDisabledCalled } @objc public static var supportedEvents: [String] { @@ -651,6 +653,14 @@ import React } IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version) + + // Add embedded update listener if any callback is present + let onEmbeddedMessageUpdatePresent = configDict["onEmbeddedMessageUpdatePresent"] as? Bool ?? false + let onEmbeddedMessagingDisabledPresent = configDict["onEmbeddedMessagingDisabledPresent"] as? Bool ?? false + + if onEmbeddedMessageUpdatePresent || onEmbeddedMessagingDisabledPresent { + IterableAPI.embeddedManager.addUpdateListener(self) + } } } @@ -808,3 +818,25 @@ extension ReactIterableAPI: IterableAuthDelegate { public func onTokenRegistrationFailed(_ reason: String?) { } } + +extension ReactIterableAPI: IterableEmbeddedUpdateDelegate { + public func onMessagesUpdated() { + ITBInfo() + guard shouldEmit else { + return + } + delegate?.sendEvent( + withName: EventName.handleEmbeddedMessageUpdateCalled.rawValue, + body: nil as Any?) + } + + public func onEmbeddedMessagingDisabled() { + ITBInfo() + guard shouldEmit else { + return + } + delegate?.sendEvent( + withName: EventName.handleEmbeddedMessagingDisabledCalled.rawValue, + body: nil as Any?) + } +} diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index b0a789f21..459cd2b8b 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -41,6 +41,12 @@ describe('Iterable', () => { nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); nativeEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled); nativeEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); // Clear any pending timers jest.clearAllTimers(); @@ -1229,4 +1235,224 @@ describe('Iterable', () => { expect(Iterable.embeddedManager.isEnabled).toBe(true); }); }); + + describe('embedded messaging callbacks', () => { + describe('onEmbeddedMessageUpdate', () => { + it('should call onEmbeddedMessageUpdate when handleEmbeddedMessageUpdateCalled event is emitted', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + // sets up config file and onEmbeddedMessageUpdate callback + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + config.onEmbeddedMessageUpdate = jest.fn(); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessageUpdateCalled event is emitted + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + // THEN onEmbeddedMessageUpdate callback is called + expect(config.onEmbeddedMessageUpdate).toHaveBeenCalled(); + expect(config.onEmbeddedMessageUpdate).toHaveBeenCalledTimes(1); + }); + + it('should not set up listener if onEmbeddedMessageUpdate is not provided', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + // sets up config without onEmbeddedMessageUpdate callback + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessageUpdateCalled event is emitted + // THEN no error should occur (no listener was set up) + expect(() => { + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + }).not.toThrow(); + }); + + it('should call onEmbeddedMessageUpdate multiple times when event is emitted multiple times', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + // sets up config with callback + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + config.onEmbeddedMessageUpdate = jest.fn(); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessageUpdateCalled event is emitted multiple times + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + // THEN onEmbeddedMessageUpdate callback is called three times + expect(config.onEmbeddedMessageUpdate).toHaveBeenCalledTimes(3); + }); + + it('should include onEmbeddedMessageUpdatePresent flag in config dict when callback is provided', () => { + // GIVEN a config with onEmbeddedMessageUpdate callback + const config = new IterableConfig(); + config.onEmbeddedMessageUpdate = jest.fn(); + // WHEN toDict is called + const configDict = config.toDict(); + // THEN onEmbeddedMessageUpdatePresent is true + expect(configDict.onEmbeddedMessageUpdatePresent).toBe(true); + }); + + it('should set onEmbeddedMessageUpdatePresent flag to false when callback is not provided', () => { + // GIVEN a config without onEmbeddedMessageUpdate callback + const config = new IterableConfig(); + // WHEN toDict is called + const configDict = config.toDict(); + // THEN onEmbeddedMessageUpdatePresent is false + expect(configDict.onEmbeddedMessageUpdatePresent).toBe(false); + }); + }); + + describe('onEmbeddedMessagingDisabled', () => { + it('should call onEmbeddedMessagingDisabled when handleEmbeddedMessagingDisabledCalled event is emitted', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // sets up config file and onEmbeddedMessagingDisabled callback + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + config.onEmbeddedMessagingDisabled = jest.fn(); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessagingDisabledCalled event is emitted + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // THEN onEmbeddedMessagingDisabled callback is called + expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalled(); + expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalledTimes(1); + }); + + it('should not set up listener if onEmbeddedMessagingDisabled is not provided', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // sets up config without onEmbeddedMessagingDisabled callback + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessagingDisabledCalled event is emitted + // THEN no error should occur (no listener was set up) + expect(() => { + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + }).not.toThrow(); + }); + + it('should call onEmbeddedMessagingDisabled when embedded messaging becomes unavailable', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // sets up config with callback + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + config.onEmbeddedMessagingDisabled = jest.fn(); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessagingDisabledCalled event is emitted + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // THEN onEmbeddedMessagingDisabled callback is called + expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalled(); + }); + + it('should include onEmbeddedMessagingDisabledPresent flag in config dict when callback is provided', () => { + // GIVEN a config with onEmbeddedMessagingDisabled callback + const config = new IterableConfig(); + config.onEmbeddedMessagingDisabled = jest.fn(); + // WHEN toDict is called + const configDict = config.toDict(); + // THEN onEmbeddedMessagingDisabledPresent is true + expect(configDict.onEmbeddedMessagingDisabledPresent).toBe(true); + }); + + it('should set onEmbeddedMessagingDisabledPresent flag to false when callback is not provided', () => { + // GIVEN a config without onEmbeddedMessagingDisabled callback + const config = new IterableConfig(); + // WHEN toDict is called + const configDict = config.toDict(); + // THEN onEmbeddedMessagingDisabledPresent is false + expect(configDict.onEmbeddedMessagingDisabledPresent).toBe(false); + }); + }); + + describe('both embedded callbacks', () => { + it('should call both callbacks independently when both are provided', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + nativeEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // sets up config with both callbacks + const config = new IterableConfig(); + config.logReactNativeSdkCalls = false; + config.onEmbeddedMessageUpdate = jest.fn(); + config.onEmbeddedMessagingDisabled = jest.fn(); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // WHEN handleEmbeddedMessageUpdateCalled event is emitted + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + // THEN only onEmbeddedMessageUpdate is called + expect(config.onEmbeddedMessageUpdate).toHaveBeenCalled(); + expect(config.onEmbeddedMessagingDisabled).not.toHaveBeenCalled(); + // Reset mocks + jest.clearAllMocks(); + // WHEN handleEmbeddedMessagingDisabledCalled event is emitted + nativeEmitter.emit( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); + // THEN only onEmbeddedMessagingDisabled is called + expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalled(); + expect(config.onEmbeddedMessageUpdate).not.toHaveBeenCalled(); + }); + + it('should set both presence flags in config dict when both callbacks are provided', () => { + // GIVEN a config with both callbacks + const config = new IterableConfig(); + config.onEmbeddedMessageUpdate = jest.fn(); + config.onEmbeddedMessagingDisabled = jest.fn(); + // WHEN toDict is called + const configDict = config.toDict(); + // THEN both presence flags are true + expect(configDict.onEmbeddedMessageUpdatePresent).toBe(true); + expect(configDict.onEmbeddedMessagingDisabledPresent).toBe(true); + }); + }); + }); }); diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index d9c98a572..983fab49b 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -950,6 +950,12 @@ export class Iterable { RNEventEmitter.removeAllListeners( IterableEventName.handleAuthFailureCalled ); + RNEventEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessageUpdateCalled + ); + RNEventEmitter.removeAllListeners( + IterableEventName.handleEmbeddedMessagingDisabledCalled + ); } /** @@ -1083,6 +1089,26 @@ export class Iterable { } ); } + + if (Iterable.savedConfig.enableEmbeddedMessaging) { + if (Iterable.savedConfig.onEmbeddedMessageUpdate) { + RNEventEmitter.addListener( + IterableEventName.handleEmbeddedMessageUpdateCalled, + () => { + Iterable.savedConfig.onEmbeddedMessageUpdate?.(); + } + ); + } + + if (Iterable.savedConfig.onEmbeddedMessagingDisabled) { + RNEventEmitter.addListener( + IterableEventName.handleEmbeddedMessagingDisabledCalled, + () => { + Iterable.savedConfig.onEmbeddedMessagingDisabled?.(); + } + ); + } + } } /** diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index aeebfb91e..34befbbc8 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -339,6 +339,42 @@ export class IterableConfig { */ enableEmbeddedMessaging = false; + /** + * A callback function that is called when embedded messages are updated. + * + * This callback is triggered when the local cache of embedded messages changes, + * such as when new messages arrive or existing messages are removed. + * + * @example + * ```typescript + * const config = new IterableConfig(); + * config.onEmbeddedMessageUpdate = () => { + * console.log('Embedded messages updated!'); + * // Refresh your UI to display the latest messages + * }; + * Iterable.initialize('', config); + * ``` + */ + onEmbeddedMessageUpdate?: () => void; + + /** + * A callback function that is called when embedded messaging is disabled. + * + * This callback is triggered when embedded messaging becomes unavailable, + * which can happen due to configuration issues or API errors. + * + * @example + * ```typescript + * const config = new IterableConfig(); + * config.onEmbeddedMessagingDisabled = () => { + * console.warn('Embedded messaging has been disabled'); + * // Hide embedded message UI or show error state + * }; + * Iterable.initialize('', config); + * ``` + */ + onEmbeddedMessagingDisabled?: () => void; + /** * Converts the IterableConfig instance to a dictionary object. * @@ -377,6 +413,21 @@ export class IterableConfig { */ // eslint-disable-next-line eqeqeq authHandlerPresent: this.authHandler != undefined, + /** + * A boolean indicating if an embedded message update callback is present. + * + * TODO: Figure out if this is purposeful + */ + // eslint-disable-next-line eqeqeq + onEmbeddedMessageUpdatePresent: this.onEmbeddedMessageUpdate != undefined, + /** + * A boolean indicating if an embedded messaging disabled callback is present. + * + * TODO: Figure out if this is purposeful + */ + // eslint-disable-next-line eqeqeq + onEmbeddedMessagingDisabledPresent: + this.onEmbeddedMessagingDisabled != undefined, /** The log level for the SDK. */ logLevel: this.logLevel, expiringAuthTokenRefreshPeriod: this.expiringAuthTokenRefreshPeriod, diff --git a/src/core/enums/IterableEventName.ts b/src/core/enums/IterableEventName.ts index 4a44cbb40..6ea79c754 100644 --- a/src/core/enums/IterableEventName.ts +++ b/src/core/enums/IterableEventName.ts @@ -19,4 +19,8 @@ export enum IterableEventName { handleAuthSuccessCalled = 'handleAuthSuccessCalled', /** Event that fires when authentication with Iterable fails */ handleAuthFailureCalled = 'handleAuthFailureCalled', + /** Event that fires when embedded messages are updated */ + handleEmbeddedMessageUpdateCalled = 'handleEmbeddedMessageUpdateCalled', + /** Event that fires when embedded messaging is disabled */ + handleEmbeddedMessagingDisabledCalled = 'handleEmbeddedMessagingDisabledCalled', }