Skip to content

Commit

Permalink
Migrate CardClient Analytics Logic Into CardAnalytics Class (#297)
Browse files Browse the repository at this point in the history
* Creat CardAnalytics class.

* Re-factor approve order analytics names.

* Refactor additional analytics event names.

* Create new success without 3DS event.

* Implement CardAnalytics events for 3DS.

* Extract CardAnalytics from CardClient.

* Fix CardClientUnitTest after CardClient analytics refactor.

* Fix detekt errors.
  • Loading branch information
sshropshire authored Nov 25, 2024
1 parent 36bbdd2 commit 6ac1752
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.paypal.android.cardpayments

import com.paypal.android.corepayments.analytics.AnalyticsService

@Suppress("TooManyFunctions")
class CardAnalytics(
private val analyticsService: AnalyticsService
) {

// region Approve Order
fun notifyApproveOrderStarted(orderId: String) {
val eventName = "card-payments:approve-order:started"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderSucceeded(orderId: String) {
val eventName = "card-payments:approve-order:succeeded"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderFailed(orderId: String) {
val eventName = "card-payments:approve-order:failed"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderAuthChallengeReceived(orderId: String) {
val eventName = "card-payments:approve-order:auth-challenge-received"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderAuthChallengeStarted(orderId: String?) {
val eventName = "card-payments:approve-order:auth-challenge-started"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderAuthChallengeSucceeded(orderId: String) {
val eventName = "card-payments:approve-order:auth-challenge-succeeded"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderAuthChallengeCanceled(orderId: String?) {
val eventName = "card-payments:approve-order:auth-challenge-canceled"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderAuthChallengeFailed(orderId: String?) {
val eventName = "card-payments:approve-order:auth-challenge-failed"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}

fun notifyApproveOrderUnknownError(orderId: String?) {
val eventName = "card-payments:approve-order:unknown-error"
analyticsService.sendAnalyticsEvent(eventName, orderId)
}
// endregion

// region Vault
fun notifyVaultStarted(setupTokenId: String) {
val eventName = "card-payments:vault:started"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultSucceeded(setupTokenId: String) {
val eventName = "card-payments:vault:succeeded"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultFailed(setupTokenId: String?) {
val eventName = "card-payments:vault:failed"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultAuthChallengeReceived(setupTokenId: String) {
val eventName = "card-payments:vault:auth-challenge-received"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultAuthChallengeStarted(setupTokenId: String?) {
val eventName = "card-payments:vault:auth-challenge-started"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultAuthChallengeSucceeded(setupTokenId: String) {
val eventName = "card-payments:vault:auth-challenge-succeeded"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultAuthChallengeCanceled(setupTokenId: String?) {
val eventName = "card-payments:vault:auth-challenge-canceled"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultAuthChallengeFailed(setupTokenId: String?) {
val eventName = "card-payments:vault:auth-challenge-failed"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}

fun notifyVaultUnknownError(setupTokenId: String?) {
val eventName = "card-payments:vault:unknown-error"
analyticsService.sendAnalyticsEvent(eventName, setupTokenId)
}
// endregion
}
174 changes: 104 additions & 70 deletions CardPayments/src/main/java/com/paypal/android/cardpayments/CardClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
class CardClient internal constructor(
private val checkoutOrdersAPI: CheckoutOrdersAPI,
private val paymentMethodTokensAPI: DataVaultPaymentMethodTokensAPI,
private val analyticsService: AnalyticsService,
private val analytics: CardAnalytics,
private val authChallengeLauncher: CardAuthLauncher,
private val dispatcher: CoroutineDispatcher
) {
Expand All @@ -39,11 +39,15 @@ class CardClient internal constructor(

private var approveOrderId: String? = null

// TODO: remove once try-catch is removed
private val approveOrderExceptionHandler = CoreCoroutineExceptionHandler { error ->
notifyApproveOrderFailure(error, approveOrderId)
analytics.notifyApproveOrderUnknownError(null)
approveOrderListener?.onApproveOrderFailure(error)
}

// TODO: remove once try-catch is removed
private val vaultExceptionHandler = CoreCoroutineExceptionHandler { error ->
analytics.notifyVaultUnknownError(null)
cardVaultListener?.onVaultFailure(error)
}

Expand All @@ -56,7 +60,7 @@ class CardClient internal constructor(
constructor(context: Context, configuration: CoreConfig) : this(
CheckoutOrdersAPI(configuration),
DataVaultPaymentMethodTokensAPI(configuration),
AnalyticsService(context.applicationContext, configuration),
CardAnalytics(AnalyticsService(context.applicationContext, configuration)),
CardAuthLauncher(),
Dispatchers.Main
)
Expand All @@ -70,37 +74,27 @@ class CardClient internal constructor(
fun approveOrder(cardRequest: CardRequest) {
// TODO: deprecate this method and offer auth challenge integration pattern (similar to vault)
approveOrderId = cardRequest.orderId
analyticsService.sendAnalyticsEvent("card-payments:3ds:started", cardRequest.orderId)
analytics.notifyApproveOrderStarted(cardRequest.orderId)

CoroutineScope(dispatcher).launch(approveOrderExceptionHandler) {
// TODO: migrate away from throwing exceptions to result objects
try {
val response = checkoutOrdersAPI.confirmPaymentSource(cardRequest)
analyticsService.sendAnalyticsEvent(
"card-payments:3ds:confirm-payment-source:succeeded",
cardRequest.orderId
)

if (response.payerActionHref == null) {
analytics.notifyApproveOrderSucceeded(response.orderId)
val result =
response.run { CardResult(orderId = orderId, status = status?.name) }
notifyApproveOrderSuccess(result)
approveOrderListener?.onApproveOrderSuccess(result)
} else {
analyticsService.sendAnalyticsEvent(
"card-payments:3ds:confirm-payment-source:challenge-required",
cardRequest.orderId
)
analytics.notifyApproveOrderAuthChallengeReceived(cardRequest.orderId)
approveOrderListener?.onApproveOrderThreeDSecureWillLaunch()

val url = Uri.parse(response.payerActionHref)
val authChallenge = CardAuthChallenge.ApproveOrder(url, cardRequest)
approveOrderListener?.onApproveOrderAuthorizationRequired(authChallenge)
}
} catch (error: PayPalSDKError) {
analyticsService.sendAnalyticsEvent(
"card-payments:3ds:confirm-payment-source:failed",
cardRequest.orderId
)
analytics.notifyApproveOrderFailed(cardRequest.orderId)
throw error
}
}
Expand All @@ -116,81 +110,121 @@ class CardClient internal constructor(
* and card to use for vaulting.
*/
fun vault(context: Context, cardVaultRequest: CardVaultRequest) {
analytics.notifyVaultStarted(cardVaultRequest.setupTokenId)

val applicationContext = context.applicationContext
CoroutineScope(dispatcher).launch(vaultExceptionHandler) {
val updateSetupTokenResult = cardVaultRequest.run {
paymentMethodTokensAPI.updateSetupToken(applicationContext, setupTokenId, card)
}
try {
val updateSetupTokenResult = cardVaultRequest.run {
paymentMethodTokensAPI.updateSetupToken(applicationContext, setupTokenId, card)
}

val approveHref = updateSetupTokenResult.approveHref
if (approveHref == null) {
val result = updateSetupTokenResult.run { CardVaultResult(setupTokenId, status) }
cardVaultListener?.onVaultSuccess(result)
} else {
val url = Uri.parse(approveHref)
val authChallenge = CardAuthChallenge.Vault(url = url, request = cardVaultRequest)
cardVaultListener?.onVaultAuthorizationRequired(authChallenge)
val approveHref = updateSetupTokenResult.approveHref
if (approveHref == null) {
analytics.notifyVaultSucceeded(updateSetupTokenResult.setupTokenId)
val result =
updateSetupTokenResult.run { CardVaultResult(setupTokenId, status) }
cardVaultListener?.onVaultSuccess(result)
} else {
analytics.notifyVaultAuthChallengeReceived(updateSetupTokenResult.setupTokenId)
val url = Uri.parse(approveHref)
val authChallenge =
CardAuthChallenge.Vault(url = url, request = cardVaultRequest)
cardVaultListener?.onVaultAuthorizationRequired(authChallenge)
}
} catch (error: PayPalSDKError) {
analytics.notifyVaultFailed(cardVaultRequest.setupTokenId)
throw error
}
}
}

/**
* Present an auth challenge received from a [CardClient.approveOrder] or [CardClient.vault] result.
*/
fun presentAuthChallenge(activity: ComponentActivity, authChallenge: CardAuthChallenge) =
authChallengeLauncher.presentAuthChallenge(activity, authChallenge)
fun presentAuthChallenge(
activity: ComponentActivity,
authChallenge: CardAuthChallenge
): CardPresentAuthChallengeResult {
val result = authChallengeLauncher.presentAuthChallenge(activity, authChallenge)
captureAuthChallengePresentationAnalytics(result, authChallenge)
return result
}

private fun captureAuthChallengePresentationAnalytics(
result: CardPresentAuthChallengeResult,
authChallenge: CardAuthChallenge
) = when (result) {
is CardPresentAuthChallengeResult.Success -> {
when (authChallenge) {
// TODO: see if we can get order id from somewhere
is CardAuthChallenge.ApproveOrder ->
analytics.notifyApproveOrderAuthChallengeStarted(null)

// TODO: see if we can get setup token from somewhere
is CardAuthChallenge.Vault ->
analytics.notifyVaultAuthChallengeStarted(null)
}
}

is CardPresentAuthChallengeResult.Failure -> {
when (authChallenge) {
// TODO: see if we can get order id from somewhere
is CardAuthChallenge.ApproveOrder ->
analytics.notifyApproveOrderAuthChallengeFailed(null)

// TODO: see if we can get setup token id from somewhere
is CardAuthChallenge.Vault ->
analytics.notifyVaultAuthChallengeFailed(null)
}
}
}

fun completeAuthChallenge(intent: Intent, authState: String): CardStatus {
val status = authChallengeLauncher.completeAuthRequest(intent, authState)
when (status) {
is CardStatus.VaultSuccess -> notifyVaultSuccess(status.result)
is CardStatus.VaultError -> notifyVaultFailure(status.error)
is CardStatus.VaultCanceled -> notifyVaultCancelation()
is CardStatus.ApproveOrderError ->
notifyApproveOrderFailure(status.error, status.orderId)

is CardStatus.ApproveOrderSuccess -> notifyApproveOrderSuccess(status.result)
is CardStatus.ApproveOrderCanceled -> notifyApproveOrderCanceled(status.orderId)
is CardStatus.UnknownError -> {
Log.d("PayPalSDK", "An unknown error occurred: ${status.error.message}")
is CardStatus.VaultSuccess -> {
analytics.notifyVaultAuthChallengeSucceeded(status.result.setupTokenId)
cardVaultListener?.onVaultSuccess(status.result)
}

CardStatus.NoResult -> {
// ignore
is CardStatus.VaultError -> {
// TODO: see if we can access setup token id for analytics tracking
analytics.notifyVaultAuthChallengeFailed(null)
cardVaultListener?.onVaultFailure(status.error)
}
}
return status
}

private fun notifyApproveOrderCanceled(orderId: String?) {
analyticsService.sendAnalyticsEvent("card-payments:3ds:challenge:user-canceled", orderId)
approveOrderListener?.onApproveOrderCanceled()
}
is CardStatus.VaultCanceled -> {
// TODO: see if we can access setup token id for analytics tracking
analytics.notifyVaultAuthChallengeCanceled(null)
// TODO: consider either adding a listener method or next major version returning a result type
cardVaultListener?.onVaultFailure(PayPalSDKError(1, "User Canceled"))
}

private fun notifyApproveOrderSuccess(result: CardResult) {
analyticsService.sendAnalyticsEvent("card-payments:3ds:succeeded", result.orderId)
approveOrderListener?.onApproveOrderSuccess(result)
}
is CardStatus.ApproveOrderError -> {
analytics.notifyApproveOrderAuthChallengeFailed(status.orderId)
approveOrderListener?.onApproveOrderFailure(status.error)
}

private fun notifyApproveOrderFailure(error: PayPalSDKError, orderId: String?) {
analyticsService.sendAnalyticsEvent("card-payments:3ds:failed", orderId)
approveOrderListener?.onApproveOrderFailure(error)
}
is CardStatus.ApproveOrderSuccess -> {
analytics.notifyApproveOrderAuthChallengeSucceeded(status.result.orderId)
approveOrderListener?.onApproveOrderSuccess(status.result)
}

private fun notifyVaultSuccess(result: CardVaultResult) {
analyticsService.sendAnalyticsEvent("card:browser-login:canceled", result.setupTokenId)
cardVaultListener?.onVaultSuccess(result)
}
is CardStatus.ApproveOrderCanceled -> {
analytics.notifyApproveOrderAuthChallengeCanceled(status.orderId)
approveOrderListener?.onApproveOrderCanceled()
}

private fun notifyVaultFailure(error: PayPalSDKError) {
analyticsService.sendAnalyticsEvent("card:failed", null)
cardVaultListener?.onVaultFailure(error)
}
is CardStatus.UnknownError -> {
Log.d("PayPalSDK", "An unknown error occurred: ${status.error.message}")
}

private fun notifyVaultCancelation() {
analyticsService.sendAnalyticsEvent("paypal-web-payments:browser-login:canceled", null)
// TODO: consider either adding a listener method or next major version returning a result type
cardVaultListener?.onVaultFailure(PayPalSDKError(1, "User Canceled"))
CardStatus.NoResult -> {
// ignore
}
}
return status
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.paypal.android.cardpayments.api.CheckoutOrdersAPI
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResponse
import com.paypal.android.corepayments.OrderStatus
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.corepayments.analytics.AnalyticsService
import io.mockk.Called
import io.mockk.coEvery
import io.mockk.every
Expand Down Expand Up @@ -58,7 +57,7 @@ class CardClientUnitTest {
private val checkoutOrdersAPI = mockk<CheckoutOrdersAPI>(relaxed = true)
private val paymentMethodTokensAPI = mockk<DataVaultPaymentMethodTokensAPI>(relaxed = true)

private val analyticsService = mockk<AnalyticsService>(relaxed = true)
private val cardAnalytics = mockk<CardAnalytics>(relaxed = true)
private val confirmPaymentSourceResponse =
ConfirmPaymentSourceResponse(orderId, OrderStatus.APPROVED)

Expand Down Expand Up @@ -405,7 +404,7 @@ class CardClientUnitTest {
val sut = CardClient(
checkoutOrdersAPI,
paymentMethodTokensAPI,
analyticsService,
cardAnalytics,
cardAuthLauncher,
dispatcher
)
Expand Down

0 comments on commit 6ac1752

Please sign in to comment.