Skip to content

Commit 4c2c9e0

Browse files
committed
feat(SDK-520): replace magic 1000ms timeouts with named, configurable constants
- Add IterableConfig.androidWakeDelayMs and authCallbackTimeoutMs to expose the two hardcoded timeouts as tunable, documented values. - Replace the fixed-window auth callback gate with an event-driven Promise latch; the timer survives only as a safety-net fallback.
1 parent f713bf3 commit 4c2c9e0

4 files changed

Lines changed: 363 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@
1212
- Added `Iterable.registerDeviceToken(token)` to re-enable push for the current device.
1313
- Android accepts an FCM token string.
1414
- iOS accepts a continuous hex string representation of the APNS token.
15+
- Replaced the two hardcoded 1000ms magic timeouts in the RN SDK with named,
16+
documented, and configurable values (SDK-520).
17+
- Added `IterableConfig.androidWakeDelayMs` (default `1000`) to tune the
18+
Android deep-link wake delay before the SDK invokes `urlHandler`. Set to
19+
`0` to dispatch synchronously.
20+
- Added `IterableConfig.authCallbackTimeoutMs` (default `1000`) to tune the
21+
safety-net timeout for the auth callback latch.
22+
- The auth callback gate is now event-driven: the native
23+
`handleAuthSuccessCalled` / `handleAuthFailureCalled` events resolve the
24+
latch immediately. The timer survives only as a fallback when no native
25+
event arrives within the configured window, instead of being the primary
26+
resolution mechanism.
1527

1628
## 3.0.1
1729

src/core/classes/Iterable.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ describe('Iterable', () => {
334334
expect(config.pushIntegrationName).toBe(undefined);
335335
expect(config.urlHandler).toBe(undefined);
336336
expect(config.useInMemoryStorageForInApps).toBe(false);
337+
expect(config.androidWakeDelayMs).toBe(1000);
338+
expect(config.authCallbackTimeoutMs).toBe(1000);
337339
const configDict = config.toDict();
338340
expect(configDict.allowedProtocols).toEqual([]);
339341
expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false);
@@ -350,6 +352,17 @@ describe('Iterable', () => {
350352
expect(configDict.pushIntegrationName).toBe(undefined);
351353
expect(configDict.urlHandlerPresent).toBe(false);
352354
expect(configDict.useInMemoryStorageForInApps).toBe(false);
355+
expect(configDict.androidWakeDelayMs).toBe(1000);
356+
expect(configDict.authCallbackTimeoutMs).toBe(1000);
357+
});
358+
359+
it('should allow overriding androidWakeDelayMs and authCallbackTimeoutMs', () => {
360+
const config = new IterableConfig();
361+
config.androidWakeDelayMs = 1500;
362+
config.authCallbackTimeoutMs = 2500;
363+
const configDict = config.toDict();
364+
expect(configDict.androidWakeDelayMs).toBe(1500);
365+
expect(configDict.authCallbackTimeoutMs).toBe(2500);
353366
});
354367
});
355368

@@ -1607,6 +1620,88 @@ describe('Iterable', () => {
16071620
expect(MockLinking.openURL).toBeCalledWith(expectedUrl);
16081621
});
16091622
});
1623+
1624+
it('should honor a custom androidWakeDelayMs on Android', async () => {
1625+
// GIVEN Android platform
1626+
Object.defineProperty(Platform, 'OS', {
1627+
value: 'android',
1628+
writable: true,
1629+
});
1630+
1631+
// sets up event emitter
1632+
const nativeEmitter = new NativeEventEmitter();
1633+
nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled);
1634+
1635+
// sets up config with a custom wake delay
1636+
const config = new IterableConfig();
1637+
config.logReactNativeSdkCalls = false;
1638+
config.androidWakeDelayMs = 300;
1639+
config.urlHandler = jest.fn(() => false);
1640+
1641+
// initialize Iterable object
1642+
Iterable.initialize('apiKey', config);
1643+
1644+
// GIVEN the link can be opened
1645+
MockLinking.canOpenURL = jest.fn(async () => true);
1646+
MockLinking.openURL.mockReset();
1647+
1648+
const expectedUrl = 'https://somewhere.com';
1649+
const dict = {
1650+
url: expectedUrl,
1651+
context: {
1652+
action: { type: 'openUrl' },
1653+
source: IterableActionSource.inApp,
1654+
},
1655+
};
1656+
1657+
// WHEN handleUrlCalled event is emitted
1658+
nativeEmitter.emit(IterableEventName.handleUrlCalled, dict);
1659+
1660+
// THEN the handler is called after the custom delay, not the default
1661+
return await TestHelper.delayed(400, () => {
1662+
expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context);
1663+
expect(MockLinking.openURL).toBeCalledWith(expectedUrl);
1664+
});
1665+
});
1666+
1667+
it('should dispatch synchronously on Android when androidWakeDelayMs is 0', async () => {
1668+
// GIVEN Android platform
1669+
Object.defineProperty(Platform, 'OS', {
1670+
value: 'android',
1671+
writable: true,
1672+
});
1673+
1674+
// sets up event emitter
1675+
const nativeEmitter = new NativeEventEmitter();
1676+
nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled);
1677+
1678+
// sets up config with wake delay disabled
1679+
const config = new IterableConfig();
1680+
config.logReactNativeSdkCalls = false;
1681+
config.androidWakeDelayMs = 0;
1682+
config.urlHandler = jest.fn(() => false);
1683+
1684+
// initialize Iterable object
1685+
Iterable.initialize('apiKey', config);
1686+
1687+
MockLinking.canOpenURL = jest.fn(async () => true);
1688+
MockLinking.openURL.mockReset();
1689+
1690+
const expectedUrl = 'https://somewhere.com';
1691+
const dict = {
1692+
url: expectedUrl,
1693+
context: {
1694+
action: { type: 'openUrl' },
1695+
source: IterableActionSource.inApp,
1696+
},
1697+
};
1698+
1699+
// WHEN handleUrlCalled event is emitted
1700+
nativeEmitter.emit(IterableEventName.handleUrlCalled, dict);
1701+
1702+
// THEN the handler is invoked without a setTimeout delay
1703+
expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context);
1704+
});
16101705
});
16111706

16121707
describe('re-initialization', () => {
@@ -1732,5 +1827,83 @@ describe('Iterable', () => {
17321827
expect(failureCallback).not.toBeCalled();
17331828
});
17341829
});
1830+
1831+
it('should honor a custom authCallbackTimeoutMs for the safety-net timeout', async () => {
1832+
// sets up event emitter
1833+
const nativeEmitter = new NativeEventEmitter();
1834+
nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled);
1835+
nativeEmitter.removeAllListeners(
1836+
IterableEventName.handleAuthSuccessCalled
1837+
);
1838+
nativeEmitter.removeAllListeners(
1839+
IterableEventName.handleAuthFailureCalled
1840+
);
1841+
1842+
// sets up config with a short custom auth callback timeout
1843+
const config = new IterableConfig();
1844+
config.logReactNativeSdkCalls = false;
1845+
config.authCallbackTimeoutMs = 200;
1846+
const successCallback = jest.fn();
1847+
const failureCallback = jest.fn();
1848+
const authResponse = new IterableAuthResponse();
1849+
authResponse.authToken = 'short-timeout-token';
1850+
authResponse.successCallback = successCallback;
1851+
authResponse.failureCallback = failureCallback;
1852+
config.authHandler = jest.fn(() => Promise.resolve(authResponse));
1853+
1854+
// initialize Iterable object
1855+
Iterable.initialize('apiKey', config);
1856+
1857+
// WHEN handleAuthCalled event is emitted but no success/failure event follows
1858+
nativeEmitter.emit(IterableEventName.handleAuthCalled);
1859+
1860+
// THEN the safety-net timer fires at the custom interval, not the default
1861+
return await TestHelper.delayed(300, () => {
1862+
expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(
1863+
'short-timeout-token'
1864+
);
1865+
expect(successCallback).not.toBeCalled();
1866+
expect(failureCallback).not.toBeCalled();
1867+
});
1868+
});
1869+
1870+
it('should resolve the latch immediately when the native success event arrives before the safety-net timeout', async () => {
1871+
// sets up event emitter
1872+
const nativeEmitter = new NativeEventEmitter();
1873+
nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled);
1874+
nativeEmitter.removeAllListeners(
1875+
IterableEventName.handleAuthSuccessCalled
1876+
);
1877+
nativeEmitter.removeAllListeners(
1878+
IterableEventName.handleAuthFailureCalled
1879+
);
1880+
1881+
const config = new IterableConfig();
1882+
config.logReactNativeSdkCalls = false;
1883+
config.authCallbackTimeoutMs = 2000;
1884+
const successCallback = jest.fn();
1885+
const failureCallback = jest.fn();
1886+
const authResponse = new IterableAuthResponse();
1887+
authResponse.authToken = 'fast-success-token';
1888+
authResponse.successCallback = successCallback;
1889+
authResponse.failureCallback = failureCallback;
1890+
config.authHandler = jest.fn(() => Promise.resolve(authResponse));
1891+
1892+
Iterable.initialize('apiKey', config);
1893+
1894+
// WHEN handleAuthCalled and handleAuthSuccessCalled both fire
1895+
nativeEmitter.emit(IterableEventName.handleAuthCalled);
1896+
nativeEmitter.emit(IterableEventName.handleAuthSuccessCalled);
1897+
1898+
// THEN successCallback resolves on the microtask queue, well before the
1899+
// 2000ms safety-net timeout.
1900+
return await TestHelper.delayed(50, () => {
1901+
expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(
1902+
'fast-success-token'
1903+
);
1904+
expect(successCallback).toBeCalled();
1905+
expect(failureCallback).not.toBeCalled();
1906+
});
1907+
});
17351908
});
17361909
});

0 commit comments

Comments
 (0)