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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.2
### Updates
- Added `baseline-browser-mapping`
Expand Down
34 changes: 25 additions & 9 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
34 changes: 34 additions & 0 deletions src/core/classes/IterableApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});

// ====================================================== //
Expand Down
Loading