From 13fca393b8aaaaa8c4f9c0b2c8b1a9a50f24cdb4 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 21 May 2026 16:27:00 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Updated=20the=20SDK=20initialize=20?= =?UTF-8?q?method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++ ios/RNIterableAPI/ReactIterableAPI.swift | 34 +++++++++++++++++------- src/core/classes/IterableApi.test.ts | 34 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74bcf9f30..1cbf81219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased + +### Fixes +- Fix iOS-side `Iterable.initialize` promise hang. + - The iOS bridge now resolves the promise immediately after calling the native SDK's sync initializer, matching the Android bridge's behavior. + - Previously the promise could wait on the first in-app messages fetch (and any associated auth retry budget) before resolving, leading to multi-second to multi-minute hangs under certain configurations. + - The native iOS SDK is fully usable the moment `Iterable.initialize` is called; nothing about JS-side correctness requires waiting on the promise. + ## 2.2.0 ### Updates diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index c7b5fc745..fecd2abb9 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -645,24 +645,40 @@ import React name: Notification.Name.iterableInboxChanged, object: nil) DispatchQueue.main.async { - IterableAPI.initialize2( - apiKey: apiKey, - launchOptions: launchOptions, - config: iterableConfig, - apiEndPointOverride: apiEndPointOverride - ) { result in - resolver(result) + // The native iOS SDK is fully usable the moment IterableAPI.initialize + // returns. The legacy initialize2(callback:) overload fires its callback + // only after inAppManager.start() resolves the first in-app messages + // fetch, which can take 60s+ under any auth or network friction and + // blocks the JS promise on a signal that does not actually represent + // "SDK ready". Android's RNIterableAPIModuleImpl.initializeWithApiKey + // resolves promise.resolve(true) immediately after sync init - this + // brings iOS to parity. See SDK-478. + if let apiEndPointOverride = apiEndPointOverride { + IterableAPI.initialize2( + apiKey: apiKey, + launchOptions: launchOptions, + config: iterableConfig, + apiEndPointOverride: apiEndPointOverride + ) + } else { + IterableAPI.initialize( + apiKey: apiKey, + launchOptions: launchOptions, + config: iterableConfig + ) } 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) } + + resolver(true) } } diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index 77a229c58..64d620b3b 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -70,6 +70,24 @@ describe('IterableApi', () => { ); expect(result).toBe(true); }); + + // SDK-478: the bridge contract is "resolve true immediately after calling + // sync IterableAPI.initialize on the native side". The JS layer is a pure + // passthrough. This pins that the JS code does not add an await on any + // additional async work between the native call and the returned promise. + // Regression guard for the 2021 contract drift that gated the JS promise + // on the first in-app messages fetch (commit 4c357126). + it('resolves promptly without awaiting any additional async work', async () => { + MockRNIterableAPI.initializeWithApiKey.mockResolvedValueOnce(true); + const startedAt = Date.now(); + const result = await IterableApi.initializeWithApiKey('test-api-key', { + config: new IterableConfig(), + version: '1.0.0', + }); + const elapsedMs = Date.now() - startedAt; + expect(result).toBe(true); + expect(elapsedMs).toBeLessThan(50); + }); }); describe('initialize2WithApiKey', () => { @@ -119,6 +137,22 @@ describe('IterableApi', () => { ); expect(result).toBe(true); }); + + // SDK-478: same contract as initializeWithApiKey above. initialize2 is only + // used for staging/test endpoint overrides; its JS-side behavior is still + // "resolve immediately with whatever native returns" - no additional waits. + it('resolves promptly without awaiting any additional async work', async () => { + MockRNIterableAPI.initialize2WithApiKey.mockResolvedValueOnce(true); + const startedAt = Date.now(); + const result = await IterableApi.initialize2WithApiKey('test-api-key', { + config: new IterableConfig(), + version: '1.0.0', + apiEndPoint: 'https://api.staging.iterable.com', + }); + const elapsedMs = Date.now() - startedAt; + expect(result).toBe(true); + expect(elapsedMs).toBeLessThan(50); + }); }); // ====================================================== //