Skip to content

Commit

Permalink
Add app switch analytics calls for Venmo (#1218)
Browse files Browse the repository at this point in the history
* WIP - Add app switch analytics calls for Venmo

* refactoring

* fix tests

* Add url parameter for passing the appSwitchUrl for Venmo analytic events

* Save Venmo URL to VenmoRepository and send value with Venmo analytic events

* Venmo Analytics - add additional app switch and handle return events

---------

Co-authored-by: Sai <[email protected]>
  • Loading branch information
tdchow and saperi22 authored Nov 21, 2024
1 parent 9966290 commit 12e3bb0
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class AnalyticsClient internal constructor(
private val configurationLoader: ConfigurationLoader = ConfigurationLoader.instance,
private val merchantRepository: MerchantRepository = MerchantRepository.instance
) {

private val applicationContext: Context
get() = merchantRepository.applicationContext

Expand All @@ -43,7 +44,8 @@ class AnalyticsClient internal constructor(
endTime = analyticsEventParams.endTime,
endpoint = analyticsEventParams.endpoint,
experiment = analyticsEventParams.experiment,
paymentMethodsDisplayed = analyticsEventParams.paymentMethodsDisplayed
paymentMethodsDisplayed = analyticsEventParams.paymentMethodsDisplayed,
appSwitchUrl = analyticsEventParams.appSwitchUrl
)
configurationLoader.loadConfiguration { result ->
if (result is ConfigurationLoaderResult.Success) {
Expand Down Expand Up @@ -239,6 +241,7 @@ class AnalyticsClient internal constructor(
.putOpt(FPTI_KEY_MERCHANT_EXPERIMENT, event.experiment)
.putOpt(FPTI_KEY_MERCHANT_PAYMENT_METHODS_DISPLAYED,
event.paymentMethodsDisplayed.ifEmpty { null })
.putOpt(FPTI_KEY_URL, event.appSwitchUrl)
return json.toString()
}

Expand Down Expand Up @@ -270,6 +273,9 @@ class AnalyticsClient internal constructor(
}

companion object {

val lazyInstance: Lazy<AnalyticsClient> = lazy { AnalyticsClient() }

private const val FPTI_ANALYTICS_URL = "https://api-m.paypal.com/v1/tracking/batch/events"

private const val FPTI_KEY_PAYPAL_CONTEXT_ID = "paypal_context_id"
Expand All @@ -288,6 +294,7 @@ class AnalyticsClient internal constructor(
private const val FPTI_KEY_ENDPOINT = "endpoint"
private const val FPTI_KEY_MERCHANT_EXPERIMENT = "experiment"
private const val FPTI_KEY_MERCHANT_PAYMENT_METHODS_DISPLAYED = "payment_methods_displayed"
private const val FPTI_KEY_URL = "url"

private const val FPTI_BATCH_KEY_VENMO_INSTALLED = "venmo_installed"
private const val FPTI_BATCH_KEY_PAYPAL_INSTALLED = "paypal_installed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ internal data class AnalyticsEvent(
val endTime: Long? = null,
val endpoint: String? = null,
val experiment: String? = null,
val paymentMethodsDisplayed: List<String> = emptyList()
val paymentMethodsDisplayed: List<String> = emptyList(),
val appSwitchUrl: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ data class AnalyticsEventParams @JvmOverloads constructor(
var endTime: Long? = null,
var endpoint: String? = null,
val experiment: String? = null,
val paymentMethodsDisplayed: List<String> = emptyList()
val paymentMethodsDisplayed: List<String> = emptyList(),
val appSwitchUrl: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ internal class ConfigurationLoader(
* TODO: AnalyticsClient must be lazy due to the circular dependency between ConfigurationLoader and AnalyticsClient
* This should be refactored to remove the circular dependency.
*/
lazyAnalyticsClient: Lazy<AnalyticsClient> = lazy { AnalyticsClient(httpClient) },
lazyAnalyticsClient: Lazy<AnalyticsClient> = lazy {
AnalyticsClient(httpClient = httpClient)
},
) {
private val analyticsClient: AnalyticsClient by lazyAnalyticsClient

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class AnalyticsClientUnitTest {
private lateinit var sut: AnalyticsClient

private var timestamp: Long = 0
private val returnUrlScheme = "com.braintreepayments.demo.braintree"

@Before
@Throws(InvalidArgumentException::class, GeneralSecurityException::class, IOException::class)
Expand Down Expand Up @@ -74,6 +75,7 @@ class AnalyticsClientUnitTest {
every { time.currentTime } returns 123
every { merchantRepository.authorization } returns authorization
every { merchantRepository.applicationContext } returns context
every { merchantRepository.returnUrlScheme } returns returnUrlScheme

configurationLoader = MockkConfigurationLoaderBuilder()
.configuration(configuration)
Expand Down Expand Up @@ -103,7 +105,7 @@ class AnalyticsClientUnitTest {
)
} returns mockk()

sut.sendEvent(eventName)
sut.sendEvent(eventName, AnalyticsEventParams(appSwitchUrl = returnUrlScheme))

val workSpec = workRequestSlot.captured.workSpec
assertEquals(AnalyticsWriteToDbWorker::class.java.name, workSpec.workerClassName)
Expand All @@ -115,7 +117,8 @@ class AnalyticsClientUnitTest {
"event_name": "sample-event-name",
"t": 123,
"is_vault": false,
"tenant_name": "Braintree"
"tenant_name": "Braintree",
"url": "$returnUrlScheme"
}
"""
val actualJSON = workSpec.input.getString(WORK_INPUT_KEY_ANALYTICS_JSON)!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ internal object VenmoAnalytics {
const val TOKENIZE_SUCCEEDED = "venmo:tokenize:succeeded"
const val APP_SWITCH_CANCELED = "venmo:tokenize:app-switch:canceled"

// Additional Detail Events
// Launching App Switch events
const val APP_SWITCH_STARTED = "venmo:tokenize:app-switch:started"
const val APP_SWITCH_SUCCEEDED = "venmo:tokenize:app-switch:succeeded"
const val APP_SWITCH_FAILED = "venmo:tokenize:app-switch:failed"

// Handle return events
const val HANDLE_RETURN_STARTED = "venmo:tokenize:handle-return:started"
const val HANDLE_RETURN_SUCCEEDED = "venmo:tokenize:handle-return:succeeded"
const val HANDLE_RETURN_FAILED = "venmo:tokenize:handle-return:failed"
const val HANDLE_RETURN_NO_RESULT = "venmo:tokenize:handle-return:no-result"
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class VenmoClient internal constructor(
private val sharedPrefsWriter: VenmoSharedPrefsWriter = VenmoSharedPrefsWriter(),
private val analyticsParamRepository: AnalyticsParamRepository = AnalyticsParamRepository.instance,
private val merchantRepository: MerchantRepository = MerchantRepository.instance,
private val venmoRepository: VenmoRepository = VenmoRepository.instance
) {
/**
* Used for linking events from the client to server side request
Expand Down Expand Up @@ -183,6 +184,8 @@ class VenmoClient internal constructor(
.appendQueryParameter("customerClient", "MOBILE_APP")
.build()

venmoRepository.venmoUrl = venmoBaseURL

val browserSwitchOptions = BrowserSwitchOptions()
.requestCode(BraintreeRequestCodes.VENMO.code)
.url(venmoBaseURL)
Expand Down Expand Up @@ -335,7 +338,7 @@ class VenmoClient internal constructor(

private val analyticsParams: AnalyticsEventParams
get() {
val eventParameters = AnalyticsEventParams()
val eventParameters = AnalyticsEventParams(appSwitchUrl = venmoRepository.venmoUrl.toString())
eventParameters.payPalContextId = payPalContextId
eventParameters.linkType = LINK_TYPE
eventParameters.isVaultRequest = isVaultRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ import com.braintreepayments.api.BrowserSwitchClient
import com.braintreepayments.api.BrowserSwitchException
import com.braintreepayments.api.BrowserSwitchFinalResult
import com.braintreepayments.api.BrowserSwitchStartResult
import com.braintreepayments.api.core.AnalyticsClient
import com.braintreepayments.api.core.AnalyticsEventParams
import com.braintreepayments.api.core.BraintreeException

/**
* Responsible for launching the Venmo app to authenticate users
*/
class VenmoLauncher internal constructor(
private val browserSwitchClient: BrowserSwitchClient
private val browserSwitchClient: BrowserSwitchClient,
private val venmoRepository: VenmoRepository,
lazyAnalyticsClient: Lazy<AnalyticsClient>,
) {
constructor() : this(BrowserSwitchClient())

constructor() : this(
browserSwitchClient = BrowserSwitchClient(),
venmoRepository = VenmoRepository.instance,
lazyAnalyticsClient = AnalyticsClient.lazyInstance
)

private val analyticsClient: AnalyticsClient by lazyAnalyticsClient

/**
* Launches the Venmo authentication flow by switching to the Venmo app or a mobile browser, if
Expand All @@ -32,9 +43,11 @@ class VenmoLauncher internal constructor(
activity: ComponentActivity,
paymentAuthRequest: VenmoPaymentAuthRequest.ReadyToLaunch
): VenmoPendingRequest {
analyticsClient.sendEvent(VenmoAnalytics.APP_SWITCH_STARTED, analyticsEventParams)
try {
assertCanPerformBrowserSwitch(activity, paymentAuthRequest.requestParams)
} catch (browserSwitchException: BrowserSwitchException) {
analyticsClient.sendEvent(VenmoAnalytics.APP_SWITCH_FAILED, analyticsEventParams)
val manifestInvalidError = createBrowserSwitchError(browserSwitchException)
return VenmoPendingRequest.Failure(manifestInvalidError)
}
Expand All @@ -43,8 +56,15 @@ class VenmoLauncher internal constructor(
paymentAuthRequest.requestParams.browserSwitchOptions
)
return when (request) {
is BrowserSwitchStartResult.Failure -> VenmoPendingRequest.Failure(request.error)
is BrowserSwitchStartResult.Started -> VenmoPendingRequest.Started(request.pendingRequest)
is BrowserSwitchStartResult.Failure -> {
analyticsClient.sendEvent(VenmoAnalytics.APP_SWITCH_FAILED, analyticsEventParams)
VenmoPendingRequest.Failure(request.error)
}

is BrowserSwitchStartResult.Started -> {
analyticsClient.sendEvent(VenmoAnalytics.APP_SWITCH_SUCCEEDED, analyticsEventParams)
VenmoPendingRequest.Started(request.pendingRequest)
}
}
}

Expand All @@ -71,15 +91,23 @@ class VenmoLauncher internal constructor(
pendingRequest: VenmoPendingRequest.Started,
intent: Intent
): VenmoPaymentAuthResult {
analyticsClient.sendEvent(VenmoAnalytics.HANDLE_RETURN_STARTED, analyticsEventParams)
return when (val browserSwitchResult =
browserSwitchClient.completeRequest(intent, pendingRequest.pendingRequestString)) {
is BrowserSwitchFinalResult.Success -> VenmoPaymentAuthResult.Success(
browserSwitchResult
)
is BrowserSwitchFinalResult.Success -> {
analyticsClient.sendEvent(VenmoAnalytics.HANDLE_RETURN_SUCCEEDED, analyticsEventParams)
VenmoPaymentAuthResult.Success(browserSwitchResult)
}

is BrowserSwitchFinalResult.Failure -> VenmoPaymentAuthResult.Failure(browserSwitchResult.error)
is BrowserSwitchFinalResult.Failure -> {
analyticsClient.sendEvent(VenmoAnalytics.HANDLE_RETURN_FAILED, analyticsEventParams)
VenmoPaymentAuthResult.Failure(browserSwitchResult.error)
}

is BrowserSwitchFinalResult.NoResult -> VenmoPaymentAuthResult.NoResult
is BrowserSwitchFinalResult.NoResult -> {
analyticsClient.sendEvent(VenmoAnalytics.HANDLE_RETURN_NO_RESULT, analyticsEventParams)
VenmoPaymentAuthResult.NoResult
}
}
}

Expand All @@ -104,14 +132,18 @@ class VenmoLauncher internal constructor(
browserSwitchClient.assertCanPerformBrowserSwitch(activity, params.browserSwitchOptions)
}

private val analyticsEventParams by lazy {
AnalyticsEventParams(appSwitchUrl = venmoRepository.venmoUrl.toString())
}

companion object {
private const val VENMO_PACKAGE_NAME = "com.venmo"
private fun createBrowserSwitchError(exception: BrowserSwitchException): Exception {
return BraintreeException(
"AndroidManifest.xml is incorrectly configured or another app defines the same " +
"browser switch url as this app. See https://developer.paypal.com/" +
"braintree/docs/guides/client-sdk/setup/android/v4#browser-switch-setup " +
"for the correct configuration: " + exception.message
"browser switch url as this app. See https://developer.paypal.com/" +
"braintree/docs/guides/client-sdk/setup/android/v4#browser-switch-setup " +
"for the correct configuration: " + exception.message
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.braintreepayments.api.venmo

import android.net.Uri

/**
* An internal, in memory repository that holds properties specific for the Venmo payment flow.
*/
internal class VenmoRepository {

/**
* The Venmo URL that is used to load the CCT or app switch into the Venmo payment flow.
*/
var venmoUrl: Uri? = null

companion object {

/**
* Singleton instance of the VenmoRepository.
*/
val instance: VenmoRepository by lazy { VenmoRepository() }
}
}
Loading

0 comments on commit 12e3bb0

Please sign in to comment.