diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d40ce8f5..208ae1215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- `IterableDataRegion` now exposes stable cross-SDK identifiers and lookup helpers: `getRegionCode()` (e.g. `"EU"`), `getCode()` (`0` for US, `1` for EU, matching the React Native and Flutter SDKs), and the static `IterableDataRegion.from(String)` / `IterableDataRegion.from(int)` factories. `from(String)` accepts a region code in any case and also accepts a full API endpoint URL, so the iOS SDK's string-based data region values resolve without translation. **No action required** — the existing `setDataRegion(IterableDataRegion.EU)` API is unchanged. + +### Fixed +- An unrecognised data region no longer resolves silently. `IterableDataRegion.from(...)` and `setDataRegion(...)` log a warning naming the supported values before falling back to `US`, so a misconfigured region surfaces in the logs instead of quietly routing EU-destined data to the US data center. `setDataRegion(null)` now falls back to `US` with a warning instead of leaving the region unset. + +### Deprecated +- `IterableConstants.BASE_URL_API` and `IterableConstants.BASE_URL_LINKS` — both are hardcoded to the US data region and are unused by the SDK, which resolves its endpoint from the configured `IterableDataRegion`. Use `IterableDataRegion.getEndpoint()` instead. They still resolve to the same values, so no action is required in this release. **They will be removed in 3.12.0** — if you reference either constant, switch to `IterableDataRegion.getEndpoint()` before upgrading to that version. ## [3.10.0] ### Added diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index d9e6b2542..a2f3fdd2c 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -362,10 +362,21 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) { /** * Set the data region used by the SDK + *
+ * To resolve a region from a string or numeric identifier (for example when bridging from a + * cross-platform wrapper), use {@link IterableDataRegion#from(String)} or + * {@link IterableDataRegion#from(int)}, which fall back to + * {@link IterableDataRegion#US} and log on unrecognised values. + * * @param dataRegion enum value that determines which endpoint to use, defaults to IterableDataRegion.US */ @NonNull public Builder setDataRegion(@NonNull IterableDataRegion dataRegion) { + if (dataRegion == null) { + IterableLogger.w("IterableConfig", "setDataRegion received null, defaulting to " + IterableDataRegion.US.getRegionCode()); + this.dataRegion = IterableDataRegion.US; + return this; + } this.dataRegion = dataRegion; return this; } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index 6c896f6d7..403c12a05 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -11,7 +11,18 @@ public final class IterableConstants { public static final String ACTION_PUSH_REGISTRATION = "com.iterable.push.ACTION_PUSH_REGISTRATION"; //Hosts + /** + * @deprecated These constants are hardcoded to the US data region and are unused by the SDK. + * Use {@link IterableDataRegion#getEndpoint()} instead, which respects the configured region. + * Scheduled for removal in 3.12.0. + */ + @Deprecated public static final String BASE_URL_API = "https://api.iterable.com/api/"; + /** + * @deprecated Unused by the SDK and hardcoded to the US data region. + * Scheduled for removal in 3.12.0. + */ + @Deprecated public static final String BASE_URL_LINKS = "https://links.iterable.com/"; //API Fields diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java index aec57c0f2..5fb051265 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java @@ -1,16 +1,109 @@ package com.iterable.iterableapi; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Data region determining which data center and endpoints the SDK sends data to. + * Defaults to {@link #US}; EU-hosted projects must select {@link #EU}. + */ public enum IterableDataRegion { - US("https://api.iterable.com/api/"), - EU("https://api.eu.iterable.com/api/"); + US(0, "US", "https://api.iterable.com/api/"), + EU(1, "EU", "https://api.eu.iterable.com/api/"); + + private static final String TAG = "IterableDataRegion"; + private final int code; + private final String regionCode; private final String endpoint; - IterableDataRegion(String endpoint) { + IterableDataRegion(int code, String regionCode, String endpoint) { + this.code = code; + this.regionCode = regionCode; this.endpoint = endpoint; } public String getEndpoint() { return this.endpoint; } + + /** + * Stable numeric identifier for this region, matching the values used by the React Native and + * Flutter SDKs. Unlike {@link #ordinal()} this is guaranteed not to shift if regions are added. + */ + public int getCode() { + return this.code; + } + + /** + * Stable short identifier for this region (for example {@code "EU"}), matching the values used + * by Iterable's other SDKs. + */ + @NonNull + public String getRegionCode() { + return this.regionCode; + } + + /** + * Resolves a region from its short identifier (e.g. {@code "EU"}), accepting either case. Also + * accepts a full API endpoint URL, so values from the iOS SDK's string-based data region can be + * passed through unchanged. + *
+ * Unrecognised or null values fall back to {@link #US} and are logged, matching the behaviour of + * Iterable's other SDKs. + * + * @param value region identifier, or {@code null} + * @return the matching region, or {@link #US} if the value is not recognised + */ + @NonNull + public static IterableDataRegion from(@Nullable String value) { + if (value == null || value.trim().isEmpty()) { + IterableLogger.w(TAG, "No data region specified, defaulting to " + US.regionCode); + return US; + } + + String normalized = value.trim(); + for (IterableDataRegion region : values()) { + if (normalized.equalsIgnoreCase(region.regionCode) || normalized.equalsIgnoreCase(region.endpoint)) { + return region; + } + } + + IterableLogger.w(TAG, "Unsupported data region \"" + value + "\", defaulting to " + US.regionCode + + ". Supported values: " + supportedRegionCodes()); + return US; + } + + /** + * Resolves a region from its numeric identifier, as used by the React Native and Flutter SDKs. + *
+ * Unrecognised values fall back to {@link #US} and are logged, matching the behaviour of + * Iterable's other SDKs. + * + * @param code region identifier, see {@link #getCode()} + * @return the matching region, or {@link #US} if the code is not recognised + */ + @NonNull + public static IterableDataRegion from(int code) { + for (IterableDataRegion region : values()) { + if (region.code == code) { + return region; + } + } + + IterableLogger.w(TAG, "Unsupported data region code " + code + ", defaulting to " + US.regionCode + + ". Supported values: " + supportedRegionCodes()); + return US; + } + + private static String supportedRegionCodes() { + StringBuilder builder = new StringBuilder(); + for (IterableDataRegion region : values()) { + if (builder.length() > 0) { + builder.append(", "); + } + builder.append(region.regionCode).append(" (").append(region.code).append(")"); + } + return builder.toString(); + } } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java index 33ecddd3f..d92f5825b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java @@ -87,9 +87,6 @@ static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiReque String baseUrl = getBaseUrl(); try { - if (overrideUrl != null && !overrideUrl.isEmpty()) { - baseUrl = overrideUrl; - } if (iterableApiRequest.requestType == IterableApiRequest.GET) { Uri.Builder builder = Uri.parse(baseUrl + iterableApiRequest.resourcePath).buildUpon(); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt index b017c66a6..2ac9803f5 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt @@ -21,6 +21,16 @@ class IterableConfigTest { val config: IterableConfig = configBuilder.build() assertThat(config.dataRegion, `is`(IterableDataRegion.EU)) } + + /** Only reachable from Java, where the `@NonNull` parameter can still be passed null. */ + @Test + fun nullDataRegionFallsBackToUs() { + val builder = IterableConfig.Builder() + val setter = IterableConfig.Builder::class.java + .getMethod("setDataRegion", IterableDataRegion::class.java) + setter.invoke(builder, null) + assertThat(builder.build().dataRegion, `is`(IterableDataRegion.US)) + } @Test fun defaultWebViewBaseUrl() { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt new file mode 100644 index 000000000..03499b0b7 --- /dev/null +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt @@ -0,0 +1,81 @@ +package com.iterable.iterableapi + +import org.hamcrest.Matchers.`is` +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThat +import org.junit.Test + +class IterableDataRegionTest { + + @Test + fun endpointsMatchDataCenters() { + assertEquals("https://api.iterable.com/api/", IterableDataRegion.US.endpoint) + assertEquals("https://api.eu.iterable.com/api/", IterableDataRegion.EU.endpoint) + } + + /** These identifiers are part of the cross-SDK contract; changing them breaks the wrappers. */ + @Test + fun stableIdentifiersMatchOtherSdks() { + assertEquals(0, IterableDataRegion.US.code) + assertEquals(1, IterableDataRegion.EU.code) + assertEquals("US", IterableDataRegion.US.regionCode) + assertEquals("EU", IterableDataRegion.EU.regionCode) + } + + @Test + fun fromRegionCode() { + assertThat(IterableDataRegion.from("US"), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from("EU"), `is`(IterableDataRegion.EU)) + } + + @Test + fun fromRegionCodeIsCaseAndWhitespaceInsensitive() { + assertThat(IterableDataRegion.from("eu"), `is`(IterableDataRegion.EU)) + assertThat(IterableDataRegion.from("Eu"), `is`(IterableDataRegion.EU)) + assertThat(IterableDataRegion.from(" EU "), `is`(IterableDataRegion.EU)) + } + + /** iOS expresses the data region as the endpoint URL itself, so those values must resolve. */ + @Test + fun fromIosStyleEndpointUrl() { + assertThat( + IterableDataRegion.from("https://api.eu.iterable.com/api/"), + `is`(IterableDataRegion.EU) + ) + assertThat( + IterableDataRegion.from("https://api.iterable.com/api/"), + `is`(IterableDataRegion.US) + ) + } + + @Test + fun fromUnsupportedStringFallsBackToUs() { + assertThat(IterableDataRegion.from("APAC"), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(""), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(" "), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(null as String?), `is`(IterableDataRegion.US)) + } + + @Test + fun fromCode() { + assertThat(IterableDataRegion.from(0), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(1), `is`(IterableDataRegion.EU)) + } + + @Test + fun fromUnsupportedCodeFallsBackToUs() { + assertThat(IterableDataRegion.from(2), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(-1), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(Int.MAX_VALUE), `is`(IterableDataRegion.US)) + } + + /** Round-trips guard the wrappers, which serialize the region and resolve it back. */ + @Test + fun identifiersRoundTrip() { + for (region in IterableDataRegion.values()) { + assertThat(IterableDataRegion.from(region.code), `is`(region)) + assertThat(IterableDataRegion.from(region.regionCode), `is`(region)) + assertThat(IterableDataRegion.from(region.endpoint), `is`(region)) + } + } +}