From 126d2d3406d8d02be4be6e6d63ffb060f4a8106e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Thu, 9 Oct 2025 11:36:18 +0200 Subject: [PATCH 1/2] implement SPInternalFlags and add geoOverride to DefaultRequest --- .../sourcepoint/mobile_core/Coordinator.kt | 15 ++++++--- .../mobile_core/models/SPInternalFlags.kt | 6 ++++ .../mobile_core/network/SourcepointClient.kt | 32 ++++++++++++------- .../network/requests/DefaultRequest.kt | 2 +- 4 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPInternalFlags.kt diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt index ced525f1..c35f4b9e 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt @@ -20,6 +20,7 @@ import com.sourcepoint.mobile_core.models.SPCampaignType.Preferences import com.sourcepoint.mobile_core.models.SPCampaigns import com.sourcepoint.mobile_core.models.SPError import com.sourcepoint.mobile_core.models.SPIDFAStatus +import com.sourcepoint.mobile_core.models.SPInternalFlags import com.sourcepoint.mobile_core.models.SPMessageLanguage import com.sourcepoint.mobile_core.models.SPPropertyName import com.sourcepoint.mobile_core.models.consents.CCPAConsent @@ -64,10 +65,12 @@ class Coordinator( private val campaigns: SPCampaigns, private val repository: Repository = Repository(), private val timeoutInSeconds: Int = 5, + private val internalFlags: SPInternalFlags = SPInternalFlags(), private val spClient: SPClient = SourcepointClient( accountId = accountId, propertyId = propertyId, - requestTimeoutInSeconds = timeoutInSeconds + requestTimeoutInSeconds = timeoutInSeconds, + internalFlags = internalFlags ), internal var state: State = repository.state ?: State(accountId = accountId, propertyId = propertyId), private var authId: String? = state.authId, @@ -141,13 +144,15 @@ class Coordinator( propertyName: SPPropertyName, campaigns: SPCampaigns, timeoutInSeconds: Int = 5, + internalFlags: SPInternalFlags = SPInternalFlags() ): this( accountId = accountId, propertyId = propertyId, propertyName = propertyName, campaigns = campaigns, repository = Repository(), - timeoutInSeconds = timeoutInSeconds + timeoutInSeconds = timeoutInSeconds, + internalFlags = internalFlags ) @Suppress("Unused") @@ -157,14 +162,16 @@ class Coordinator( propertyName: SPPropertyName, campaigns: SPCampaigns, timeoutInSeconds: Int = 5, - state: State? = null + state: State? = null, + internalFlags: SPInternalFlags = SPInternalFlags() ): this( accountId = accountId, propertyId = propertyId, propertyName = propertyName, campaigns = campaigns, state = state ?: Repository().state ?: State(accountId = accountId, propertyId = propertyId), - timeoutInSeconds = timeoutInSeconds + timeoutInSeconds = timeoutInSeconds, + internalFlags = internalFlags ) init { diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPInternalFlags.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPInternalFlags.kt new file mode 100644 index 00000000..b949aee8 --- /dev/null +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPInternalFlags.kt @@ -0,0 +1,6 @@ +package com.sourcepoint.mobile_core.models + +data class SPInternalFlags( + val usePreprod: Boolean = false, + val geoOverride: String? = null, +) diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt index 4a93cd79..8aba856e 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt @@ -11,6 +11,7 @@ import com.sourcepoint.mobile_core.models.SPCampaignType import com.sourcepoint.mobile_core.models.SPClientTimeout import com.sourcepoint.mobile_core.models.SPError import com.sourcepoint.mobile_core.models.SPIDFAStatus +import com.sourcepoint.mobile_core.models.SPInternalFlags import com.sourcepoint.mobile_core.models.SPNetworkError import com.sourcepoint.mobile_core.models.SPUnableToParseBodyError import com.sourcepoint.mobile_core.models.consents.GDPRConsent @@ -151,7 +152,8 @@ class SourcepointClient( private val propertyId: Int, httpEngine: HttpClientEngine?, private val device: DeviceInformation, - private val requestTimeoutInSeconds: Int + private val requestTimeoutInSeconds: Int, + private val internalFlags: SPInternalFlags ): SPClient { private val config: HttpClientConfig<*>.() -> Unit = { install(HttpTimeout) { requestTimeoutMillis = requestTimeoutInSeconds.toLong() * 1000 } @@ -180,12 +182,18 @@ class SourcepointClient( } private val http = if (httpEngine != null) HttpClient(httpEngine, config) else HttpClient(config) - constructor(accountId: Int, propertyId: Int, requestTimeoutInSeconds: Int = 5) : this( - accountId, - propertyId, + constructor( + accountId: Int, + propertyId: Int, + requestTimeoutInSeconds: Int = 5, + internalFlags: SPInternalFlags = SPInternalFlags() + ) : this( + accountId = accountId, + propertyId = propertyId, httpEngine = null, device = DeviceInformation(), - requestTimeoutInSeconds = requestTimeoutInSeconds + requestTimeoutInSeconds = requestTimeoutInSeconds, + internalFlags = internalFlags ) constructor( @@ -193,12 +201,14 @@ class SourcepointClient( propertyId: Int, httpEngine: HttpClientEngine, requestTimeoutInSeconds: Int = 5, + internalFlags: SPInternalFlags = SPInternalFlags() ) : this( accountId, propertyId, httpEngine = httpEngine, device = DeviceInformation(), - requestTimeoutInSeconds = requestTimeoutInSeconds + requestTimeoutInSeconds = requestTimeoutInSeconds, + internalFlags = internalFlags ) private val baseWrapperUrl = "https://cdn.privacy-mgmt.com/" @@ -223,7 +233,7 @@ class SourcepointClient( executeAPIRequest(PV_DATA) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "v2", "pv-data") - withParams(DefaultRequest()) + withParams(DefaultRequest(geoOverride = internalFlags.geoOverride)) }.build()) { contentType(ContentType.Application.Json) setBody(request) @@ -256,7 +266,7 @@ class SourcepointClient( executeAPIRequest(endpoint) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "v2", "choice", fromCampaign, actionType.type.toString()) - withParams(DefaultRequest()) + withParams(DefaultRequest(geoOverride = internalFlags.geoOverride)) }.build()) { contentType(ContentType.Application.Json) setBody(request) @@ -316,7 +326,7 @@ class SourcepointClient( executeAPIRequest(IDFA_STATUS) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "metrics", "v1", "apple-tracking") - withParams(DefaultRequest()) + withParams(DefaultRequest(geoOverride = internalFlags.geoOverride)) }.build()) { contentType(ContentType.Application.Json) setBody( @@ -348,7 +358,7 @@ class SourcepointClient( executeAPIRequest(CUSTOM_CONSENT) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "tcfv2", "v1", "gdpr", "custom-consent") - withParams(DefaultRequest()) + withParams(DefaultRequest(geoOverride = internalFlags.geoOverride)) }.build()) { contentType(ContentType.Application.Json) setBody( @@ -392,7 +402,7 @@ class SourcepointClient( executeAPIRequest(ERROR_METRICS) { http.post(URLBuilder(baseWrapperUrl).apply { path("wrapper", "metrics", "v1", "custom-metrics") - withParams(DefaultRequest()) + withParams(DefaultRequest(geoOverride = internalFlags.geoOverride)) }.build()) { contentType(ContentType.Application.Json) setBody( diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/requests/DefaultRequest.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/requests/DefaultRequest.kt index 227720a3..1b8acaee 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/requests/DefaultRequest.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/requests/DefaultRequest.kt @@ -5,7 +5,7 @@ import com.sourcepoint.mobile_core.DeviceInformation import kotlinx.serialization.Serializable @Serializable -open class DefaultRequest { +open class DefaultRequest(val geoOverride: String? = null) { val env = "prod" val scriptType = "mobile-core-${DeviceInformation().osName.name}" val scriptVersion = BuildConfig.Version From ddedeee69e02951049bae4cc89d58fc1b800fca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Thu, 9 Oct 2025 11:36:32 +0200 Subject: [PATCH 2/2] implement getUsnatLocation api call --- .../sourcepoint/mobile_core/Coordinator.kt | 2 ++ .../sourcepoint/mobile_core/ICoordinator.kt | 4 +++ .../sourcepoint/mobile_core/models/SPError.kt | 1 + .../mobile_core/models/SPMessageLanguage.kt | 1 - .../mobile_core/network/SourcepointClient.kt | 12 +++++++++ .../responses/UsnatLocationResponse.kt | 10 ++++++++ .../mobile_core/CoordinatorTest.kt | 19 ++++++++++++-- .../mobile_core/mocks/SPClientMock.kt | 5 ++++ .../network/SourcepointClientTest.kt | 25 +++++++++++++------ 9 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/responses/UsnatLocationResponse.kt diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt index c35f4b9e..8a3e27ce 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/Coordinator.kt @@ -1122,4 +1122,6 @@ class Coordinator( override fun setTranslateMessage(value: Boolean) { includeData.translateMessage = value } + + override suspend fun getUsnatLocation() = spClient.getUsnatLocation() } diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/ICoordinator.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/ICoordinator.kt index 7d00d9a1..aecff977 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/ICoordinator.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/ICoordinator.kt @@ -12,6 +12,7 @@ import com.sourcepoint.mobile_core.models.SPMessageLanguage import com.sourcepoint.mobile_core.models.SPNetworkError import com.sourcepoint.mobile_core.models.SPUnknownNetworkError import com.sourcepoint.mobile_core.models.consents.SPUserData +import com.sourcepoint.mobile_core.network.responses.UsnatLocationResponse import kotlinx.serialization.json.JsonObject import kotlin.coroutines.cancellation.CancellationException @@ -58,4 +59,7 @@ interface ICoordinator { fun clearLocalData() fun setTranslateMessage(value: Boolean) + + @Throws(CancellationException::class) + suspend fun getUsnatLocation(): UsnatLocationResponse } diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt index 88c24244..9a708288 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPError.kt @@ -108,5 +108,6 @@ enum class InvalidAPICode(val type: String) { GDPR_PRIVACY_MANAGER("_GDPR-privacy-manager"), CCPA_MESSAGE("_CCPA-message"), GDPR_MESSAGE("_GDPR-message"), + USNAT_LOCATION(type = "_usnat_location"), EMPTY("") } diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPMessageLanguage.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPMessageLanguage.kt index b6a688d5..cf10807a 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPMessageLanguage.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/models/SPMessageLanguage.kt @@ -1,4 +1,3 @@ -@file:Suppress("unused") package com.sourcepoint.mobile_core.models import kotlinx.serialization.KSerializer diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt index 8aba856e..3b0b7689 100644 --- a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/SourcepointClient.kt @@ -40,6 +40,7 @@ import com.sourcepoint.mobile_core.network.responses.MetaDataResponse import com.sourcepoint.mobile_core.network.responses.PreferencesChoiceResponse import com.sourcepoint.mobile_core.network.responses.PvDataResponse import com.sourcepoint.mobile_core.network.responses.USNatChoiceResponse +import com.sourcepoint.mobile_core.network.responses.UsnatLocationResponse import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.call.body @@ -115,6 +116,9 @@ interface SPClient { @Throws(SPNetworkError::class, SPUnableToParseBodyError::class, CancellationException::class, HttpRequestTimeoutException::class) suspend fun getMessages(request: MessagesRequest): MessagesResponse + @Throws(SPNetworkError::class, SPUnableToParseBodyError::class, CancellationException::class, HttpRequestTimeoutException::class) + suspend fun getUsnatLocation(): UsnatLocationResponse + suspend fun postReportIdfaStatus( propertyId: Int?, uuid: String?, @@ -313,6 +317,14 @@ class SourcepointClient( }.build()).bodyOr(::reportErrorAndThrow) } + override suspend fun getUsnatLocation(): UsnatLocationResponse = + executeAPIRequest(USNAT_LOCATION) { + http.get(URLBuilder(baseWrapperUrl).apply { + path("usnat", "admin", "location") + withParams(DefaultRequest(geoOverride = internalFlags.geoOverride)) + }.build()).bodyOr(::reportErrorAndThrow) + } + override suspend fun postReportIdfaStatus( propertyId: Int?, uuid: String?, diff --git a/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/responses/UsnatLocationResponse.kt b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/responses/UsnatLocationResponse.kt new file mode 100644 index 00000000..89617d37 --- /dev/null +++ b/core/src/commonMain/kotlin/com/sourcepoint/mobile_core/network/responses/UsnatLocationResponse.kt @@ -0,0 +1,10 @@ +package com.sourcepoint.mobile_core.network.responses + +import kotlinx.serialization.Serializable + +@Serializable +data class UsnatLocationResponse( + val countryCode: String? = null, + val stateCode: String? = null, + val regionCode: String? = null +) diff --git a/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/CoordinatorTest.kt b/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/CoordinatorTest.kt index bd2db022..0109be46 100644 --- a/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/CoordinatorTest.kt +++ b/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/CoordinatorTest.kt @@ -19,6 +19,7 @@ import com.sourcepoint.mobile_core.models.SPActionType.* import com.sourcepoint.mobile_core.models.SPCampaign import com.sourcepoint.mobile_core.models.SPCampaignType.* import com.sourcepoint.mobile_core.models.SPCampaigns +import com.sourcepoint.mobile_core.models.SPInternalFlags import com.sourcepoint.mobile_core.models.SPMessageLanguage import com.sourcepoint.mobile_core.models.SPMessageLanguage.ENGLISH import com.sourcepoint.mobile_core.models.SPPropertyName @@ -468,7 +469,7 @@ class CoordinatorTest { sampleRate = 1.0f, additionsChangeDate = consents.gdpr!!.consents!!.dateCreated + 1.days, legalBasisChangeDate = originalMetaData.gdpr!!.legalBasisChangeDate, - vendorListId = originalMetaData.gdpr!!.vendorListId, + vendorListId = originalMetaData.gdpr.vendorListId, )) } ) @@ -491,7 +492,7 @@ class CoordinatorTest { sampleRate = 1.0f, additionsChangeDate = originalMetaData.gdpr!!.additionsChangeDate, legalBasisChangeDate = consents.gdpr!!.consents!!.dateCreated + 1.days, - vendorListId = originalMetaData.gdpr!!.vendorListId, + vendorListId = originalMetaData.gdpr.vendorListId, )) } ) @@ -631,4 +632,18 @@ class CoordinatorTest { coordinator.loadMessages() assertTrue(pvDataCalled) } + + @Test + fun getUsnatLocation() = runTestWithRetries { + Coordinator( + accountId = 99, + propertyId = 99, + propertyName = SPPropertyName.create("foo"), + campaigns = SPCampaigns(), + internalFlags = SPInternalFlags(geoOverride = "US-FL") + ).getUsnatLocation().apply { + assertEquals("US", countryCode) + assertEquals("FL", stateCode) + } + } } diff --git a/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/mocks/SPClientMock.kt b/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/mocks/SPClientMock.kt index 0e8a1774..1657361d 100644 --- a/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/mocks/SPClientMock.kt +++ b/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/mocks/SPClientMock.kt @@ -26,6 +26,7 @@ import com.sourcepoint.mobile_core.network.responses.MetaDataResponse import com.sourcepoint.mobile_core.network.responses.PreferencesChoiceResponse import com.sourcepoint.mobile_core.network.responses.PvDataResponse import com.sourcepoint.mobile_core.network.responses.USNatChoiceResponse +import com.sourcepoint.mobile_core.network.responses.UsnatLocationResponse @Suppress("MemberVisibilityCanBePrivate") class SPClientMock( @@ -41,6 +42,7 @@ class SPClientMock( var getMessages: (() -> MessagesResponse?)? = null, var customConsentGDPR: (() -> GDPRConsent?)? = null, var deleteCustomConsentGDPR: (() -> GDPRConsent?)? = null, + var getUsnatLocation: (() -> UsnatLocationResponse?)? = null, ) : SPClient { override suspend fun getMetaData(campaigns: MetaDataRequest.Campaigns) = getMetaData?.invoke() ?: @@ -94,6 +96,9 @@ class SPClientMock( original?.getMessages(request) ?: MessagesResponse(campaigns = emptyList(), localState = "", nonKeyedLocalState = "") + override suspend fun getUsnatLocation(): UsnatLocationResponse = + getUsnatLocation?.invoke() ?: original?.getUsnatLocation() ?: UsnatLocationResponse() + override suspend fun postReportIdfaStatus( propertyId: Int?, uuid: String?, diff --git a/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/network/SourcepointClientTest.kt b/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/network/SourcepointClientTest.kt index 385e8640..d3e8114c 100644 --- a/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/network/SourcepointClientTest.kt +++ b/core/src/commonTest/kotlin/com/sourcepoint/mobile_core/network/SourcepointClientTest.kt @@ -8,6 +8,7 @@ import com.sourcepoint.mobile_core.asserters.assertTrue import com.sourcepoint.mobile_core.models.SPActionType import com.sourcepoint.mobile_core.models.SPClientTimeout import com.sourcepoint.mobile_core.models.SPIDFAStatus +import com.sourcepoint.mobile_core.models.SPInternalFlags import com.sourcepoint.mobile_core.models.SPNetworkError import com.sourcepoint.mobile_core.models.SPPropertyName import com.sourcepoint.mobile_core.models.SPUnableToParseBodyError @@ -45,11 +46,8 @@ class SourcepointClientTest { private val propertyName = SPPropertyName.create("mobile.multicampaign.demo") private val preferencesConfigId = "67e14cda9efe9bd2a90fa84a" private val preferencesMessageId = "1306779" - private val api = SourcepointClient( - accountId = accountId, - propertyId = propertyId, - httpEngine = PlatformHttpClient.create().engine - ) + private val httpEngine = PlatformHttpClient.create().engine + private var api = SourcepointClient(accountId, propertyId, httpEngine) private fun mock(response: String = """{}""", status: Int = 200, delayInSeconds: Int = 0) = MockEngine { _ -> delay(delayInSeconds.toLong() * 1000) @@ -89,7 +87,7 @@ class SourcepointClientTest { assertNotNull(response.preferences?.additionsChangeDate) assertNotNull(response.globalcmp) - response.globalcmp?.apply { + response.globalcmp.apply { assertNotEmpty(vendorListId) assertNotEmpty(applicableSections) assertTrue(applies) @@ -244,7 +242,7 @@ class SourcepointClientTest { response.campaigns.forEach { campaign -> assertNotNull(campaign.url, "Empty url for ${campaign.type}") assertNotNull(campaign.message, "Empty message for ${campaign.type}") - assertNotEmpty(campaign.message?.messageJson, "Empty message_json for ${campaign.type}") + assertNotEmpty(campaign.message.messageJson, "Empty message_json for ${campaign.type}") assertNotNull(campaign.messageMetaData, "Empty messageMetaData for ${campaign.type}") assertCampaignConsentsFromMessages(campaign) } @@ -496,4 +494,17 @@ class SourcepointClientTest { } } } + + @Test + fun testGetUsnatLocation() = runTestWithRetries { + SourcepointClient( + accountId, + propertyId, + httpEngine, + internalFlags = SPInternalFlags(geoOverride = "US-FL") + ).getUsnatLocation().apply { + assertEquals("US", countryCode) + assertEquals("FL", stateCode) + } + } }