diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/LinkType.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/LinkType.kt index 40eea266fe..68d0e72252 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/LinkType.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/LinkType.kt @@ -9,6 +9,6 @@ import androidx.annotation.RestrictTo */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) enum class LinkType(val stringValue: String) { - UNIVERSAL("universal"), - DEEPLINK("deeplink") + APP_SWITCH("universal"), + APP_LINK("deeplink") } diff --git a/Demo/src/main/res/layout/fragment_paypal.xml b/Demo/src/main/res/layout/fragment_paypal.xml index b1b1b9a7fb..ec515bcdc6 100644 --- a/Demo/src/main/res/layout/fragment_paypal.xml +++ b/Demo/src/main/res/layout/fragment_paypal.xml @@ -21,7 +21,8 @@ + android:layout_height="wrap_content" + android:inputType="textEmailAddress"/> diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalAnalytics.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalAnalytics.kt index 7789867d84..d8abc15df8 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalAnalytics.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalAnalytics.kt @@ -7,4 +7,13 @@ internal object PayPalAnalytics { const val TOKENIZATION_FAILED = "paypal:tokenize:failed" const val TOKENIZATION_SUCCEEDED = "paypal:tokenize:succeeded" const val BROWSER_LOGIN_CANCELED = "paypal:tokenize:browser-login:canceled" + + // Additional Conversion events + const val HANDLE_RETURN_STARTED = "paypal:tokenize:handle-return:started" + + // App Switch events + const val APP_SWITCH_STARTED = "paypal:tokenize:app-switch:started" + const val APP_SWITCH_SUCCEEDED = "paypal:tokenize:app-switch:succeeded" + const val APP_SWITCH_FAILED = "paypal:tokenize:app-switch:failed" + const val APP_SWITCH_CANCELED = "paypal:tokenize:app-switch:canceled" } diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt index b0bae0dade..21181f35fa 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt @@ -9,6 +9,7 @@ import com.braintreepayments.api.core.BraintreeClient import com.braintreepayments.api.core.BraintreeException import com.braintreepayments.api.core.BraintreeRequestCodes import com.braintreepayments.api.core.Configuration +import com.braintreepayments.api.core.LinkType import com.braintreepayments.api.core.UserCanceledException import com.braintreepayments.api.paypal.PayPalPaymentIntent.Companion.fromString import com.braintreepayments.api.sharedutils.Json @@ -29,6 +30,11 @@ class PayPalClient internal constructor( */ private var payPalContextId: String? = null + /** + * Used for sending the type of flow, universal vs deeplink to FPTI + */ + private var linkType: LinkType? = null + /** * True if `tokenize()` was called with a Vault request object type */ @@ -93,8 +99,17 @@ class PayPalClient internal constructor( error: Exception? -> if (payPalResponse != null) { payPalContextId = payPalResponse.pairingId + val isAppSwitchFlow = internalPayPalClient.isAppSwitchEnabled(payPalRequest) && + internalPayPalClient.isPayPalInstalled(context) + linkType = if (isAppSwitchFlow) LinkType.APP_SWITCH else LinkType.APP_LINK + try { payPalResponse.browserSwitchOptions = buildBrowserSwitchOptions(payPalResponse) + + if (isAppSwitchFlow) { + braintreeClient.sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_STARTED, analyticsParams) + } + callback.onPayPalPaymentAuthRequest( PayPalPaymentAuthRequest.ReadyToLaunch(payPalResponse) ) @@ -164,9 +179,17 @@ class PayPalClient internal constructor( val approvalUrl = Json.optString(metadata, "approval-url", null) val successUrl = Json.optString(metadata, "success-url", null) val paymentType = Json.optString(metadata, "payment-type", "unknown") - val isBillingAgreement = paymentType.equals("billing-agreement", ignoreCase = true) val tokenKey = if (isBillingAgreement) "ba_token" else "token" + val switchInitiatedTime = Uri.parse(approvalUrl).getQueryParameter("switch_initiated_time") + val isAppSwitchFlow = !switchInitiatedTime.isNullOrEmpty() + + if (isAppSwitchFlow) { + braintreeClient.sendAnalyticsEvent( + PayPalAnalytics.HANDLE_RETURN_STARTED, + analyticsParams + ) + } approvalUrl?.let { val pairingId = Uri.parse(approvalUrl).getQueryParameter(tokenKey) @@ -195,18 +218,19 @@ class PayPalClient internal constructor( if (payPalAccountNonce != null) { callbackTokenizeSuccess( callback, - PayPalResult.Success(payPalAccountNonce) + PayPalResult.Success(payPalAccountNonce), + isAppSwitchFlow ) } else if (error != null) { - callbackTokenizeFailure(callback, PayPalResult.Failure(error)) + callbackTokenizeFailure(callback, PayPalResult.Failure(error), isAppSwitchFlow) } } } catch (e: UserCanceledException) { - callbackBrowserSwitchCancel(callback, PayPalResult.Cancel) + callbackBrowserSwitchCancel(callback, PayPalResult.Cancel, isAppSwitchFlow) } catch (e: JSONException) { - callbackTokenizeFailure(callback, PayPalResult.Failure(e)) + callbackTokenizeFailure(callback, PayPalResult.Failure(e), isAppSwitchFlow) } catch (e: PayPalBrowserSwitchException) { - callbackTokenizeFailure(callback, PayPalResult.Failure(e)) + callbackTokenizeFailure(callback, PayPalResult.Failure(e), isAppSwitchFlow) } } @@ -258,25 +282,43 @@ class PayPalClient internal constructor( private fun callbackBrowserSwitchCancel( callback: PayPalTokenizeCallback, - cancel: PayPalResult.Cancel + cancel: PayPalResult.Cancel, + isAppSwitchFlow: Boolean ) { braintreeClient.sendAnalyticsEvent(PayPalAnalytics.BROWSER_LOGIN_CANCELED, analyticsParams) + + if (isAppSwitchFlow) { + braintreeClient.sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_CANCELED, analyticsParams) + } + callback.onPayPalResult(cancel) } private fun callbackTokenizeFailure( callback: PayPalTokenizeCallback, - failure: PayPalResult.Failure + failure: PayPalResult.Failure, + isAppSwitchFlow: Boolean ) { braintreeClient.sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_FAILED, analyticsParams) + + if (isAppSwitchFlow) { + braintreeClient.sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_FAILED, analyticsParams) + } + callback.onPayPalResult(failure) } private fun callbackTokenizeSuccess( callback: PayPalTokenizeCallback, - success: PayPalResult.Success + success: PayPalResult.Success, + isAppSwitchFlow: Boolean ) { braintreeClient.sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_SUCCEEDED, analyticsParams) + + if (isAppSwitchFlow) { + braintreeClient.sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_SUCCEEDED, analyticsParams) + } + callback.onPayPalResult(success) } @@ -284,6 +326,7 @@ class PayPalClient internal constructor( get() { return AnalyticsEventParams( payPalContextId = payPalContextId, + linkType = linkType?.stringValue, isVaultRequest = isVaultRequest ) } diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt index ff1616de41..55601fceff 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt @@ -153,12 +153,12 @@ internal class PayPalInternalClient( .build() } - private fun isAppSwitchEnabled(payPalRequest: PayPalRequest): Boolean { + fun isAppSwitchEnabled(payPalRequest: PayPalRequest): Boolean { return (payPalRequest is PayPalVaultRequest) && payPalRequest.enablePayPalAppSwitch } - private fun isPayPalInstalled(context: Context): Boolean { + fun isPayPalInstalled(context: Context): Boolean { return deviceInspector.isPayPalInstalled(context) } diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java index 1ddb5ce594..de3084e69b 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java @@ -284,6 +284,54 @@ public void createPaymentAuthRequest_whenCheckoutRequest_sendsPayPalRequestViaIn any(PayPalInternalClientCallback.class)); } + @Test + public void createPaymentAuthRequest_whenVaultRequest_sendsAppSwitchStartedEvent() { + PayPalVaultRequest payPalVaultRequest = new PayPalVaultRequest(true); + payPalVaultRequest.setUserAuthenticationEmail("some@email.com"); + payPalVaultRequest.setEnablePayPalAppSwitch(true); + payPalVaultRequest.setMerchantAccountId("sample-merchant-account-id"); + + PayPalPaymentAuthRequestParams paymentAuthRequest = new PayPalPaymentAuthRequestParams( + payPalVaultRequest, + null, + "https://example.com/approval/url", + "sample-client-metadata-id", + null, + "https://example.com/success/url" + ); + PayPalInternalClient payPalInternalClient = + new MockPayPalInternalClientBuilder().sendRequestSuccess(paymentAuthRequest) + .build(); + + when(payPalInternalClient.isPayPalInstalled(activity)).thenReturn(true); + when(payPalInternalClient.isAppSwitchEnabled(payPalVaultRequest)).thenReturn(true); + + BraintreeClient braintreeClient = + new MockBraintreeClientBuilder().configuration(payPalEnabledConfig).build(); + + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient); + sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PayPalPaymentAuthRequest.class); + verify(paymentAuthCallback).onPayPalPaymentAuthRequest(captor.capture()); + + PayPalPaymentAuthRequest request = captor.getValue(); + assertTrue(request instanceof PayPalPaymentAuthRequest.ReadyToLaunch); + PayPalPaymentAuthRequestParams paymentAuthRequestCaptured = + ((PayPalPaymentAuthRequest.ReadyToLaunch) request).getRequestParams(); + + BrowserSwitchOptions browserSwitchOptions = paymentAuthRequestCaptured.getBrowserSwitchOptions(); + assertEquals(BraintreeRequestCodes.PAYPAL.getCode(), browserSwitchOptions.getRequestCode()); + assertFalse(browserSwitchOptions.isLaunchAsNewTask()); + + + AnalyticsEventParams params = new AnalyticsEventParams(); + params.setVaultRequest(true); + params.setLinkType("universal"); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_STARTED, params); + } + @Test public void tokenize_withBillingAgreement_tokenizesResponseOnSuccess() throws JSONException { PayPalInternalClient payPalInternalClient = new MockPayPalInternalClientBuilder().build(); @@ -451,4 +499,115 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_callsBackResult() params.setVaultRequest(false); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_SUCCEEDED, params); } + + @Test + public void tokenize_whenPayPalInternalClientTokenizeResult_sendsAppSwitchSucceededEvents() + throws JSONException { + PayPalAccountNonce payPalAccountNonce = mock(PayPalAccountNonce.class); + PayPalInternalClient payPalInternalClient = + new MockPayPalInternalClientBuilder().tokenizeSuccess(payPalAccountNonce).build(); + + String approvalUrl = + "sample-scheme://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN&switch_initiated_time=17166111926211"; + + BrowserSwitchFinalResult.Success browserSwitchResult = mock(BrowserSwitchFinalResult.Success.class); + + when(browserSwitchResult.getRequestMetadata()).thenReturn( + new JSONObject().put("client-metadata-id", "sample-client-metadata-id") + .put("merchant-account-id", "sample-merchant-account-id") + .put("intent", "authorize").put("approval-url", approvalUrl) + .put("success-url", "https://example.com/success") + .put("payment-type", "single-payment")); + + Uri uri = Uri.parse(approvalUrl); + when(browserSwitchResult.getReturnUrl()).thenReturn(uri); + + PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( + browserSwitchResult); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient); + + sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PayPalResult.class); + verify(payPalTokenizeCallback).onPayPalResult(captor.capture()); + + PayPalResult result = captor.getValue(); + assertTrue(result instanceof PayPalResult.Success); + assertEquals(payPalAccountNonce, ((PayPalResult.Success) result).getNonce()); + + AnalyticsEventParams params = new AnalyticsEventParams(); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.HANDLE_RETURN_STARTED, params); + params.setPayPalContextId("EC-HERMES-SANDBOX-EC-TOKEN"); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_SUCCEEDED, params); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_SUCCEEDED, params); + } + + @Test + public void tokenize_whenPayPalNotEnabled_sendsAppSwitchFailedEvents() throws JSONException { + PayPalInternalClient payPalInternalClient = new MockPayPalInternalClientBuilder().build(); + + String approvalUrl = "https://some-scheme/onetouch/v1/cancel?switch_initiated_time=17166111926211"; + + BrowserSwitchFinalResult.Success browserSwitchResult = mock(BrowserSwitchFinalResult.Success.class); + + when(browserSwitchResult.getRequestMetadata()).thenReturn( + new JSONObject().put("client-metadata-id", "sample-client-metadata-id") + .put("merchant-account-id", "sample-merchant-account-id") + .put("intent", "authorize").put("approval-url", "https://some-scheme/onetouch/v1/cancel?token=SOME-BA&switch_initiated_time=17166111926211") + .put("success-url", "https://example.com/cancel") + .put("payment-type", "single-payment")); + + Uri uri = Uri.parse(approvalUrl); + when(browserSwitchResult.getReturnUrl()).thenReturn(uri); + + PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( + browserSwitchResult); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient); + + sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); + + AnalyticsEventParams params = new AnalyticsEventParams(); + params.setPayPalContextId("SOME-BA"); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_FAILED, params); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_FAILED, params); + } + + @Test + public void tokenize_whenCancelUriReceived_sendsAppSwitchCanceledEvents() + throws JSONException { + PayPalInternalClient payPalInternalClient = new MockPayPalInternalClientBuilder().build(); + + String approvalUrl = "https://some-scheme/onetouch/v1/cancel?switch_initiated_time=17166111926211"; + + BrowserSwitchFinalResult.Success browserSwitchResult = mock(BrowserSwitchFinalResult.Success.class); + + when(browserSwitchResult.getRequestMetadata()).thenReturn( + new JSONObject().put("client-metadata-id", "sample-client-metadata-id") + .put("merchant-account-id", "sample-merchant-account-id") + .put("intent", "authorize").put("approval-url", approvalUrl) + .put("success-url", "https://example.com/success") + .put("payment-type", "single-payment")); + + Uri uri = Uri.parse(approvalUrl); + when(browserSwitchResult.getReturnUrl()).thenReturn(uri); + + PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( + browserSwitchResult); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient); + + sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PayPalResult.class); + verify(payPalTokenizeCallback).onPayPalResult(captor.capture()); + + PayPalResult result = captor.getValue(); + assertTrue(result instanceof PayPalResult.Cancel); + + AnalyticsEventParams params = new AnalyticsEventParams(); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.BROWSER_LOGIN_CANCELED, params); + verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_CANCELED, params); + } } \ No newline at end of file