diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index caef783e6..616a55e75 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -593,6 +593,15 @@ public boolean handleIterableCustomAction(@NonNull IterableAction action, @NonNu eventDataJson.put("context", actionContextJson); WritableMap eventData = Serialization.convertJsonToMap(eventDataJson); sendEvent(EventName.handleCustomActionCalled.name(), eventData); + + // Emit notification opened event when source is push + if (actionContext.source == IterableActionContext.Source.PUSH) { + JSONObject notifEventDataJson = new JSONObject(); + notifEventDataJson.put("action", actionJson); + notifEventDataJson.put("context", actionContextJson); + WritableMap notifEventData = Serialization.convertJsonToMap(notifEventDataJson); + sendEvent(EventName.handleNotificationOpenedCalled.name(), notifEventData); + } } catch (JSONException e) { IterableLogger.e(TAG, "Failed handling custom action"); } @@ -632,6 +641,16 @@ public boolean handleIterableURL(@NonNull Uri uri, @NonNull IterableActionContex eventDataJson.put("context", actionContextJson); WritableMap eventData = Serialization.convertJsonToMap(eventDataJson); sendEvent(EventName.handleUrlCalled.name(), eventData); + + // Emit notification opened event when source is push + if (actionContext.source == IterableActionContext.Source.PUSH) { + JSONObject notifEventDataJson = new JSONObject(); + JSONObject actionJson = Serialization.actionToJson(actionContext.action); + notifEventDataJson.put("action", actionJson); + notifEventDataJson.put("context", actionContextJson); + WritableMap notifEventData = Serialization.convertJsonToMap(notifEventDataJson); + sendEvent(EventName.handleNotificationOpenedCalled.name(), notifEventData); + } } catch (JSONException e) { IterableLogger.e(TAG, e.getLocalizedMessage()); } @@ -808,6 +827,7 @@ enum EventName { handleCustomActionCalled, handleEmbeddedMessageUpdateCalled, handleEmbeddedMessagingDisabledCalled, + handleNotificationOpenedCalled, handleInAppCalled, handleUrlCalled, receivedIterableEmbeddedMessagesChanged, diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index c7b5fc745..8a8305940 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -35,6 +35,7 @@ import React case handleAuthFailureCalled case handleEmbeddedMessageUpdateCalled case handleEmbeddedMessagingDisabledCalled + case handleNotificationOpenedCalled } @objc public static var supportedEvents: [String] { @@ -710,6 +711,18 @@ extension ReactIterableAPI: IterableURLDelegate { "url": url.absoluteString, "context": contextDict, ] as [String: Any]) + + // Emit notification opened event when source is push + if context.source == .push { + let actionDict = ReactIterableAPI.actionToDictionary(action: context.action) + delegate?.sendEvent( + withName: EventName.handleNotificationOpenedCalled.rawValue, + body: [ + "action": actionDict, + "context": contextDict, + ] as [String: Any]) + } + return true } @@ -749,6 +762,17 @@ extension ReactIterableAPI: IterableCustomActionDelegate { "action": actionDict, "context": contextDict, ]) + + // Emit notification opened event when source is push + if context.source == .push { + delegate?.sendEvent( + withName: EventName.handleNotificationOpenedCalled.rawValue, + body: [ + "action": actionDict, + "context": contextDict, + ]) + } + return true } } diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 983fab49b..03f3a6822 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -956,6 +956,9 @@ export class Iterable { RNEventEmitter.removeAllListeners( IterableEventName.handleEmbeddedMessagingDisabledCalled ); + RNEventEmitter.removeAllListeners( + IterableEventName.handleNotificationOpenedCalled + ); } /** @@ -1109,6 +1112,17 @@ export class Iterable { ); } } + + if (Iterable.savedConfig.notificationOpenedHandler) { + RNEventEmitter.addListener( + IterableEventName.handleNotificationOpenedCalled, + (dict) => { + const action = IterableAction.fromDict(dict.action); + const context = IterableActionContext.fromDict(dict.context); + Iterable.savedConfig.notificationOpenedHandler!(action, context); + } + ); + } } /** diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index 34befbbc8..10ad77c27 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -375,6 +375,30 @@ export class IterableConfig { */ onEmbeddedMessagingDisabled?: () => void; + /** + * A callback function that is called when a push notification is opened/pressed. + * + * This callback fires for all push notification opens, regardless of whether + * the notification has a URL action, custom action, or no action at all. + * It provides access to the raw action and context data from the notification. + * + * @param action - The action associated with the notification press. + * @param context - The context in which the action was triggered, including the source. + * + * @example + * ```typescript + * const config = new IterableConfig(); + * config.notificationOpenedHandler = (action, context) => { + * console.log('Notification opened:', action, context); + * }; + * Iterable.initialize('', config); + * ``` + */ + notificationOpenedHandler?: ( + action: IterableAction, + context: IterableActionContext + ) => void; + /** * Converts the IterableConfig instance to a dictionary object. * @@ -440,6 +464,9 @@ export class IterableConfig { encryptionEnforced: this.encryptionEnforced, retryPolicy: this.retryPolicy, enableEmbeddedMessaging: this.enableEmbeddedMessaging, + // eslint-disable-next-line eqeqeq + notificationOpenedHandlerPresent: + this.notificationOpenedHandler != undefined, }; } } diff --git a/src/core/enums/IterableEventName.ts b/src/core/enums/IterableEventName.ts index 6ea79c754..29e57974f 100644 --- a/src/core/enums/IterableEventName.ts +++ b/src/core/enums/IterableEventName.ts @@ -23,4 +23,6 @@ export enum IterableEventName { handleEmbeddedMessageUpdateCalled = 'handleEmbeddedMessageUpdateCalled', /** Event that fires when embedded messaging is disabled */ handleEmbeddedMessagingDisabledCalled = 'handleEmbeddedMessagingDisabledCalled', + /** Event that fires when a push notification is opened/pressed */ + handleNotificationOpenedCalled = 'handleNotificationOpenedCalled', }