Skip to content

Commit

Permalink
Allow Manual Browser Switch Handling (#730)
Browse files Browse the repository at this point in the history
* Add fetchBrowserSwitchResultIfOneExists to PayPalClient and un-deprecate BraintreeClient only constructor and onBrowserSwitchResult with callback method.

* Add PayPal Manual Browser Switch integration pattern.

* Add Manual browser switch code snippet to migration guide.

* Update MIGRATION guide for manual browser switching.

* Clean up PayPalClient methods.

* Update docs.

* Add docs for new parseBrowserSwitch result.

* Write documentation for updated PayPalClient manual browser switching logic.

* Update naming to match plurality.

* Update clients to use new browser switch methods.

* Add tokenize call to migration guide steps.

* Add TODO for Android 9 workaround.

* Use browser switch SNAPSHOT build.

* Update browser switch dependency.

* Add manual browser switching methods to LocalPaymentClient.

* Add support for manual browser switching in local payment fragment.

Signed-off-by: Jax DesMarais-Leder <[email protected]>

* Add composition tests for BraintreeClient.

* Add PayPalClient unit tests for manual browser switching.

* Add LocalPaymentClient unit test.

* Remove unecessary mention of Android 9 onNewIntent bug; the new manual integration pattern should factor this out.

* Update changelog and docs strings.

* Update CHANGELOG.md

Co-authored-by: Jax DesMarais-Leder <[email protected]>

* Update CHANGELOG.md

Co-authored-by: Jax DesMarais-Leder <[email protected]>

* Add Kotlin version bump to CHANGELOG.

---------

Signed-off-by: Jax DesMarais-Leder <[email protected]>
Co-authored-by: Jax DesMarais-Leder <[email protected]>
  • Loading branch information
sshropshire and jaxdesmarais authored Jun 15, 2023
1 parent 0f26335 commit 8deaff5
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.braintreepayments.api

import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import androidx.annotation.RestrictTo
Expand Down Expand Up @@ -345,6 +346,20 @@ open class BraintreeClient @VisibleForTesting internal constructor(
return browserSwitchClient.deliverResultFromCache(context)
}

/**
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun parseBrowserSwitchResult(context: Context, requestCode: Int, intent: Intent?) =
browserSwitchClient.parseResult(context, requestCode, intent)

/**
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun clearActiveBrowserSwitchRequests(context: Context) =
browserSwitchClient.clearActiveRequests(context)

/**
* @suppress
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.braintreepayments.api

import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import androidx.fragment.app.FragmentActivity
Expand All @@ -13,6 +14,7 @@ import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
Expand Down Expand Up @@ -447,6 +449,31 @@ class BraintreeClientUnitTest {
verify { browserSwitchClient.deliverResultFromCache(context) }
}

@Test
fun parseBrowserSwitchResult_forwardsInvocationToBrowserSwitchClient() {
val context = mockk<Context>(relaxed = true)
val params = createDefaultParams(configurationLoader, authorizationLoader)

val expected = mock<BrowserSwitchResult>()
val intent = Intent()
every { browserSwitchClient.parseResult(context, 123, intent) } returns expected

val sut = BraintreeClient(params)
val actual = sut.parseBrowserSwitchResult(context, 123, intent)
assertSame(expected, actual)
}

@Test
fun clearActiveBrowserSwitchRequests_forwardsInvocationToBrowserSwitchClient() {
val context = mockk<Context>(relaxed = true)
val params = createDefaultParams(configurationLoader, authorizationLoader)

val sut = BraintreeClient(params)
sut.clearActiveBrowserSwitchRequests(context)

verify { browserSwitchClient.clearActiveRequests(context) }
}

@Test
@Throws(BrowserSwitchException::class)
fun assertCanPerformBrowserSwitch_assertsBrowserSwitchIsPossible() {
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## unreleased

* Bump target Kotlin version to `1.8.0`
* PayPal
* Undeprecate `PayPalClient(BraintreeClient)` constructor
* Undeprecate `PayPalClient#onBrowserSwitchResult(BrowserSwitchResult, PayPalBrowserSwitchResultCallback)`
* Add `PayPalClient#parseBrowserSwitchResult(Context, Intent)` method
* Add `PayPalClient#clearActiveBrowserSwitchRequests(Context)` method
* LocalPayment
* Undeprecate `LocalPaymentClient(BraintreeClient)` constructor
* Undeprecate `LocalPaymentClient#onBrowserSwitchResult(Context, BrowserSwitchResult, LocalPaymentBrowserSwitchResultCallback)`
* Add `LocalPaymentClient#parseBrowserSwitchResult(Context, Intent)` method
* Add `LocalPaymentClient#clearActiveBrowserSwitchRequests(Context)` method
* Venmo
* Fix issue caused by `VenmoActivityResultContract` where a user cancelation is being misinterpreted as an unknown exception because the intent data is `null` (fixes #734)
* Add the following properties to `VenmoRequest`:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.braintreepayments.demo.test;

import static com.braintreepayments.AutomatorAction.click;
import static com.braintreepayments.AutomatorAssertion.text;
import static com.braintreepayments.DeviceAutomator.onDevice;
import static com.braintreepayments.UiObjectMatcher.withText;
import static com.braintreepayments.UiObjectMatcher.withTextStartingWith;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;

import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;

import com.braintreepayments.demo.test.utilities.TestHelper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4ClassRunner.class)
public class PayPalManualBrowserSwitchTest extends TestHelper {

@Before
public void setup() {
super.setup();
launchApp("Mock PayPal");
onDevice(withText("PayPal")).waitForEnabled().perform(click());

PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())
.edit()
.putBoolean("enable_manual_browser_switching", true)
.commit();
}

@Test(timeout = 60000)
public void browserSwitch_makesASinglePayment() {
onDevice(withText("Single Payment")).waitForEnabled().perform(click());
onDevice(withText("Proceed with Sandbox Purchase")).waitForExists();
onDevice(withText("Proceed with Sandbox Purchase")).perform(click());

getNonceDetails().check(text(containsString("Email: [email protected]")));

onDevice(withText("Create a Transaction")).perform(click());
onDevice(withTextStartingWith("created")).check(text(endsWith("authorized")));
}

@Test(timeout = 60000)
public void browserSwitch_makesABillingAgreement() {
onDevice(withText("Billing Agreement")).waitForEnabled().perform(click());
onDevice(withText("Proceed with Sandbox Purchase")).waitForExists();
onDevice(withText("Proceed with Sandbox Purchase")).perform(click());

getNonceDetails().check(text(containsString("Email: [email protected]")));

onDevice(withText("Create a Transaction")).perform(click());
onDevice(withTextStartingWith("created")).check(text(endsWith("authorized")));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.braintreepayments.demo;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
Expand All @@ -11,6 +13,7 @@
import androidx.navigation.fragment.NavHostFragment;

import com.braintreepayments.api.BraintreeClient;
import com.braintreepayments.api.BrowserSwitchResult;
import com.braintreepayments.api.LocalPaymentClient;
import com.braintreepayments.api.LocalPaymentListener;
import com.braintreepayments.api.LocalPaymentNonce;
Expand All @@ -21,6 +24,8 @@ public class LocalPaymentFragment extends BaseFragment implements LocalPaymentLi

private LocalPaymentClient localPaymentClient;

private boolean useManualBrowserSwitch;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Expand All @@ -29,12 +34,35 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
mIdealButton.setOnClickListener(this::launchIdeal);

BraintreeClient braintreeClient = getBraintreeClient();
localPaymentClient = new LocalPaymentClient(this, braintreeClient);
localPaymentClient.setListener(this);

useManualBrowserSwitch = Settings.isManualBrowserSwitchingEnabled(requireActivity());
if (useManualBrowserSwitch) {
localPaymentClient = new LocalPaymentClient(braintreeClient);
} else {
localPaymentClient = new LocalPaymentClient(this, braintreeClient);
localPaymentClient.setListener(this);
}
return view;
}

@Override
public void onResume() {
super.onResume();
if (useManualBrowserSwitch) {
Activity activity = requireActivity();
BrowserSwitchResult browserSwitchResult =
localPaymentClient.parseBrowserSwitchResult(activity, activity.getIntent());
if (browserSwitchResult != null) {
handleManualBrowserSwitchResult(browserSwitchResult);
}
}
}

private void handleManualBrowserSwitchResult(BrowserSwitchResult browserSwitchResult) {
Context context = requireContext();
localPaymentClient.onBrowserSwitchResult(context, browserSwitchResult, this::handleLocalPaymentResult);
localPaymentClient.clearActiveBrowserSwitchRequests(context);
}

public void launchIdeal(View v) {
if (!Settings.SANDBOX_ENV_NAME.equals(Settings.getEnvironment(getActivity()))) {
handleError(new Exception("To use this feature, enable the \"Sandbox\" environment."));
Expand Down
42 changes: 38 additions & 4 deletions Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.braintreepayments.demo.PayPalRequestFactory.createPayPalCheckoutRequest;
import static com.braintreepayments.demo.PayPalRequestFactory.createPayPalVaultRequest;

import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
Expand All @@ -15,6 +16,7 @@
import androidx.navigation.fragment.NavHostFragment;

import com.braintreepayments.api.BraintreeClient;
import com.braintreepayments.api.BrowserSwitchResult;
import com.braintreepayments.api.DataCollector;
import com.braintreepayments.api.PayPalAccountNonce;
import com.braintreepayments.api.PayPalClient;
Expand All @@ -28,8 +30,11 @@ public class PayPalFragment extends BaseFragment implements PayPalListener {

private BraintreeClient braintreeClient;
private PayPalClient payPalClient;

private DataCollector dataCollector;

private boolean useManualBrowserSwitch;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Expand All @@ -42,13 +47,42 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c

braintreeClient = getBraintreeClient();

payPalClient = new PayPalClient(this, braintreeClient);
payPalClient.setListener(this);
useManualBrowserSwitch = Settings.isManualBrowserSwitchingEnabled(requireActivity());
if (useManualBrowserSwitch) {
payPalClient = new PayPalClient(braintreeClient);
} else {
payPalClient = new PayPalClient(this, braintreeClient);
payPalClient.setListener(this);
}

amount = RandomDollarAmount.getNext();
return view;
}

@Override
public void onResume() {
super.onResume();
if (useManualBrowserSwitch) {
Activity activity = requireActivity();
BrowserSwitchResult browserSwitchResult =
payPalClient.parseBrowserSwitchResult(activity, activity.getIntent());
if (browserSwitchResult != null) {
handleBrowserSwitchResult(browserSwitchResult);
}
}
}

private void handleBrowserSwitchResult(BrowserSwitchResult browserSwitchResult) {
payPalClient.onBrowserSwitchResult(browserSwitchResult, ((payPalAccountNonce, error) -> {
if (payPalAccountNonce != null) {
handlePayPalResult(payPalAccountNonce);
} else if (error != null) {
handleError(error);
}
}));
payPalClient.clearActiveBrowserSwitchRequests(requireContext());
}

public void launchSinglePayment(View v) {
launchPayPal(false);
}
Expand Down Expand Up @@ -90,7 +124,7 @@ private void handlePayPalResult(PaymentMethodNonce paymentMethodNonce) {
super.onPaymentMethodNonceCreated(paymentMethodNonce);

PayPalFragmentDirections.ActionPayPalFragmentToDisplayNonceFragment action =
PayPalFragmentDirections.actionPayPalFragmentToDisplayNonceFragment(paymentMethodNonce);
PayPalFragmentDirections.actionPayPalFragmentToDisplayNonceFragment(paymentMethodNonce);
action.setTransactionAmount(amount);
action.setDeviceData(deviceData);

Expand All @@ -100,7 +134,7 @@ private void handlePayPalResult(PaymentMethodNonce paymentMethodNonce) {

@Override
public void onPayPalSuccess(@NonNull PayPalAccountNonce payPalAccountNonce) {
handlePayPalResult(payPalAccountNonce);
handlePayPalResult(payPalAccountNonce);
}

@Override
Expand Down
4 changes: 4 additions & 0 deletions Demo/src/main/java/com/braintreepayments/demo/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,8 @@ public static boolean vaultVenmo(Context context) {
public static boolean isAmexRewardsBalanceEnabled(Context context) {
return getPreferences(context).getBoolean("amex_rewards_balance", false);
}

public static boolean isManualBrowserSwitchingEnabled(Context context) {
return getPreferences(context).getBoolean("enable_manual_browser_switching", false);
}
}
3 changes: 3 additions & 0 deletions Demo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@
<string name="paypal_request_address_scope">Request Address Scope</string>
<string name="paypal_request_address_scope_summary">Request address scope from the user</string>
<string name="three_d_secure">3D Secure</string>
<string name="browser_switch">Browser Switch</string>
<string name="enable_three_d_secure">Enable 3D Secure</string>
<string name="enable_three_d_secure_summary">3D Secure will automatically be run on all Card payment methods</string>
<string name="enable_manual_browser_switching">Enable Manual Browser Switching</string>
<string name="enable_manual_browser_switching_summary">Use alternate integration pattern to give the Demo app more control over browser switching</string>
<string name="require_three_d_secure">Require 3D Secure</string>
<string name="require_three_d_secure_summary">Requires a successful authentication for 3D Secure transactions</string>
<string name="paypal_disable_signature_verification">Disable app switch signature verification</string>
Expand Down
11 changes: 11 additions & 0 deletions Demo/src/main/res/xml/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,15 @@

</PreferenceCategory>

<PreferenceCategory
android:title="@string/browser_switch">

<CheckBoxPreference
android:key="enable_manual_browser_switching"
android:title="@string/enable_manual_browser_switching"
android:summary="@string/enable_manual_browser_switching_summary"
android:defaultValue="false" />

</PreferenceCategory>

</PreferenceScreen>
Loading

0 comments on commit 8deaff5

Please sign in to comment.