Skip to content

Commit

Permalink
[App Switch] Construct Uri to launch PayPal App (#1092)
Browse files Browse the repository at this point in the history
* Pass app linkk return uri as paremeter

* Update json parameters with uri

* Update UTs

* Catch Uri null

* Address PR comment

* Rename parameter

* Update validation to append new parameters

* Map payPalApprovalURL key from response

* Add and fix UTs

* Add method to verified if paypal is installed

* Check linkType to parse response model

* Add UTs and textures

* Add method to append query items and create app switch uri

* Fix tests and modify baToken validation

* Add linkType enum

* Update LinkType on venmo client

* Update PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.java

Change BraintreeException instead of Exception on internal client

Co-authored-by: sshropshire <[email protected]>

* Add missing import

* Change isPayPalInstalled method to property

* Linting

* Revert changes on isPayPalInstalled

* Lint LinkTye

* Revert linkTyope validation to instantiate PaymentResource

* Use linkType enum instead of String

* Fix UTs

* Add RestrictTo on LinkType enum

---------

Co-authored-by: sshropshire <[email protected]>
  • Loading branch information
richherrera and sshropshire authored Aug 6, 2024
1 parent 7e31bb2 commit ee52cee
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ class BraintreeClient @VisibleForTesting internal constructor(
this.launchesBrowserSwitchAsNewTask = launchesBrowserSwitchAsNewTask
}

fun isPayPalInstalled(): Boolean {
return deviceInspector.isPayPalInstalled(applicationContext)
}

private fun createAuthError(): BraintreeException {
val clientSDKSetupURL =
"https://developer.paypal.com/braintree/docs/guides/client-sdk/setup/android/v4#initialization"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.braintreepayments.api.core

import androidx.annotation.RestrictTo

/**
* Used to describe the link type for analytics
* Note: This enum is exposed for internal Braintree use only. Do not use.
* It is not covered by Semantic Versioning and may change or be removed at any time.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public enum class LinkType(val stringValue: String) {
UNIVERSAL("universal"),
DEEPLINK("deeplink")
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import com.braintreepayments.api.core.ApiClient;
import com.braintreepayments.api.core.BraintreeClient;
import com.braintreepayments.api.core.BraintreeException;
import com.braintreepayments.api.core.LinkType;
import com.braintreepayments.api.datacollector.DataCollector;
import com.braintreepayments.api.datacollector.DataCollectorInternalRequest;

Expand Down Expand Up @@ -45,8 +47,11 @@ class PayPalInternalClient {
this.appLink = (appLinkUri != null) ? appLinkUri.toString() : null;
}

void sendRequest(final Context context, final PayPalRequest payPalRequest,
final PayPalInternalClientCallback callback) {
void sendRequest(
final Context context,
final PayPalRequest payPalRequest,
final PayPalInternalClientCallback callback
) {
braintreeClient.getConfiguration((configuration, configError) -> {
if (configuration == null) {
callback.onResult(null, configError);
Expand All @@ -59,6 +64,10 @@ void sendRequest(final Context context, final PayPalRequest payPalRequest,
? SETUP_BILLING_AGREEMENT_ENDPOINT : CREATE_SINGLE_PAYMENT_ENDPOINT;
String url = String.format("/v1/%s", endpoint);
String appLinkReturn = isBillingAgreement ? appLink : null;

final LinkType linkType = (isBillingAgreement &&
((PayPalVaultRequest) payPalRequest).getEnablePayPalAppSwitch() &&
braintreeClient.isPayPalInstalled()) ? LinkType.UNIVERSAL : LinkType.DEEPLINK;

String requestBody = payPalRequest.createRequestBody(
configuration,
Expand Down Expand Up @@ -102,8 +111,20 @@ void sendRequest(final Context context, final PayPalRequest payPalRequest,
}

paymentAuthRequest
.clientMetadataId(clientMetadataId)
.approvalUrl(parsedRedirectUri.toString());
.clientMetadataId(clientMetadataId);

if (linkType == LinkType.UNIVERSAL) {
if (pairingId != null && !pairingId.isEmpty()) {
paymentAuthRequest
.approvalUrl(createAppSwitchUri(parsedRedirectUri).toString());
} else {
callback.onResult(null, new BraintreeException("Missing BA Token for PayPal App Switch."));
return;
}
} else {
paymentAuthRequest
.approvalUrl(parsedRedirectUri.toString());
}
}
callback.onResult(paymentAuthRequest, null);

Expand Down Expand Up @@ -137,6 +158,13 @@ void tokenize(PayPalAccount payPalAccount, final PayPalInternalTokenizeCallback
});
}

private Uri createAppSwitchUri(Uri uri) {
return uri.buildUpon()
.appendQueryParameter("source", "braintree_sdk")
.appendQueryParameter("switch_initiated_time", String.valueOf(System.currentTimeMillis()))
.build();
}

private String findPairingId(Uri redirectUri) {
String pairingId = redirectUri.getQueryParameter("ba_token");
if (pairingId == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class PayPalPaymentResource {
private static final String REDIRECT_URL_KEY = "redirectUrl";
private static final String AGREEMENT_SETUP_KEY = "agreementSetup";
private static final String APPROVAL_URL_KEY = "approvalUrl";
private static final String PAYPAL_APP_APPROVAL_URL_KEY = "paypalAppApprovalUrl";

private String redirectUrl;

Expand Down Expand Up @@ -47,7 +48,13 @@ static PayPalPaymentResource fromJson(String jsonString) throws JSONException {
payPalPaymentResource.redirectUrl(Json.optString(redirectJson, REDIRECT_URL_KEY, ""));
} else {
redirectJson = json.optJSONObject(AGREEMENT_SETUP_KEY);
payPalPaymentResource.redirectUrl(Json.optString(redirectJson, APPROVAL_URL_KEY, ""));
String payPalApprovalURL = Json.optString(redirectJson, PAYPAL_APP_APPROVAL_URL_KEY, "");

if (!payPalApprovalURL.isEmpty()) {
payPalPaymentResource.redirectUrl(payPalApprovalURL);
} else {
payPalPaymentResource.redirectUrl(Json.optString(redirectJson, APPROVAL_URL_KEY, ""));
}
}
return payPalPaymentResource;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static junit.framework.TestCase.assertNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
Expand Down Expand Up @@ -551,6 +552,66 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess(
assertEquals(expectedUrl, payPalPaymentAuthRequestParams.getApprovalUrl());
}

@Test
public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess_returnsPayPalURL() {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.configuration(configuration)
.authorizationSuccess(clientToken)
.appLinkReturnUri(Uri.parse("https://example.com"))
.sendPOSTSuccessfulResponse(Fixtures.PAYPAL_HERMES_RESPONSE_WITH_PAYPAL_REDIRECT_URL)
.isPayPalInstalled(true)
.build();

PayPalInternalClient sut = new PayPalInternalClient(braintreeClient, dataCollector, apiClient);

PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true);
payPalRequest.setUserAuthenticationEmail("[email protected]");
payPalRequest.setEnablePayPalAppSwitch(true);

sut.sendRequest(context, payPalRequest, payPalInternalClientCallback);

ArgumentCaptor<PayPalPaymentAuthRequestParams> captor = ArgumentCaptor.forClass(
PayPalPaymentAuthRequestParams.class);
verify(payPalInternalClientCallback).onResult(captor.capture(), (Exception) isNull());

PayPalPaymentAuthRequestParams payPalPaymentAuthRequestParams = captor.getValue();
assertTrue(payPalPaymentAuthRequestParams.isBillingAgreement());

Uri approvalUri = Uri.parse(payPalPaymentAuthRequestParams.getApprovalUrl());
String pairingId = approvalUri.getQueryParameter("ba_token");
assertNotNull(pairingId);
assertEquals(pairingId, payPalPaymentAuthRequestParams.getPairingId());
assertNotNull(approvalUri.getQueryParameter("source"));
assertNotNull(approvalUri.getQueryParameter("switch_initiated_time"));
assertEquals(approvalUri.getHost(), "paypal.com");
}

@Test
public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess_returnsApprovalURL() {
BraintreeClient braintreeClient = new MockBraintreeClientBuilder()
.configuration(configuration)
.authorizationSuccess(clientToken)
.appLinkReturnUri(Uri.parse("https://example.com"))
.sendPOSTSuccessfulResponse(Fixtures.PAYPAL_HERMES_RESPONSE_WITH_APPROVAL_URL)
.build();

PayPalInternalClient sut = new PayPalInternalClient(braintreeClient, dataCollector, apiClient);

PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true);

sut.sendRequest(context, payPalRequest, payPalInternalClientCallback);

ArgumentCaptor<PayPalPaymentAuthRequestParams> captor = ArgumentCaptor.forClass(
PayPalPaymentAuthRequestParams.class);
verify(payPalInternalClientCallback).onResult(captor.capture(), (Exception) isNull());

String expectedUrl = "https://www.example.com/some?ba_token=fake-ba-token";
PayPalPaymentAuthRequestParams payPalPaymentAuthRequestParams = captor.getValue();
assertTrue(payPalPaymentAuthRequestParams.isBillingAgreement());
assertEquals("fake-ba-token", payPalPaymentAuthRequestParams.getPairingId());
assertEquals(expectedUrl, payPalPaymentAuthRequestParams.getApprovalUrl());
}

@Test
public void sendRequest_withPayPalCheckoutRequest_callsBackPayPalResponseOnSuccess() {
when(dataCollector.getClientMetadataId(context, configuration, true)).thenReturn("sample-client-metadata-id");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ public void fromJson_parsesRedirectUrlFromBillingAgreementPaymentResource() thro
PayPalPaymentResource sut = PayPalPaymentResource.fromJson(billingAgreementJson);
assertEquals("www.example.com/redirect", sut.getRedirectUrl());
}

@Test
public void fromJson_parsesRedirectUrlFromBillingAgreementPaymentResource_returnsPayPalRedirectUrl() throws JSONException {
String billingAgreementJson = new JSONObject()
.put("agreementSetup", new JSONObject()
.put("approvalUrl", "www.example.com/redirect")
.put("paypalAppApprovalUrl", "www.paypal.example.com/redirect")
).toString();

PayPalPaymentResource sut = PayPalPaymentResource.fromJson(billingAgreementJson);
assertEquals("www.paypal.example.com/redirect", sut.getRedirectUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,27 @@ object Fixtures {
}
"""

// language=JSON
const val PAYPAL_HERMES_RESPONSE_WITH_PAYPAL_REDIRECT_URL = """
{
"agreementSetup":{
"tokenId":"fake-ba-token",
"approvalUrl":"https://www.example.com/some?ba_token=fake-ba-token",
"paypalAppApprovalUrl":"https://paypal.com/some?ba_token=fake-ba-token"
}
}
"""

// language=JSON
const val PAYPAL_HERMES_RESPONSE_WITH_APPROVAL_URL = """
{
"agreementSetup":{
"tokenId":"fake-ba-token",
"approvalUrl":"https://www.example.com/some?ba_token=fake-ba-token"
}
}
"""

// language=JSON
const val PAYPAL_OTC_RESPONSE = """
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class MockBraintreeClientBuilder {

private ActivityInfo activityInfo;
private boolean launchesBrowserSwitchAsNewTask;
private boolean isPayPalInstalled;

public MockBraintreeClientBuilder configuration(Configuration configuration) {
this.configuration = configuration;
Expand Down Expand Up @@ -113,6 +114,12 @@ public MockBraintreeClientBuilder launchesBrowserSwitchAsNewTask(
return this;
}

public MockBraintreeClientBuilder isPayPalInstalled(
boolean isPayPalInstalled) {
this.isPayPalInstalled = isPayPalInstalled;
return this;
}

public MockBraintreeClientBuilder appLinkReturnUri(
Uri appLinkReturnUri) {
this.appLinkReturnUri = appLinkReturnUri;
Expand All @@ -128,6 +135,7 @@ public BraintreeClient build() {
when(braintreeClient.getManifestActivityInfo(any())).thenReturn(activityInfo);
when(braintreeClient.launchesBrowserSwitchAsNewTask()).thenReturn(
launchesBrowserSwitchAsNewTask);
when(braintreeClient.isPayPalInstalled()).thenReturn(isPayPalInstalled);
when(braintreeClient.getAppLinkReturnUri()).thenReturn(appLinkReturnUri);

doAnswer((Answer<Void>) invocation -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.braintreepayments.api.core.BraintreeRequestCodes;
import com.braintreepayments.api.core.ClientToken;
import com.braintreepayments.api.core.Configuration;
import com.braintreepayments.api.core.LinkType;
import com.braintreepayments.api.core.MetadataBuilder;

import org.json.JSONException;
Expand All @@ -35,7 +36,7 @@
*/
public class VenmoClient {

private static final String LINK_TYPE = "universal";
private static final LinkType LINK_TYPE = LinkType.UNIVERSAL;
private final BraintreeClient braintreeClient;
private final VenmoApi venmoApi;
private final VenmoSharedPrefsWriter sharedPrefsWriter;
Expand Down Expand Up @@ -328,7 +329,7 @@ private void callbackTokenizeFailure(VenmoTokenizeCallback callback, VenmoResult
private AnalyticsEventParams getAnalyticsParams() {
AnalyticsEventParams eventParameters = new AnalyticsEventParams();
eventParameters.setPayPalContextId(payPalContextId);
eventParameters.setLinkType(LINK_TYPE);
eventParameters.setLinkType(LINK_TYPE.getStringValue());
eventParameters.setVaultRequest(isVaultRequest);
return eventParameters;
}
Expand Down

0 comments on commit ee52cee

Please sign in to comment.