Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c77afd4
docs(SDK-549): document logout() as native logoutUser equivalent
jferrao-itrbl Sep 12, 2026
1ecb939
feat(SDK-550): expose disableDeviceForAllUsers on the RN SDK
jferrao-itrbl Sep 15, 2026
8c0841b
fix(SDK-550): log Android disableDeviceForAllUsers no-op in Metro
jferrao-itrbl Sep 21, 2026
ecb2a17
Merge pull request #901 from Iterable/feature/SDK-550-disable-device-…
jferrao-itrbl Sep 22, 2026
09db5e4
docs(SDK-549): clarify iOS public logoutUser is not bridged
jferrao-itrbl Sep 22, 2026
31d59d2
Merge pull request #900 from Iterable/feature/SDK-549-logout-user-parity
jferrao-itrbl Sep 22, 2026
a8faf4a
Merge branch 'master' into feature/SDK-548-feature-parity
jferrao-itrbl Sep 24, 2026
24cf9ad
feat(SDK-555): wire Android keychain encryption config
jferrao-itrbl Sep 24, 2026
910818e
docs(SDK-555): clarify deprecated encryption docs and precedence test
jferrao-itrbl Sep 24, 2026
785f73e
feat(SDK-557): bridge Android decryption failure handler to JS
jferrao-itrbl Sep 24, 2026
828f419
fix(SDK-557): declare decryption event on iOS supportedEvents
jferrao-itrbl Sep 24, 2026
62b6a85
feat(SDK-559): expose Iterable.authManager.getAuthToken()
jferrao-itrbl Sep 24, 2026
3470f54
feat(SDK-749): bridge embedded messaging sync callbacks
jferrao-itrbl Sep 27, 2026
d0ffb1d
test(SDK-749): cover sync callback gating and independence
jferrao-itrbl Sep 27, 2026
c14cc78
refactor(SDK-555): simplify keychain encryption bridge wiring
jferrao-itrbl Sep 28, 2026
92973bf
Merge pull request #905 from Iterable/feature/SDK-555-keychain-encryp…
jferrao-itrbl Sep 28, 2026
a8f09f0
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-557-de…
jferrao-itrbl Sep 29, 2026
ffcd73f
fix(SDK-557): always wire Android decryption bridge handler
jferrao-itrbl Sep 30, 2026
abc50de
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-559-ge…
jferrao-itrbl Sep 30, 2026
34a55a9
Merge pull request #906 from Iterable/feature/SDK-557-decryption-fail…
jferrao-itrbl Sep 30, 2026
9bb08d4
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-559-ge…
jferrao-itrbl Sep 30, 2026
f0b1080
Merge pull request #907 from Iterable/feature/SDK-559-get-auth-token
jferrao-itrbl Sep 30, 2026
663908e
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-749-su…
jferrao-itrbl Oct 1, 2026
ccc3f0a
fix(SDK-748): forward appAlreadyRunning on Android push open
jferrao-itrbl Oct 1, 2026
7e4ce35
test(SDK-748): assert Android forwards appAlreadyRunning
jferrao-itrbl Oct 1, 2026
d4973d0
chore: gitignore local .artifacts directory
jferrao-itrbl Oct 1, 2026
41df363
fix(SDK-748): use empty embedded sync handler overrides
jferrao-itrbl Oct 1, 2026
81fe6c8
feat(SDK-750): expose jsonOnly on in-app message payloads
jferrao-itrbl Oct 1, 2026
fe0fe08
fix(SDK-750): drop no-op jsonOnly put catch on Android
jferrao-itrbl Oct 1, 2026
7602034
fix(SDK-750): declare JSONException for jsonOnly bridge put
jferrao-itrbl Oct 1, 2026
70024a9
feat(embedded): add placement IDs to syncMessages (SDK-751)
jferrao-itrbl Oct 1, 2026
f840cb0
test(embedded): cover null and empty syncMessages paths (SDK-751)
jferrao-itrbl Oct 1, 2026
9a4b3ab
Merge pull request #908 from Iterable/feature/SDK-749-success-failure…
jferrao-itrbl Oct 2, 2026
aaa9f4b
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-748-fo…
jferrao-itrbl Oct 5, 2026
87f9d09
docs(SDK-748): tighten changelog; reuse test:android:unit in CI
jferrao-itrbl Oct 6, 2026
8d2ec19
Merge pull request #912 from Iterable/feature/SDK-748-forward-app-alr…
jferrao-itrbl Oct 6, 2026
acc5808
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-750-ex…
jferrao-itrbl Oct 7, 2026
a1ec1e9
Merge branch 'master' into feature/SDK-751-sync-messages-placement-ids
jferrao-itrbl Oct 8, 2026
2083a1e
Merge branch 'master' into feature/SDK-750-expose-json-only
jferrao-itrbl Oct 8, 2026
68b0540
Merge branch 'master' into feature/SDK-548-feature-parity
jferrao-itrbl Oct 8, 2026
441d758
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-751-sy…
jferrao-itrbl Oct 8, 2026
5b10201
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-750-ex…
jferrao-itrbl Oct 8, 2026
e4a5253
Merge branch 'master' into feature/SDK-548-feature-parity
jferrao-itrbl Oct 12, 2026
fb03834
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-750-ex…
jferrao-itrbl Oct 12, 2026
29ec5df
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-751-sy…
jferrao-itrbl Oct 12, 2026
0530d83
test(SDK-750): cover jsonOnly native bridge serialization
jferrao-itrbl Oct 12, 2026
86228fc
docs(embedded): document Android scoped sync caveat (SDK-798)
jferrao-itrbl Oct 12, 2026
ac86101
Merge pull request #913 from Iterable/feature/SDK-750-expose-json-only
jferrao-itrbl Oct 12, 2026
76eb311
Merge branch 'feature/SDK-548-feature-parity' into feature/SDK-751-sy…
jferrao-itrbl Oct 12, 2026
07cdfba
Merge pull request #914 from Iterable/feature/SDK-751-sync-messages-p…
jferrao-itrbl Oct 12, 2026
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ jobs:
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"

- name: Run Android library unit tests
if: env.turbo_cache_hit != 1
run: yarn test:android:unit

build-ios:
runs-on: macos-15
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ docs/

# Agents
.agent/
.artifacts/
.claude/
.cursor/
.opencode/
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
## Unreleased

### Fixes

- Fixed `Iterable.trackPushOpenWithCampaignId` on Android so the `appAlreadyRunning` argument is forwarded to the native SDK instead of always being sent as `false`.

### Updates

- Added optional placement IDs to `Iterable.embeddedManager.syncMessages()` so apps can sync embedded messages for specific placements only (SDK-751). When omitted, null, or empty, behavior matches the previous sync-all call.
- **Android:** Scoped sync (`syncMessages([id, …])`) matches the pinned iterableapi behavior and may remove cached messages for placements not included in the sync response (SDK-798). iOS preserves unrequested placements. Prefer `syncMessages()` without placement IDs on Android until the native fix ships and the Android SDK pin is bumped.
- Added `IterableInAppMessage.jsonOnly` so `inAppHandler` and in-app message fetches can distinguish JSON-only messages from HTML in-apps (SDK-750).
- Added `IterableConfig.onEmbeddedMessagingSyncSucceeded` and
`IterableConfig.onEmbeddedMessagingSyncFailed` so apps can observe embedded
messaging sync outcomes (SDK-749). Failure callbacks receive a nullable reason
string forwarded from the native SDKs. On iOS, subscription-inactive or
invalid API key failures may invoke both `onEmbeddedMessagingDisabled` and
`onEmbeddedMessagingSyncFailed`; on Android only
`onEmbeddedMessagingDisabled` is invoked for those cases (native SDK
behavior).
- Added `Iterable.authManager.getAuthToken()` to read the JWT currently held by the native SDK (SDK-559).
- iOS: `IterableAPI.authToken`.
- Android: `IterableApi.getAuthToken()`.
- Added `IterableConfig.decryptionFailureHandler` (Android only) to notify the app when native keychain decryption fails (SDK-557). The SDK clears stored PII and disables encryption for the device before invoking the callback; iOS has no equivalent native API and ignores this option.
- Added `IterableConfig.keychainEncryption` (default `true`, Android only) so React Native apps can control native `IterableConfig.Builder.setKeychainEncryption`.
- Deprecated `IterableConfig.encryptionEnforced`, which never had an effect.
- Added `Iterable.disableDeviceForAllUsers()` to unregister this device's push token from every user associated with the device (SDK-550).
- iOS: forwards to native `IterableAPI.disableDeviceForAllUsers()`.
- 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.
## 3.2.1

### Fixes
Expand Down
8 changes: 8 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}

testOptions {
unitTests.all {
testLogging {
exceptionFormat "full"
}
}
}

sourceSets {
main {
if (isNewArchitectureEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

import androidx.annotation.Nullable;

import org.json.JSONException;
import org.json.JSONObject;

public class RNIterableInternal {

private static String TAG = "RNIterableInternal";

public static String getEmail() {
return IterableApi.getInstance().getEmail();
}
Expand All @@ -16,16 +15,31 @@ public static String getUserId() {
return IterableApi.getInstance().getUserId();
}

public static JSONObject getInAppMessageJson(IterableInAppMessage message) {
return message.toJSONObject();
@Nullable
public static String getAuthToken() {
return IterableApi.getInstance().getAuthToken();
}

public static JSONObject getInAppMessageJson(IterableInAppMessage message)
throws JSONException {
JSONObject messageJson = message.toJSONObject();
// Overwrite native serialization (may use integer 1) so the bridge sends a boolean for JS.
messageJson.put("jsonOnly", message.isJsonOnly());
return messageJson;
}

public static IterableInAppMessage getMessageById(String messageId) {
return IterableApi.getInstance().getInAppManager().getMessageById(messageId);
}

public static void trackPushOpenWithCampaignId(Integer campaignId, Integer templateId, String messageId, @Nullable JSONObject dataFields) {
IterableApi.getInstance().trackPushOpen(campaignId, templateId, messageId, dataFields);
public static void trackPushOpenWithCampaignId(
Integer campaignId,
Integer templateId,
String messageId,
boolean appAlreadyRunning,
@Nullable JSONObject dataFields) {
IterableApi.getInstance().trackPushOpen(
campaignId, templateId, messageId, appAlreadyRunning, dataFields);
}

public static void setAttributionInfo(IterableAttributionInfo attributionInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.iterable.iterableapi.IterableAuthManager;
import com.iterable.iterableapi.IterableConfig;
import com.iterable.iterableapi.IterableCustomActionHandler;
import com.iterable.iterableapi.IterableDecryptionFailureHandler;
import com.iterable.iterableapi.IterableEmbeddedMessage;
import com.iterable.iterableapi.IterableEmbeddedUpdateHandler;
import com.iterable.iterableapi.IterableHelper;
Expand All @@ -53,7 +54,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableInAppManager.Listener, IterableEmbeddedUpdateHandler {
public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableDecryptionFailureHandler, IterableInAppManager.Listener, IterableEmbeddedUpdateHandler {
public static final String NAME = "RNIterableAPI";

private static String TAG = "RNIterableAPIModule";
Expand Down Expand Up @@ -93,6 +94,10 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S
configBuilder.setAuthHandler(this);
}

// IterableKeychain is created once with config.decryptionFailureHandler; the JS
// listener remains conditional on decryptionFailureHandlerPresent.
configBuilder.setDecryptionFailureHandler(this);

// Check if embedded messaging is enabled before building config
boolean enableEmbeddedMessaging = configReadableMap.hasKey("enableEmbeddedMessaging") && configReadableMap.getBoolean("enableEmbeddedMessaging");

Expand Down Expand Up @@ -161,6 +166,9 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap,
configBuilder.setAuthHandler(this);
}

// See initializeWithApiKey — keychain caches this handler at first construction.
configBuilder.setDecryptionFailureHandler(this);

// NOTE: There does not seem to be a way to set the API endpoint
// override in the Android SDK. Check with @Ayyanchira and @evantk91 to
// see what the best approach is.
Expand Down Expand Up @@ -249,6 +257,10 @@ public void getUserId(Promise promise) {
promise.resolve(RNIterableInternal.getUserId());
}

public void getAuthToken(Promise promise) {
promise.resolve(RNIterableInternal.getAuthToken());
}

public void trackEvent(String name, @Nullable ReadableMap dataFields) {
IterableLogger.v(TAG, "trackEvent");
IterableApi.getInstance().track(name, optSerializedDataFields(dataFields));
Expand All @@ -265,7 +277,12 @@ public void trackPurchase(double total, ReadableArray items, @Nullable ReadableM
}

public void trackPushOpenWithCampaignId(double campaignId, @Nullable Double templateId, String messageId, boolean appAlreadyRunning, @Nullable ReadableMap dataFields) {
RNIterableInternal.trackPushOpenWithCampaignId((int) campaignId, templateId != null ? templateId.intValue() : null, messageId, optSerializedDataFields(dataFields));
RNIterableInternal.trackPushOpenWithCampaignId(
(int) campaignId,
templateId != null ? templateId.intValue() : null,
messageId,
appAlreadyRunning,
optSerializedDataFields(dataFields));
}

public void updateSubscriptions(@Nullable ReadableArray emailListIds, @Nullable ReadableArray unsubscribedChannelIds, @Nullable ReadableArray unsubscribedMessageTypeIds, @Nullable ReadableArray subscribedMessageTypeIds, double campaignId, double templateId) {
Expand Down Expand Up @@ -391,6 +408,10 @@ public void disableDeviceForCurrentUser() {
IterableApi.getInstance().disablePush();
}

public void disableDeviceForAllUsers() {
IterableLogger.w(TAG, "disableDeviceForAllUsers is not supported on Android; use disableDeviceForCurrentUser. There is no public native equivalent.");
}

public void registerDeviceToken(String token) {
IterableLogger.v(TAG, "registerDeviceToken");
IterableApi.getInstance().registerDeviceToken(token);
Expand Down Expand Up @@ -628,9 +649,8 @@ public boolean handleIterableCustomAction(@NonNull IterableAction action, @NonNu
public IterableInAppHandler.InAppResponse onNewInApp(@NonNull IterableInAppMessage message) {
IterableLogger.printInfo();

JSONObject messageJson = RNIterableInternal.getInAppMessageJson(message);

try {
JSONObject messageJson = RNIterableInternal.getInAppMessageJson(message);
WritableMap eventData = Serialization.convertJsonToMap(messageJson);
jsCallBackLatch = new CountDownLatch(1);
sendEvent(EventName.handleInAppCalled.name(), eventData);
Expand Down Expand Up @@ -677,6 +697,24 @@ public String onAuthTokenRequested() {
}
}

private static final String DECRYPTION_FAILURE_DEFAULT_MESSAGE = "Decryption failed";

@Override
public void onDecryptionFailed(Exception exception) {
JSONObject messageJson = new JSONObject();
try {
String message = exception != null ? exception.getMessage() : null;
if (message == null || message.isEmpty()) {
message = DECRYPTION_FAILURE_DEFAULT_MESSAGE;
}
messageJson.put("message", message);
WritableMap eventData = Serialization.convertJsonToMap(messageJson);
sendEvent(EventName.handleDecryptionFailureCalled.name(), eventData);
} catch (JSONException e) {
IterableLogger.e(TAG, "Failed to send decryption failure event");
}
}

@Override
public void onAuthFailure(AuthFailure authFailure) {
// Create a JSON object for the authFailure object
Expand Down Expand Up @@ -744,21 +782,33 @@ public void onEmbeddedMessagingDisabled() {
@Override
public void onEmbeddedMessagingSyncSucceeded() {
IterableLogger.d(TAG, "onEmbeddedMessagingSyncSucceeded");
sendEvent(EventName.handleEmbeddedMessagingSyncSucceededCalled.name(), null);
}

@Override
public void onEmbeddedMessagingSyncFailed(@Nullable String reason) {
IterableLogger.d(TAG, "onEmbeddedMessagingSyncFailed: " + reason);
sendEvent(EventName.handleEmbeddedMessagingSyncFailedCalled.name(), reason);
}
// ---------------------------------------------------------------------------------------
// endregion

// ---------------------------------------------------------------------------------------
// region Embedded messaging

public void syncEmbeddedMessages() {
IterableLogger.d(TAG, "syncEmbeddedMessages");
IterableApi.getInstance().getEmbeddedManager().syncMessages();
public void syncEmbeddedMessages(@Nullable ReadableArray placementIds) {
IterableLogger.d(TAG, "syncEmbeddedMessages for placements: " + placementIds);

if (placementIds == null || placementIds.size() == 0) {
IterableApi.getInstance().getEmbeddedManager().syncMessages();
} else {
IterableLogger.w(TAG, "Scoped syncEmbeddedMessages may remove cached messages for placements not in the sync response (SDK-798). Prefer syncMessages() without placement IDs on Android until iterableapi is fixed.");
Long[] ids = new Long[placementIds.size()];
for (int i = 0; i < placementIds.size(); i++) {
ids[i] = (long) placementIds.getInt(i);
}
IterableApi.getInstance().getEmbeddedManager().syncMessages(ids);
}
}

public void startEmbeddedSession() {
Expand Down Expand Up @@ -839,8 +889,11 @@ enum EventName {
handleAuthFailureCalled,
handleAuthSuccessCalled,
handleCustomActionCalled,
handleDecryptionFailureCalled,
handleEmbeddedMessageUpdateCalled,
handleEmbeddedMessagingDisabledCalled,
handleEmbeddedMessagingSyncSucceededCalled,
handleEmbeddedMessagingSyncFailedCalled,
handleInAppCalled,
handleUrlCalled,
receivedIterableEmbeddedMessagesChanged,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ static JSONObject messageContentToJsonObject(IterableInAppMessage.Content conten
return messageContent;
}

static JSONArray serializeInAppMessages(List<IterableInAppMessage> inAppMessages) {
static JSONArray serializeInAppMessages(List<IterableInAppMessage> inAppMessages)
throws JSONException {
JSONArray inAppMessagesJson = new JSONArray();
for (IterableInAppMessage message : inAppMessages) {
JSONObject messageJson = RNIterableInternal.getInAppMessageJson(message);
Expand Down Expand Up @@ -250,6 +251,9 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte
configBuilder.setEnableEmbeddedMessaging(iterableContextJSON.optBoolean("enableEmbeddedMessaging"));
}

configBuilder.setKeychainEncryption(
iterableContextJSON.optBoolean("keychainEncryption", true));

if (iterableContextJSON.has("retryPolicy")) {
JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy");
int maxRetry = retryPolicyJson.getInt("maxRetry");
Expand Down
14 changes: 12 additions & 2 deletions android/src/newarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public void getUserId(Promise promise) {
moduleImpl.getUserId(promise);
}

@Override
public void getAuthToken(Promise promise) {
moduleImpl.getAuthToken(promise);
}

@Override
public void trackEvent(String name, @Nullable ReadableMap dataFields) {
moduleImpl.trackEvent(name, dataFields);
Expand Down Expand Up @@ -137,6 +142,11 @@ public void disableDeviceForCurrentUser() {
moduleImpl.disableDeviceForCurrentUser();
}

@Override
public void disableDeviceForAllUsers() {
moduleImpl.disableDeviceForAllUsers();
}

@Override
public void registerDeviceToken(String token) {
moduleImpl.registerDeviceToken(token);
Expand Down Expand Up @@ -230,8 +240,8 @@ public void pauseAuthRetries(boolean pauseRetry) {
}

@Override
public void syncEmbeddedMessages() {
moduleImpl.syncEmbeddedMessages();
public void syncEmbeddedMessages(@Nullable ReadableArray placementIds) {
moduleImpl.syncEmbeddedMessages(placementIds);
}

@Override
Expand Down
14 changes: 12 additions & 2 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public void getUserId(Promise promise) {
moduleImpl.getUserId(promise);
}

@ReactMethod
public void getAuthToken(Promise promise) {
moduleImpl.getAuthToken(promise);
}

@ReactMethod
public void trackEvent(String name, @Nullable ReadableMap dataFields) {
moduleImpl.trackEvent(name, dataFields);
Expand Down Expand Up @@ -138,6 +143,11 @@ public void disableDeviceForCurrentUser() {
moduleImpl.disableDeviceForCurrentUser();
}

@ReactMethod
public void disableDeviceForAllUsers() {
moduleImpl.disableDeviceForAllUsers();
}

@ReactMethod
public void registerDeviceToken(String token) {
moduleImpl.registerDeviceToken(token);
Expand Down Expand Up @@ -234,8 +244,8 @@ public void pauseAuthRetries(boolean pauseRetry) {
}

@ReactMethod
public void syncEmbeddedMessages() {
moduleImpl.syncEmbeddedMessages();
public void syncEmbeddedMessages(@Nullable ReadableArray placementIds) {
moduleImpl.syncEmbeddedMessages(placementIds);
}

@ReactMethod
Expand Down
Loading
Loading