Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,21 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) {

/**
* Set the data region used by the SDK
* <p>
* 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
Loading