diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..da48b763c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +tab_width = 4 \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index ff23fd388..3ffe98b0e 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -398,6 +398,11 @@ private void onForeground() { fetchRemoteConfiguration(); } + if (_applicationContext == null || sharedInstance.getMainActivityContext() == null) { + IterableLogger.w(TAG, "onForeground: _applicationContext is null"); + return; + } + boolean systemNotificationEnabled = NotificationManagerCompat.from(_applicationContext).areNotificationsEnabled(); SharedPreferences sharedPref = sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); @@ -1435,4 +1440,4 @@ public void trackEmbeddedSession(@NonNull IterableEmbeddedSession session) { apiClient.trackEmbeddedSession(session); } //endregion -} +} \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index d6763e1de..2ef39a467 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -221,6 +221,7 @@ public final class IterableConstants { public static final String ITERABLE_IN_APP_BUTTONS = "buttons"; public static final String ITERABLE_IN_APP_COLOR = "color"; public static final String ITERABLE_IN_APP_CONTENT = "content"; + public static final String ITERABLE_IN_APP_JSON_ONLY = "jsonOnly"; public static final String ITERABLE_IN_APP_COUNT = "count"; public static final String ITERABLE_IN_APP_MAIN_IMAGE = "mainImage"; public static final String ITERABLE_IN_APP_MESSAGE = "inAppMessages"; diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppDisplayer.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppDisplayer.java index 99a31cbf8..66dd34792 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppDisplayer.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppDisplayer.java @@ -20,6 +20,11 @@ boolean isShowingInApp() { } boolean showMessage(@NonNull IterableInAppMessage message, IterableInAppLocation location, @NonNull final IterableHelper.IterableUrlCallback clickCallback) { + // Early return for JSON-only messages + if (message.isJsonOnly()) { + return false; + } + Activity currentActivity = activityMonitor.getCurrentActivity(); // Prevent double display if (currentActivity != null) { diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppManager.java index 3e073bc8e..9a8589baf 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppManager.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppManager.java @@ -405,7 +405,6 @@ private void processMessages() { IterableLogger.printInfo(); List messages = getMessages(); - List messagesByPriorityLevel = getMessagesSortedByPriorityLevel(messages); for (IterableInAppMessage message : messagesByPriorityLevel) { @@ -414,6 +413,14 @@ private void processMessages() { InAppResponse response = handler.onNewInApp(message); IterableLogger.d(TAG, "Response: " + response); message.setProcessed(true); + + if (message.isJsonOnly()) { + setRead(message, true, null, null); + message.setConsumed(true); + api.inAppConsume(message, null, null, null, null); + return; + } + if (response == InAppResponse.SHOW) { boolean consume = !message.isInboxMessage(); showMessage(message, consume, null); @@ -519,4 +526,4 @@ public void run() { } }); } -} +} \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppMessage.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppMessage.java index 2d94ccab4..6b948a4b1 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppMessage.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppMessage.java @@ -31,6 +31,7 @@ public class IterableInAppMessage { private boolean loadedHtmlFromJson = false; private boolean markedForDeletion = false; private @Nullable IterableInAppStorage inAppStorageInterface; + private final boolean jsonOnly; IterableInAppMessage(@NonNull String messageId, @NonNull Content content, @@ -41,7 +42,8 @@ public class IterableInAppMessage { @NonNull Double priorityLevel, @Nullable Boolean saveToInbox, @Nullable InboxMetadata inboxMetadata, - @Nullable Long campaignId) { + @Nullable Long campaignId, + boolean jsonOnly) { this.messageId = messageId; this.content = content; @@ -50,9 +52,10 @@ public class IterableInAppMessage { this.expiresAt = expiresAt; this.trigger = trigger; this.priorityLevel = priorityLevel; - this.saveToInbox = saveToInbox; + this.saveToInbox = saveToInbox != null ? (saveToInbox && !jsonOnly) : null; this.inboxMetadata = inboxMetadata; this.campaignId = campaignId; + this.jsonOnly = jsonOnly; } static class Trigger { @@ -246,7 +249,7 @@ public Date getExpiresAt() { @NonNull public Content getContent() { - if (content.html == null) { + if (content.html == null && !jsonOnly) { content.html = inAppStorageInterface.getHTML(messageId); } return content; @@ -322,59 +325,75 @@ public void markForDeletion(boolean delete) { this.markedForDeletion = delete; } + public boolean isJsonOnly() { + return jsonOnly; + } + static IterableInAppMessage fromJSONObject(@NonNull JSONObject messageJson, @Nullable IterableInAppStorage storageInterface) { if (messageJson == null) { return null; } - JSONObject contentJson = messageJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_CONTENT); - if (contentJson == null) { - return null; - } - String messageId = messageJson.optString(IterableConstants.KEY_MESSAGE_ID); final Long campaignId = IterableUtil.retrieveValidCampaignIdOrNull(messageJson, IterableConstants.KEY_CAMPAIGN_ID); + boolean jsonOnly = messageJson.optBoolean(IterableConstants.ITERABLE_IN_APP_JSON_ONLY, false); + + JSONObject customPayload = messageJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_CUSTOM_PAYLOAD); + if (customPayload == null && jsonOnly) { + customPayload = new JSONObject(); + } + + Content content; + if (jsonOnly) { + content = new Content("", new Rect(), 0.0, false, new InAppDisplaySettings(false, new InAppBgColor(null, 0.0f))); + } else { + JSONObject contentJson = messageJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_CONTENT); + if (contentJson == null) { + return null; + } + if (customPayload == null) { + customPayload = contentJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_LEGACY_PAYLOAD); + } + + String html = contentJson.optString(IterableConstants.ITERABLE_IN_APP_HTML, null); + JSONObject inAppDisplaySettingsJson = contentJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_DISPLAY_SETTINGS); + Rect padding = getPaddingFromPayload(inAppDisplaySettingsJson); + double backgroundAlpha = contentJson.optDouble(IterableConstants.ITERABLE_IN_APP_BACKGROUND_ALPHA, 0); + boolean shouldAnimate = inAppDisplaySettingsJson.optBoolean(IterableConstants.ITERABLE_IN_APP_SHOULD_ANIMATE, false); + JSONObject bgColorJson = inAppDisplaySettingsJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_BGCOLOR); + + String bgColorInHex = null; + double bgAlpha = 0.0f; + if (bgColorJson != null) { + bgColorInHex = bgColorJson.optString(IterableConstants.ITERABLE_IN_APP_BGCOLOR_HEX); + bgAlpha = bgColorJson.optDouble(IterableConstants.ITERABLE_IN_APP_BGCOLOR_ALPHA); + } + + InAppDisplaySettings inAppDisplaySettings = new InAppDisplaySettings(shouldAnimate, new InAppBgColor(bgColorInHex, bgAlpha)); + content = new Content(html, padding, backgroundAlpha, shouldAnimate, inAppDisplaySettings); + } + long createdAtLong = messageJson.optLong(IterableConstants.ITERABLE_IN_APP_CREATED_AT); Date createdAt = createdAtLong != 0 ? new Date(createdAtLong) : null; long expiresAtLong = messageJson.optLong(IterableConstants.ITERABLE_IN_APP_EXPIRES_AT); Date expiresAt = expiresAtLong != 0 ? new Date(expiresAtLong) : null; - String html = contentJson.optString(IterableConstants.ITERABLE_IN_APP_HTML, null); - JSONObject inAppDisplaySettingsJson = contentJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_DISPLAY_SETTINGS); - Rect padding = getPaddingFromPayload(inAppDisplaySettingsJson); - double backgroundAlpha = contentJson.optDouble(IterableConstants.ITERABLE_IN_APP_BACKGROUND_ALPHA, 0); - boolean shouldAnimate = inAppDisplaySettingsJson.optBoolean(IterableConstants.ITERABLE_IN_APP_SHOULD_ANIMATE, false); - JSONObject bgColorJson = inAppDisplaySettingsJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_BGCOLOR); - - String bgColorInHex = null; - double bgAlpha = 0.0f; - if (bgColorJson != null) { - bgColorInHex = bgColorJson.optString(IterableConstants.ITERABLE_IN_APP_BGCOLOR_HEX); - bgAlpha = bgColorJson.optDouble(IterableConstants.ITERABLE_IN_APP_BGCOLOR_ALPHA); - } - - InAppDisplaySettings inAppDisplaySettings = new InAppDisplaySettings(shouldAnimate, new InAppBgColor(bgColorInHex, bgAlpha)); JSONObject triggerJson = messageJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_TRIGGER); Trigger trigger = Trigger.fromJSONObject(triggerJson); - JSONObject customPayload = messageJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_CUSTOM_PAYLOAD); - if (customPayload == null) { - customPayload = contentJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_LEGACY_PAYLOAD); - } - if (customPayload == null) { - customPayload = new JSONObject(); - } - double priorityLevel = messageJson.optDouble(IterableConstants.ITERABLE_IN_APP_PRIORITY_LEVEL, IterableConstants.ITERABLE_IN_APP_PRIORITY_LEVEL_UNASSIGNED); + double priorityLevel = messageJson.optDouble(IterableConstants.ITERABLE_IN_APP_PRIORITY_LEVEL, + IterableConstants.ITERABLE_IN_APP_PRIORITY_LEVEL_UNASSIGNED); + + Boolean saveToInbox = messageJson.has(IterableConstants.ITERABLE_IN_APP_SAVE_TO_INBOX) ? + messageJson.optBoolean(IterableConstants.ITERABLE_IN_APP_SAVE_TO_INBOX) : null; - Boolean saveToInbox = messageJson.has(IterableConstants.ITERABLE_IN_APP_SAVE_TO_INBOX) ? messageJson.optBoolean(IterableConstants.ITERABLE_IN_APP_SAVE_TO_INBOX) : null; JSONObject inboxPayloadJson = messageJson.optJSONObject(IterableConstants.ITERABLE_IN_APP_INBOX_METADATA); InboxMetadata inboxMetadata = InboxMetadata.fromJSONObject(inboxPayloadJson); - IterableInAppMessage message = new IterableInAppMessage( messageId, - new Content(html, padding, backgroundAlpha, shouldAnimate, inAppDisplaySettings), + content, customPayload, createdAt, expiresAt, @@ -382,10 +401,11 @@ static IterableInAppMessage fromJSONObject(@NonNull JSONObject messageJson, @Nul priorityLevel, saveToInbox, inboxMetadata, - campaignId); + campaignId, + jsonOnly); message.inAppStorageInterface = storageInterface; - if (html != null) { + if (!jsonOnly && content.html != null && !content.html.isEmpty()) { message.setLoadedHtmlFromJson(true); } message.processed = messageJson.optBoolean(IterableConstants.ITERABLE_IN_APP_PROCESSED, false); @@ -410,6 +430,9 @@ JSONObject toJSONObject() { if (expiresAt != null) { messageJson.putOpt(IterableConstants.ITERABLE_IN_APP_EXPIRES_AT, expiresAt.getTime()); } + if (jsonOnly) { + messageJson.put(IterableConstants.ITERABLE_IN_APP_JSON_ONLY, 1); + } messageJson.putOpt(IterableConstants.ITERABLE_IN_APP_TRIGGER, trigger.toJSONObject()); @@ -435,7 +458,7 @@ JSONObject toJSONObject() { messageJson.putOpt(IterableConstants.ITERABLE_IN_APP_CUSTOM_PAYLOAD, customPayload); if (saveToInbox != null) { - messageJson.putOpt(IterableConstants.ITERABLE_IN_APP_SAVE_TO_INBOX, saveToInbox); + messageJson.putOpt(IterableConstants.ITERABLE_IN_APP_SAVE_TO_INBOX, saveToInbox && !jsonOnly); } if (inboxMetadata != null) { messageJson.putOpt(IterableConstants.ITERABLE_IN_APP_INBOX_METADATA, inboxMetadata.toJSONObject()); @@ -472,6 +495,9 @@ private void onChanged() { * @return */ static Rect getPaddingFromPayload(JSONObject paddingOptions) { + if (paddingOptions == null) { + return new Rect(0, 0, 0, 0); + } Rect rect = new Rect(); rect.top = decodePadding(paddingOptions.optJSONObject("top")); rect.left = decodePadding(paddingOptions.optJSONObject("left")); @@ -529,4 +555,4 @@ static JSONObject encodePadding(int padding) throws JSONException { } return paddingJson; } -} +} \ No newline at end of file diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/InAppTestUtils.java b/iterableapi/src/test/java/com/iterable/iterableapi/InAppTestUtils.java index 27410daf0..cabeaba12 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/InAppTestUtils.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/InAppTestUtils.java @@ -30,7 +30,8 @@ public static IterableInAppMessage getTestInboxInAppWithId(String messageId) { new Double(300.5), true, null, - null + null, + false ); } } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java index e646236b8..baf937434 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java @@ -397,7 +397,23 @@ public void testAuthTokenPresentInRequest() throws Exception { public void testAuthFailureReturns401() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); dispatcher.enqueueResponse("/events/inAppConsume", new MockResponse().setResponseCode(401).setBody("{\"code\": \"InvalidJwtPayload\"}")); - IterableApi.getInstance().inAppConsume(new IterableInAppMessage("asd", null, null, null, null, null, 0.0, null, null, (long) 2), null, null); + IterableApi.getInstance().inAppConsume( + new IterableInAppMessage( + "asd", // messageId + null, // content + null, // customPayload + null, // createdAt + null, // expiresAt + null, // trigger + 0.0, // priorityLevel + null, // saveToInbox + null, // inboxMetadata + 2L, // campaignId + false // jsonOnly - since this is a test message, not a JSON message + ), + null, // urlHandler + null // customActionHandler + ); Robolectric.flushForegroundThreadScheduler(); assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT); } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java index c39de0b63..98bdee405 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java @@ -48,6 +48,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; +import static org.mockito.ArgumentMatchers.argThat; +import static org.junit.Assert.assertNotNull; public class IterableInAppManagerTest extends BaseTest { @@ -406,6 +408,387 @@ public void testMessagePersistentReadStateFromServer() throws Exception { assertTrue(inboxMessages.get(0).isRead()); } + @Test + public void testJsonOnlyMessageProcessing() throws Exception { + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(createJsonOnlyPayload())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + // First sync to get messages + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + // Bring app to foreground to trigger processing of immediate messages + ActivityController activityController = Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify immediate trigger message was processed + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(IterableInAppMessage.class); + verify(inAppHandler).onNewInApp(messageCaptor.capture()); + + IterableInAppMessage message = messageCaptor.getValue(); + assertEquals("message1", message.getMessageId()); + assertEquals("immediate", message.getCustomPayload().getString("key")); + assertTrue(message.isJsonOnly()); + + // Verify never trigger message was not processed + verify(inAppHandler, never()).onNewInApp(argThat(msg -> + msg.getMessageId().equals("message2"))); + } + + @Test + public void testJsonOnlyMessageDisplay() throws Exception { + // Create payload with only a never-trigger message + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "never")) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message1"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + + // Create InAppManager with mock displayer + IterableInAppDisplayer mockDisplayer = mock(IterableInAppDisplayer.class); + IterableInAppManager inAppManager = spy(new IterableInAppManager( + IterableApi.sharedInstance, + new IterableDefaultInAppHandler(), + 30.0, + new IterableInAppMemoryStorage(), + IterableActivityMonitor.getInstance(), + mockDisplayer)); + IterableApi.sharedInstance = new IterableApi(inAppManager); + + // Process messages + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + // Only the "never" trigger message should remain in queue + assertEquals(1, inAppManager.getMessages().size()); + + // Verify no messages were displayed + verify(mockDisplayer, never()).showMessage( + any(IterableInAppMessage.class), + any(IterableInAppLocation.class), + any(IterableHelper.IterableUrlCallback.class)); + } + + @Test + public void testJsonOnlyMessageInboxBehavior() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", true) + .put("jsonOnly", true) + .put("customPayload", new JSONObject() + .put("key", "value")) + .put("inboxMetadata", new JSONObject() + .put("title", "Test Title") + .put("subtitle", "Test Subtitle")) + .put("messageId", "message1"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + // Directly sync messages since we're testing inbox state rather than trigger behavior + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + // Verify message is not in inbox + List inboxMessages = inAppManager.getInboxMessages(); + assertEquals(0, inboxMessages.size()); + } + + @Test + public void testJsonOnlyMessageContentBehavior() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("jsonOnly", true) + .put("customPayload", new JSONObject() + .put("key", "customValue")) + .put("content", new JSONObject() + .put("payload", new JSONObject() + .put("key", "contentValue"))) + .put("trigger", new JSONObject().put("type", "immediate")) + .put("messageId", "message1"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + // First sync to get messages + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + // Bring app to foreground to trigger immediate message + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify customPayload is used instead of content.payload + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(IterableInAppMessage.class); + verify(inAppHandler).onNewInApp(messageCaptor.capture()); + assertEquals("customValue", messageCaptor.getValue().getCustomPayload().getString("key")); + } + + @Test + public void testJsonOnlyInAppMessageParsing() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("messageType", "Mobile") + .put("typeOfContent", "Static") + .put("customPayload", new JSONObject() + .put("key1", "value1") + .put("key2", 42) + .put("key3", new JSONObject().put("nested", true))) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message1") + .put("campaignId", 1))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + List messages = inAppManager.getMessages(); + assertEquals(1, messages.size()); + + IterableInAppMessage message = messages.get(0); + assertEquals("value1", message.getCustomPayload().getString("key1")); + assertEquals(42, message.getCustomPayload().getInt("key2")); + assertTrue(message.getCustomPayload().getJSONObject("key3").getBoolean("nested")); + } + + @Test + public void testJsonOnlyInAppMessageDelegateCallbacks() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "immediate")) + .put("trigger", new JSONObject().put("type", "immediate")) + .put("messageId", "message1")) + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "never")) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message2"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + + // Create InAppManager with mock handler + final IterableInAppHandler inAppHandler = mock(IterableInAppHandler.class); + IterableInAppManager inAppManager = spy(new IterableInAppManager( + IterableApi.sharedInstance, + inAppHandler, + 30.0, + new IterableInAppMemoryStorage(), + IterableActivityMonitor.getInstance(), + mock(IterableInAppDisplayer.class))); + IterableApi.sharedInstance = new IterableApi(inAppManager); + + // First sync to get messages + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + // Process messages by bringing app to foreground + ActivityController activityController = Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify immediate trigger message was processed + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(IterableInAppMessage.class); + verify(inAppHandler).onNewInApp(messageCaptor.capture()); + assertEquals("immediate", messageCaptor.getValue().getCustomPayload().getString("key")); + assertEquals("message1", messageCaptor.getValue().getMessageId()); + + // Verify never trigger message was not processed + verify(inAppHandler, never()).onNewInApp(argThat(msg -> + msg.getMessageId().equals("message2"))); + } + + @Test + public void testJsonOnlyInAppMessageWithoutCustomPayload() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("messageType", "Mobile") + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message1") + .put("campaignId", 1))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + List messages = inAppManager.getMessages(); + assertEquals(1, messages.size()); + + // In Android, when customPayload is not provided, an empty JSONObject is created + JSONObject customPayload = messages.get(0).getCustomPayload(); + assertNotNull("Custom payload should not be null", customPayload); + assertEquals("Custom payload should be empty when not provided", 0, customPayload.length()); + } + + @Test + public void testJsonOnlyMessageWithEmptyPayload() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject()) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message1"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + List messages = inAppManager.getMessages(); + assertEquals(1, messages.size()); + assertEquals("Custom payload should be empty", 0, messages.get(0).getCustomPayload().length()); + } + + @Test + public void testJsonOnlyMessageCannotBeSavedToInbox() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", true) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "value")) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message1") + .put("inboxMetadata", new JSONObject() + .put("title", "JSON Message") + .put("subtitle", "Test Subtitle") + .put("icon", "test-icon.png")))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + List inboxMessages = inAppManager.getInboxMessages(); + assertEquals(0, inboxMessages.size()); + } + + @Test + public void testJsonOnlyMessageIgnoresContentPayload() throws Exception { + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject() + .put("key", "customValue")) + .put("content", new JSONObject() + .put("payload", new JSONObject() + .put("key", "contentValue"))) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message1"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + List messages = inAppManager.getMessages(); + assertEquals(1, messages.size()); + assertEquals("customValue", messages.get(0).getCustomPayload().getString("key")); + } + + @Test + public void testJsonOnlyInAppMessageProcessingAndDisplay() throws Exception { + // Create payload similar to iOS test + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject() + .put("key", "value")) + .put("content", new JSONObject() + .put("html", "") + .put("inAppDisplaySettings", new JSONObject() + .put("left", new JSONObject().put("percentage", 0)) + .put("top", new JSONObject().put("percentage", 0)) + .put("right", new JSONObject().put("percentage", 0)) + .put("bottom", new JSONObject().put("percentage", 0)))) + .put("trigger", new JSONObject().put("type", "immediate")) + .put("messageId", "message1") + .put("campaignId", 1))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + + // Create InAppManager with mock handler and displayer + IterableInAppDisplayer mockDisplayer = mock(IterableInAppDisplayer.class); + final IterableInAppHandler inAppHandler = mock(IterableInAppHandler.class); + + IterableInAppManager inAppManager = spy(new IterableInAppManager( + IterableApi.sharedInstance, + inAppHandler, + 30.0, + new IterableInAppMemoryStorage(), + IterableActivityMonitor.getInstance(), + mockDisplayer)); + IterableApi.sharedInstance = new IterableApi(inAppManager); + + // First sync to get messages + inAppManager.syncInApp(); + shadowOf(getMainLooper()).idle(); + + // Process messages by bringing app to foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify handler was called with correct message + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(IterableInAppMessage.class); + verify(inAppHandler).onNewInApp(messageCaptor.capture()); + assertEquals("value", messageCaptor.getValue().getCustomPayload().getString("key")); + + // Verify displayer was never called + verify(mockDisplayer, never()).showMessage( + any(IterableInAppMessage.class), + any(IterableInAppLocation.class), + any(IterableHelper.IterableUrlCallback.class)); + + // Verify message was consumed (not in queue) + assertEquals(0, inAppManager.getMessages().size()); + } + + private String createJsonOnlyPayload() throws JSONException { + return new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() // Immediate trigger + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "immediate")) + .put("trigger", new JSONObject().put("type", "immediate")) + .put("messageId", "message1")) + .put(new JSONObject() // Never trigger + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "never")) + .put("trigger", new JSONObject().put("type", "never")) + .put("messageId", "message2"))) + .toString(); + } + private static class IterableSkipInAppHandler implements IterableInAppHandler { @NonNull @Override @@ -413,4 +796,42 @@ public InAppResponse onNewInApp(@NonNull IterableInAppMessage message) { return InAppResponse.SKIP; } } + + @Test + public void testJsonOnlyMessageConsume() throws Exception { + // Create payload with a JSON-only message + JSONObject payload = new JSONObject() + .put("inAppMessages", new JSONArray() + .put(new JSONObject() + .put("saveToInbox", false) + .put("jsonOnly", true) + .put("customPayload", new JSONObject().put("key", "value")) + .put("trigger", new JSONObject().put("type", "immediate")) + .put("messageId", "message1"))); + + dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); + + // Create InAppManager with spied IterableApi + IterableApi spyApi = spy(IterableApi.sharedInstance); + IterableInAppManager inAppManager = new IterableInAppManager( + spyApi, + new IterableDefaultInAppHandler(), + 30.0, + new IterableInAppMemoryStorage(), + IterableActivityMonitor.getInstance(), + mock(IterableInAppDisplayer.class)); + + // Process messages by bringing app to foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify inAppConsume was called with the correct parameters + verify(spyApi).inAppConsume( + argThat(message -> message.getMessageId().equals("message1")), + eq(null), + eq(null), + eq(null), + eq(null) + ); + } }