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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class NetworkMock : SuperwallAPI {
@Throws(Exception::class)
override suspend fun getAssignments(): Either<List<Assignment>, NetworkError> = Either.Success(assignments)

override suspend fun matchMMPInstall(installReferrerClickId: Long?): Boolean = false

override suspend fun webEntitlementsByUserId(
userId: UserId,
deviceId: DeviceVendorId,
Expand Down
25 changes: 24 additions & 1 deletion superwall/src/main/java/com/superwall/sdk/Superwall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.OpenedDe
import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.OpenedURL
import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.OpenedUrlInChrome
import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.RequestPermission
import com.superwall.sdk.storage.DidTrackAppInstall
import com.superwall.sdk.storage.LatestCustomerInfo
import com.superwall.sdk.storage.ReviewCount
import com.superwall.sdk.storage.ReviewData
Expand All @@ -80,6 +81,7 @@ import com.superwall.sdk.store.transactions.TransactionManager
import com.superwall.sdk.store.transactions.TransactionManager.PurchaseSource.*
import com.superwall.sdk.utilities.flatten
import com.superwall.sdk.utilities.withErrorTracking
import com.superwall.sdk.web.DeepLinkReferrer
import com.superwall.sdk.web.WebPaywallRedeemer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -677,12 +679,33 @@ class Superwall(

ioScope.launch {
withErrorTracking {
val hadTrackedAppInstallBeforeConfigure =
dependencyContainer.storage.read(DidTrackAppInstall) ?: false

dependencyContainer.storage.recordAppInstall {
track(event = it)
}

// Implicitly wait
dependencyContainer.configManager.fetchConfiguration()
dependencyContainer.identityManager.configure()

if (
dependencyContainer.storage.shouldAttemptInitialMMPInstallAttributionMatch(
hadTrackedAppInstallBeforeConfigure = hadTrackedAppInstallBeforeConfigure,
appInstalledAtMillis = dependencyContainer.deviceHelper.appInstalledAtMillis,
)
) {
val installReferrerClickId =
DeepLinkReferrer({ context }, ioScope)
.checkForMmpClickId()
.getOrNull()

dependencyContainer.storage.recordMMPInstallAttributionRequest {
dependencyContainer.network.matchMMPInstall(installReferrerClickId)
}
}

dependencyContainer.configManager.fetchConfiguration()
}.toResult().fold({
CoroutineScope(Dispatchers.Main).launch {
completion?.invoke(Result.success(Unit))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.superwall.sdk.analytics.internal.trackable

import com.superwall.sdk.analytics.superwall.AttributionMatchInfo
import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.analytics.superwall.TransactionProduct
import com.superwall.sdk.config.models.Survey
Expand Down Expand Up @@ -143,6 +144,21 @@ sealed class InternalSuperwallEvent(
)
}

class AttributionMatch(
val info: AttributionMatchInfo,
override val audienceFilterParams: Map<String, Any> = emptyMap(),
) : InternalSuperwallEvent(SuperwallEvent.AttributionMatch(info)) {
override suspend fun getSuperwallParameters(): Map<String, Any> =
listOfNotNull(
"provider" to info.provider.rawName,
"matched" to info.matched,
info.source?.let { "source" to it },
info.confidence?.let { "confidence" to it.rawName },
info.matchScore?.let { "match_score" to it },
info.reason?.let { "reason" to it },
).toMap()
}

class IdentityAlias(
override var audienceFilterParams: HashMap<String, Any> = HashMap(),
) : InternalSuperwallEvent(SuperwallEvent.IdentityAlias()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.superwall.sdk.analytics.superwall

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Information about an install attribution result emitted by Superwall.
*/
data class AttributionMatchInfo(
val provider: Provider,
val matched: Boolean,
val source: String? = null,
val confidence: Confidence? = null,
val matchScore: Double? = null,
val reason: String? = null,
) {
/**
* The attribution provider that produced the result.
*/
@Serializable
enum class Provider(
val rawName: String,
) {
@SerialName("mmp")
MMP("mmp"),
}

/**
* The confidence level returned by the attribution provider.
*/
@Serializable
enum class Confidence(
val rawName: String,
) {
@SerialName("high")
HIGH("high"),

@SerialName("medium")
MEDIUM("medium"),

@SerialName("low")
LOW("low"),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,16 @@ sealed class SuperwallEvent {
get() = "user_attributes"
}

/**
* When install attribution is resolved or fails to resolve.
*/
data class AttributionMatch(
val info: AttributionMatchInfo,
) : SuperwallEvent() {
override val rawName: String
get() = "attribution_match"
}

data class NonRecurringProductPurchase(
val product: TransactionProduct,
val paywallInfo: PaywallInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ enum class SuperwallEvents(
ReviewGranted("review_granted"),
ReviewDenied("review_denied"),
IntegrationAttributes("integration_attributes"),
AttributionMatch("attribution_match"),
CustomerInfoDidChange("customerInfo_didChange"),
PermissionRequested("permission_requested"),
PermissionGranted("permission_granted"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class SuperwallOptions() {
override val collectorHost: String,
override val scheme: String,
override val port: Int?,
override val subscriptionHost: String = baseHost,
override val enrichmentHost: String = baseHost,
) : NetworkEnvironment(baseHost)
}

Expand Down Expand Up @@ -129,6 +131,8 @@ internal fun SuperwallOptions.NetworkEnvironment.toMap(): Map<String, Any> =
"host_domain" to hostDomain,
"base_host" to baseHost,
"collector_host" to collectorHost,
"subscription_host" to subscriptionHost,
"enrichment_host" to enrichmentHost,
"scheme" to scheme,
port?.let { "port" to it },
).toMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.superwall.sdk.network.BaseHostService
import com.superwall.sdk.network.CollectorService
import com.superwall.sdk.network.EnrichmentService
import com.superwall.sdk.network.JsonFactory
import com.superwall.sdk.network.MmpService
import com.superwall.sdk.network.Network
import com.superwall.sdk.network.RequestExecutor
import com.superwall.sdk.network.SubscriptionService
Expand Down Expand Up @@ -356,6 +357,29 @@ class DependencyContainer(
factory = this,
customHttpUrlConnection = httpConnection,
),
mmpService =
MmpService(
host = api.subscription.host,
version = "/",
factory = this,
json =
Json(from = json()) {
ignoreUnknownKeys = true
namingStrategy = null
},
customHttpUrlConnection =
CustomHttpUrlConnection(
json =
Json(from = json()) {
ignoreUnknownKeys = true
namingStrategy = null
},
requestExecutor =
RequestExecutor { debugging, requestId ->
makeHeaders(debugging, requestId)
},
),
),
factory = this,
)
errorTracker = ErrorTracker(scope = ioScope, cache = storage)
Expand Down
74 changes: 74 additions & 0 deletions superwall/src/main/java/com/superwall/sdk/network/MmpService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.superwall.sdk.network

import com.superwall.sdk.analytics.superwall.AttributionMatchInfo
import com.superwall.sdk.dependencies.ApiFactory
import com.superwall.sdk.network.session.CustomHttpUrlConnection
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement

@Serializable
data class MmpMatchRequest(
val platform: String,
val appUserId: String? = null,
val deviceId: String? = null,
val vendorId: String? = null,
val installReferrerClickId: Long? = null,
val appVersion: String? = null,
val sdkVersion: String? = null,
val osVersion: String? = null,
val deviceModel: String? = null,
val deviceLocale: String? = null,
val deviceLanguageCode: String? = null,
val timezoneOffsetSeconds: Int? = null,
val screenWidth: Int? = null,
val screenHeight: Int? = null,
val devicePixelRatio: Double? = null,
val bundleId: String? = null,
val clientTimestamp: String? = null,
val metadata: Map<String, String>? = null,
)

@Serializable
data class MmpMatchResponse(
val matched: Boolean,
val confidence: AttributionMatchInfo.Confidence? = null,
val matchScore: Double? = null,
val clickId: Long? = null,
val linkId: String? = null,
val network: String? = null,
val redirectUrl: String? = null,
val queryParams: Map<String, JsonElement>? = null,
val acquisitionAttributes: Map<String, JsonElement>? = null,
val matchedAt: String? = null,
val breakdown: Map<String, JsonElement>? = null,
)

class MmpService(
override val host: String,
override val version: String,
val factory: ApiFactory,
json: Json,
override val customHttpUrlConnection: CustomHttpUrlConnection,
) : NetworkService() {
override suspend fun makeHeaders(
isForDebugging: Boolean,
requestId: String,
): Map<String, String> = factory.makeHeaders(isForDebugging, requestId)

private val json =
Json(json) {
namingStrategy = null
explicitNulls = false
ignoreUnknownKeys = true
coerceInputValues = true
}

suspend fun matchInstall(request: MmpMatchRequest) =
post<MmpMatchResponse>(
"api/match",
retryCount = 2,
body = json.encodeToString(request).toByteArray(),
)
}
Loading
Loading