Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Updates

- 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
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 @@ -21,8 +20,12 @@ public static String getAuthToken() {
return IterableApi.getInstance().getAuthToken();
}

public static JSONObject getInAppMessageJson(IterableInAppMessage message) {
return message.toJSONObject();
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,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
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package com.iterable.iterableapi;

import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockedStatic;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class RNIterableInternalTest {

@Test
Expand Down Expand Up @@ -38,4 +48,32 @@ public void trackPushOpenWithCampaignId_forwardsAppAlreadyRunningFalse() {
eq(10), eq(20), eq("message-id"), eq(false), isNull());
}
}

@Test
public void getInAppMessageJson_overwritesNativeIntegerJsonOnlyWithBooleanTrue()
throws JSONException {
IterableInAppMessage message = mock(IterableInAppMessage.class);
JSONObject nativeJson = new JSONObject();
nativeJson.put("jsonOnly", 1);
when(message.toJSONObject()).thenReturn(nativeJson);
when(message.isJsonOnly()).thenReturn(true);

JSONObject bridgeJson = RNIterableInternal.getInAppMessageJson(message);

assertTrue(bridgeJson.getBoolean("jsonOnly"));
}

@Test
public void getInAppMessageJson_writesBooleanFalseForHtmlMessages()
throws JSONException {
IterableInAppMessage message = mock(IterableInAppMessage.class);
JSONObject nativeJson = new JSONObject();
nativeJson.put("messageId", "msg-1");
when(message.toJSONObject()).thenReturn(nativeJson);
when(message.isJsonOnly()).thenReturn(false);

JSONObject bridgeJson = RNIterableInternal.getInAppMessageJson(message);

assertFalse(bridgeJson.getBoolean("jsonOnly"));
}
}
1 change: 1 addition & 0 deletions ios/RNIterableAPI/Serialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ extension IterableInAppMessage {
dict["customPayload"] = customPayload
dict["read"] = read
dict["priorityLevel"] = priorityLevel
dict["jsonOnly"] = jsonOnly

return dict
}
Expand Down
100 changes: 100 additions & 0 deletions src/__tests__/IterableInApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,106 @@ describe('Iterable In App', () => {
);
});

test('inAppHandler_jsonOnlyTrue_flagSetOnMessage', () => {
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(IterableEventName.handleInAppCalled);

const config = new IterableConfig();
config.inAppHandler = jest.fn((_message: IterableInAppMessage) => {
return IterableInAppShowResponse.show;
});

Iterable.initialize('apiKey', config);

const messageDict = {
messageId: 'json-only-msg',
campaignId: 1234,
trigger: { type: IterableInAppTriggerType.immediate },
priorityLevel: 300.5,
jsonOnly: true,
};
const expectedMessage = new IterableInAppMessage(
'json-only-msg',
1234,
new IterableInAppTrigger(IterableInAppTriggerType.immediate),
undefined,
undefined,
false,
undefined,
undefined,
false,
300.5,
true
);

nativeEmitter.emit(IterableEventName.handleInAppCalled, messageDict);

expect(config.inAppHandler).toBeCalledWith(expectedMessage);
});

test('inAppHandler_jsonOnlyFalse_flagSetOnHtmlMessage', () => {
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(IterableEventName.handleInAppCalled);

const config = new IterableConfig();
config.inAppHandler = jest.fn((_message: IterableInAppMessage) => {
return IterableInAppShowResponse.show;
});

Iterable.initialize('apiKey', config);

const messageDict = {
messageId: 'html-msg',
campaignId: 1234,
trigger: { type: IterableInAppTriggerType.immediate },
priorityLevel: 300.5,
jsonOnly: false,
};
const expectedMessage = new IterableInAppMessage(
'html-msg',
1234,
new IterableInAppTrigger(IterableInAppTriggerType.immediate),
undefined,
undefined,
false,
undefined,
undefined,
false,
300.5,
false
);

nativeEmitter.emit(IterableEventName.handleInAppCalled, messageDict);

expect(config.inAppHandler).toBeCalledWith(expectedMessage);
});

test('getMessages_jsonOnlyFlag_roundTripsFromBridgePayload', async () => {
const messageDicts = [
{
messageId: 'json-only',
campaignId: 1,
trigger: { type: IterableInAppTriggerType.immediate },
jsonOnly: true,
},
{
messageId: 'html',
campaignId: 2,
trigger: { type: IterableInAppTriggerType.immediate },
jsonOnly: false,
},
];
const messages = messageDicts.map((message) =>
IterableInAppMessage.fromDict(message)
);

MockRNIterableAPI.setMessages(messages);

const messagesObtained = await Iterable.inAppManager?.getMessages();
expect(messagesObtained?.[0]?.jsonOnly).toBe(true);
expect(messagesObtained?.[1]?.jsonOnly).toBe(false);
});

test('getMessages_noParams_returnsMessages', async () => {
// GIVEN a list of in-app messages representing the local queue
const messageDicts = [
Expand Down
26 changes: 26 additions & 0 deletions src/inApp/classes/IterableInAppMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,30 @@ describe('IterableInAppMessage.fromDict', () => {
expect(message.inboxMetadata).toBeUndefined();
});
});

describe('jsonOnly', () => {
it('parses jsonOnly true when present', () => {
const message = IterableInAppMessage.fromDict({
Comment thread
jferrao-itrbl marked this conversation as resolved.
...baseDict,
jsonOnly: true,
});

expect(message.jsonOnly).toBe(true);
});

it('parses jsonOnly false when present', () => {
const message = IterableInAppMessage.fromDict({
...baseDict,
jsonOnly: false,
});

expect(message.jsonOnly).toBe(false);
});

it('defaults jsonOnly to false when omitted', () => {
const message = IterableInAppMessage.fromDict({ ...baseDict });

expect(message.jsonOnly).toBe(false);
});
});
});
22 changes: 19 additions & 3 deletions src/inApp/classes/IterableInAppMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ export class IterableInAppMessage {
*/
readonly priorityLevel: number;

/**
* Whether this message is JSON-only (custom payload without Iterable HTML UI).
* For JSON-only messages, `inAppHandler` show/skip does not control display on native;
* use this flag to branch custom handling instead of inferring from `customPayload` alone.
*/
readonly jsonOnly: boolean;

/**
* Constructs an instance of IterableInAppMessage.
*
Expand All @@ -103,6 +110,7 @@ export class IterableInAppMessage {
* @param customPayload - A custom payload associated with the message.
* @param read - A boolean indicating whether the message has been read.
* @param priorityLevel - The priority level of the message.
* @param jsonOnly - Whether the message is JSON-only (defaults to `false`).
*/
constructor(
messageId: string,
Expand All @@ -114,7 +122,8 @@ export class IterableInAppMessage {
inboxMetadata: IterableInboxMetadata | undefined,
customPayload: unknown | undefined,
read: boolean,
priorityLevel: number
priorityLevel: number,
jsonOnly: boolean = false
Comment thread
jferrao-itrbl marked this conversation as resolved.
) {
this.campaignId = campaignId;
this.messageId = messageId;
Expand All @@ -126,6 +135,7 @@ export class IterableInAppMessage {
this.customPayload = customPayload;
this.read = read;
this.priorityLevel = priorityLevel;
this.jsonOnly = jsonOnly;
}

/**
Expand All @@ -147,7 +157,8 @@ export class IterableInAppMessage {
inAppMessage?.inboxMetadata,
inAppMessage?.customPayload,
inAppMessage?.read,
inAppMessage?.priorityLevel
inAppMessage?.priorityLevel,
inAppMessage?.jsonOnly ?? false
);
}

Expand Down Expand Up @@ -197,6 +208,10 @@ export class IterableInAppMessage {
const read = IterableUtil.readBoolean(dict, 'read');

const priorityLevel = dict.priorityLevel;
const jsonOnly = IterableUtil.readBoolean(
dict as Record<string, unknown>,
'jsonOnly'
);

return new IterableInAppMessage(
messageId,
Expand All @@ -212,7 +227,8 @@ export class IterableInAppMessage {
inboxMetadata,
customPayload,
read,
priorityLevel
priorityLevel,
jsonOnly
);
}
}
2 changes: 2 additions & 0 deletions src/inApp/types/IterableInAppMessageRaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ export type IterableInAppMessageRaw = {
read?: boolean;
/** The priority level of the message. */
priorityLevel?: number;
/** Whether the message is JSON-only (no HTML UI); show/skip in `inAppHandler` does not apply. */
jsonOnly?: boolean;
};
Loading