GoogleChromeLabs / bubblewrap

Bubblewrap is a Command Line Interface (CLI) that helps developers to create a Project for an Android application that launches an existing Progressive Web App (PWAs) using a Trusted Web Activity.
Apache License 2.0
2.32k stars 160 forks source link

Appsflyer. The conversion data doesn't come to TWA. #664

Open LupusX5 opened 2 years ago

LupusX5 commented 2 years ago

I have created an app using Bubblewrap, with the enabled feature of Appsflyer – it's needed to receive the google play referrer information.

But I have kind of unexpected behaviour:

When I launch the app the following happens: on open the app opens a url with the appsflyer_id query parameter. I.e. the following url is being opened: https://angus.surge.sh/?appsflyer_id=xxxxxxxx. However, it doesn't receive any cookies in response afterwards as expected according to Appsflyer documentation.

Screenshot №1 + source URL: https://support.appsflyer.com/hc/en-us/articles/360002330178-Using-AppsFlyer-with-TWA#append-appsflyer-id-and-customer-user-id-to-url:

image

Screenshot №2: expected cookies on the client side (the same source as Screenshot №1):

image

And this is my source code: https://github.com/LupusX5/twa-demo

Could you please help solve this issue?

LupusX5 commented 2 years ago

Partly resolved: the issues with getting the converision data and sharing it with the server is fixed. However I'm still searching for better option to pass the conversion data to the url. Within a few days may come up with a solution and will share the code.

andreban commented 2 years ago

I guess the best place to document the AppsFlyer flow is in the AppsFlyer docs themselves. Nevertheless, any updates here?

LupusX5 commented 2 years ago

I guess the best place to document the AppsFlyer flow is in the AppsFlyer docs themselves. Nevertheless, any updates here?

Programming language used in this comment: Kotlin.

Hello! Yep. I have managed to use it with Chrome Custom Tabs, but not still figured out how to do that with the TWA. The issue is that for Chrome Custom Tabs I see the way of using it, and in TWA there's one thing that doesn't let me to get normally the idea.

So this is the problematic place for me:

class LauncherActivity : LauncherActivity() {
    override fun getLaunchingUrl(): Uri {
        val uri = super.getLaunchingUrl()
        val appsFlyerId = AppsFlyerLib.getInstance().getAppsFlyerUID(this)
        uri
            .buildUpon()
            .appendQueryParameter("appsflyer_id", appsFlyerId)
            .build()
        return uri
    }
...
}

So what's the issue here. The issue is that I can't launch this code or any of its parts inside an AppsFlyer caller function.

Let me show you what I'm talking about with a working example. Here in this case I can do what I need, but I need it for TWA as well :) I guess many people would love to have it as it makes marketing more efficient.

package com.example.app
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.appsflyer.AppsFlyerConversionListener
import com.appsflyer.AppsFlyerLib
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import android.net.Uri
import com.example.app.R
import android.content.ComponentName
import android.graphics.Color

class MainActivity : AppCompatActivity() {

    val CUSTOM_TAB_PACKAGE_NAME = "com.android.chrome";

    lateinit var webView: WebView
    private val host = "https://example.com"
    private val baseUrl = "${host}/?"
    private val errorUrl = "${host}/?error=error-appsflyer-not-working"

    private var mCustomTabsServiceConnection: CustomTabsServiceConnection? = null
    private var mClient: CustomTabsClient? = null
    private var mCustomTabsSession: CustomTabsSession? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        supportActionBar!!.hide()
        setContentView(R.layout.activity_main)

        // A container function inside of which the AppsFlyer Caller function is fired, which contains a nested Chrome Custom Tabs 
       // function to ALWAYS get the data from appsflyer.
        stack()

        mCustomTabsServiceConnection = object : CustomTabsServiceConnection() {
            override fun onCustomTabsServiceConnected(componentName: ComponentName, customTabsClient: CustomTabsClient) {
                //Pre-warming
                mClient = customTabsClient
                mClient?.warmup(0L)
                mCustomTabsSession = mClient?.newSession(null)
            }

            override fun onServiceDisconnected(name: ComponentName) {
                mClient = null
            }
        }

        CustomTabsClient.bindCustomTabsService(this, CUSTOM_TAB_PACKAGE_NAME,
            mCustomTabsServiceConnection as CustomTabsServiceConnection
        );
    }

    // placing AppsFlyer Caller function to a separate function to launch some more useful functions
    // in future
    private fun stack()  {
        AFcaller()
    }

    // AppsFlyer Caller function
    private fun AFcaller() {
//        InstallReferrer.installerReferrer(applicationContext)
        val conversionListener: AppsFlyerConversionListener = object : AppsFlyerConversionListener {
            /* Returns the attribution data. Note - the same conversion data is returned every time per install */
            override fun onConversionDataSuccess(conversionData: Map<String, Any>) {
                // setting conversion data to the companion object
                setInstallData(conversionData)

                // getting conversion data from the companion object and adding it to the 
                // base URL
                loadCustomTabForSite(baseUrl+InstallConversionData)
            }

            override fun onConversionDataFail(errorMessage: String) {
                loadCustomTabForSite(errorUrl)
            }

            /* Called only when a Deep Link is opened */
            override fun onAppOpenAttribution(conversionData: Map<String, String>) {
                for (attrName in conversionData.keys) {
                    loadCustomTabForSite(baseUrl+InstallConversionData)
                }

            }

            override fun onAttributionFailure(errorMessage: String) {
                loadCustomTabForSite(errorUrl)

            }
        }

        /* This API enables AppsFlyer to detect installations, sessions, and updates. */
        AppsFlyerLib.getInstance()
            .init(AF_DEV_KEY, conversionListener, applicationContext)
        AppsFlyerLib.getInstance().start(this)

        /* Set to true to see the debug logs. Comment out or set to false to stop the function */AppsFlyerLib.getInstance()
            .setDebugLog(true)
    }

    // This function allows me to do what the in-built TWA function doesn't allow: use it in other 
    // functions and pass the needed url as a parameter. In case with TWA it would be the same url all the
    // time, but what's really needed is to pass the parameters in a timely manner to the url, from where
    // JavaScript on the webpage will read the conversion data and will display promotions to the users
    // based on this data.
    fun loadCustomTabForSite(url: String) {
        val customTabsIntent = CustomTabsIntent.Builder(mCustomTabsSession)
            .setShowTitle(true)
            .build()

        customTabsIntent.launchUrl(this, Uri.parse(url))

    }

    companion object {
        private const val AF_DEV_KEY = "KEY"

        var InstallConversionData = ""
        var sessionCount = 0
        fun setInstallData(conversionData: Map<String, Any>) {
            if (sessionCount == 0) {
                val googlePlayInstallReferrer = """&googlePlayInstallReferrer=${conversionData["gp_referrer"]}""".trimIndent()
                val installCampaign = """&campaign=${conversionData["campaign"]}""".trimIndent()
                val adsetInfo = """&adset=${conversionData["adset"]}""".trimIndent()
                val adName = """&afadname=${conversionData["af_ad"]}""".trimIndent()
                val afAdset = """&afadset=${conversionData["af_adset"]}""".trimIndent()
                val installType = """install_type=${conversionData["af_status"]}""".trimIndent()
                val mediaSource = """&media_source=${conversionData["media_source"]}""".trimIndent()
                val installTime = """&install_time=${conversionData["install_time"]}""".trimIndent()
                val clickTime = """&click_time=${conversionData["click_time"]}""".trimIndent()
                val isFirstLaunch = """&is_first_launch=${conversionData["is_first_launch"]}""".trimIndent()
                val advertisingId = """&advertising_id=${conversionData["advertising_id"]}""".trimIndent()
                val timeStamp = """&timestamp=${conversionData["ts"]}""".trimIndent()

                InstallConversionData += installType +
                        googlePlayInstallReferrer +
                        installCampaign +
                        adsetInfo +
                        adName +
                        afAdset +
                        mediaSource +
                        installTime +
                        clickTime +
                        isFirstLaunch +
                        advertisingId +
                        timeStamp
                sessionCount++
            }
        }
    }

    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }

}
rickwalking commented 1 year ago

Hello @LupusX5. Were you able to resolve this issue and get conversation data on the TWA?