From c9f8e4d8cc67831e882ee4f61d899dc47c764211 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 20 May 2026 13:52:16 -0700 Subject: [PATCH 1/2] chore: remove legacy architecture support, and enforce new architecture requirements --- CHANGELOG.md | 3 + README.md | 14 +- android/build.gradle | 21 +- .../reactnative}/RNIterableAPIModule.java | 2 - .../reactnative/RNIterableAPIPackage.java | 3 +- .../oldarch/java/com/RNIterableAPIModule.java | 273 ------------------ example/src/NativeJwtTokenModule.ts | 12 +- ios/RNIterableAPI/RNIterableAPI.h | 15 +- ios/RNIterableAPI/RNIterableAPI.mm | 265 +---------------- package.json | 2 +- src/__mocks__/jest.setup.ts | 14 +- src/api/NativeRNIterableAPI.ts | 13 +- src/api/bridge.ts | 8 + src/api/index.ts | 8 +- src/core/classes/Iterable.ts | 29 +- src/core/classes/IterableApi.test.ts | 35 ++- src/core/classes/IterableApi.ts | 104 ++++--- src/core/classes/IterableAttributionInfo.ts | 81 ++++++ 18 files changed, 227 insertions(+), 675 deletions(-) rename android/src/{newarch/java/com => main/java/com/iterable/reactnative}/RNIterableAPIModule.java (98%) delete mode 100644 android/src/oldarch/java/com/RNIterableAPIModule.java create mode 100644 src/api/bridge.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f452e98b1..e057c0fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 3.0.0 +- **Breaking:** Removed Legacy Architecture support. Apps must use React Native New Architecture (TurboModules). + ## 2.2.2 ### Updates - Added `baseline-browser-mapping` diff --git a/README.md b/README.md index d2f597b05..b4b289a67 100644 --- a/README.md +++ b/README.md @@ -69,17 +69,11 @@ View the [API documentation](https://iterable-react-native-sdk.netlify.app). ## Architecture Support -Iterable's React Native SDK supports [React Native's New -Architecture](https://reactnative.dev/architecture/landing-page), including -TurboModules and Fabric. +Iterable's React Native SDK requires [React Native's New +Architecture](https://reactnative.dev/architecture/landing-page). -**IMPORTANT**: Iterable's React Native SDK supports React Native's Legacy Architecture, but it -is no longer actively maintained. Use at your own risk. - -Notes: - -- Ensure your app is configured for New Architecture per the React Native docs. -- The example app in this repository is configured with New Architecture enabled. +If your React Native version is less than 0.76, ensure your app has New Architecture enabled. +The example app in this repository is configured with New Architecture. ## Beta Versions diff --git a/android/build.gradle b/android/build.gradle index 6a3eb970b..045e6cf8f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,16 +15,9 @@ buildscript { } } -def isNewArchitectureEnabled() { - return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" -} - apply plugin: "com.android.library" apply plugin: "kotlin-android" - -if (isNewArchitectureEnabled()) { - apply plugin: "com.facebook.react" -} +apply plugin: "com.facebook.react" def getExtOrDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["RNIterable_" + name] @@ -59,7 +52,6 @@ android { defaultConfig { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") - buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) } buildFeatures { @@ -81,16 +73,6 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - sourceSets { - main { - if (isNewArchitectureEnabled()) { - java.srcDirs += ['src/newarch'] - } else { - java.srcDirs += ['src/oldarch'] - } - } - } - // Add this to match the Iterable SDK group ID group = "com.iterable" } @@ -108,4 +90,3 @@ dependencies { api "com.iterable:iterableapi:3.6.2" // api project(":iterableapi") // links to local android SDK repo rather than by release } - diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java similarity index 98% rename from android/src/newarch/java/com/RNIterableAPIModule.java rename to android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java index 9abe0d070..f5676c0a0 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java @@ -7,8 +7,6 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.iterable.iterableapi.AuthFailure; -import com.iterable.iterableapi.IterableLogger; public class RNIterableAPIModule extends NativeRNIterableAPISpec { private final ReactApplicationContext reactContext; diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java index 3fade361b..105db2145 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java @@ -30,14 +30,13 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { @Override public Map getReactModuleInfos() { Map moduleInfos = new HashMap<>(); - boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; moduleInfos.put(RNIterableAPIModuleImpl.NAME, new ReactModuleInfo( RNIterableAPIModuleImpl.NAME, RNIterableAPIModuleImpl.NAME, false, // canOverrideExistingModule false, // needsEagerInit false, // isCxxModule - isTurboModule // isTurboModule + true // isTurboModule )); return moduleInfos; } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java deleted file mode 100644 index ce0a6280c..000000000 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ /dev/null @@ -1,273 +0,0 @@ -package com.iterable.reactnative; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; - -public class RNIterableAPIModule extends ReactContextBaseJavaModule { - private final ReactApplicationContext reactContext; - private static RNIterableAPIModuleImpl moduleImpl; - - RNIterableAPIModule(ReactApplicationContext context) { - super(context); - this.reactContext = context; - if (moduleImpl == null) { - moduleImpl = new RNIterableAPIModuleImpl(reactContext); - } - } - - @NonNull - @Override - public String getName() { - return RNIterableAPIModuleImpl.NAME; - } - - @ReactMethod - public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, String version, Promise promise) { - moduleImpl.initializeWithApiKey(apiKey, configReadableMap, version, promise); - } - - @ReactMethod - public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, String version, String apiEndPointOverride, Promise promise) { - moduleImpl.initialize2WithApiKey(apiKey, configReadableMap, version, apiEndPointOverride, promise); - } - - @ReactMethod - public void setEmail(@Nullable String email, @Nullable String authToken) { - moduleImpl.setEmail(email, authToken); - } - - @ReactMethod - public void updateEmail(String email, @Nullable String authToken) { - moduleImpl.updateEmail(email, authToken); - } - - @ReactMethod - public void getEmail(Promise promise) { - moduleImpl.getEmail(promise); - } - - @ReactMethod - public void sampleMethod(String stringArgument, int numberArgument, Callback callback) { - moduleImpl.sampleMethod(stringArgument, numberArgument, callback); - } - - @ReactMethod - public void setUserId(@Nullable String userId, @Nullable String authToken) { - moduleImpl.setUserId(userId, authToken); - } - - @ReactMethod - public void updateUser(ReadableMap dataFields, boolean mergeNestedObjects) { - moduleImpl.updateUser(dataFields, mergeNestedObjects); - } - - @ReactMethod - public void getUserId(Promise promise) { - moduleImpl.getUserId(promise); - } - - @ReactMethod - public void trackEvent(String name, @Nullable ReadableMap dataFields) { - moduleImpl.trackEvent(name, dataFields); - } - - @ReactMethod - public void updateCart(ReadableArray items) { - moduleImpl.updateCart(items); - } - - @ReactMethod - public void trackPurchase(double total, ReadableArray items, @Nullable ReadableMap dataFields) { - moduleImpl.trackPurchase(total, items, dataFields); - } - - @ReactMethod - public void trackPushOpenWithCampaignId(double campaignId, @Nullable Double templateId, String messageId, boolean appAlreadyRunning, @Nullable ReadableMap dataFields) { - moduleImpl.trackPushOpenWithCampaignId(campaignId, templateId, messageId, appAlreadyRunning, dataFields); - } - - @ReactMethod - public void updateSubscriptions(@Nullable ReadableArray emailListIds, @Nullable ReadableArray unsubscribedChannelIds, @Nullable ReadableArray unsubscribedMessageTypeIds, @Nullable ReadableArray subscribedMessageTypeIds, double campaignId, double templateId) { - moduleImpl.updateSubscriptions(emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, subscribedMessageTypeIds, campaignId, templateId); - } - - @ReactMethod - public void showMessage(String messageId, boolean consume, final Promise promise) { - moduleImpl.showMessage(messageId, consume, promise); - } - - @ReactMethod - public void setReadForMessage(String messageId, boolean read) { - moduleImpl.setReadForMessage(messageId, read); - } - - @ReactMethod - public void removeMessage(String messageId, double location, double deleteSource) { - moduleImpl.removeMessage(messageId, location, deleteSource); - } - - @ReactMethod - public void getHtmlInAppContentForMessage(String messageId, final Promise promise) { - moduleImpl.getHtmlInAppContentForMessage(messageId, promise); - } - - @ReactMethod - public void getAttributionInfo(Promise promise) { - moduleImpl.getAttributionInfo(promise); - } - - @ReactMethod - public void setAttributionInfo(@Nullable ReadableMap attributionInfoReadableMap) { - moduleImpl.setAttributionInfo(attributionInfoReadableMap); - } - - @ReactMethod - public void getLastPushPayload(Promise promise) { - moduleImpl.getLastPushPayload(promise); - } - - @ReactMethod - public void disableDeviceForCurrentUser() { - moduleImpl.disableDeviceForCurrentUser(); - } - - @ReactMethod - public void handleAppLink(String uri, Promise promise) { - moduleImpl.handleAppLink(uri, promise); - } - - @ReactMethod - public void trackInAppOpen(String messageId, double location) { - moduleImpl.trackInAppOpen(messageId, location); - } - - @ReactMethod - public void trackInAppClick(String messageId, double location, String clickedUrl) { - moduleImpl.trackInAppClick(messageId, location, clickedUrl); - } - - @ReactMethod - public void trackInAppClose(String messageId, double location, double source, @Nullable String clickedUrl) { - moduleImpl.trackInAppClose(messageId, location, source, clickedUrl); - } - - @ReactMethod - public void inAppConsume(String messageId, double location, double source) { - moduleImpl.inAppConsume(messageId, location, source); - } - - @ReactMethod - public void getInAppMessages(Promise promise) { - moduleImpl.getInAppMessages(promise); - } - - @ReactMethod - public void getInboxMessages(Promise promise) { - moduleImpl.getInboxMessages(promise); - } - - @ReactMethod - public void getUnreadInboxMessagesCount(Promise promise) { - moduleImpl.getUnreadInboxMessagesCount(promise); - } - - @ReactMethod - public void setInAppShowResponse(double number) { - moduleImpl.setInAppShowResponse(number); - } - - @ReactMethod - public void setAutoDisplayPaused(final boolean paused) { - moduleImpl.setAutoDisplayPaused(paused); - } - - @ReactMethod - public void wakeApp() { - moduleImpl.wakeApp(); - } - - @ReactMethod - public void startSession(ReadableArray visibleRows) { - moduleImpl.startSession(visibleRows); - } - - @ReactMethod - public void endSession() { - moduleImpl.endSession(); - } - - @ReactMethod - public void updateVisibleRows(ReadableArray visibleRows) { - moduleImpl.updateVisibleRows(visibleRows); - } - - @ReactMethod - public void addListener(String eventName) { - moduleImpl.addListener(eventName); - } - - @ReactMethod - public void removeListeners(double count) { - moduleImpl.removeListeners(count); - } - - @ReactMethod - public void passAlongAuthToken(@Nullable String authToken) { - moduleImpl.passAlongAuthToken(authToken); - } - - @ReactMethod - public void pauseAuthRetries(boolean pauseRetry) { - moduleImpl.pauseAuthRetries(pauseRetry); - } - - @ReactMethod - public void syncEmbeddedMessages() { - moduleImpl.syncEmbeddedMessages(); - } - - @ReactMethod - public void startEmbeddedSession() { - moduleImpl.startEmbeddedSession(); - } - - @ReactMethod - public void endEmbeddedSession() { - moduleImpl.endEmbeddedSession(); - } - - @ReactMethod - public void startEmbeddedImpression(String messageId, double placementId) { - moduleImpl.startEmbeddedImpression(messageId, (int) placementId); - } - - @ReactMethod - public void pauseEmbeddedImpression(String messageId) { - moduleImpl.pauseEmbeddedImpression(messageId); - } - - @ReactMethod - public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { - moduleImpl.getEmbeddedMessages(placementIds, promise); - } - - @ReactMethod - public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { - moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); - } - - public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { - moduleImpl.sendEvent(eventName, eventData); - } - - void onInboxUpdated() { - moduleImpl.onInboxUpdated(); - } -} diff --git a/example/src/NativeJwtTokenModule.ts b/example/src/NativeJwtTokenModule.ts index 464cdc37b..5f086354b 100644 --- a/example/src/NativeJwtTokenModule.ts +++ b/example/src/NativeJwtTokenModule.ts @@ -1,4 +1,4 @@ -import { NativeModules, TurboModuleRegistry } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; import type { TurboModule } from 'react-native'; export interface Spec extends TurboModule { @@ -10,18 +10,10 @@ export interface Spec extends TurboModule { ): Promise; } -// Try to use TurboModule if available (New Architecture) -// Fall back to NativeModules (Old Architecture) -const isTurboModuleEnabled = - '__turboModuleProxy' in global && - (global as Record).__turboModuleProxy != null; - let JwtTokenModule: Spec | null = null; try { - JwtTokenModule = isTurboModuleEnabled - ? TurboModuleRegistry.getEnforcing('JwtTokenModule') - : NativeModules.JwtTokenModule; + JwtTokenModule = TurboModuleRegistry.getEnforcing('JwtTokenModule'); } catch { // Module not available - will throw error when used console.warn('JwtTokenModule native module is not available yet'); diff --git a/ios/RNIterableAPI/RNIterableAPI.h b/ios/RNIterableAPI/RNIterableAPI.h index a8b725452..8967f88d4 100644 --- a/ios/RNIterableAPI/RNIterableAPI.h +++ b/ios/RNIterableAPI/RNIterableAPI.h @@ -1,18 +1,9 @@ #import #import +#import +#import +#import -#if RCT_NEW_ARCH_ENABLED - - #import - #import - #import - #import @interface RNIterableAPI : RCTEventEmitter -#else - #import -@interface RNIterableAPI : RCTEventEmitter - -#endif - @end diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 4c2e5622c..9369d6e10 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -1,9 +1,6 @@ #import "RNIterableAPI.h" - -#if RCT_NEW_ARCH_ENABLED - #import "RNIterableAPISpec.h" - #import -#endif +#import "RNIterableAPISpec.h" +#import #import @@ -54,9 +51,7 @@ - (void)sendEventWithName:(NSString *_Nonnull)name result:(double)result { [self sendEventWithName:name body:@(result)]; } -#if RCT_NEW_ARCH_ENABLED - -// MARK: - New Architecture functions exposed to JS +// MARK: - TurboModule functions exposed to JS - (void)startObserving { [(ReactIterableAPI *)_swiftAPI startObserving]; @@ -325,258 +320,4 @@ - (void)wakeApp { return std::make_shared(params); } -#else - -// MARK: - RCTBridgeModule integration for Legacy Architecture - -RCT_EXPORT_METHOD(startObserving) { - [(ReactIterableAPI *)_swiftAPI startObserving]; -} - -RCT_EXPORT_METHOD(stopObserving) { - [(ReactIterableAPI *)_swiftAPI stopObserving]; -} - -RCT_EXPORT_METHOD( - initializeWithApiKey : (NSString *)apiKey config : (NSDictionary *) - config version : (NSString *)version resolve : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI initializeWithApiKey:apiKey - config:config - version:version - resolver:resolve - rejecter:reject]; -} - -RCT_EXPORT_METHOD( - initialize2WithApiKey : (NSString *)apiKey config : (NSDictionary *) - config version : (NSString *)version apiEndPointOverride : (NSString *) - apiEndPointOverride resolve : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI initialize2WithApiKey:apiKey - config:config - version:version - apiEndPointOverride:apiEndPointOverride - resolver:resolve - rejecter:reject]; -} - -RCT_EXPORT_METHOD(setEmail : (NSString *_Nullable) - email authToken : (NSString *_Nullable)authToken) { - [_swiftAPI setEmail:email authToken:authToken]; -} - -RCT_EXPORT_METHOD(getEmail : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getEmail:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(setUserId : (NSString *_Nullable) - userId authToken : (NSString *_Nullable)authToken) { - [_swiftAPI setUserId:userId authToken:authToken]; -} - -RCT_EXPORT_METHOD(getUserId : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getUserId:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(setInAppShowResponse : (double)inAppShowResponse) { - [_swiftAPI setInAppShowResponse:inAppShowResponse]; -} - -RCT_EXPORT_METHOD(getInAppMessages : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getInAppMessages:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(getInboxMessages : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getInboxMessages:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(getUnreadInboxMessagesCount : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getUnreadInboxMessagesCount:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(showMessage : (NSString *)messageId consume : (BOOL) - consume resolve : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI showMessage:messageId - consume:consume - resolver:resolve - rejecter:reject]; -} - -RCT_EXPORT_METHOD(removeMessage : (NSString *)messageId location : (double) - location source : (double)source) { - [_swiftAPI removeMessage:messageId location:location source:source]; -} - -RCT_EXPORT_METHOD(setReadForMessage : (NSString *)messageId read : (BOOL)read) { - [_swiftAPI setReadForMessage:messageId read:read]; -} - -RCT_EXPORT_METHOD(setAutoDisplayPaused : (BOOL)autoDisplayPaused) { - [_swiftAPI setAutoDisplayPaused:autoDisplayPaused]; -} - -RCT_EXPORT_METHOD(trackEvent : (NSString *)name dataFields : (NSDictionary *) - dataFields) { - [_swiftAPI trackEvent:name dataFields:dataFields]; -} - -RCT_EXPORT_METHOD( - trackPushOpenWithCampaignId : (double)campaignId templateId : (NSNumber *) - templateId messageId : (NSString *)messageId appAlreadyRunning : (BOOL) - appAlreadyRunning dataFields : (NSDictionary *)dataFields) { - [_swiftAPI trackPushOpenWithCampaignId:campaignId - templateId:templateId - messageId:messageId - appAlreadyRunning:appAlreadyRunning - dataFields:dataFields]; -} - -RCT_EXPORT_METHOD(trackInAppOpen : (NSString *)messageId location : (double) - location) { - [_swiftAPI trackInAppOpen:messageId location:location]; -} - -RCT_EXPORT_METHOD(trackInAppClick : (NSString *)messageId location : (double) - location clickedUrl : (NSString *)clickedUrl) { - [_swiftAPI trackInAppClick:messageId location:location clickedUrl:clickedUrl]; -} - -RCT_EXPORT_METHOD(trackInAppClose : (NSString *)messageId location : (double) - location source : (double)source clickedUrl : (NSString *) - clickedUrl) { - [_swiftAPI trackInAppClose:messageId - location:location - source:source - clickedUrl:clickedUrl]; -} - -RCT_EXPORT_METHOD(inAppConsume : (NSString *)messageId location : (double) - location source : (double)source) { - [_swiftAPI inAppConsume:messageId location:location source:source]; -} - -RCT_EXPORT_METHOD(updateCart : (NSArray *)items) { - [_swiftAPI updateCart:items]; -} - -RCT_EXPORT_METHOD(trackPurchase : (double)total items : (NSArray *) - items dataFields : (NSDictionary *)dataFields) { - [_swiftAPI trackPurchase:total items:items dataFields:dataFields]; -} - -RCT_EXPORT_METHOD(updateUser : (NSDictionary *)dataFields mergeNestedObjects : ( - BOOL)mergeNestedObjects) { - [_swiftAPI updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; -} - -RCT_EXPORT_METHOD(updateEmail : (NSString *)email authToken : (NSString *) - authToken) { - [_swiftAPI updateEmail:email authToken:authToken]; -} - -RCT_EXPORT_METHOD(getAttributionInfo : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getAttributionInfo:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(setAttributionInfo : (NSDictionary *)attributionInfo) { - [_swiftAPI setAttributionInfo:attributionInfo]; -} - -RCT_EXPORT_METHOD(disableDeviceForCurrentUser) { - [_swiftAPI disableDeviceForCurrentUser]; -} - -RCT_EXPORT_METHOD(getLastPushPayload : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getLastPushPayload:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(getHtmlInAppContentForMessage : (NSString *) - messageId resolve : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getHtmlInAppContentForMessage:messageId - resolver:resolve - rejecter:reject]; -} - -RCT_EXPORT_METHOD(handleAppLink : (NSString *)appLink resolve : ( - RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD( - updateSubscriptions : (NSArray *)emailListIds unsubscribedChannelIds : ( - NSArray *) - unsubscribedChannelIds unsubscribedMessageTypeIds : (NSArray *) - unsubscribedMessageTypeIds subscribedMessageTypeIds : (NSArray *) - subscribedMessageTypeIds campaignId : (double) - campaignId templateId : (double)templateId) { - [_swiftAPI updateSubscriptions:emailListIds - unsubscribedChannelIds:unsubscribedChannelIds - unsubscribedMessageTypeIds:unsubscribedMessageTypeIds - subscribedMessageTypeIds:subscribedMessageTypeIds - campaignId:campaignId - templateId:templateId]; -} - -RCT_EXPORT_METHOD(startSession : (NSArray *)visibleRows) { - [_swiftAPI startSession:visibleRows]; -} - -RCT_EXPORT_METHOD(endSession) { [_swiftAPI endSession]; } - -RCT_EXPORT_METHOD(updateVisibleRows : (NSArray *)visibleRows) { - [_swiftAPI updateVisibleRows:visibleRows]; -} - -RCT_EXPORT_METHOD(passAlongAuthToken : (NSString *_Nullable)authToken) { - [_swiftAPI passAlongAuthToken:authToken]; -} - -RCT_EXPORT_METHOD(pauseAuthRetries : (BOOL)pauseRetry) { - [_swiftAPI pauseAuthRetries:pauseRetry]; -} - -RCT_EXPORT_METHOD(startEmbeddedSession) { - [_swiftAPI startEmbeddedSession]; -} - -RCT_EXPORT_METHOD(endEmbeddedSession) { - [_swiftAPI endEmbeddedSession]; -} - -RCT_EXPORT_METHOD(syncEmbeddedMessages) { - [_swiftAPI syncEmbeddedMessages]; -} - -RCT_EXPORT_METHOD(getEmbeddedMessages : (NSArray *_Nullable)placementIds resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getEmbeddedMessages:placementIds resolver:resolve rejecter:reject]; -} - -RCT_EXPORT_METHOD(startEmbeddedImpression : (NSString *)messageId placementId : (double)placementId) { - [_swiftAPI startEmbeddedImpression:messageId placementId:placementId]; -} - -RCT_EXPORT_METHOD(pauseEmbeddedImpression : (NSString *)messageId) { - [_swiftAPI pauseEmbeddedImpression:messageId]; -} - -RCT_EXPORT_METHOD(trackEmbeddedClick : (NSDictionary *)message buttonId : (NSString *_Nullable)buttonId clickedUrl : (NSString *_Nullable)clickedUrl) { - [_swiftAPI trackEmbeddedClick:message buttonId:buttonId clickedUrl:clickedUrl]; -} - -RCT_EXPORT_METHOD(wakeApp) { - // Placeholder function -- this method is only used in Android -} - -#endif - @end diff --git a/package.json b/package.json index f4b019dbb..2f6b5f9c0 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ } }, "create-react-native-library": { - "type": "module-legacy", + "type": "module", "languages": "kotlin-swift", "version": "0.41.2" } diff --git a/src/__mocks__/jest.setup.ts b/src/__mocks__/jest.setup.ts index 96c17ca52..4d5b2e197 100644 --- a/src/__mocks__/jest.setup.ts +++ b/src/__mocks__/jest.setup.ts @@ -1,7 +1,13 @@ import * as ReactNative from 'react-native'; -import { MockRNIterableAPI } from './MockRNIterableAPI'; import { MockLinking } from './MockLinking'; +import { MockRNIterableAPI } from './MockRNIterableAPI'; + +jest.mock('../api', () => ({ + __esModule: true, + default: MockRNIterableAPI, + RNIterableAPI: MockRNIterableAPI, +})); const mockNativeEventEmitter = new (require('events').EventEmitter)() as import('events').EventEmitter; @@ -42,14 +48,8 @@ jest.mock('react-native-webview', () => { }); jest.doMock('react-native', () => { - // Extend ReactNative return Object.setPrototypeOf( { - // Mock RNIterableAPI - NativeModules: { - ...ReactNative.NativeModules, - RNIterableAPI: MockRNIterableAPI, - }, Linking: MockLinking, NativeEventEmitter: mockNativeEventEmitterConstructor, }, diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 68fcace64..511cff323 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -146,15 +146,4 @@ export interface Spec extends TurboModule { removeListeners(count: number): void; } -// Check if we're in a test environment -const isTestEnvironment = () => { - return ( - typeof jest !== 'undefined' || - process.env.NODE_ENV === 'test' || - process.env.JEST_WORKER_ID !== undefined - ); -}; - -export default isTestEnvironment() - ? undefined - : TurboModuleRegistry.getEnforcing('RNIterableAPI'); +export default TurboModuleRegistry.getEnforcing('RNIterableAPI'); diff --git a/src/api/bridge.ts b/src/api/bridge.ts new file mode 100644 index 000000000..a012b7e0e --- /dev/null +++ b/src/api/bridge.ts @@ -0,0 +1,8 @@ +/** Serializable map type used by React Native codegen for the native bridge. */ +export type BridgeRecord = { [key: string]: string | number | boolean }; + +export const asBridgeRecord = (value: unknown): BridgeRecord => + value as BridgeRecord; + +export const asBridgeRecordArray = (value: unknown): BridgeRecord[] => + value as BridgeRecord[]; diff --git a/src/api/index.ts b/src/api/index.ts index 9c327891b..bc1600221 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,6 @@ -import { NativeModules } from 'react-native'; -import BridgelessModule from './NativeRNIterableAPI'; - -export const RNIterableAPI = BridgelessModule ?? NativeModules.RNIterableAPI; +import NativeRNIterableAPI from './NativeRNIterableAPI'; +import type { Spec } from './NativeRNIterableAPI'; +export type { Spec }; +export const RNIterableAPI: Spec = NativeRNIterableAPI; export default RNIterableAPI; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 983fab49b..58eb996eb 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -16,7 +16,10 @@ import { callUrlHandler } from '../utils/callUrlHandler'; import { IterableAction } from './IterableAction'; import { IterableActionContext } from './IterableActionContext'; import { IterableApi } from './IterableApi'; -import { IterableAttributionInfo } from './IterableAttributionInfo'; +import { + IterableAttributionInfo, + type IterableAttributionInfoInput, +} from './IterableAttributionInfo'; import { IterableAuthManager } from './IterableAuthManager'; import { IterableAuthResponse } from './IterableAuthResponse'; import type { IterableCommerceItem } from './IterableCommerceItem'; @@ -383,25 +386,7 @@ export class Iterable { * ``` */ static getAttributionInfo(): Promise { - return IterableApi.getAttributionInfo().then( - ( - dict: { - campaignId: number; - templateId: number; - messageId: string; - } | null - ) => { - if (dict) { - return new IterableAttributionInfo( - dict.campaignId as number, - dict.templateId as number, - dict.messageId as string - ); - } else { - return undefined; - } - } - ); + return IterableApi.getAttributionInfo(); } /** @@ -428,7 +413,9 @@ export class Iterable { * Iterable.setAttributionInfo(attributionInfo); * ``` */ - static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { + static setAttributionInfo( + attributionInfo?: IterableAttributionInfoInput | null + ) { IterableApi.setAttributionInfo(attributionInfo); } diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index 77a229c58..13337cbf1 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -1092,6 +1092,21 @@ describe('IterableApi', () => { expect(result?.messageId).toBe('msg123'); }); + it('should coerce string bridge values into IterableAttributionInfo', async () => { + MockRNIterableAPI.getAttributionInfo = jest.fn().mockResolvedValue({ + campaignId: '123', + templateId: '456', + messageId: 'msg123', + }); + + const result = await IterableApi.getAttributionInfo(); + + expect(result).toBeInstanceOf(IterableAttributionInfo); + expect(result?.campaignId).toBe(123); + expect(result?.templateId).toBe(456); + expect(result?.messageId).toBe('msg123'); + }); + it('should return undefined when attribution info is null', async () => { // GIVEN null attribution info MockRNIterableAPI.getAttributionInfo = jest.fn().mockResolvedValue(null); @@ -1112,12 +1127,28 @@ describe('IterableApi', () => { // WHEN setAttributionInfo is called IterableApi.setAttributionInfo(attributionInfo); - // THEN RNIterableAPI.setAttributionInfo is called with attribution info + // THEN RNIterableAPI.setAttributionInfo is called with bridge-serialized info expect(MockRNIterableAPI.setAttributionInfo).toBeCalledWith( - attributionInfo + attributionInfo.toBridge() ); }); + it('should call RNIterableAPI.setAttributionInfo with a plain object', () => { + const attributionInfo = { + campaignId: 99, + templateId: 88, + messageId: 'plain-json', + }; + + IterableApi.setAttributionInfo(attributionInfo); + + expect(MockRNIterableAPI.setAttributionInfo).toBeCalledWith({ + campaignId: 99, + templateId: 88, + messageId: 'plain-json', + }); + }); + it('should call RNIterableAPI.setAttributionInfo with undefined', () => { // GIVEN undefined attribution info const attributionInfo = undefined; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index daf14253a..cf91c89db 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -1,6 +1,12 @@ import { Platform } from 'react-native'; import RNIterableAPI from '../../api'; +import { + asBridgeRecord, + asBridgeRecordArray, + type BridgeRecord, +} from '../../api/bridge'; +import type { IterableAuthResponse } from './IterableAuthResponse'; import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; @@ -8,7 +14,10 @@ import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppD import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; -import { IterableAttributionInfo } from './IterableAttributionInfo'; +import { + IterableAttributionInfo, + type IterableAttributionInfoInput, +} from './IterableAttributionInfo'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; @@ -145,7 +154,10 @@ export class IterableApi { */ static updateUser(dataFields: unknown, mergeNestedObjects: boolean) { IterableLogger.log('updateUser: ', dataFields, mergeNestedObjects); - return RNIterableAPI.updateUser(dataFields, mergeNestedObjects); + return RNIterableAPI.updateUser( + asBridgeRecord(dataFields), + mergeNestedObjects + ); } /** @@ -156,7 +168,10 @@ export class IterableApi { */ static updateEmail(email: string, authToken?: string | null) { IterableLogger.log('updateEmail: ', email, authToken); - return RNIterableAPI.updateEmail(email, authToken); + return (RNIterableAPI.updateEmail as ( + email: string, + authToken?: string | null + ) => void)(email, authToken); } // ---- End USER MANAGEMENT ---- // @@ -196,12 +211,18 @@ export class IterableApi { appAlreadyRunning, dataFields ); - return RNIterableAPI.trackPushOpenWithCampaignId( + return (RNIterableAPI.trackPushOpenWithCampaignId as ( + campaignId: number, + templateId: number, + messageId: string | null | undefined, + appAlreadyRunning: boolean, + dataFields?: BridgeRecord + ) => void)( campaignId, templateId, messageId, appAlreadyRunning, - dataFields + dataFields !== undefined ? asBridgeRecord(dataFields) : undefined ); } @@ -223,7 +244,11 @@ export class IterableApi { dataFields?: unknown; }) { IterableLogger.log('trackPurchase: ', total, items, dataFields); - return RNIterableAPI.trackPurchase(total, items, dataFields); + return RNIterableAPI.trackPurchase( + total, + asBridgeRecordArray(items), + dataFields !== undefined ? asBridgeRecord(dataFields) : undefined + ); } /** @@ -323,7 +348,10 @@ export class IterableApi { dataFields?: unknown; }) { IterableLogger.log('trackEvent: ', name, dataFields); - return RNIterableAPI.trackEvent(name, dataFields); + return RNIterableAPI.trackEvent( + name, + dataFields !== undefined ? asBridgeRecord(dataFields) : undefined + ); } // ---- End TRACKING ---- // @@ -347,9 +375,18 @@ export class IterableApi { * * @param authToken - The auth token to pass along */ - static passAlongAuthToken(authToken: string | null | undefined) { + static passAlongAuthToken( + authToken: string | null | undefined + ): Promise { IterableLogger.log('passAlongAuthToken: ', authToken); - return RNIterableAPI.passAlongAuthToken(authToken); + const result = RNIterableAPI.passAlongAuthToken(authToken) as unknown; + if ( + result !== undefined && + typeof (result as Promise).then === 'function' + ) { + return result as Promise; + } + return Promise.resolve(undefined); } // ---- End AUTH ---- // @@ -464,7 +501,7 @@ export class IterableApi { messageId: string ): Promise { IterableLogger.log('getHtmlInAppContentForMessage: ', messageId); - return RNIterableAPI.getHtmlInAppContentForMessage(messageId); + return RNIterableAPI.getHtmlInAppContentForMessage(messageId) as unknown as Promise; } /** @@ -484,7 +521,7 @@ export class IterableApi { */ static startSession(visibleRows: IterableInboxImpressionRowInfo[]) { IterableLogger.log('startSession: ', visibleRows); - return RNIterableAPI.startSession(visibleRows); + return RNIterableAPI.startSession(asBridgeRecordArray(visibleRows)); } /** @@ -502,7 +539,7 @@ export class IterableApi { */ static updateVisibleRows(visibleRows: IterableInboxImpressionRowInfo[] = []) { IterableLogger.log('updateVisibleRows: ', visibleRows); - return RNIterableAPI.updateVisibleRows(visibleRows); + return RNIterableAPI.updateVisibleRows(asBridgeRecordArray(visibleRows)); } // ---- End IN-APP ---- // @@ -560,7 +597,9 @@ export class IterableApi { placementIds: number[] | null ): Promise { IterableLogger.log('getEmbeddedMessages: ', placementIds); - return RNIterableAPI.getEmbeddedMessages(placementIds as number[]); + return RNIterableAPI.getEmbeddedMessages( + placementIds as number[] + ) as Promise; } /** @@ -572,7 +611,11 @@ export class IterableApi { clickedUrl: string | null ) { IterableLogger.log('trackEmbeddedClick: ', message, buttonId, clickedUrl); - return RNIterableAPI.trackEmbeddedClick(message, buttonId, clickedUrl); + return RNIterableAPI.trackEmbeddedClick( + asBridgeRecord(message), + buttonId, + clickedUrl + ); } // ---- End EMBEDDED ---- // @@ -588,7 +631,7 @@ export class IterableApi { */ static updateCart(items: IterableCommerceItem[]) { IterableLogger.log('updateCart: ', items); - return RNIterableAPI.updateCart(items); + return RNIterableAPI.updateCart(asBridgeRecordArray(items)); } /** @@ -667,27 +710,10 @@ export class IterableApi { /** * Get the attribution info. */ - static getAttributionInfo() { + static getAttributionInfo(): Promise { IterableLogger.log('getAttributionInfo'); - // FIXME: What if this errors? - return RNIterableAPI.getAttributionInfo().then( - ( - dict: { - campaignId: number; - templateId: number; - messageId: string; - } | null - ) => { - if (dict) { - return new IterableAttributionInfo( - dict.campaignId as number, - dict.templateId as number, - dict.messageId as string - ); - } else { - return undefined; - } - } + return RNIterableAPI.getAttributionInfo().then((dict) => + IterableAttributionInfo.fromBridge(dict) ); } @@ -696,9 +722,13 @@ export class IterableApi { * * @param attributionInfo - The attribution info. */ - static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { + static setAttributionInfo( + attributionInfo?: IterableAttributionInfoInput | null + ) { IterableLogger.log('setAttributionInfo: ', attributionInfo); - return RNIterableAPI.setAttributionInfo(attributionInfo); + return (RNIterableAPI.setAttributionInfo as ( + dict: BridgeRecord | null | undefined + ) => void)(IterableAttributionInfo.toBridgeRecord(attributionInfo)); } // ---- End MOSC ---- // diff --git a/src/core/classes/IterableAttributionInfo.ts b/src/core/classes/IterableAttributionInfo.ts index 46f8d9130..b3fa09ac4 100644 --- a/src/core/classes/IterableAttributionInfo.ts +++ b/src/core/classes/IterableAttributionInfo.ts @@ -1,3 +1,18 @@ +import { asBridgeRecord, type BridgeRecord } from '../../api/bridge'; + +/** Shape returned by native `getAttributionInfo` / expected by `setAttributionInfo`. */ +export type AttributionInfoBridge = { + campaignId: number; + templateId: number; + messageId: string; +}; + +/** Accepted input for {@link IterableAttributionInfo.parseToBridge}. */ +export type IterableAttributionInfoInput = + | IterableAttributionInfo + | AttributionInfoBridge + | BridgeRecord; + /** * Represents attribution information for an Iterable campaign. */ @@ -28,4 +43,70 @@ export class IterableAttributionInfo { this.templateId = templateId; this.messageId = messageId; } + + /** Plain map for the React Native bridge (native SDK JSON serialization). */ + toBridge(): AttributionInfoBridge { + return { + campaignId: this.campaignId, + templateId: this.templateId, + messageId: this.messageId, + }; + } + + /** + * Normalize a class instance, plain object, or native bridge payload into the + * shape expected by the native layer. + */ + static parseToBridge( + value: IterableAttributionInfoInput | null | undefined + ): AttributionInfoBridge | undefined { + if (value == null) { + return undefined; + } + if (value instanceof IterableAttributionInfo) { + return value.toBridge(); + } + + const campaignId = value.campaignId; + const templateId = value.templateId; + const messageId = value.messageId; + if ( + campaignId === undefined || + templateId === undefined || + messageId === undefined + ) { + return undefined; + } + + return { + campaignId: Number(campaignId), + templateId: Number(templateId), + messageId: String(messageId), + }; + } + + /** + * Build from a native bridge payload, plain object, or an existing instance. + */ + static fromBridge( + value: IterableAttributionInfoInput | null | undefined + ): IterableAttributionInfo | undefined { + const bridge = IterableAttributionInfo.parseToBridge(value); + if (!bridge) { + return undefined; + } + return new IterableAttributionInfo( + bridge.campaignId, + bridge.templateId, + bridge.messageId + ); + } + + /** Bridge record for TurboModule / native calls. */ + static toBridgeRecord( + value: IterableAttributionInfoInput | null | undefined + ): BridgeRecord | undefined { + const bridge = IterableAttributionInfo.parseToBridge(value); + return bridge !== undefined ? asBridgeRecord(bridge) : undefined; + } } From 3e5d02d365b4b4630e9bfb4f252deb4286aaeb2e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 20 May 2026 17:23:04 -0700 Subject: [PATCH 2/2] fix: ensure proper namespace handling in build.gradle --- android/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/build.gradle b/android/build.gradle index 045e6cf8f..fb7c70329 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -39,7 +39,9 @@ def supportsNamespace() { android { if (supportsNamespace()) { namespace "com.iterable.reactnative" + } + if (supportsNamespace()) { sourceSets { main { manifest.srcFile "src/main/AndroidManifestNew.xml"