diff --git a/.github/workflows/deployRelease.yml b/.github/workflows/deployRelease.yml index 405df46..57f47e8 100644 --- a/.github/workflows/deployRelease.yml +++ b/.github/workflows/deployRelease.yml @@ -4,6 +4,7 @@ on: push: branches: - 'main' + - 'revamp/parameter-pasrsing' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: @@ -40,7 +41,7 @@ jobs: - name: Update library version env: - PUBLISH_VERSION: 0.0.${{ github.run_number }} + PUBLISH_VERSION: 1.0.${{ github.run_number }} run: echo PUBLISH_VERSION=$PUBLISH_VERSION > ./versions.properties - name: Update local properties @@ -57,4 +58,4 @@ jobs: run: cat ./local.properties - name: Publish to GitHub Packages - run: ./gradlew publishAllPublicationsToGithubRepository \ No newline at end of file + run: ./gradlew publishAllPublicationsToGithubRepository diff --git a/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.android.kt b/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.android.kt index 582554b..00c8dae 100644 --- a/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.android.kt +++ b/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.android.kt @@ -6,17 +6,7 @@ import com.appsflyer.AppsFlyerLib import com.appsflyer.attribution.AppsFlyerRequestListener import com.appsflyer.deeplink.DeepLinkListener import com.appsflyer.deeplink.DeepLinkResult -import com.metacto.kmm.appsflyer.model.getDeepLinkValue -import com.metacto.kmm.appsflyer.model.getDeepLinkMetadata -import com.metacto.kmm.appsflyer.model.getDestinationPath -import com.metacto.kmm.appsflyer.model.toError -import com.metacto.kmm.appsflyer.model.hasDescopeToken -import com.metacto.kmm.appsflyer.model.hasLoginType -import com.metacto.kmm.appsflyer.model.getLoginType -import com.metacto.kmm.appsflyer.model.getAfSub1 -import com.metacto.kmm.appsflyer.model.DeeplinkSource -import com.metacto.kmm.appsflyer.model.UdlStatus -import com.metacto.kmm.appsflyer.model.GcdAfStatus +import com.metacto.kmm.appsflyer.model.* import com.metacto.kmm.appsflyer.util.getAppAttributionResult import com.metacto.kmm.appsflyer.model.DeepLinkResult as KMMDeepLinkResult @@ -34,87 +24,71 @@ actual class OneLinkService actual constructor( override fun onConversionDataSuccess(p0: MutableMap?) { if (p0 != null) { val appConversionResult = this@OneLinkService.getAppAttributionResult(p0) - options.listener.onAppAttribution( - appConversionResult.isOrganic, - appConversionResult.extras - ) - - val gcdMediaSource = appConversionResult.extras["media_source"]?.toString() - val gcdCampaign = appConversionResult.extras["campaign"]?.toString() @Suppress("UNCHECKED_CAST") val extras = appConversionResult.extras as Map + @Suppress("UNCHECKED_CAST") + val conversion = p0 as Map - val result = KMMDeepLinkResult( - destination = null, - campaign = gcdCampaign, - campaignId = null, - clickHttpReferrer = null, - isDeferred = false, - mediaSource = gcdMediaSource, - matchType = null, - clickEventJson = null, - metadata = null, - deeplinkSource = DeeplinkSource.GCD, - hasDescopeToken = hasDescopeToken(extras), - hasLoginType = hasLoginType(extras), - loginType = getLoginType(extras), + val isFirstLaunch = (extras["is_first_launch"] as? String)?.toBoolean() + ?: (extras["is_first_launch"] as? Boolean) + ?: false + + if (!isFirstLaunch) return + + val deepLinkValue = extras.getDeepLinkValue() + val destination = deepLinkValue?.parseDestination() + val gcdStatus = if (appConversionResult.isOrganic) GcdAfStatus.ORGANIC else GcdAfStatus.NON_ORGANIC + + val result = extras.buildDeepLinkResult( + source = DeeplinkSource.GCD, udlStatus = null, - udlMatchType = null, - gcdAfStatus = if (appConversionResult.isOrganic) GcdAfStatus.ORGANIC else GcdAfStatus.NON_ORGANIC, - gcdMediaSource = gcdMediaSource, - gcdCampaign = gcdCampaign, - afSub1 = getAfSub1(extras), - extraLink = null + gcdStatus = gcdStatus, + isDeferred = true, + destination = destination, + clickEvent = extras, + conversion = conversion ) - options.listener.onDeepLinkingResult(result) + + options.listener.onAttributionData(result) } } override fun onConversionDataFail(p0: String?) { - // No-op + if (p0 != null) { + options.listener.onDeepLinkingError(DeepLinkError.Generic(p0)) + } } override fun onAppOpenAttribution(p0: MutableMap?) { if (p0 != null) { val appConversionResult = this@OneLinkService.getAppAttributionResult(p0) - options.listener.onAppAttribution( - appConversionResult.isOrganic, - appConversionResult.extras - ) - - val gcdMediaSource = appConversionResult.extras["media_source"]?.toString() - val gcdCampaign = appConversionResult.extras["campaign"]?.toString() @Suppress("UNCHECKED_CAST") val extras = appConversionResult.extras as Map + @Suppress("UNCHECKED_CAST") + val conversion = p0 as Map - val result = KMMDeepLinkResult( - destination = null, - campaign = gcdCampaign, - campaignId = null, - clickHttpReferrer = null, - isDeferred = false, - mediaSource = gcdMediaSource, - matchType = null, - clickEventJson = null, - metadata = null, - deeplinkSource = DeeplinkSource.GCD, - hasDescopeToken = hasDescopeToken(extras), - hasLoginType = hasLoginType(extras), - loginType = getLoginType(extras), + val deepLinkValue = extras.getDeepLinkValue() + val destination = deepLinkValue?.parseDestination() + val gcdStatus = if (appConversionResult.isOrganic) GcdAfStatus.ORGANIC else GcdAfStatus.NON_ORGANIC + + val result = extras.buildDeepLinkResult( + source = DeeplinkSource.GCD, udlStatus = null, - udlMatchType = null, - gcdAfStatus = if (appConversionResult.isOrganic) GcdAfStatus.ORGANIC else GcdAfStatus.NON_ORGANIC, - gcdMediaSource = gcdMediaSource, - gcdCampaign = gcdCampaign, - afSub1 = getAfSub1(extras), - extraLink = null + gcdStatus = gcdStatus, + isDeferred = false, + destination = destination, + clickEvent = extras, + conversion = conversion ) - options.listener.onDeepLinkingResult(result) + + options.listener.onAttributionData(result) } } override fun onAttributionFailure(p0: String?) { - // No-op + if (p0 != null) { + options.listener.onDeepLinkingError(DeepLinkError.Generic(p0)) + } } } @@ -123,36 +97,24 @@ actual class OneLinkService actual constructor( DeepLinkResult.Status.FOUND -> { val deepLink = deepLinkResult.deepLink val clickEventValues = deepLink.clickEvent.toMap() - val fullDeepLinkValue = deepLink.deepLinkValue ?: this.getDeepLinkValue(clickEventValues) - val metadata = this.getDeepLinkMetadata(fullDeepLinkValue, clickEventValues) - - val result = KMMDeepLinkResult( - destination = fullDeepLinkValue?.let { this.getDestinationPath(fullDeepLinkValue) }, - campaign = deepLink.campaign, - campaignId = deepLink.campaignId, - clickHttpReferrer = deepLink.clickHttpReferrer, - isDeferred = deepLink.isDeferred, - mediaSource = deepLink.mediaSource, - matchType = deepLink.matchType, - clickEventJson = deepLink.clickEvent.toString(), - metadata = metadata, - deeplinkSource = DeeplinkSource.UDL, - hasDescopeToken = hasDescopeToken(metadata.extras), - hasLoginType = hasLoginType(metadata.extras), - loginType = getLoginType(metadata.extras), + val deepLinkValue = deepLink.deepLinkValue ?: clickEventValues.getDeepLinkValue() + val destination = deepLinkValue?.parseDestination() + + val result = clickEventValues.buildDeepLinkResult( + source = DeeplinkSource.UDL, udlStatus = UdlStatus.FOUND, - udlMatchType = deepLink.matchType, - gcdAfStatus = null, - gcdMediaSource = null, - gcdCampaign = null, - afSub1 = getAfSub1(clickEventValues), - extraLink = deepLink.clickEvent.getString("link") + gcdStatus = null, + isDeferred = deepLink.isDeferred, + destination = destination, + clickEvent = clickEventValues, + conversion = null ) + options.listener.onDeepLinkingResult(result) } DeepLinkResult.Status.NOT_FOUND -> { - options.listener.onDeepLinkingResult(null) + options.listener.onDeepLinkNotFound(KMMDeepLinkResult.notFound()) } else -> { @@ -166,9 +128,9 @@ actual class OneLinkService actual constructor( options.enableDebugLog?.let { setDebugLog(it) } options.minTimeBetweenSessions?.let { setMinTimeBetweenSessions(it) } if (options.appInviteOneLinkTemplateId != null) { - // Set the OneLink template id for share invite links setAppInviteOneLink(options.appInviteOneLinkTemplateId) } + subscribeForDeepLink(deepLinkListener) init(options.devAppKey, conversionListener, options.context as Context) } @@ -206,6 +168,7 @@ actual class OneLinkService actual constructor( *options.oneLinkCustomDomains?.toTypedArray().orEmpty() ) } + initialize() AppsFlyerLib.getInstance().start(options.context as Context, options.devAppKey) } diff --git a/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkError.kt b/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkError.kt index a31e248..fc4fd85 100644 --- a/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkError.kt +++ b/appsflyer/src/androidMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkError.kt @@ -2,12 +2,15 @@ package com.metacto.kmm.appsflyer.model import com.appsflyer.deeplink.DeepLinkResult -actual open class DeepLinkError { +actual open class DeepLinkError( + open val message: String? = null +) { data object Timeout : DeepLinkError() data object Network : DeepLinkError() data object HttpStatusCode : DeepLinkError() data object Unexpected : DeepLinkError() data object DeveloperError : DeepLinkError() + data class Generic(override val message: String) : DeepLinkError(message) } diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/OneLinkListener.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/OneLinkListener.kt index 5e4e33e..2b46dc3 100644 --- a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/OneLinkListener.kt +++ b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/OneLinkListener.kt @@ -4,11 +4,8 @@ import com.metacto.kmm.appsflyer.model.DeepLinkError import com.metacto.kmm.appsflyer.model.DeepLinkResult interface OneLinkListener { - fun onAppAttribution( - isOrganic: Boolean, - extras: Map? - ) - - fun onDeepLinkingResult(result: DeepLinkResult?) + fun onDeepLinkingResult(result: DeepLinkResult) + fun onDeepLinkNotFound(result: DeepLinkResult) + fun onAttributionData(result: DeepLinkResult) fun onDeepLinkingError(error: DeepLinkError) } \ No newline at end of file diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkEnums.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkEnums.kt index 93be2aa..b510f32 100644 --- a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkEnums.kt +++ b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkEnums.kt @@ -5,11 +5,6 @@ enum class DeeplinkSource { GCD } -enum class LoginType { - EMAIL, - PHONE -} - enum class UdlStatus { FOUND, NOT_FOUND diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkHelpers.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkHelpers.kt new file mode 100644 index 0000000..06d5564 --- /dev/null +++ b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkHelpers.kt @@ -0,0 +1,262 @@ +package com.metacto.kmm.appsflyer.model + +import com.metacto.kmm.appsflyer.util.AppsFlyerConstants + +fun Map.getDeepLinkValue(): String? { + return this[AppsFlyerConstants.DEEP_LINK_VALUE]?.toString() +} + +fun String.parseDestination(): String? { + if (!this.contains("__")) { + return this + } + + val values = this.split("__") + return values.find { it.startsWith(AppsFlyerConstants.DEEP_LINK_DESTINATION) }?.substringAfter("=") +} + +internal fun Map.getString(key: String): String? = + this[key]?.toString()?.takeIf { it != "null" && it.isNotEmpty() } + +internal fun Map.getBoolean(key: String): Boolean? { + return when (val value = this[key]) { + is Boolean -> value + is String -> value.toBoolean() + is Number -> value.toInt() != 0 + else -> null + } +} + +fun Map.buildOrigin(source: DeeplinkSource, udlStatus: UdlStatus?, gcdStatus: GcdAfStatus?, isDeferred: Boolean?): Origin { + return Origin( + source = source, + udlStatus = udlStatus, + status = gcdStatus, + matchType = getString(AppsFlyerConstants.MATCH_TYPE), + isDeferred = isDeferred + ) +} + +fun Map.buildCampaign(): Campaign { + return Campaign( + mediaSource = getString(AppsFlyerConstants.MEDIA_SOURCE) ?: getString(AppsFlyerConstants.PID), + name = getString(AppsFlyerConstants.CAMPAIGN) ?: getString(AppsFlyerConstants.C), + id = getString(AppsFlyerConstants.CAMPAIGN_ID) ?: getString(AppsFlyerConstants.AF_C_ID), + channel = getString(AppsFlyerConstants.AF_CHANNEL), + siteId = getString(AppsFlyerConstants.AF_SITEID), + subSiteId = getString(AppsFlyerConstants.AF_SUB_SITEID), + partner = getString(AppsFlyerConstants.AF_PRT), + marketingPartner = getString(AppsFlyerConstants.AF_MP), + agency = getString(AppsFlyerConstants.AGENCY), + accountId = getString(AppsFlyerConstants.AF_PARTNER_ACCOUNT_ID), + keywords = getString(AppsFlyerConstants.AF_KEYWORDS) + ) +} + +fun Map.buildAdvertisement(): Advertisement { + return Advertisement( + adset = getString(AppsFlyerConstants.ADSET) ?: getString(AppsFlyerConstants.AF_ADSET), + adsetId = getString(AppsFlyerConstants.ADSET_ID) ?: getString(AppsFlyerConstants.AF_ADSET_ID), + adgroup = getString(AppsFlyerConstants.ADGROUP), + adgroupId = getString(AppsFlyerConstants.ADGROUP_ID), + adgroupName = getString(AppsFlyerConstants.ADGROUP_NAME), + ad = getString(AppsFlyerConstants.AF_AD), + adId = getString(AppsFlyerConstants.AD_ID) ?: getString(AppsFlyerConstants.AF_AD_ID), + adType = getString(AppsFlyerConstants.AF_AD_TYPE), + adFormat = getString(AppsFlyerConstants.AF_AD_FORMAT), + adEventId = getString(AppsFlyerConstants.AD_EVENT_ID) + ) +} + +fun Map.buildDeepLink(destination: String?): DeepLink { + return DeepLink( + destination = destination, + value = getDeepLinkValue(), + sub1 = getString(AppsFlyerConstants.DEEP_LINK_SUB1), + sub2 = getString(AppsFlyerConstants.DEEP_LINK_SUB2), + sub3 = getString(AppsFlyerConstants.DEEP_LINK_SUB3), + sub4 = getString(AppsFlyerConstants.DEEP_LINK_SUB4), + sub5 = getString(AppsFlyerConstants.DEEP_LINK_SUB5), + sub6 = getString(AppsFlyerConstants.DEEP_LINK_SUB6), + sub7 = getString(AppsFlyerConstants.DEEP_LINK_SUB7), + sub8 = getString(AppsFlyerConstants.DEEP_LINK_SUB8), + sub9 = getString(AppsFlyerConstants.DEEP_LINK_SUB9), + sub10 = getString(AppsFlyerConstants.DEEP_LINK_SUB10), + scheme = getString(AppsFlyerConstants.BASE_DEEP_LINK), + forceDeeplink = getBoolean(AppsFlyerConstants.AF_FORCE_DEEPLINK), + redirect = getString(AppsFlyerConstants.AF_R), + webRedirect = getString(AppsFlyerConstants.AF_WEB_DP), + androidUrl = getString(AppsFlyerConstants.AF_ANDROID_URL), + iosUrl = getString(AppsFlyerConstants.AF_IOS_URL) + ) +} + +fun Map.buildTimestamps(): Timestamps { + return Timestamps( + clickTime = getString(AppsFlyerConstants.CLICK_TIME), + installTime = getString(AppsFlyerConstants.INSTALL_TIME), + isFirstLaunch = getBoolean(AppsFlyerConstants.IS_FIRST_LAUNCH), + clickLookback = getString(AppsFlyerConstants.AF_CLICK_LOOKBACK), + viewthroughLookback = getString(AppsFlyerConstants.AF_VIEWTHROUGH_LOOKBACK), + reengagementWindow = getString(AppsFlyerConstants.AF_REENGAGEMENT_WINDOW) + ) +} + +fun Map.buildCost(): Cost { + return Cost( + original = getString(AppsFlyerConstants.ORIG_COST), + centsUsd = getString(AppsFlyerConstants.COST_CENTS_USD), + currency = getString(AppsFlyerConstants.AF_COST_CURRENCY), + value = getString(AppsFlyerConstants.AF_COST_VALUE), + model = getString(AppsFlyerConstants.AF_COST_MODEL), + perInstall = getString(AppsFlyerConstants.AF_CPI) + ) +} + +fun Map.buildDevice(): Device { + return Device( + os = getString(AppsFlyerConstants.AF_OS), + osVersion = getString(AppsFlyerConstants.AF_OS_VERSION), + model = getString(AppsFlyerConstants.AF_MODEL), + userAgent = getString(AppsFlyerConstants.AF_UA), + ipAddress = getString(AppsFlyerConstants.AF_IP), + mediaType = getString(AppsFlyerConstants.AF_MEDIA_TYPE), + android = buildAndroidIdentifiers(), + ios = buildIosIdentifiers() + ) +} + +fun Map.buildAndroidIdentifiers(): AndroidIdentifiers { + return AndroidIdentifiers( + advertisingId = getString(AppsFlyerConstants.ADVERTISING_ID), + advertisingIdSha1 = getString(AppsFlyerConstants.SHA1_ADVERTISING_ID), + advertisingIdMd5 = getString(AppsFlyerConstants.MD5_ADVERTISING_ID), + androidId = getString(AppsFlyerConstants.ANDROID_ID), + androidIdSha1 = getString(AppsFlyerConstants.SHA1_ANDROID_ID), + androidIdMd5 = getString(AppsFlyerConstants.MD5_ANDROID_ID), + imei = getString(AppsFlyerConstants.IMEI), + imeiSha1 = getString(AppsFlyerConstants.SHA1_IMEI), + imeiMd5 = getString(AppsFlyerConstants.MD5_IMEI), + oaid = getString(AppsFlyerConstants.OAID), + oaidSha1 = getString(AppsFlyerConstants.SHA1_OAID), + oaidMd5 = getString(AppsFlyerConstants.MD5_OAID), + fireAdvertisingId = getString(AppsFlyerConstants.FIRE_ADVERTISING_ID), + emailSha1 = getString(AppsFlyerConstants.SHA1_EL), + storeListing = getString(AppsFlyerConstants.AF_ANDROID_STORE_CSL) + ) +} + +fun Map.buildIosIdentifiers(): IosIdentifiers { + return IosIdentifiers( + idfa = getString(AppsFlyerConstants.IDFA), + idfaSha1 = getString(AppsFlyerConstants.SHA1_IDFA), + idfaMd5 = getString(AppsFlyerConstants.MD5_IDFA), + idfv = getString(AppsFlyerConstants.IDFV), + idfvSha1 = getString(AppsFlyerConstants.SHA1_IDFV), + idfvMd5 = getString(AppsFlyerConstants.MD5_IDFV), + mac = getString(AppsFlyerConstants.MAC), + macSha1 = getString(AppsFlyerConstants.SHA1_MAC), + productPage = getString(AppsFlyerConstants.AF_IOS_STORE_CPP) + ) +} + +fun Map.buildRetargeting(): Retargeting { + return Retargeting( + isRetargeting = getBoolean(AppsFlyerConstants.IS_RETARGETING), + conversionType = getString(AppsFlyerConstants.RETARGETING_CONVERSION_TYPE) + ) +} + +fun Map.buildEngagement(): Engagement { + return Engagement( + isIncentivized = getBoolean(AppsFlyerConstants.IS_INCENTIVIZED), + clickId = getString(AppsFlyerConstants.CLICKID), + referrer = getString(AppsFlyerConstants.HTTP_REFERRER) + ) +} + +fun Map.buildViewability(): Viewability { + return Viewability( + videoTotalLength = getString(AppsFlyerConstants.AF_VIDEO_TOTAL_LENGTH), + videoPlayedLength = getString(AppsFlyerConstants.AF_VIDEO_PLAYED_LENGTH), + playablePlayedLength = getString(AppsFlyerConstants.AF_PLAYABLE_PLAYED_LENGTH), + adTimeViewed = getString(AppsFlyerConstants.AF_AD_TIME_VIEWED), + adDisplayedPercent = getString(AppsFlyerConstants.AF_AD_DISPLAYED_PERCENT), + audioTotalLength = getString(AppsFlyerConstants.AF_AUDIO_TOTAL_LENGTH), + audioPlayedLength = getString(AppsFlyerConstants.AF_AUDIO_PLAYED_LENGTH) + ) +} + +fun Map.buildNetwork(): Network { + return Network( + meta = MetaParameters( + isFacebook = getBoolean(AppsFlyerConstants.IS_FB), + isPaid = getBoolean(AppsFlyerConstants.IS_PAID), + isInstagram = getBoolean(AppsFlyerConstants.IS_INSTAGRAM) + ), + google = GoogleParameters( + accountId = getString(AppsFlyerConstants.EXTERNAL_ACCOUNT_ID), + clickId = getString(AppsFlyerConstants.GCLID), + network = getString(AppsFlyerConstants.NETWORK), + clickUrl = getString(AppsFlyerConstants.CLICK_URL), + latitude = getString(AppsFlyerConstants.LAT), + videoId = getString(AppsFlyerConstants.VIDEO_ID) + ), + campaignType = getString(AppsFlyerConstants.CAMPAIGN_TYPE) + ) +} + +fun Map.buildCustomParameters(): CustomParameters { + return CustomParameters( + sub1 = getString(AppsFlyerConstants.AF_SUB1), + sub2 = getString(AppsFlyerConstants.AF_SUB2), + sub3 = getString(AppsFlyerConstants.AF_SUB3), + sub4 = getString(AppsFlyerConstants.AF_SUB4), + sub5 = getString(AppsFlyerConstants.AF_SUB5) + ) +} + +fun Map.buildSocialPreview(): SocialPreview { + return SocialPreview( + title = getString(AppsFlyerConstants.AF_OG_TITLE), + description = getString(AppsFlyerConstants.AF_OG_DESCRIPTION), + image = getString(AppsFlyerConstants.AF_OG_IMAGE) + ) +} + +fun Map.buildSystem(): System { + return System( + referralCode = getString(AppsFlyerConstants.AF_REF), + parameterForwarding = getBoolean(AppsFlyerConstants.AF_PARAM_FORWARDING), + baseParametersForward = getBoolean(AppsFlyerConstants.AF_BASE_PARAMS_FORWARD) + ) +} + +fun Map.buildDeepLinkResult( + source: DeeplinkSource, + udlStatus: UdlStatus?, + gcdStatus: GcdAfStatus?, + isDeferred: Boolean?, + destination: String?, + clickEvent: Map?, + conversion: Map? +): DeepLinkResult { + return DeepLinkResult( + origin = buildOrigin(source, udlStatus, gcdStatus, isDeferred), + campaign = buildCampaign(), + advertisement = buildAdvertisement(), + deepLink = buildDeepLink(destination), + timestamps = buildTimestamps(), + cost = buildCost(), + device = buildDevice(), + retargeting = buildRetargeting(), + engagement = buildEngagement(), + viewability = buildViewability(), + network = buildNetwork(), + customParameters = buildCustomParameters(), + socialPreview = buildSocialPreview(), + system = buildSystem(), + clickEvent = clickEvent, + conversion = conversion + ) +} \ No newline at end of file diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkMetadata.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkMetadata.kt deleted file mode 100644 index aed2094..0000000 --- a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkMetadata.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.metacto.kmm.appsflyer.model - -import com.metacto.kmm.appsflyer.OneLinkService -import com.metacto.kmm.appsflyer.util.AppsFlyerConstants -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive - -data class DeepLinkMetadata( - val referrerName: String?, - val baseDeepLinkPath: String?, - val channel: String?, - val campaign: String?, - val referrerCustomerId: String?, - val referrerUID: String?, - val referrerImageURL: String?, - val extras: Map? -) - -fun OneLinkService.getDeepLinkValue(values: Map): String? { - return values[AppsFlyerConstants.DEEP_LINK_VALUE]?.toString() -} - -fun OneLinkService.getDestinationPath(fullDeepLinkValue: String): String? { - return fullDeepLinkValue.parseJsonDestination() ?: fullDeepLinkValue.parseNormalDestination() -} - -internal fun String.parseJsonDestination(): String? { - return try { - val destinationValue = Json.parseToJsonElement(this.trim()).let { - it.jsonObject[AppsFlyerConstants.DEEP_LINK_DESTINATION]?.jsonPrimitive?.content - } - - val values = destinationValue?.split("__") - return values?.firstOrNull() - } catch (_: Throwable) { - null - } -} - -internal fun String.parseNormalDestination(): String? { - val values = this.split("__") - return values.find { it.startsWith(AppsFlyerConstants.DEEP_LINK_DESTINATION) }?.substringAfter("=") -} - -fun hasDescopeToken(extras: Map?): Boolean { - return extras?.containsKey("dt") == true -} - -fun hasLoginType(extras: Map?): Boolean { - return extras?.containsKey("lt") == true -} - -fun getLoginType(extras: Map?): LoginType? { - val value = extras?.get("lt")?.toString() - return when (value) { - "email" -> LoginType.EMAIL - "phone" -> LoginType.PHONE - else -> null - } -} - -fun getAfSub1(extras: Map?): String? { - return extras?.get("af_sub1")?.toString() -} - -fun OneLinkService.getDeepLinkMetadata(deepLinkValue: String?, extraValues: Map): DeepLinkMetadata { - val values = deepLinkValue?.split("__").orEmpty() - val referrerName = - values.find { it.startsWith(AppsFlyerConstants.REFERRER_NAME) }?.substringAfter("=") - val baseDeepLinkPath = - values.find { it.startsWith(AppsFlyerConstants.BASE_DEEP_LINK) }?.substringAfter("=") - val channel = values.find { it.startsWith(AppsFlyerConstants.CHANNEL) }?.substringAfter("=") - val campaign = values.find { it.startsWith(AppsFlyerConstants.CAMPAIGN) }?.substringAfter("=") - val referrerCustomerId = - values.find { it.startsWith(AppsFlyerConstants.REFERRER_CUSTOMER_ID) }?.substringAfter("=") - val referrerUID = - values.find { it.startsWith(AppsFlyerConstants.REFERRER_UID) }?.substringAfter("=") - val referrerImageURL = - values.find { it.startsWith(AppsFlyerConstants.REFERRER_IMAGE_URL) }?.substringAfter("=") - val extras = values.filter { - !it.startsWith(AppsFlyerConstants.REFERRER_NAME) - && !it.startsWith(AppsFlyerConstants.DEEP_LINK_VALUE) - && !it.startsWith(AppsFlyerConstants.BASE_DEEP_LINK) - && !it.startsWith(AppsFlyerConstants.CHANNEL) - && !it.startsWith(AppsFlyerConstants.CAMPAIGN) - && !it.startsWith(AppsFlyerConstants.REFERRER_CUSTOMER_ID) - && !it.startsWith(AppsFlyerConstants.DEEP_LINK_DESTINATION) - && !it.startsWith(AppsFlyerConstants.REFERRER_UID) - && !it.startsWith(AppsFlyerConstants.REFERRER_IMAGE_URL) - }.map { - it.substringBefore("=") to it.substringAfter("=") - } - - val extraData = (extras.toMap() as Map).toMutableMap() - extraData.putAll(extraValues) - - return DeepLinkMetadata( - referrerName = referrerName, - baseDeepLinkPath = baseDeepLinkPath, - channel = channel, - campaign = campaign, - referrerCustomerId = referrerCustomerId, - referrerUID = referrerUID, - referrerImageURL = referrerImageURL, - extras = extraData - ) -} \ No newline at end of file diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkResult.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkResult.kt index 674e977..990c774 100644 --- a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkResult.kt +++ b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkResult.kt @@ -1,24 +1,208 @@ package com.metacto.kmm.appsflyer.model +data class Origin( + val source: DeeplinkSource, + val udlStatus: UdlStatus? = null, + val status: GcdAfStatus? = null, + val matchType: String? = null, + val isDeferred: Boolean? = null +) + +data class Campaign( + val mediaSource: String? = null, + val name: String? = null, + val id: String? = null, + val channel: String? = null, + val siteId: String? = null, + val subSiteId: String? = null, + val partner: String? = null, + val marketingPartner: String? = null, + val agency: String? = null, + val accountId: String? = null, + val keywords: String? = null +) + +data class Advertisement( + val adset: String? = null, + val adsetId: String? = null, + val adgroup: String? = null, + val adgroupId: String? = null, + val adgroupName: String? = null, + val ad: String? = null, + val adId: String? = null, + val adType: String? = null, + val adFormat: String? = null, + val adEventId: String? = null +) + +data class DeepLink( + val destination: String? = null, + val value: String? = null, + val sub1: String? = null, + val sub2: String? = null, + val sub3: String? = null, + val sub4: String? = null, + val sub5: String? = null, + val sub6: String? = null, + val sub7: String? = null, + val sub8: String? = null, + val sub9: String? = null, + val sub10: String? = null, + val scheme: String? = null, + val forceDeeplink: Boolean? = null, + val redirect: String? = null, + val webRedirect: String? = null, + val androidUrl: String? = null, + val iosUrl: String? = null +) + +data class Timestamps( + val clickTime: String? = null, + val installTime: String? = null, + val isFirstLaunch: Boolean? = null, + val clickLookback: String? = null, + val viewthroughLookback: String? = null, + val reengagementWindow: String? = null +) + +data class Cost( + val original: String? = null, + val centsUsd: String? = null, + val currency: String? = null, + val value: String? = null, + val model: String? = null, + val perInstall: String? = null +) + +data class Device( + val os: String? = null, + val osVersion: String? = null, + val model: String? = null, + val userAgent: String? = null, + val ipAddress: String? = null, + val mediaType: String? = null, + val android: AndroidIdentifiers = AndroidIdentifiers(), + val ios: IosIdentifiers = IosIdentifiers() +) + +data class AndroidIdentifiers( + val advertisingId: String? = null, + val advertisingIdSha1: String? = null, + val advertisingIdMd5: String? = null, + val androidId: String? = null, + val androidIdSha1: String? = null, + val androidIdMd5: String? = null, + val imei: String? = null, + val imeiSha1: String? = null, + val imeiMd5: String? = null, + val oaid: String? = null, + val oaidSha1: String? = null, + val oaidMd5: String? = null, + val fireAdvertisingId: String? = null, + val emailSha1: String? = null, + val storeListing: String? = null +) + +data class IosIdentifiers( + val idfa: String? = null, + val idfaSha1: String? = null, + val idfaMd5: String? = null, + val idfv: String? = null, + val idfvSha1: String? = null, + val idfvMd5: String? = null, + val mac: String? = null, + val macSha1: String? = null, + val productPage: String? = null +) + +data class Retargeting( + val isRetargeting: Boolean? = null, + val conversionType: String? = null +) + +data class Engagement( + val isIncentivized: Boolean? = null, + val clickId: String? = null, + val referrer: String? = null +) + +data class Viewability( + val videoTotalLength: String? = null, + val videoPlayedLength: String? = null, + val playablePlayedLength: String? = null, + val adTimeViewed: String? = null, + val adDisplayedPercent: String? = null, + val audioTotalLength: String? = null, + val audioPlayedLength: String? = null +) + +data class Network( + val meta: MetaParameters? = null, + val google: GoogleParameters? = null, + val campaignType: String? = null +) + +data class MetaParameters( + val isFacebook: Boolean? = null, + val isPaid: Boolean? = null, + val isInstagram: Boolean? = null +) + +data class GoogleParameters( + val accountId: String? = null, + val clickId: String? = null, + val network: String? = null, + val clickUrl: String? = null, + val latitude: String? = null, + val videoId: String? = null +) + +data class CustomParameters( + val sub1: String? = null, + val sub2: String? = null, + val sub3: String? = null, + val sub4: String? = null, + val sub5: String? = null +) + +data class SocialPreview( + val title: String? = null, + val description: String? = null, + val image: String? = null +) + +data class System( + val referralCode: String? = null, + val parameterForwarding: Boolean? = null, + val baseParametersForward: Boolean? = null +) + data class DeepLinkResult( - val destination: String?, - val campaign: String?, - val campaignId: String?, - val clickHttpReferrer: String?, - val isDeferred: Boolean?, - val mediaSource: String?, - val matchType: String?, - val clickEventJson: String?, - val metadata: DeepLinkMetadata?, - val deeplinkSource: DeeplinkSource, - val hasDescopeToken: Boolean, - val hasLoginType: Boolean, - val loginType: LoginType?, - val udlStatus: UdlStatus?, - val udlMatchType: String?, - val gcdAfStatus: GcdAfStatus?, - val gcdMediaSource: String?, - val gcdCampaign: String?, - val afSub1: String?, - val extraLink: String? -) \ No newline at end of file + val origin: Origin, + val campaign: Campaign = Campaign(), + val advertisement: Advertisement = Advertisement(), + val deepLink: DeepLink = DeepLink(), + val timestamps: Timestamps = Timestamps(), + val cost: Cost = Cost(), + val device: Device = Device(), + val retargeting: Retargeting = Retargeting(), + val engagement: Engagement = Engagement(), + val viewability: Viewability = Viewability(), + val network: Network = Network(), + val customParameters: CustomParameters = CustomParameters(), + val socialPreview: SocialPreview = SocialPreview(), + val system: System = System(), + val clickEvent: Map? = null, + val conversion: Map? = null +) { + companion object { + fun notFound(): DeepLinkResult { + return DeepLinkResult( + origin = Origin( + source = DeeplinkSource.UDL, + udlStatus = UdlStatus.NOT_FOUND + ) + ) + } + } +} \ No newline at end of file diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/AppsFlyerConstants.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/AppsFlyerConstants.kt index 5f6c606..b6a7653 100644 --- a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/AppsFlyerConstants.kt +++ b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/AppsFlyerConstants.kt @@ -1,20 +1,143 @@ package com.metacto.kmm.appsflyer.util object AppsFlyerConstants { + const val PID = "pid" + const val C = "c" + const val AF_PRT = "af_prt" + const val AF_MP = "af_mp" + const val CLICKID = "clickid" + const val AF_SITEID = "af_siteid" + const val AF_SUB_SITEID = "af_sub_siteid" + const val AF_C_ID = "af_c_id" + const val AF_ADSET = "af_adset" + const val AF_ADSET_ID = "af_adset_id" + const val AF_AD = "af_ad" + const val AF_AD_ID = "af_ad_id" + const val AF_AD_TYPE = "af_ad_type" + const val AF_AD_FORMAT = "af_ad_format" + const val AF_CLICK_LOOKBACK = "af_click_lookback" + const val AF_VIEWTHROUGH_LOOKBACK = "af_viewthrough_lookback" + const val AF_CHANNEL = "af_channel" + const val AF_KEYWORDS = "af_keywords" + const val AF_COST_MODEL = "af_cost_model" + const val AF_COST_CURRENCY = "af_cost_currency" + const val AF_COST_VALUE = "af_cost_value" + + const val AF_SUB1 = "af_sub1" + const val AF_SUB2 = "af_sub2" + const val AF_SUB3 = "af_sub3" + const val AF_SUB4 = "af_sub4" + const val AF_SUB5 = "af_sub5" + + const val DEEP_LINK_SUB1 = "deep_link_sub1" + const val DEEP_LINK_SUB2 = "deep_link_sub2" + const val DEEP_LINK_SUB3 = "deep_link_sub3" + const val DEEP_LINK_SUB4 = "deep_link_sub4" + const val DEEP_LINK_SUB5 = "deep_link_sub5" + const val DEEP_LINK_SUB6 = "deep_link_sub6" + const val DEEP_LINK_SUB7 = "deep_link_sub7" + const val DEEP_LINK_SUB8 = "deep_link_sub8" + const val DEEP_LINK_SUB9 = "deep_link_sub9" + const val DEEP_LINK_SUB10 = "deep_link_sub10" + + const val AF_R = "af_r" + const val AF_WEB_DP = "af_web_dp" + const val BASE_DEEP_LINK = "af_dp" + const val AF_FORCE_DEEPLINK = "af_force_deeplink" + const val AF_REF = "af_ref" + const val IS_INCENTIVIZED = "is_incentivized" + const val AF_PARAM_FORWARDING = "af_param_forwarding" + const val AF_BASE_PARAMS_FORWARD = "af_base_params_forward" + const val AF_PARTNER_ACCOUNT_ID = "af_partner_account_id" + const val AF_UA = "af_ua" + const val AF_IP = "af_ip" + const val AF_OS = "af_os" + const val AF_OS_VERSION = "af_os_version" + const val AF_MODEL = "af_model" + const val AF_MEDIA_TYPE = "af_media_type" + const val DEEP_LINK_VALUE = "deep_link_value" + const val AF_OG_TITLE = "af_og_title" + const val AF_OG_DESCRIPTION = "af_og_description" + const val AF_OG_IMAGE = "af_og_image" + const val IS_RETARGETING = "is_retargeting" + const val AF_REENGAGEMENT_WINDOW = "af_reengagement_window" + + const val AF_VIDEO_TOTAL_LENGTH = "af_video_total_length" + const val AF_VIDEO_PLAYED_LENGTH = "af_video_played_length" + const val AF_PLAYABLE_PLAYED_LENGTH = "af_playable_played_length" + const val AF_AD_TIME_VIEWED = "af_ad_time_viewed" + const val AF_AD_DISPLAYED_PERCENT = "af_ad_displayed_percent" + const val AF_AUDIO_TOTAL_LENGTH = "af_audio_total_length" + const val AF_AUDIO_PLAYED_LENGTH = "af_audio_played_length" + + const val ADVERTISING_ID = "advertising_id" + const val SHA1_ADVERTISING_ID = "sha1_advertising_id" + const val MD5_ADVERTISING_ID = "md5_advertising_id" + const val ANDROID_ID = "android_id" + const val SHA1_ANDROID_ID = "sha1_android_id" + const val MD5_ANDROID_ID = "md5_android_id" + const val IMEI = "imei" + const val SHA1_IMEI = "sha1_imei" + const val MD5_IMEI = "md5_imei" + const val OAID = "oaid" + const val SHA1_OAID = "sha1_oaid" + const val MD5_OAID = "md5_oaid" + const val AF_ANDROID_URL = "af_android_url" + const val SHA1_EL = "sha1_el" + const val FIRE_ADVERTISING_ID = "fire_advertising_id" + const val AF_ANDROID_STORE_CSL = "af_android_store_csl" + + const val IDFA = "idfa" + const val IDFV = "idfv" + const val AF_IOS_URL = "af_ios_url" + const val AF_IOS_STORE_CPP = "af_ios_store_cpp" + const val SHA1_IDFA = "sha1_idfa" + const val SHA1_IDFV = "sha1_idfv" + const val MAC = "mac" + const val MD5_IDFA = "md5_idfa" + const val MD5_IDFV = "md5_idfv" + const val SHA1_MAC = "sha1_mac" + + const val MEDIA_SOURCE = "media_source" + const val INSTALL_TIME = "install_time" + const val CLICK_TIME = "click_time" + const val IS_FIRST_LAUNCH = "is_first_launch" const val AF_STATUS = "af_status" + const val ADGROUP_ID = "adgroup_id" + const val ADSET = "adset" + const val ADSET_ID = "adset_id" + const val AGENCY = "agency" + const val CAMPAIGN = "campaign" + const val CAMPAIGN_ID = "campaign_id" + const val HTTP_REFERRER = "http_referrer" + const val RETARGETING_CONVERSION_TYPE = "retargeting_conversion_type" + const val ORIG_COST = "orig_cost" + const val COST_CENTS_USD = "cost_cents_USD" + const val AF_CPI = "af_cpi" + + const val MATCH_TYPE = "match_type" + const val AD_ID = "ad_id" + const val IS_FB = "is_fb" + const val IS_PAID = "is_paid" + const val ADGROUP = "adgroup" + const val ADGROUP_NAME = "adgroup_name" + const val IS_INSTAGRAM = "is_instagram" + + const val EXTERNAL_ACCOUNT_ID = "external_account_id" + const val GCLID = "gclid" + const val LAT = "lat" + const val NETWORK = "network" + const val CLICK_URL = "click_url" + const val VIDEO_ID = "video_id" + const val AD_EVENT_ID = "ad_event_id" + + const val CAMPAIGN_TYPE = "campaign_type" + const val AF_ORGANIC_STATUS = "Organic" + const val DEEP_LINK_DESTINATION = "deep_link_destination" - const val DEEP_LINK_VALUE = "deep_link_value" const val REFERRER_NAME = "af_referrer_name" - const val BASE_DEEP_LINK = "af_dp" - const val CHANNEL = "af_channel" - const val CAMPAIGN = "campaign" const val REFERRER_CUSTOMER_ID = "af_referrer_customer_id" const val REFERRER_UID = "af_referrer_uid" const val REFERRER_IMAGE_URL = "af_referrer_image_url" - - const val LINK = "link" - const val SCHEME = "scheme" - const val HOST = "host" - const val MEDIA_SOURCE = "media_source" -} \ No newline at end of file +} diff --git a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/Extensions.kt b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/Extensions.kt index 6855a22..4ca4aaf 100644 --- a/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/Extensions.kt +++ b/appsflyer/src/commonMain/kotlin/com/metacto/kmm/appsflyer/util/Extensions.kt @@ -34,7 +34,7 @@ fun ShareLinkGenerator.generateDeepLinkValue( val stringBuilder = StringBuilder() stringBuilder.append("${AppsFlyerConstants.DEEP_LINK_DESTINATION}=$destination") channel?.let { - stringBuilder.append("__${AppsFlyerConstants.CHANNEL}=$it") + stringBuilder.append("__${AppsFlyerConstants.AF_CHANNEL}=$it") } referrerCustomerId?.let { stringBuilder.append("__${AppsFlyerConstants.REFERRER_CUSTOMER_ID}=$it") diff --git a/appsflyer/src/commonTest/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkMetadataTest.kt b/appsflyer/src/commonTest/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkMetadataTest.kt deleted file mode 100644 index 150aed8..0000000 --- a/appsflyer/src/commonTest/kotlin/com/metacto/kmm/appsflyer/model/DeepLinkMetadataTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.metacto.kmm.appsflyer.model - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class DeepLinkMetadataTest { - - @Test - fun testParseJsonDestination_withValidJSON() { - val jsonInput = """{"deep_link_destination":"/auth/install-preauth"}""" - val result = jsonInput.parseJsonDestination() - assertEquals("/auth/install-preauth", result) - } - - @Test - fun testParseJsonDestination_withJSONContainingPathWithParameters() { - val jsonInput = """{"deep_link_destination":"/property/tt1ysu65br0a1u0up45b4awx__af_channel=share__af_referrer_customer_id=gesiws1opky86sz97o6wvo9m"}""" - val result = jsonInput.parseJsonDestination() - assertEquals("/property/tt1ysu65br0a1u0up45b4awx", result) - } - - @Test - fun testParseJsonDestination_withJSONContainingWhitespace() { - val jsonInput = """ {"deep_link_destination":"/auth/install-preauth"} """ - val result = jsonInput.parseJsonDestination() - assertEquals("/auth/install-preauth", result) - } - - @Test - fun testParseJsonDestination_withInvalidJSON() { - val invalidJsonInput = """{"deep_link_destination":"/auth/install-preauth""" - val result = invalidJsonInput.parseJsonDestination() - assertNull(result) - } - - @Test - fun testParseJsonDestination_withEmptyDestinationValue() { - val jsonInput = """{"deep_link_destination":""}""" - val result = jsonInput.parseJsonDestination() - assertEquals("", result) - } - - @Test - fun testParseJsonDestination_withMissingDeepLinkDestinationKey() { - val jsonInput = """{"other_key":"value"}""" - val result = jsonInput.parseJsonDestination() - assertNull(result) - } - - @Test - fun testParseNormalDestination_withSimpleKeyValueFormat() { - val normalInput = "deep_link_destination=/property/tt1ysu65br0a1u0up45b4awx__af_channel=share" - val result = normalInput.parseNormalDestination() - assertEquals("/property/tt1ysu65br0a1u0up45b4awx", result) - } - - @Test - fun testParseNormalDestination_withFullParameterString() { - val normalInput = "deep_link_destination=/property/tt1ysu65br0a1u0up45b4awx__af_channel=share__af_referrer_customer_id=gesiws1opky86sz97o6wvo9m__af_referrer_name=Mahmoud Elshamy__username=shamyyoun" - val result = normalInput.parseNormalDestination() - assertEquals("/property/tt1ysu65br0a1u0up45b4awx", result) - } - - @Test - fun testParseNormalDestination_withOnlyDestinationValue() { - val normalInput = "deep_link_destination=/auth/install-preauth" - val result = normalInput.parseNormalDestination() - assertEquals("/auth/install-preauth", result) - } - - @Test - fun testParseNormalDestination_returnsNullWhenDestinationKeyNotFound() { - val normalInput = "af_channel=share__af_referrer_customer_id=gesiws1opky86sz97o6wvo9m" - val result = normalInput.parseNormalDestination() - assertNull(result) - } - - @Test - fun testParseNormalDestination_withEmptyString() { - val result = "".parseNormalDestination() - assertNull(result) - } - - @Test - fun testParseNormalDestination_withDestinationAtDifferentPosition() { - val normalInput = "af_channel=share__deep_link_destination=/home__af_referrer_customer_id=test" - val result = normalInput.parseNormalDestination() - assertEquals("/home", result) - } -} diff --git a/appsflyer/src/commonTest/kotlin/com/metacto/kmm/appsflyer/model/HelperFunctionsTest.kt b/appsflyer/src/commonTest/kotlin/com/metacto/kmm/appsflyer/model/HelperFunctionsTest.kt deleted file mode 100644 index 7658f13..0000000 --- a/appsflyer/src/commonTest/kotlin/com/metacto/kmm/appsflyer/model/HelperFunctionsTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.metacto.kmm.appsflyer.model - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class HelperFunctionsTest { - - @Test - fun testHasDescopeToken_withDtKey() { - val extras: Map = mapOf("dt" to "sometoken") - assertTrue(hasDescopeToken(extras)) - } - - @Test - fun testHasDescopeToken_withoutDtKey() { - val extras: Map = mapOf("other_key" to "value") - assertFalse(hasDescopeToken(extras)) - } - - @Test - fun testHasDescopeToken_withNullExtras() { - assertFalse(hasDescopeToken(null)) - } - - @Test - fun testHasDescopeToken_withEmptyExtras() { - val extras: Map = emptyMap() - assertFalse(hasDescopeToken(extras)) - } - - @Test - fun testHasLoginType_withLtKey() { - val extras: Map = mapOf("lt" to "email") - assertTrue(hasLoginType(extras)) - } - - @Test - fun testHasLoginType_withoutLtKey() { - val extras: Map = mapOf("other_key" to "value") - assertFalse(hasLoginType(extras)) - } - - @Test - fun testHasLoginType_withNullExtras() { - assertFalse(hasLoginType(null)) - } - - @Test - fun testHasLoginType_withEmptyExtras() { - val extras: Map = emptyMap() - assertFalse(hasLoginType(extras)) - } - - @Test - fun testGetLoginType_withEmail() { - val extras: Map = mapOf("lt" to "email") - assertEquals(LoginType.EMAIL, getLoginType(extras)) - } - - @Test - fun testGetLoginType_withPhone() { - val extras: Map = mapOf("lt" to "phone") - assertEquals(LoginType.PHONE, getLoginType(extras)) - } - - @Test - fun testGetLoginType_withInvalidValue() { - val extras: Map = mapOf("lt" to "invalid") - assertNull(getLoginType(extras)) - } - - @Test - fun testGetLoginType_withNullExtras() { - assertNull(getLoginType(null)) - } - - @Test - fun testGetLoginType_withEmptyExtras() { - val extras: Map = emptyMap() - assertNull(getLoginType(extras)) - } - - @Test - fun testGetLoginType_withMissingKey() { - val extras: Map = mapOf("other_key" to "value") - assertNull(getLoginType(extras)) - } - - @Test - fun testGetAfSub1_withValidValue() { - val extras: Map = mapOf("af_sub1" to "test_value") - assertEquals("test_value", getAfSub1(extras)) - } - - @Test - fun testGetAfSub1_withNullExtras() { - assertNull(getAfSub1(null)) - } - - @Test - fun testGetAfSub1_withEmptyExtras() { - val extras: Map = emptyMap() - assertNull(getAfSub1(extras)) - } - - @Test - fun testGetAfSub1_withMissingKey() { - val extras: Map = mapOf("other_key" to "value") - assertNull(getAfSub1(extras)) - } - - @Test - fun testGetAfSub1_withNumericValue() { - val extras: Map = mapOf("af_sub1" to 12345) - assertEquals("12345", getAfSub1(extras)) - } -} diff --git a/appsflyer/src/iosMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.ios.kt b/appsflyer/src/iosMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.ios.kt index 18b9935..1decab7 100644 --- a/appsflyer/src/iosMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.ios.kt +++ b/appsflyer/src/iosMain/kotlin/com/metacto/kmm/appsflyer/OneLinkService.ios.kt @@ -7,18 +7,7 @@ import AppsFlyerLib.AppsFlyerDeepLinkDelegateProtocol import AppsFlyerLib.AppsFlyerDeepLinkResult import AppsFlyerLib.AppsFlyerLib import AppsFlyerLib.AppsFlyerLibDelegateProtocol -import com.metacto.kmm.appsflyer.model.DeepLinkError -import com.metacto.kmm.appsflyer.model.DeepLinkResult -import com.metacto.kmm.appsflyer.model.getDeepLinkValue -import com.metacto.kmm.appsflyer.model.getDeepLinkMetadata -import com.metacto.kmm.appsflyer.model.getDestinationPath -import com.metacto.kmm.appsflyer.model.hasDescopeToken -import com.metacto.kmm.appsflyer.model.hasLoginType -import com.metacto.kmm.appsflyer.model.getLoginType -import com.metacto.kmm.appsflyer.model.getAfSub1 -import com.metacto.kmm.appsflyer.model.DeeplinkSource -import com.metacto.kmm.appsflyer.model.UdlStatus -import com.metacto.kmm.appsflyer.model.GcdAfStatus +import com.metacto.kmm.appsflyer.model.* import com.metacto.kmm.appsflyer.util.getAppAttributionResult import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.convert @@ -43,9 +32,7 @@ actual class OneLinkService actual constructor( actual fun initialize() { AppsFlyerLib.shared().apply { - options.devAppKey.let { - setAppsFlyerDevKey(options.devAppKey) - } + setAppsFlyerDevKey(options.devAppKey) setAppleAppID(options.appleAppId!!) @@ -72,38 +59,24 @@ actual class OneLinkService actual constructor( AFSDKDeepLinkResultStatus.AFSDKDeepLinkResultStatusFound -> { val deepLink = result.deepLink val clickEventValues = deepLink?.clickEvent?.toMap() ?: emptyMap() - val fullDeepLinkValue = - deepLink?.deeplinkValue ?: this.getDeepLinkValue(clickEventValues) - val metadata = this.getDeepLinkMetadata(fullDeepLinkValue, clickEventValues) - - val deepLinkResult = DeepLinkResult( - destination = fullDeepLinkValue?.let { this.getDestinationPath(fullDeepLinkValue) }, - campaign = deepLink?.campaign, - campaignId = deepLink?.campaignId, - clickHttpReferrer = deepLink?.clickHTTPReferrer, - isDeferred = deepLink?.isDeferred, - mediaSource = deepLink?.mediaSource, - matchType = deepLink?.matchType, - clickEventJson = deepLink?.clickEvent.toString(), - metadata = metadata, - deeplinkSource = DeeplinkSource.UDL, - hasDescopeToken = hasDescopeToken(metadata.extras), - hasLoginType = hasLoginType(metadata.extras), - loginType = getLoginType(metadata.extras), + val deepLinkValue = deepLink?.deeplinkValue ?: clickEventValues.getDeepLinkValue() + val destination = deepLinkValue?.parseDestination() + + val deepLinkResult = clickEventValues.buildDeepLinkResult( + source = DeeplinkSource.UDL, udlStatus = UdlStatus.FOUND, - udlMatchType = deepLink?.matchType, - gcdAfStatus = null, - gcdMediaSource = null, - gcdCampaign = null, - afSub1 = getAfSub1(clickEventValues), - extraLink = clickEventValues["link"] as? String + gcdStatus = null, + isDeferred = deepLink?.isDeferred, + destination = destination, + clickEvent = clickEventValues, + conversion = null ) options.listener.onDeepLinkingResult(deepLinkResult) } AFSDKDeepLinkResultStatus.AFSDKDeepLinkResultStatusNotFound -> { - options.listener.onDeepLinkingResult(null) + options.listener.onDeepLinkNotFound(DeepLinkResult.notFound()) } AFSDKDeepLinkResultStatus.AFSDKDeepLinkResultStatusFailure -> { @@ -111,47 +84,42 @@ actual class OneLinkService actual constructor( } else -> { - options.listener.onDeepLinkingResult(null) + options.listener.onDeepLinkNotFound(DeepLinkResult.notFound()) } } } override fun onConversionDataFail(error: NSError) { - // No-op + options.listener.onDeepLinkingError(DeepLinkError(error)) } override fun onConversionDataSuccess(conversionInfo: Map) { val appConversionResult = this.getAppAttributionResult(conversionInfo) - options.listener.onAppAttribution(appConversionResult.isOrganic, appConversionResult.extras) - - val gcdMediaSource = appConversionResult.extras["media_source"]?.toString() - val gcdCampaign = appConversionResult.extras["campaign"]?.toString() @Suppress("UNCHECKED_CAST") val extras = appConversionResult.extras as Map - val deepLinkResult = DeepLinkResult( - destination = null, - campaign = gcdCampaign, - campaignId = null, - clickHttpReferrer = null, - isDeferred = false, - mediaSource = gcdMediaSource, - matchType = null, - clickEventJson = null, - metadata = null, - deeplinkSource = DeeplinkSource.GCD, - hasDescopeToken = hasDescopeToken(extras), - hasLoginType = hasLoginType(extras), - loginType = getLoginType(extras), + val isFirstLaunch = extras.getBoolean("is_first_launch") ?: false + if (!isFirstLaunch) return + + val deepLinkValue = (conversionInfo["deep_link_value"] ?: extras["deep_link_value"])?.toString() + val destination = deepLinkValue?.parseDestination() + val gcdStatus = if (appConversionResult.isOrganic) GcdAfStatus.ORGANIC else GcdAfStatus.NON_ORGANIC + + val deepLinkResult = extras.buildDeepLinkResult( + source = DeeplinkSource.GCD, udlStatus = null, - udlMatchType = null, - gcdAfStatus = if (appConversionResult.isOrganic) GcdAfStatus.ORGANIC else GcdAfStatus.NON_ORGANIC, - gcdMediaSource = gcdMediaSource, - gcdCampaign = gcdCampaign, - afSub1 = getAfSub1(extras), - extraLink = null + gcdStatus = gcdStatus, + isDeferred = isFirstLaunch, + destination = destination, + clickEvent = null, + conversion = conversionInfo ) - options.listener.onDeepLinkingResult(deepLinkResult) + + options.listener.onAttributionData(deepLinkResult) + } + + override fun onAppOpenAttributionFailure(error: NSError) { + options.listener.onDeepLinkingError(DeepLinkError(error)) } actual fun start(