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