Skip to content

Commit 24cf9ad

Browse files
feat(SDK-555): wire Android keychain encryption config
Add IterableConfig.keychainEncryption (default true) and apply it on the Android bridge, deprecating encryptionEnforced which was never honored. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a8faf4a commit 24cf9ad

4 files changed

Lines changed: 39 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Updates
44

5+
- Added `IterableConfig.keychainEncryption` (default `true`, Android only) so React Native apps can control native `IterableConfig.Builder.setKeychainEncryption`. Deprecated `IterableConfig.encryptionEnforced`, which was serialized but never applied on Android (SDK-555).
56
- Added `Iterable.disableDeviceForAllUsers()` to unregister this device's push token from every user associated with the device (SDK-550).
67
- iOS: forwards to native `IterableAPI.disableDeviceForAllUsers()`.
78
- Android: graceful no-op that logs a warning; use `disableDeviceForCurrentUser()` to disable push for the current user. There is no public native "all users" equivalent.

‎android/src/main/java/com/iterable/reactnative/Serialization.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte
250250
configBuilder.setEnableEmbeddedMessaging(iterableContextJSON.optBoolean("enableEmbeddedMessaging"));
251251
}
252252

253+
boolean keychainEncryption = true;
254+
if (iterableContextJSON.has("keychainEncryption")) {
255+
keychainEncryption = iterableContextJSON.optBoolean("keychainEncryption");
256+
} else if (iterableContextJSON.optBoolean("encryptionEnforced")) {
257+
keychainEncryption = true;
258+
}
259+
configBuilder.setKeychainEncryption(keychainEncryption);
260+
253261
if (iterableContextJSON.has("retryPolicy")) {
254262
JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy");
255263
int maxRetry = retryPolicyJson.getInt("maxRetry");

‎src/core/classes/Iterable.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ describe('Iterable', () => {
336336
expect(config.customActionHandler).toBe(undefined);
337337
expect(config.dataRegion).toBe(IterableDataRegion.US);
338338
expect(config.enableEmbeddedMessaging).toBe(false);
339+
expect(config.keychainEncryption).toBe(true);
339340
expect(config.encryptionEnforced).toBe(false);
340341
expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0);
341342
expect(config.inAppDisplayInterval).toBe(30.0);
@@ -355,6 +356,7 @@ describe('Iterable', () => {
355356
expect(configDict.customActionHandlerPresent).toBe(false);
356357
expect(configDict.dataRegion).toBe(IterableDataRegion.US);
357358
expect(configDict.enableEmbeddedMessaging).toBe(false);
359+
expect(configDict.keychainEncryption).toBe(true);
358360
expect(configDict.encryptionEnforced).toBe(false);
359361
expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0);
360362
expect(configDict.inAppDisplayInterval).toBe(30.0);
@@ -375,6 +377,23 @@ describe('Iterable', () => {
375377
expect(configDict.androidWakeDelayMs).toBe(1500);
376378
expect(configDict.authCallbackTimeoutMs).toBe(2500);
377379
});
380+
381+
it('should allow opting out of Android keychain encryption', () => {
382+
const config = new IterableConfig();
383+
config.keychainEncryption = false;
384+
const configDict = config.toDict();
385+
expect(config.keychainEncryption).toBe(false);
386+
expect(configDict.keychainEncryption).toBe(false);
387+
});
388+
389+
it('should keep keychainEncryption enabled when only deprecated encryptionEnforced is set', () => {
390+
const config = new IterableConfig();
391+
config.encryptionEnforced = true;
392+
const configDict = config.toDict();
393+
expect(config.keychainEncryption).toBe(true);
394+
expect(configDict.keychainEncryption).toBe(true);
395+
expect(configDict.encryptionEnforced).toBe(true);
396+
});
378397
});
379398

380399
describe('urlHandler', () => {

‎src/core/classes/IterableConfig.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ export class IterableConfig {
324324
pushPlatform: IterablePushPlatform = IterablePushPlatform.auto;
325325

326326
/**
327+
* Android only: whether Iterable encrypts PII (email, userId, auth token) in on-device keychain storage.
328+
* When `true` (the default), data is encrypted; when `false`, it is stored in plaintext.
329+
* iOS always uses the system Keychain and ignores this option.
330+
*/
331+
keychainEncryption = true;
332+
333+
/**
334+
* @deprecated Use {@link IterableConfig.keychainEncryption} instead. This field is still serialized for
335+
* back-compat on Android when `keychainEncryption` is absent from the bridge payload.
336+
*
327337
* Android only feature: This controls whether the SDK should enforce encryption for all PII stored on disk.
328338
* By default, the SDK will not enforce encryption and may fallback to unencrypted storage in case the encryption fails.
329339
*/
@@ -482,6 +492,7 @@ export class IterableConfig {
482492
useInMemoryStorageForInApps: this.useInMemoryStorageForInApps,
483493
dataRegion: this.dataRegion,
484494
pushPlatform: this.pushPlatform,
495+
keychainEncryption: this.keychainEncryption,
485496
encryptionEnforced: this.encryptionEnforced,
486497
retryPolicy: this.retryPolicy,
487498
enableEmbeddedMessaging: this.enableEmbeddedMessaging,

0 commit comments

Comments
 (0)