diff --git a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxMessageFragment.java b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxMessageFragment.java index a47b66ffb..ebf5d6e57 100644 --- a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxMessageFragment.java +++ b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxMessageFragment.java @@ -14,6 +14,7 @@ import com.iterable.iterableapi.IterableApi; import com.iterable.iterableapi.IterableInAppLocation; import com.iterable.iterableapi.IterableInAppMessage; +import com.iterable.iterableapi.IterableUtil; import com.iterable.iterableapi.ui.R; import java.util.List; @@ -76,7 +77,8 @@ private IterableInAppMessage getMessageById(String messageId) { private void loadMessage() { message = getMessageById(messageId); if (message != null) { - webView.loadDataWithBaseURL("", message.getContent().html, "text/html", "UTF-8", ""); + // Use configured base URL to enable CORS for external resources (e.g., custom fonts) + webView.loadDataWithBaseURL(IterableUtil.getWebViewBaseUrl(), message.getContent().html, "text/html", "UTF-8", ""); webView.setWebViewClient(webViewClient); if (!loaded) { IterableApi.getInstance().trackInAppOpen(message, IterableInAppLocation.INBOX); diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 4341c128e..94c1ef82b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -1824,6 +1824,7 @@ 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/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 2725c3de9..6e4bf7c45 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -140,6 +140,23 @@ public class IterableConfig { @Nullable final IterableAPIMobileFrameworkInfo mobileFrameworkInfo; + /** + * Base URL for Webview content loading. Specifically used to enable CORS for external resources. + * If null or empty, defaults to empty string (original behavior with about:blank origin). + * Set this to according to your CORS settings for example (e.g., "https://app.iterable.com") to allow external resource loading. + */ + @Nullable + final String webViewBaseUrl; + + /** + * Get the configured WebView base URL + * @return Base URL for WebView content, or null if not configured + */ + @Nullable + public String getWebViewBaseUrl() { + return webViewBaseUrl; + } + private IterableConfig(Builder builder) { pushIntegrationName = builder.pushIntegrationName; urlHandler = builder.urlHandler; @@ -165,6 +182,7 @@ private IterableConfig(Builder builder) { iterableUnknownUserHandler = builder.iterableUnknownUserHandler; decryptionFailureHandler = builder.decryptionFailureHandler; mobileFrameworkInfo = builder.mobileFrameworkInfo; + webViewBaseUrl = builder.webViewBaseUrl; } public static class Builder { @@ -192,6 +210,7 @@ public static class Builder { private int eventThresholdLimit = 100; private IterableIdentityResolution identityResolution = new IterableIdentityResolution(); private IterableUnknownUserHandler iterableUnknownUserHandler; + private String webViewBaseUrl; public Builder() {} @@ -434,9 +453,22 @@ public Builder setMobileFrameworkInfo(@NonNull IterableAPIMobileFrameworkInfo mo return this; } + /** + * Set the base URL for WebView content loading. Used to enable CORS for external resources. + * If not set or null, defaults to empty string (original behavior with about:blank origin). + * Set this according to your CORS settings (e.g., "https://app.iterable.com") to allow external resource loading. + * @param webViewBaseUrl Base URL for WebView content + */ + @NonNull + public Builder setWebViewBaseUrl(@Nullable String webViewBaseUrl) { + this.webViewBaseUrl = webViewBaseUrl; + return this; + } + @NonNull public IterableConfig build() { return new IterableConfig(this); } } + } \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtil.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtil.java index 593972257..f5b52a9c6 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtil.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtil.java @@ -88,4 +88,8 @@ static boolean writeFile(File file, String content) { static boolean isUrlOpenAllowed(@NonNull String url) { return instance.isUrlOpenAllowed(url); } + + public static String getWebViewBaseUrl() { + return instance.getWebViewBaseUrl(); + } } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtilImpl.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtilImpl.java index de8fd116a..dda8be076 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtilImpl.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableUtilImpl.java @@ -193,4 +193,21 @@ static boolean isUrlOpenAllowed(@NonNull String url) { return false; } + + /** + * Returns the configured WebView base URL for enabling CORS for external resources. + * If not configured, defaults to empty string (original behavior with about:blank origin). + * @return Base URL string or empty string if not configured + */ + static String getWebViewBaseUrl() { + try { + IterableConfig config = IterableApi.getInstance().config; + if (config != null && config.webViewBaseUrl != null) { + return config.webViewBaseUrl; + } + } catch (Exception e) { + IterableLogger.w(TAG, "Failed to get configured WebView baseURL, using empty default", e); + } + return ""; + } } \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebView.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebView.java index c5e72c226..424f6bf1b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebView.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebView.java @@ -43,7 +43,8 @@ void createWithHtml(IterableWebView.HTMLNotificationCallbacks notificationDialog // start loading the in-app // specifically use loadDataWithBaseURL and not loadData, as mentioned in https://stackoverflow.com/a/58181704/13111386 - loadDataWithBaseURL("", html, MIME_TYPE, ENCODING, ""); + // Use configured base URL to enable CORS for external resources (e.g., custom fonts) + loadDataWithBaseURL(IterableUtil.getWebViewBaseUrl(), html, MIME_TYPE, ENCODING, ""); } interface HTMLNotificationCallbacks { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt index c64476107..b017c66a6 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt @@ -1,6 +1,7 @@ package com.iterable.iterableapi import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.nullValue import org.junit.Assert.* import org.junit.Test @@ -21,6 +22,21 @@ class IterableConfigTest { assertThat(config.dataRegion, `is`(IterableDataRegion.EU)) } + @Test + fun defaultWebViewBaseUrl() { + val configBuilder: IterableConfig.Builder = IterableConfig.Builder() + val config: IterableConfig = configBuilder.build() + assertThat(config.webViewBaseUrl, `is`(nullValue())) + } + + @Test + fun setWebViewBaseUrl() { + val configBuilder: IterableConfig.Builder = IterableConfig.Builder() + .setWebViewBaseUrl("https://app.iterable.com") + val config: IterableConfig = configBuilder.build() + assertThat(config.webViewBaseUrl, `is`("https://app.iterable.com")) + } + @Test fun defaultDisableKeychainEncryption() { val configBuilder: IterableConfig.Builder = IterableConfig.Builder() diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebViewTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebViewTest.java new file mode 100644 index 000000000..6b7306ffb --- /dev/null +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebViewTest.java @@ -0,0 +1,232 @@ +package com.iterable.iterableapi; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class IterableWebViewTest extends BaseTest { + + private IterableWebView webView; + private IterableWebView webViewSpy; + + @Before + public void setUp() { + IterableTestUtils.createIterableApiNew(); + webView = new IterableWebView(getContext()); + webViewSpy = spy(webView); + } + + @After + public void tearDown() { + IterableTestUtils.resetIterableApi(); + } + + // ===== Base URL Configuration Tests ===== + + @Test + public void testGetWebViewBaseUrl_DefaultConfiguration() { + // Test: When webViewBaseUrl is not configured, should return empty string + String baseUrl = IterableUtil.getWebViewBaseUrl(); + assertEquals("Default webViewBaseUrl should be empty string", "", baseUrl); + } + + @Test + public void testGetWebViewBaseUrl_CustomConfiguration() { + // Test: When webViewBaseUrl is configured, should return the configured value + String customBaseUrl = "https://app.iterable.com"; + + IterableConfig config = new IterableConfig.Builder() + .setWebViewBaseUrl(customBaseUrl) + .build(); + + IterableApi.initialize(getContext(), "test-api-key", config); + + String baseUrl = IterableUtil.getWebViewBaseUrl(); + assertEquals("Custom webViewBaseUrl should be returned", customBaseUrl, baseUrl); + } + + @Test + public void testGetWebViewBaseUrl_EUConfiguration() { + // Test: EU region configuration + String euBaseUrl = "https://app.eu.iterable.com"; + + IterableConfig config = new IterableConfig.Builder() + .setWebViewBaseUrl(euBaseUrl) + .build(); + + IterableApi.initialize(getContext(), "test-api-key", config); + + String baseUrl = IterableUtil.getWebViewBaseUrl(); + assertEquals("EU webViewBaseUrl should be returned", euBaseUrl, baseUrl); + } + + @Test + public void testGetWebViewBaseUrl_NullConfiguration() { + // Test: When webViewBaseUrl is explicitly set to null, should return empty string + IterableConfig config = new IterableConfig.Builder() + .setWebViewBaseUrl(null) + .build(); + + IterableApi.initialize(getContext(), "test-api-key", config); + + String baseUrl = IterableUtil.getWebViewBaseUrl(); + assertEquals("Null webViewBaseUrl should return empty string", "", baseUrl); + } + + @Test + public void testGetWebViewBaseUrl_EmptyStringConfiguration() { + // Test: When webViewBaseUrl is explicitly set to empty string, should return empty string + IterableConfig config = new IterableConfig.Builder() + .setWebViewBaseUrl("") + .build(); + + IterableApi.initialize(getContext(), "test-api-key", config); + + String baseUrl = IterableUtil.getWebViewBaseUrl(); + assertEquals("Empty webViewBaseUrl should return empty string", "", baseUrl); + } + + @Test + public void testGetWebViewBaseUrl_ExceptionHandling() { + // Test: Exception handling when SDK is not initialized properly + IterableTestUtils.resetIterableApi(); + + // This should not throw an exception and should return empty string + String baseUrl = IterableUtil.getWebViewBaseUrl(); + assertEquals("Exception case should return empty string", "", baseUrl); + } + + // ===== WebView Integration Tests ===== + + @Test + public void testCreateWithHtml_DefaultConfiguration_UsesEmptyBaseUrl() { + // Test: WebView uses empty string as base URL when not configured (about:blank origin) + MockHTMLNotificationCallbacks mockCallbacks = new MockHTMLNotificationCallbacks(); + String testHtml = "
Test Content"; + + webViewSpy.createWithHtml(mockCallbacks, testHtml); + + // Verify loadDataWithBaseURL was called with empty string (default behavior) + ArgumentCaptor