Skip to content

Commit 9e6161d

Browse files
authored
SDK-478 resolve init promise immediately on iOS SDK, matching Android SDK (#869)
* ✨ Updated the SDK initialize method * 🧪 Increased the tests wait time to avoid flakyness from CI * docs: clarify bridge fix and InAppManager-vs-init distinction in CHANGELOG Spells out what the iOS bridge actually fixed, that IterableAPI.initialize is sync and non-failable, and that the previous user-visible error came from the in-app messages fetch retry budget, not from SDK initialization.
1 parent 37673e3 commit 9e6161d

3 files changed

Lines changed: 66 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
## Unreleased
22

3+
### Fixes
4+
- Fixed the React Native bridge contract for `Iterable.initialize` on iOS.
5+
- The iOS bridge now calls the synchronous native initializer (`IterableAPI.initialize`) and resolves the JS promise immediately, matching the Android bridge which has always behaved this way.
6+
- Previously, the iOS bridge wired the JS promise to the `initialize2(callback:)` overload, whose callback fires when the first in-app messages fetch settles, not when the SDK is ready to use. That callback is an `InAppManager.start()` signal and has nothing to do with whether `IterableAPI.initialize` succeeded.
7+
- On iOS, `IterableAPI.initialize(...)` is synchronous, non-failable, and returns `Void`. The native SDK is fully usable the moment it returns. There is no init-error channel to await on the native side, and JS callers should not treat the promise as one. `await Iterable.initialize(...)` is supported for API symmetry but does not gate SDK readiness on any async work.
8+
- The user-visible symptom of the old contract was a multi-second to multi-minute hang on the JS promise under JWT or network friction, surfaced as an init failure even though the SDK had already initialized successfully. The error originated in the in-app messages fetch retry budget, not in initialization.
9+
310
### Updates
411

512
- Added `Iterable.registerDeviceToken(token)` to re-enable push for the current device.
@@ -13,7 +20,6 @@
1320
- Updated RN compatibility to 0.85
1421

1522
## 3.0.0
16-
1723
### Updates
1824

1925
- Added embedded messaging functionality. This includes the ability to:

ios/RNIterableAPI/ReactIterableAPI.swift

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -668,24 +668,40 @@ import React
668668
name: Notification.Name.iterableInboxChanged, object: nil)
669669

670670
DispatchQueue.main.async {
671-
IterableAPI.initialize2(
672-
apiKey: apiKey,
673-
launchOptions: launchOptions,
674-
config: iterableConfig,
675-
apiEndPointOverride: apiEndPointOverride
676-
) { result in
677-
resolver(result)
671+
// The native iOS SDK is fully usable the moment IterableAPI.initialize
672+
// returns. The legacy initialize2(callback:) overload fires its callback
673+
// only after inAppManager.start() resolves the first in-app messages
674+
// fetch, which can take 60s+ under any auth or network friction and
675+
// blocks the JS promise on a signal that does not actually represent
676+
// "SDK ready". Android's RNIterableAPIModuleImpl.initializeWithApiKey
677+
// resolves promise.resolve(true) immediately after sync init - this
678+
// brings iOS to parity. See SDK-478.
679+
if let apiEndPointOverride = apiEndPointOverride {
680+
IterableAPI.initialize2(
681+
apiKey: apiKey,
682+
launchOptions: launchOptions,
683+
config: iterableConfig,
684+
apiEndPointOverride: apiEndPointOverride
685+
)
686+
} else {
687+
IterableAPI.initialize(
688+
apiKey: apiKey,
689+
launchOptions: launchOptions,
690+
config: iterableConfig
691+
)
678692
}
679693

680694
IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version)
681-
695+
682696
// Add embedded update listener if any callback is present
683697
let onEmbeddedMessageUpdatePresent = configDict["onEmbeddedMessageUpdatePresent"] as? Bool ?? false
684698
let onEmbeddedMessagingDisabledPresent = configDict["onEmbeddedMessagingDisabledPresent"] as? Bool ?? false
685-
699+
686700
if onEmbeddedMessageUpdatePresent || onEmbeddedMessagingDisabledPresent {
687701
IterableAPI.embeddedManager.addUpdateListener(self)
688702
}
703+
704+
resolver(true)
689705
}
690706
}
691707

src/core/classes/IterableApi.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ describe('IterableApi', () => {
7070
);
7171
expect(result).toBe(true);
7272
});
73+
74+
// SDK-478: the bridge contract is "resolve true immediately after calling
75+
// sync IterableAPI.initialize on the native side". The JS layer is a pure
76+
// passthrough. This pins that the JS code does not add an await on any
77+
// additional async work between the native call and the returned promise.
78+
// Regression guard for the 2021 contract drift that gated the JS promise
79+
// on the first in-app messages fetch (commit 4c357126).
80+
it('resolves promptly without awaiting any additional async work', async () => {
81+
MockRNIterableAPI.initializeWithApiKey.mockResolvedValueOnce(true);
82+
const startedAt = Date.now();
83+
const result = await IterableApi.initializeWithApiKey('test-api-key', {
84+
config: new IterableConfig(),
85+
version: '1.0.0',
86+
});
87+
const elapsedMs = Date.now() - startedAt;
88+
expect(result).toBe(true);
89+
expect(elapsedMs).toBeLessThan(250);
90+
});
7391
});
7492

7593
describe('initialize2WithApiKey', () => {
@@ -119,6 +137,22 @@ describe('IterableApi', () => {
119137
);
120138
expect(result).toBe(true);
121139
});
140+
141+
// SDK-478: same contract as initializeWithApiKey above. initialize2 is only
142+
// used for staging/test endpoint overrides; its JS-side behavior is still
143+
// "resolve immediately with whatever native returns" - no additional waits.
144+
it('resolves promptly without awaiting any additional async work', async () => {
145+
MockRNIterableAPI.initialize2WithApiKey.mockResolvedValueOnce(true);
146+
const startedAt = Date.now();
147+
const result = await IterableApi.initialize2WithApiKey('test-api-key', {
148+
config: new IterableConfig(),
149+
version: '1.0.0',
150+
apiEndPoint: 'https://api.staging.iterable.com',
151+
});
152+
const elapsedMs = Date.now() - startedAt;
153+
expect(result).toBe(true);
154+
expect(elapsedMs).toBeLessThan(250);
155+
});
122156
});
123157

124158
// ====================================================== //

0 commit comments

Comments
 (0)