Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions Build ReactNativeSdkExample_2026-05-12T16-27-11.txt

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle:8.7.2')
// AGP/Kotlin/RNGP versions are supplied by @react-native/gradle-plugin (see node_modules/@react-native/gradle-plugin/gradle/libs.versions.toml). Do not pin here unless overriding RN intentionally.
classpath("com.android.tools.build:gradle")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AGP version was stripped ('com.android.tools.build:gradle:8.7.2'"com.android.tools.build:gradle"). This silently delegates the AGP version to the react-native-gradle-plugin's BOM. If that's intentional for RN 0.84, please add a Groovy comment explaining why, since:

  • Gradle 9 may warn on classpath deps without coordinates
  • the AGP version will now float with every RN patch
  • consumers customizing buildscript dependency order can hit unexpected resolutions
    If unintentional, pin to whatever AGP RN 0.84.1 ships with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this — the version removal is intentional.

This matches the RN 0.84 app template (react-native-community/template 0.84-stable) and the create-react-native-library example pattern: the example app does not pin AGP/Kotlin/RNGP versions in buildscript.

Versions are resolved via @react-native/gradle-plugin, which is included as a composite build in settings.gradle (pluginManagement { includeBuild(...) } + includeBuild(...)). The plugin’s version catalog (node_modules/@react-native/gradle-plugin/gradle/libs.versions.toml) defines AGP 8.12.0 and Kotlin 2.1.20 for RN 0.84.1. That’s the setup Meta documents for versionless classpath deps (see also facebook/react-native#48953).

I added a comment above the classpath block in example/android/build.gradle so future readers know this is deliberate and shouldn’t re-pin without a reason.

Note: this applies to the example app only. The library’s android/build.gradle still pins AGP explicitly for standalone android/ builds (no RNGP includeBuild there). I’m updating that pin from 8.7.28.12.0 (and the library Gradle wrapper to meet AGP 8.12’s minimum) in a separate change so it stays aligned with our RN 0.84.1 target without fighting the host app’s RNGP-managed versions.

classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
Comment on lines +16 to 18
}
Expand Down
90 changes: 52 additions & 38 deletions example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions example/ios/ReactNativeSdkExample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
Comment on lines 48 to +57
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
Expand Down
22 changes: 11 additions & 11 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"@react-navigation/native": "^7.1.14",
"@react-navigation/native-stack": "^7.0.0",
"@react-navigation/stack": "^7.4.2",
"react": "19.2.0",
"react-native": "0.83.9",
"react": "19.2.3",
"react-native": "0.84.1",
"react-native-gesture-handler": "^2.30.0",
"react-native-safe-area-context": "^5.6.0",
"react-native-screens": ">=4.19.0 <4.25.0",
Expand All @@ -25,21 +25,21 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.83.9",
"@react-native/eslint-config": "0.83.9",
"@react-native/metro-config": "0.83.9",
"@react-native/typescript-config": "0.83.9",
"@react-native-community/cli": "20.1.0",
"@react-native-community/cli-platform-android": "20.1.0",
"@react-native-community/cli-platform-ios": "20.1.0",
"@react-native/babel-preset": "0.84.1",
"@react-native/eslint-config": "0.84.1",
"@react-native/metro-config": "0.84.1",
"@react-native/typescript-config": "0.84.1",
"@types/jest": "^29.5.13",
"@types/react": "^19.2.0",
"@types/react-test-renderer": "^19.1.0",
"react-native-builder-bob": "^0.30.2",
"react-native-dotenv": "^3.4.11",
"react-test-renderer": "19.2.0"
"react-test-renderer": "19.2.3"
},
"engines": {
"node": ">=20"
"node": ">= 22.11.0"
}
}
32 changes: 7 additions & 25 deletions ios/RNIterableAPI/RNIterableAPI.mm
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,10 @@ - (void)handleAppLink:(NSString *)appLink
[_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject];
}

- (void)updateSubscriptions:(NSArray *_Nullable)emailListIds
unsubscribedChannelIds:(NSArray *_Nullable)unsubscribedChannelIds
unsubscribedMessageTypeIds:(NSArray *_Nullable)unsubscribedMessageTypeIds
subscribedMessageTypeIds:(NSArray *_Nullable)subscribedMessageTypeIds
- (void)updateSubscriptions:(NSArray *)emailListIds
unsubscribedChannelIds:(NSArray *)unsubscribedChannelIds
unsubscribedMessageTypeIds:(NSArray *)unsubscribedMessageTypeIds
subscribedMessageTypeIds:(NSArray *)subscribedMessageTypeIds
campaignId:(double)campaignId
templateId:(double)templateId {
[_swiftAPI updateSubscriptions:emailListIds
Expand Down Expand Up @@ -295,7 +295,7 @@ - (void)syncEmbeddedMessages {
[_swiftAPI syncEmbeddedMessages];
}

- (void)getEmbeddedMessages:(NSArray *_Nullable)placementIds
- (void)getEmbeddedMessages:(NSArray *)placementIds
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[_swiftAPI getEmbeddedMessages:placementIds resolver:resolve rejecter:reject];
Expand All @@ -309,28 +309,10 @@ - (void)pauseEmbeddedImpression:(NSString *)messageId {
[_swiftAPI pauseEmbeddedImpression:messageId];
}

- (void)trackEmbeddedClick:(JS::NativeRNIterableAPI::EmbeddedMessage &)message
- (void)trackEmbeddedClick:(NSDictionary *)message
buttonId:(NSString *_Nullable)buttonId
clickedUrl:(NSString *_Nullable)clickedUrl {
// The TurboModule bridge requires us to use the C++ type in the signature,
// but we need to convert it to NSDictionary to pass to Swift.
// The C++ struct wraps an NSDictionary, and the generated methods already
// return NSString*/NSNumber* types, so we just need to reconstruct the dict.
NSMutableDictionary *messageDict = [NSMutableDictionary new];

// Convert metadata (the accessor methods already return proper ObjC types)
NSMutableDictionary *metadataDict = [NSMutableDictionary new];
metadataDict[@"messageId"] = message.metadata().messageId();
metadataDict[@"placementId"] = @(message.metadata().placementId());
if (message.metadata().campaignId().has_value()) {
metadataDict[@"campaignId"] = @(*message.metadata().campaignId());
}
if (message.metadata().isProof().has_value()) {
metadataDict[@"isProof"] = @(*message.metadata().isProof());
}
messageDict[@"metadata"] = metadataDict;

[_swiftAPI trackEmbeddedClick:messageDict buttonId:buttonId clickedUrl:clickedUrl];
[_swiftAPI trackEmbeddedClick:message buttonId:buttonId clickedUrl:clickedUrl];
}

- (void)wakeApp {
Expand Down
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@
"devDependencies": {
"@commitlint/config-conventional": "^19.6.0",
"@evilmartians/lefthook": "^1.5.0",
"@react-native-community/cli": "20.0.0",
"@react-native/babel-preset": "0.83.9",
"@react-native/eslint-config": "0.83.9",
"@react-native/metro-config": "0.83.9",
"@react-native/typescript-config": "0.83.9",
"@react-native-community/cli": "20.1.0",
"@react-native/babel-preset": "0.84.1",
"@react-native/eslint-config": "0.84.1",
"@react-native/metro-config": "0.84.1",
"@react-native/typescript-config": "0.84.1",
"@react-navigation/native": "^7.1.14",
"@release-it/conventional-changelog": "^9.0.4",
"@testing-library/jest-native": "^5.4.3",
Expand All @@ -94,14 +94,14 @@
"jest": "^29.7.0",
"prettier": "^3.0.3",
"prettier-eslint": "^16.4.2",
"react": "19.2.0",
"react-native": "0.83.9",
"react": "19.2.3",
"react-native": "0.84.1",
"react-native-builder-bob": "^0.40.4",
"react-native-gesture-handler": "^2.30.0",
"react-native-safe-area-context": "^5.6.0",
"react-native-screens": ">=4.19.0 <4.25.0",
"react-native-webview": "^13.14.1",
"react-test-renderer": "19.2.0",
"react-test-renderer": "19.2.3",
"release-it": "^17.10.0",
"turbo": "^1.10.7",
"typedoc": "^0.28.13",
Expand Down
50 changes: 14 additions & 36 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,23 @@
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

// NOTE: No types can be imported because of the way new arch works, so we have
// to re-define the types here.
interface EmbeddedMessage {
metadata: {
messageId: string;
placementId: number;
campaignId?: number | null;
isProof?: boolean;
};
elements: {
buttons?:
| {
id: string;
title?: string | null;
action: { type: string; data?: string } | null;
}[]
| null;
body?: string | null;
mediaUrl?: string | null;
mediaUrlCaption?: string | null;
defaultAction?: { type: string; data?: string } | null;
text?: { id: string; text?: string | null; label?: string | null }[] | null;
title?: string | null;
} | null;
payload?: { [key: string]: string | number | boolean | null } | null;
}

// Codegen (RN 0.84+) rejects unions that include array types (e.g. `T[] | U`,
// `string | string[]`). Use capital `Object` (not TS `object`) for loose maps;
// lowercase `object` is TSObjectKeyword and fails iOS/Android codegen.
export interface Spec extends TurboModule {
// Initialization
initializeWithApiKey(
apiKey: string,
config: { [key: string]: string | number | boolean | undefined | string[] },
config: Object,
version: string
): Promise<boolean>;

initialize2WithApiKey(
apiKey: string,
config: { [key: string]: string | number | boolean | undefined | string[] },
config: Object,
version: string,
apiEndPointOverride: string
): Promise<boolean>;
Expand Down Expand Up @@ -122,12 +101,13 @@ export interface Spec extends TurboModule {
// App links
handleAppLink(appLink: string): Promise<boolean>;

// Subscriptions
// Subscriptions (arrays only in spec — RN codegen rejects `T[] | null`; callers
// may still pass null at the bridge when using typed assertions.)
updateSubscriptions(
emailListIds: number[] | null,
unsubscribedChannelIds: number[] | null,
unsubscribedMessageTypeIds: number[] | null,
subscribedMessageTypeIds: number[] | null,
emailListIds: number[],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is honest about what's happening but the resulting contract is wrong. The TurboModule now declares number[] (non-null) while the public IterableApi.ts surface still accepts number[] | null, bridged by as number[] casts.

That cast silences TS but does nothing at runtime. The Iterable backend treats null as "do not change this field" and [] as "unsubscribe from everything" — those are not interchangeable. A consumer passing null today (per the documented contract) will, on the new architecture, end up unsubscribing the user from all channels of that category.

Fix should be one of:

  1. Strip null at the JS layer (in IterableApi.ts) before calling RNIterableAPI. Make the public type number[] | undefined and short-circuit to a no-op when undefined.
  2. Add an explicit null sentinel that the native side detects (e.g. [-1]) and converts back to nil on the Swift/Kotlin side.

Option 1 is cleaner. Either way, this needs:

  • a Jest test that proves null does not become []
  • a CHANGELOG entry calling out the public API change
  • a paired docs PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RN codegen couldn’t parse number[] | null (and related unions) -- the spec was narrowed to number[] so the upgrade could build. Trying out available options now.

unsubscribedChannelIds: number[],
unsubscribedMessageTypeIds: number[],
subscribedMessageTypeIds: number[],
Comment on lines +104 to +110
campaignId: number,
templateId: number
): void;
Expand All @@ -151,11 +131,9 @@ export interface Spec extends TurboModule {
endEmbeddedSession(): void;
startEmbeddedImpression(messageId: string, placementId: number): void;
pauseEmbeddedImpression(messageId: string): void;
getEmbeddedMessages(
placementIds: number[] | null
): Promise<EmbeddedMessage[]>;
getEmbeddedMessages(placementIds: number[]): Promise<Object[]>;
trackEmbeddedClick(
message: EmbeddedMessage,
message: Object,
buttonId: string | null,
clickedUrl: string | null
): void;
Expand Down
10 changes: 5 additions & 5 deletions src/core/classes/IterableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ export class IterableApi {
placementIds: number[] | null
): Promise<IterableEmbeddedMessage[]> {
IterableLogger.log('getEmbeddedMessages: ', placementIds);
return RNIterableAPI.getEmbeddedMessages(placementIds);
return RNIterableAPI.getEmbeddedMessages(placementIds as number[]);
}

/**
Expand Down Expand Up @@ -647,10 +647,10 @@ export class IterableApi {
templateId
);
return RNIterableAPI.updateSubscriptions(
emailListIds,
unsubscribedChannelIds,
unsubscribedMessageTypeIds,
subscribedMessageTypeIds,
emailListIds as number[],
unsubscribedChannelIds as number[],
unsubscribedMessageTypeIds as number[],
subscribedMessageTypeIds as number[],
campaignId,
templateId
);
Expand Down
2 changes: 1 addition & 1 deletion src/core/classes/IterableConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,8 @@ export class IterableConfig {
*
* TODO: Figure out if this is purposeful
*/
// eslint-disable-next-line eqeqeq
onEmbeddedMessagingDisabledPresent:
// eslint-disable-next-line eqeqeq
this.onEmbeddedMessagingDisabled != undefined,
/** The log level for the SDK. */
logLevel: this.logLevel,
Expand Down
Loading
Loading