From ab5f70499890c147654ed4b94122447d29775a70 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 1 Mar 2024 08:49:16 -0600 Subject: [PATCH] Venmo Universal/App Links (#911) * Add setFallbackToWeb() to VenmoRequest * If set to true customers will fallback to a web based Venmo flow if the Venmo app is not installed * This method uses App Links instead of Deep Links * Add VenmoClient#parseBrowserSwitchResult(Context, Intent) method * Add VenmoClient#clearActiveBrowserSwitchRequests(Context) method * Add VenmoClient#onBrowserSwitchResult(BrowserSwitchResult, VenmoOnActivityResultCallback) method * This feature has been added to the Demo app, select "Venmo Fallback to Web" in the Settings menu to test this flow * Add unit tests Co-authored-by: Tim Chow Co-authored-by: Justin Warmkessel Co-authored-by: Sarah Koop --- CHANGELOG.md | 6 + .../com/braintreepayments/demo/Settings.java | 4 + .../braintreepayments/demo/VenmoFragment.java | 72 +++-- Demo/src/main/res/values/strings.xml | 2 + Demo/src/main/res/xml/settings.xml | 5 + .../braintreepayments/api/VenmoClient.java | 263 +++++++++++++++++- .../api/VenmoLifecycleObserver.java | 46 +++ .../braintreepayments/api/VenmoRequest.java | 26 ++ .../VenmoActivityResultContractUnitTest.java | 1 + .../api/VenmoClientUnitTest.java | 197 +++++++++++++ .../api/VenmoLifecycleObserverUnitTest.java | 125 +++++++++ .../api/VenmoRequestUnitTest.java | 22 ++ 12 files changed, 747 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6caa09103c..9d78288db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ * Venmo * Add `setIsFinalAmount()` to `VenmoRequest` + * Add `setFallbackToWeb()` to `VenmoRequest` + * If set to `true` customers will fallback to a web based Venmo flow if the Venmo app is not installed + * This method uses App Links instead of Deep Links + * Add `VenmoClient#parseBrowserSwitchResult(Context, Intent)` method + * Add `VenmoClient#clearActiveBrowserSwitchRequests(Context)` method + * Add `VenmoClient#onBrowserSwitchResult(BrowserSwitchResult, VenmoOnActivityResultCallback)` method * ThreeDSecure * Call cleanup method to resolve `Cardinal.getInstance` memory leak diff --git a/Demo/src/main/java/com/braintreepayments/demo/Settings.java b/Demo/src/main/java/com/braintreepayments/demo/Settings.java index e3511c4070..cccd31eca3 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/Settings.java +++ b/Demo/src/main/java/com/braintreepayments/demo/Settings.java @@ -210,6 +210,10 @@ public static boolean vaultVenmo(Context context) { return getPreferences(context).getBoolean("vault_venmo", true); } + public static boolean venmoFallbackToWeb(Context context) { + return getPreferences(context).getBoolean("venmo_fallback_to_web", false); + } + public static boolean isAmexRewardsBalanceEnabled(Context context) { return getPreferences(context).getBoolean("amex_rewards_balance", false); } diff --git a/Demo/src/main/java/com/braintreepayments/demo/VenmoFragment.java b/Demo/src/main/java/com/braintreepayments/demo/VenmoFragment.java index 5eb47a736a..b630b3d766 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/VenmoFragment.java +++ b/Demo/src/main/java/com/braintreepayments/demo/VenmoFragment.java @@ -1,5 +1,6 @@ package com.braintreepayments.demo; +import android.app.Activity; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; @@ -14,6 +15,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.braintreepayments.api.BraintreeClient; +import com.braintreepayments.api.BrowserSwitchResult; import com.braintreepayments.api.VenmoAccountNonce; import com.braintreepayments.api.VenmoClient; import com.braintreepayments.api.VenmoLineItem; @@ -28,6 +30,8 @@ public class VenmoFragment extends BaseFragment implements VenmoListener { private VenmoClient venmoClient; private BraintreeClient braintreeClient; + private boolean useManualBrowserSwitch; + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -35,6 +39,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c venmoButton = view.findViewById(R.id.venmo_button); venmoButton.setOnClickListener(this::launchVenmo); + useManualBrowserSwitch = Settings.isManualBrowserSwitchingEnabled(requireActivity()); braintreeClient = getBraintreeClient(); venmoClient = new VenmoClient(this, braintreeClient); venmoClient.setListener(this); @@ -46,7 +51,7 @@ private void handleVenmoResult(VenmoAccountNonce venmoAccountNonce) { super.onPaymentMethodNonceCreated(venmoAccountNonce); NavDirections action = - VenmoFragmentDirections.actionVenmoFragmentToDisplayNonceFragment(venmoAccountNonce); + VenmoFragmentDirections.actionVenmoFragmentToDisplayNonceFragment(venmoAccountNonce); NavHostFragment.findNavController(this).navigate(action); } @@ -57,35 +62,64 @@ public void launchVenmo(View v) { boolean shouldVault = Settings.vaultVenmo(activity) && !TextUtils.isEmpty(Settings.getCustomerId(activity)); + boolean fallbackToWeb = Settings.venmoFallbackToWeb(activity); int venmoPaymentMethodUsage = shouldVault ? VenmoPaymentMethodUsage.MULTI_USE : VenmoPaymentMethodUsage.SINGLE_USE; + VenmoRequest venmoRequest = new VenmoRequest(venmoPaymentMethodUsage); + venmoRequest.setProfileId(null); + venmoRequest.setShouldVault(shouldVault); + venmoRequest.setCollectCustomerBillingAddress(true); + venmoRequest.setCollectCustomerShippingAddress(true); + venmoRequest.setTotalAmount("20"); + venmoRequest.setSubTotalAmount("18"); + venmoRequest.setTaxAmount("1"); + venmoRequest.setShippingAmount("1"); + ArrayList lineItems = new ArrayList<>(); + lineItems.add(new VenmoLineItem(VenmoLineItem.KIND_CREDIT, "Some Item", 1, "2")); + lineItems.add(new VenmoLineItem(VenmoLineItem.KIND_DEBIT, "Two Items", 2, "10")); + venmoRequest.setLineItems(lineItems); braintreeClient.getConfiguration((configuration, error) -> { - if (venmoClient.isVenmoAppSwitchAvailable(activity)) { - VenmoRequest venmoRequest = new VenmoRequest(venmoPaymentMethodUsage); - venmoRequest.setProfileId(null); - venmoRequest.setShouldVault(shouldVault); - venmoRequest.setCollectCustomerBillingAddress(true); - venmoRequest.setCollectCustomerShippingAddress(true); - venmoRequest.setTotalAmount("20"); - venmoRequest.setSubTotalAmount("18"); - venmoRequest.setTaxAmount("1"); - venmoRequest.setShippingAmount("1"); - ArrayList lineItems = new ArrayList<>(); - lineItems.add(new VenmoLineItem(VenmoLineItem.KIND_CREDIT, "Some Item", 1, "2")); - lineItems.add(new VenmoLineItem(VenmoLineItem.KIND_DEBIT, "Two Items", 2, "10")); - venmoRequest.setLineItems(lineItems); - + if (fallbackToWeb) { + venmoRequest.setFallbackToWeb(true); venmoClient.tokenizeVenmoAccount(activity, venmoRequest); - } else if (configuration.isVenmoEnabled()) { - showDialog("Please install the Venmo app first."); } else { - showDialog("Venmo is not enabled for the current merchant."); + if (venmoClient.isVenmoAppSwitchAvailable(activity)) { + venmoClient.tokenizeVenmoAccount(activity, venmoRequest); + } else if (configuration.isVenmoEnabled()) { + showDialog("Please install the Venmo app first."); + } else { + showDialog("Venmo is not enabled for the current merchant."); + } } }); } + @Override + public void onResume() { + super.onResume(); + if (useManualBrowserSwitch) { + Activity activity = requireActivity(); + BrowserSwitchResult browserSwitchResult = + venmoClient.parseBrowserSwitchResult(activity, activity.getIntent()); + if (browserSwitchResult != null) { + handleBrowserSwitchResult(browserSwitchResult); + } + } + } + + private void handleBrowserSwitchResult(BrowserSwitchResult browserSwitchResult) { + venmoClient.onBrowserSwitchResult(browserSwitchResult, ((venmoAccountNonce, error) -> { + if (venmoAccountNonce != null) { + handleVenmoResult(venmoAccountNonce); + } else if (error != null) { + handleError(error); + } + })); + venmoClient.clearActiveBrowserSwitchRequests(requireContext()); + } + @Override public void onVenmoSuccess(@NonNull VenmoAccountNonce venmoAccountNonce) { handleVenmoResult(venmoAccountNonce); diff --git a/Demo/src/main/res/values/strings.xml b/Demo/src/main/res/values/strings.xml index e56d91145d..9749b3d611 100644 --- a/Demo/src/main/res/values/strings.xml +++ b/Demo/src/main/res/values/strings.xml @@ -99,6 +99,8 @@ Launch PayPal Native Checkout Launch PayPal Native Vault Checkout Venmo + Venmo Fallback to Web + Fallback to web if the Venmo app is not installed. Vault Venmo Vault Venmo payment methods on creation. Requires a customer id to be set. Amex diff --git a/Demo/src/main/res/xml/settings.xml b/Demo/src/main/res/xml/settings.xml index adc3477028..886ddbc2c2 100644 --- a/Demo/src/main/res/xml/settings.xml +++ b/Demo/src/main/res/xml/settings.xml @@ -173,6 +173,11 @@ android:title="@string/vault_venmo" android:summary="@string/vault_venmo_summary" android:defaultValue="true" /> + diff --git a/Venmo/src/main/java/com/braintreepayments/api/VenmoClient.java b/Venmo/src/main/java/com/braintreepayments/api/VenmoClient.java index 2b484dd8bc..2756438dcb 100644 --- a/Venmo/src/main/java/com/braintreepayments/api/VenmoClient.java +++ b/Venmo/src/main/java/com/braintreepayments/api/VenmoClient.java @@ -43,6 +43,9 @@ public class VenmoClient { @VisibleForTesting VenmoLifecycleObserver observer; + @VisibleForTesting + BrowserSwitchResult pendingBrowserSwitchResult; + /** * Create a new instance of {@link VenmoClient} from within an Activity using a {@link BraintreeClient}. * @@ -105,6 +108,9 @@ private void addObserver(@NonNull FragmentActivity activity, @NonNull Lifecycle */ public void setListener(VenmoListener listener) { this.listener = listener; + if (pendingBrowserSwitchResult != null) { + deliverBrowserSwitchResultToListener(pendingBrowserSwitchResult); + } } /** @@ -163,10 +169,14 @@ public void onResult(@Nullable final Configuration configuration, @Nullable Exce } String exceptionMessage = null; + if (!request.getFallbackToWeb()) { + if (!deviceInspector.isVenmoAppSwitchAvailable(activity)) { + exceptionMessage = "Venmo is not installed"; + } + } + if (!configuration.isVenmoEnabled()) { exceptionMessage = "Venmo is not enabled"; - } else if (!deviceInspector.isVenmoAppSwitchAvailable(activity)) { - exceptionMessage = "Venmo is not installed"; } if (exceptionMessage != null) { @@ -225,7 +235,16 @@ private void startVenmoActivityForResult( sharedPrefsWriter.persistVenmoVaultOption(activity, shouldVault); if (observer != null) { VenmoIntentData intentData = new VenmoIntentData(configuration, venmoProfileId, paymentContextId, braintreeClient.getSessionId(), braintreeClient.getIntegrationType()); - observer.launch(intentData); + if (request.getFallbackToWeb()) { + try { + startAppLinkFlow(activity, intentData); + } catch (JSONException | BrowserSwitchException exception) { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.failure"); + deliverVenmoFailure(exception); + } + } else { + observer.launch(intentData); + } } else { Intent launchIntent = getLaunchIntent(configuration, venmoProfileId, paymentContextId); activity.startActivityForResult(launchIntent, BraintreeRequestCodes.VENMO); @@ -428,6 +447,244 @@ private Intent getLaunchIntent(Configuration configuration, String profileId, St return venmoIntent; } + /** + * Use this method with the manual browser switch integration pattern. + * + * @param browserSwitchResult a {@link BrowserSwitchResult} with a {@link BrowserSwitchStatus} + * @param callback {@link VenmoOnActivityResultCallback} + */ + public void onBrowserSwitchResult(@NonNull BrowserSwitchResult browserSwitchResult, @NonNull final VenmoOnActivityResultCallback callback) { + int result = browserSwitchResult.getStatus(); + switch (result) { + case BrowserSwitchStatus.CANCELED: + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.canceled"); + callback.onResult(null, new UserCanceledException("User canceled Venmo.")); + break; + case BrowserSwitchStatus.SUCCESS: + Uri deepLinkUri = browserSwitchResult.getDeepLinkUrl(); + if (deepLinkUri != null) { + if (deepLinkUri.getPath().contains("success")) { + String resourceId = parseResourceId(String.valueOf(deepLinkUri)); + String paymentMethodNonce = parsePaymentMethodNonce(String.valueOf(deepLinkUri)); + String username = parseUsername(String.valueOf(deepLinkUri)); + Context context = braintreeClient.getApplicationContext(); + + braintreeClient.getAuthorization(new AuthorizationCallback() { + @Override + public void onAuthorizationResult(@Nullable Authorization authorization, @Nullable Exception authError) { + if (authorization != null) { + final boolean isClientTokenAuth = (authorization instanceof ClientToken); + + if (resourceId != null) { + venmoApi.createNonceFromPaymentContext(resourceId, new VenmoOnActivityResultCallback() { + @Override + public void onResult(@Nullable VenmoAccountNonce nonce, @Nullable Exception error) { + if (nonce != null) { + boolean shouldVault = sharedPrefsWriter.getVenmoVaultOption(context); + if (shouldVault && isClientTokenAuth) { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.success"); + vaultVenmoAccountNonce(nonce.getString(), callback); + } else { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.success"); + callback.onResult(nonce, null); + } + } else { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.failure"); + callback.onResult(null, error); + } + } + }); + } else if (paymentMethodNonce != null && username != null) { + boolean shouldVault = sharedPrefsWriter.getVenmoVaultOption(context); + if (shouldVault && isClientTokenAuth) { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.success"); + vaultVenmoAccountNonce(paymentMethodNonce, callback); + } else { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.success"); + VenmoAccountNonce venmoAccountNonce = new VenmoAccountNonce(paymentMethodNonce, username, false); + callback.onResult(venmoAccountNonce, null); + } + } + } else if (authError != null) { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.failure"); + callback.onResult(null, authError); + } + } + }); + } else if (deepLinkUri.getPath().contains("cancel")) { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.canceled"); + callback.onResult(null, new UserCanceledException("User canceled Venmo.")); + } else if (deepLinkUri.getPath().contains("error")) { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.failure"); + callback.onResult(null, new Exception("Error returned from Venmo.")); + } + } else { + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.failure"); + callback.onResult(null, new Exception("Unknown error")); + } + break; + } + } + + /** + * After calling {@link VenmoClient#tokenizeVenmoAccount(FragmentActivity, VenmoRequest)}, + * call this method in your Activity or Fragment's onResume() method to see if a response + * was provided through deep linking. + * + * If a BrowserSwitchResult exists, call {@link VenmoClient#onBrowserSwitchResult(BrowserSwitchResult)} + * to allow the SDK to continue tokenization of the VenmoAccount. + * + * Make sure to call {@link VenmoClient#clearActiveBrowserSwitchRequests(Context)} after + * successfully parsing a BrowserSwitchResult to guard against multiple invocations of browser + * switch event handling. + * + * @param context The context used to check for pending browser switch requests + * @param intent The intent containing a potential deep link response. May be null. + * @return {@link BrowserSwitchResult} when a result has been parsed successfully from a deep link; null when an input Intent is null + */ + @Nullable + public BrowserSwitchResult parseBrowserSwitchResult(@NonNull Context context, @Nullable Intent intent) { + int requestCode = BraintreeRequestCodes.VENMO; + return braintreeClient.parseBrowserSwitchResult(context, requestCode, intent); + } + + /** + * Make sure to call this method after {@link VenmoClient#parseBrowserSwitchResult(Context, Intent)} + * parses a {@link BrowserSwitchResult} successfully to prevent multiple invocations of browser + * switch event handling logic. + * + * @param context The context used to clear pending browser switch requests + */ + public void clearActiveBrowserSwitchRequests(@NonNull Context context) { + braintreeClient.clearActiveBrowserSwitchRequests(context); + } + + void onBrowserSwitchResult(@NonNull BrowserSwitchResult browserSwitchResult) { + this.pendingBrowserSwitchResult = browserSwitchResult; + if (listener != null) { + // NEXT_MAJOR_VERSION: remove all manual browser switching methods + deliverBrowserSwitchResultToListener(pendingBrowserSwitchResult); + } + } + + // NEXT_MAJOR_VERSION: remove all manual browser switching methods + BrowserSwitchResult getBrowserSwitchResult(FragmentActivity activity) { + return braintreeClient.getBrowserSwitchResult(activity); + } + + BrowserSwitchResult deliverBrowserSwitchResult(FragmentActivity activity) { + return braintreeClient.deliverBrowserSwitchResult(activity); + } + + BrowserSwitchResult getBrowserSwitchResultFromNewTask(FragmentActivity activity) { + return braintreeClient.getBrowserSwitchResultFromNewTask(activity); + } + + BrowserSwitchResult deliverBrowserSwitchResultFromNewTask(FragmentActivity activity) { + return braintreeClient.deliverBrowserSwitchResultFromNewTask(activity); + } + + private String parseResourceId(String deepLinkUri) { + String resourceIdFromBrowserSwitch = Uri.parse(deepLinkUri).getQueryParameter("resource_id"); + if (resourceIdFromBrowserSwitch != null) { + return resourceIdFromBrowserSwitch; + } else { + String cleanedAppSwitchUri = deepLinkUri.replaceFirst("&","?"); + String resourceIdFromAppSwitch = Uri.parse(String.valueOf(cleanedAppSwitchUri)).getQueryParameter("resource_id"); + if (resourceIdFromAppSwitch != null) { + return resourceIdFromAppSwitch; + } else { + return null; + } + } + } + + private String parsePaymentMethodNonce(String deepLinkUri) { + String paymentMethodNonceFromBrowserSwitch = Uri.parse(deepLinkUri).getQueryParameter("payment_method_nonce"); + if (paymentMethodNonceFromBrowserSwitch != null) { + return paymentMethodNonceFromBrowserSwitch; + } else { + String cleanedAppSwitchUri = deepLinkUri.replaceFirst("&","?"); + String paymentMethodNonceFromAppSwitch = Uri.parse(String.valueOf(cleanedAppSwitchUri)).getQueryParameter("payment_method_nonce"); + if (paymentMethodNonceFromAppSwitch != null) { + return paymentMethodNonceFromAppSwitch; + } else { + return null; + } + } + } + + private String parseUsername(String deepLinkUri) { + String usernameFromBrowserSwitch = Uri.parse(deepLinkUri).getQueryParameter("username"); + if (usernameFromBrowserSwitch != null) { + return usernameFromBrowserSwitch; + } else { + String cleanedAppSwitchUri = deepLinkUri.replaceFirst("&","?"); + String usernameFromAppSwitch = Uri.parse(String.valueOf(cleanedAppSwitchUri)).getQueryParameter("username"); + if (usernameFromAppSwitch != null) { + return usernameFromAppSwitch; + } else { + return null; + } + } + } + + private void deliverBrowserSwitchResultToListener(final BrowserSwitchResult browserSwitchResult) { + onBrowserSwitchResult(browserSwitchResult, new VenmoOnActivityResultCallback() { + @Override + public void onResult(@Nullable VenmoAccountNonce venmoAccountNonce, @Nullable Exception error) { + if (listener != null) { + if (venmoAccountNonce != null) { + listener.onVenmoSuccess(venmoAccountNonce); + } else if (error != null) { + listener.onVenmoFailure(error); + } + } + } + }); + + this.pendingBrowserSwitchResult = null; + } + + @VisibleForTesting + void startAppLinkFlow(FragmentActivity activity, VenmoIntentData input) throws JSONException, BrowserSwitchException { + JSONObject braintreeData = new MetadataBuilder() + .sessionId(input.getSessionId()) + .integration(input.getIntegrationType()) + .version() + .build(); + + String applicationName = "ApplicationNameUnknown"; + Context context = braintreeClient.getApplicationContext(); + if (context != null) { + if (context.getPackageManager().getApplicationLabel(context.getApplicationInfo()).toString() != null) { + applicationName = context.getPackageManager().getApplicationLabel(context.getApplicationInfo()).toString(); + } + } + + Uri venmoBaseURL = Uri.parse("https://venmo.com/go/checkout") + .buildUpon() + .appendQueryParameter("x-success", braintreeClient.getReturnUrlScheme() + "://x-callback-url/vzero/auth/venmo/success") + .appendQueryParameter("x-error", braintreeClient.getReturnUrlScheme() + "://x-callback-url/vzero/auth/venmo/error") + .appendQueryParameter("x-cancel", braintreeClient.getReturnUrlScheme() + "://x-callback-url/vzero/auth/venmo/cancel") + .appendQueryParameter("x-source", applicationName) + .appendQueryParameter("braintree_merchant_id", input.getProfileId()) + .appendQueryParameter("braintree_access_token", input.getConfiguration().getVenmoAccessToken()) + .appendQueryParameter("braintree_environment", input.getConfiguration().getVenmoEnvironment()) + .appendQueryParameter("resource_id", input.getPaymentContextId()) + .appendQueryParameter("braintree_sdk_data", braintreeData.toString()) + .appendQueryParameter("customerClient", "MOBILE_APP") + .build(); + + BrowserSwitchOptions browserSwitchOptions = new BrowserSwitchOptions() + .requestCode(BraintreeRequestCodes.VENMO) + .url(venmoBaseURL) + .returnUrlScheme(braintreeClient.getReturnUrlScheme()); + + braintreeClient.startBrowserSwitch(activity, browserSwitchOptions); + braintreeClient.sendAnalyticsEvent("pay-with-venmo.app-links.started"); + } + /** * Check if Venmo app switch is available. * diff --git a/Venmo/src/main/java/com/braintreepayments/api/VenmoLifecycleObserver.java b/Venmo/src/main/java/com/braintreepayments/api/VenmoLifecycleObserver.java index 041a852ed5..5488cd9db1 100644 --- a/Venmo/src/main/java/com/braintreepayments/api/VenmoLifecycleObserver.java +++ b/Venmo/src/main/java/com/braintreepayments/api/VenmoLifecycleObserver.java @@ -1,10 +1,18 @@ package com.braintreepayments.api; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; +import static com.braintreepayments.api.BraintreeRequestCodes.VENMO; + +import android.os.Handler; +import android.os.Looper; + import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultRegistry; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.LifecycleOwner; @@ -23,6 +31,9 @@ class VenmoLifecycleObserver implements LifecycleEventObserver { @VisibleForTesting ActivityResultLauncher activityLauncher; + @VisibleForTesting + VenmoActivityResultContract venmoActivityResultContract = new VenmoActivityResultContract(); + VenmoLifecycleObserver(ActivityResultRegistry activityResultRegistry, VenmoClient venmoClient) { this.activityResultRegistry = activityResultRegistry; this.venmoClient = venmoClient; @@ -38,6 +49,41 @@ public void onActivityResult(VenmoResult venmoResult) { } }); } + + if (event == ON_RESUME) { + FragmentActivity activity = null; + if (lifecycleOwner instanceof FragmentActivity) { + activity = (FragmentActivity) lifecycleOwner; + } else if (lifecycleOwner instanceof Fragment) { + activity = ((Fragment) lifecycleOwner).getActivity(); + } + + if (activity != null) { + final FragmentActivity finalActivity = activity; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + BrowserSwitchResult resultToDeliver = null; + + BrowserSwitchResult pendingResult = venmoClient.getBrowserSwitchResult(finalActivity); + if (pendingResult != null && pendingResult.getRequestCode() == VENMO) { + resultToDeliver = venmoClient.deliverBrowserSwitchResult(finalActivity); + } + + BrowserSwitchResult pendingResultFromCache = + venmoClient.getBrowserSwitchResultFromNewTask(finalActivity); + if (pendingResultFromCache != null && pendingResultFromCache.getRequestCode() == VENMO) { + resultToDeliver = + venmoClient.deliverBrowserSwitchResultFromNewTask(finalActivity); + } + + if (resultToDeliver != null) { + venmoClient.onBrowserSwitchResult(resultToDeliver); + } + } + }); + } + } } void launch(VenmoIntentData venmoIntentData) { diff --git a/Venmo/src/main/java/com/braintreepayments/api/VenmoRequest.java b/Venmo/src/main/java/com/braintreepayments/api/VenmoRequest.java index 397f41211d..7ce7838560 100644 --- a/Venmo/src/main/java/com/braintreepayments/api/VenmoRequest.java +++ b/Venmo/src/main/java/com/braintreepayments/api/VenmoRequest.java @@ -1,5 +1,7 @@ package com.braintreepayments.api; +import android.content.Context; +import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; @@ -26,6 +28,7 @@ public class VenmoRequest implements Parcelable { private String shippingAmount; private ArrayList lineItems; private boolean isFinalAmount; + private boolean fallbackToWeb = false; private final @VenmoPaymentMethodUsage int paymentMethodUsage; @@ -249,6 +252,27 @@ public ArrayList getLineItems() { return lineItems; } + /** + * @param fallbackToWeb Optional - Used to determine if the customer should fallback + * to the web flow if Venmo app is not installed. + * Defaults to false. + * + * If using the manual browser switch pattern, you must implement the following methods: + * {@link VenmoClient#parseBrowserSwitchResult(Context, Intent)} + * {@link VenmoClient#onBrowserSwitchResult(BrowserSwitchResult, VenmoOnActivityResultCallback)} + * {@link VenmoClient#clearActiveBrowserSwitchRequests(Context)} + */ + public void setFallbackToWeb(boolean fallbackToWeb) { + this.fallbackToWeb = fallbackToWeb; + } + + /** + * @return Whether or not to fallback to the web flow if Venmo app is not installed. + */ + public boolean getFallbackToWeb() { + return fallbackToWeb; + } + /** * @param isFinalAmount Optional - Indicates whether the purchase amount is the final amount. * Defaults to false. @@ -285,6 +309,7 @@ protected VenmoRequest(Parcel in) { totalAmount = in.readString(); lineItems = in.createTypedArrayList(VenmoLineItem.CREATOR); isFinalAmount = in.readByte() != 0; + fallbackToWeb = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @@ -319,5 +344,6 @@ public void writeToParcel(Parcel parcel, int i) { parcel.writeString(totalAmount); parcel.writeTypedList(lineItems); parcel.writeByte((byte) (isFinalAmount ? 1 : 0)); + parcel.writeByte((byte) (fallbackToWeb ? 1 : 0)); } } diff --git a/Venmo/src/test/java/com/braintreepayments/api/VenmoActivityResultContractUnitTest.java b/Venmo/src/test/java/com/braintreepayments/api/VenmoActivityResultContractUnitTest.java index e8c2668f61..ae59cfe4f5 100644 --- a/Venmo/src/test/java/com/braintreepayments/api/VenmoActivityResultContractUnitTest.java +++ b/Venmo/src/test/java/com/braintreepayments/api/VenmoActivityResultContractUnitTest.java @@ -8,6 +8,7 @@ import static com.braintreepayments.api.VenmoClient.EXTRA_MERCHANT_ID; import static com.braintreepayments.api.VenmoClient.EXTRA_RESOURCE_ID; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; diff --git a/Venmo/src/test/java/com/braintreepayments/api/VenmoClientUnitTest.java b/Venmo/src/test/java/com/braintreepayments/api/VenmoClientUnitTest.java index be54ca7f3b..fbe609197f 100644 --- a/Venmo/src/test/java/com/braintreepayments/api/VenmoClientUnitTest.java +++ b/Venmo/src/test/java/com/braintreepayments/api/VenmoClientUnitTest.java @@ -27,6 +27,7 @@ import android.content.ComponentName; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.activity.result.ActivityResultRegistry; @@ -36,6 +37,9 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; +import junit.framework.Assert; +import junit.framework.TestCase; + import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; @@ -433,6 +437,82 @@ public void tokenizeVenmoAccount_whenVenmoNotInstalled_forwardsExceptionToListen verify(braintreeClient).sendAnalyticsEvent("pay-with-venmo.app-switch.failed"); } + @Test + public void tokenizeVenmoAccount_whenFallbackToWebTrueAndAppInstalled_appSwitchIsSuccessful() { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .configuration(venmoEnabledConfiguration) + .authorizationSuccess(clientToken) + .build(); + + VenmoApi venmoApi = new MockVenmoApiBuilder() + .createPaymentContextSuccess("venmo-payment-context-id") + .build(); + + VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.SINGLE_USE); + request.setFallbackToWeb(true); + + VenmoClient sut = new VenmoClient(null, null, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.tokenizeVenmoAccount(activity, request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(activity).startActivityForResult(captor.capture(), eq(BraintreeRequestCodes.VENMO)); + assertEquals("com.venmo/com.venmo.controller.SetupMerchantActivity", + captor.getValue().getComponent().flattenToString()); + } + + @Test + public void tokenizeVenmoAccount_whenFallbackToWebTrueAndAppNotInstalled_browserSwitchIsSuccessful() { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .configuration(venmoEnabledConfiguration) + .authorizationSuccess(clientToken) + .build(); + + VenmoApi venmoApi = new MockVenmoApiBuilder() + .createPaymentContextSuccess("venmo-payment-context-id") + .build(); + + VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.SINGLE_USE); + request.setFallbackToWeb(true); + + when(deviceInspector.isVenmoAppSwitchAvailable(activity)).thenReturn(false); + + VenmoClient sut = new VenmoClient(null, null, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.tokenizeVenmoAccount(activity, request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(activity).startActivityForResult(captor.capture(), eq(BraintreeRequestCodes.VENMO)); + assertEquals("com.venmo/com.venmo.controller.SetupMerchantActivity", + captor.getValue().getComponent().flattenToString()); + } + + @Test + public void startAppLinkFlow_whenDataIsValid_returnsCorrectUri() throws JSONException, BrowserSwitchException { + braintreeClient = new MockBraintreeClientBuilder() + .returnUrlScheme("com.example") + .build(); + + Configuration configuration = mock(Configuration.class); + VenmoIntentData venmoIntentData = new VenmoIntentData(configuration, "fake-profile-id", "fake-payment-context-id", "fake-session-id", "custom"); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.startAppLinkFlow(activity, venmoIntentData); + ArgumentCaptor captor = ArgumentCaptor.forClass(BrowserSwitchOptions.class); + verify(braintreeClient).startBrowserSwitch(same(activity), captor.capture()); + + BrowserSwitchOptions browserSwitchOptions = captor.getValue(); + assertEquals(BraintreeRequestCodes.VENMO, browserSwitchOptions.getRequestCode()); + assertEquals("com.example", browserSwitchOptions.getReturnUrlScheme()); + + Uri url = browserSwitchOptions.getUrl(); + // TODO: figure out why null? + assertEquals("com.example://x-callback-url/vzero/auth/venmo/success", url.getQueryParameter("x-success")); + assertEquals("com.example://x-callback-url/vzero/auth/venmo/error", url.getQueryParameter("x-error")); + assertEquals("com.example://x-callback-url/vzero/auth/venmo/cancel", url.getQueryParameter("x-cancel")); + assertEquals("fake-profile-id", url.getQueryParameter("braintree_merchant_id")); + assertEquals("fake-payment-context-id", url.getQueryParameter("resource_id")); + assertEquals("MOBILE_APP", url.getQueryParameter("customerClient")); + } + @Test public void tokenizeVenmoAccount_whenProfileIdIsNull_appSwitchesWithMerchantId() { BraintreeClient braintreeClient = new MockBraintreeClientBuilder() @@ -1496,4 +1576,121 @@ public void onVenmoResult_withPaymentContext_withFailedVaultCall_forwardsErrorTo verify(listener).onVenmoFailure(error); verify(braintreeClient).sendAnalyticsEvent(endsWith("pay-with-venmo.vault.failed")); } + + @Test + public void setListener_whenPendingBrowserSwitchResultExists_deliversResultToListener_andSetsPendingResultNull() throws JSONException { + VenmoAccountNonce nonce = mock(VenmoAccountNonce.class); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .authorizationSuccess(clientToken) + .build(); + + VenmoApi venmoApi = new MockVenmoApiBuilder() + .createNonceFromPaymentContextSuccess(nonce) + .build(); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getStatus()).thenReturn(BrowserSwitchStatus.SUCCESS); + + String successUrl = "sample-scheme://x-callback-url/vzero/auth/venmo/success?resource_id=a-resource-id"; + when(browserSwitchResult.getRequestMetadata()).thenReturn(new JSONObject() + .put("deepLinkUrl", successUrl) + ); + + Uri uri = Uri.parse(successUrl); + when(browserSwitchResult.getDeepLinkUrl()).thenReturn(uri); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.pendingBrowserSwitchResult = browserSwitchResult; + sut.setListener(listener); + + verify(listener).onVenmoSuccess(same(nonce)); + verify(listener, never()).onVenmoFailure(any(Exception.class)); + TestCase.assertNull(sut.pendingBrowserSwitchResult); + } + + @Test + public void setListener_whenPendingBrowserSwitchResultDoesNotExist_doesNotInvokeListener() { + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .build(); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.pendingBrowserSwitchResult = null; + sut.setListener(listener); + + verify(listener, never()).onVenmoSuccess(any(VenmoAccountNonce.class)); + verify(listener, never()).onVenmoFailure(any(Exception.class)); + + TestCase.assertNull(sut.pendingBrowserSwitchResult); + } + + @Test + public void onBrowserSwitchResult_whenBrowserSwitchStatusCanceled_returnsExceptionToCallback() throws BraintreeException { + BrowserSwitchResult browserSwitchResult = + new BrowserSwitchResult(BrowserSwitchStatus.CANCELED, null, null); + + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.setListener(listener); + sut.onBrowserSwitchResult(browserSwitchResult); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(listener).onVenmoFailure(captor.capture()); + verify(listener, never()).onVenmoSuccess(any(VenmoAccountNonce.class)); + + Exception exception = captor.getValue(); + Assert.assertTrue(exception instanceof UserCanceledException); + assertEquals("User canceled Venmo.", exception.getMessage()); + org.junit.Assert.assertFalse(((UserCanceledException) exception).isExplicitCancelation()); + } + + @Test + public void onBrowserSwitchResult_whenDeepLinkUriContainsCanceled_returnsExceptionToCallback() throws BraintreeException { + BrowserSwitchResult browserSwitchResult = + new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, null, Uri.parse("sample-scheme://x-callback-url/vzero/auth/venmo/cancel")); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.setListener(listener); + sut.onBrowserSwitchResult(browserSwitchResult); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(listener).onVenmoFailure(captor.capture()); + verify(listener, never()).onVenmoSuccess(any(VenmoAccountNonce.class)); + + Exception exception = captor.getValue(); + Assert.assertTrue(exception instanceof UserCanceledException); + assertEquals("User canceled Venmo.", exception.getMessage()); + } + + @Test + public void onBrowserSwitchResult_whenDeepLinkUriContainsError_returnsExceptionToCallback() throws BraintreeException { + BrowserSwitchResult browserSwitchResult = + new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, null, Uri.parse("sample-scheme://x-callback-url/vzero/auth/venmo/error")); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.setListener(listener); + sut.onBrowserSwitchResult(browserSwitchResult); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(listener).onVenmoFailure(captor.capture()); + verify(listener, never()).onVenmoSuccess(any(VenmoAccountNonce.class)); + + Exception exception = captor.getValue(); + Assert.assertTrue(exception instanceof Exception); + assertEquals("Error returned from Venmo.", exception.getMessage()); + } + + @Test + public void onBrowserSwitchResult_whenListenerNull_setsPendingBrowserSwitchResult_andDoesNotDeliver() throws BraintreeException { + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); + + VenmoClient sut = new VenmoClient(activity, lifecycle, braintreeClient, venmoApi, sharedPrefsWriter, deviceInspector); + sut.onBrowserSwitchResult(browserSwitchResult); + + verify(listener, never()).onVenmoFailure(any(Exception.class)); + verify(listener, never()).onVenmoSuccess(any(VenmoAccountNonce.class)); + } } \ No newline at end of file diff --git a/Venmo/src/test/java/com/braintreepayments/api/VenmoLifecycleObserverUnitTest.java b/Venmo/src/test/java/com/braintreepayments/api/VenmoLifecycleObserverUnitTest.java index 2c5b021e1f..f54d24716a 100644 --- a/Venmo/src/test/java/com/braintreepayments/api/VenmoLifecycleObserverUnitTest.java +++ b/Venmo/src/test/java/com/braintreepayments/api/VenmoLifecycleObserverUnitTest.java @@ -1,15 +1,22 @@ package com.braintreepayments.api; +import static android.os.Looper.getMainLooper; +import static com.braintreepayments.api.BraintreeRequestCodes.THREE_D_SECURE; +import static com.braintreepayments.api.BraintreeRequestCodes.VENMO; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultRegistry; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; @@ -84,4 +91,122 @@ public void launch_launchesActivity() throws JSONException { sut.launch(venmoIntentData); verify(activityResultLauncher).launch(venmoIntentData); } + + @Test + public void onResume_whenLifeCycleObserverIsFragment_venmoClientDeliversResultWithFragmentActivity() { + ActivityResultRegistry activityResultRegistry = mock(ActivityResultRegistry.class); + Fragment fragment = mock(Fragment.class); + FragmentActivity activity = mock(FragmentActivity.class); + when(fragment.getActivity()).thenReturn(activity); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getRequestCode()).thenReturn(VENMO); + + VenmoClient venmoClient = mock(VenmoClient.class); + when(venmoClient.getBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + when(venmoClient.deliverBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + + VenmoLifecycleObserver sut = new VenmoLifecycleObserver(activityResultRegistry, venmoClient); + sut.onStateChanged(fragment, Lifecycle.Event.ON_RESUME); + + // Ref: https://robolectric.org/blog/2019/06/04/paused-looper/ + shadowOf(getMainLooper()).idle(); + verify(venmoClient).onBrowserSwitchResult(same(browserSwitchResult)); + } + + @Test + public void onResume_whenLifeCycleObserverIsActivity_venmoClientDeliversResultWithSameActivity() { + ActivityResultRegistry activityResultRegistry = mock(ActivityResultRegistry.class); + FragmentActivity activity = mock(FragmentActivity.class); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getRequestCode()).thenReturn(VENMO); + + VenmoClient venmoClient = mock(VenmoClient.class); + when(venmoClient.getBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + when(venmoClient.deliverBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + + VenmoLifecycleObserver sut = new VenmoLifecycleObserver(activityResultRegistry, venmoClient); + sut.onStateChanged(activity, Lifecycle.Event.ON_RESUME); + + shadowOf(getMainLooper()).idle(); + verify(venmoClient).onBrowserSwitchResult(same(browserSwitchResult)); + } + + @Test + public void onResume_whenLifeCycleObserverIsFragment_venmoClientDeliversResultFromCacheWithFragmentActivity() { + ActivityResultRegistry activityResultRegistry = mock(ActivityResultRegistry.class); + Fragment fragment = mock(Fragment.class); + FragmentActivity activity = mock(FragmentActivity.class); + when(fragment.getActivity()).thenReturn(activity); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getRequestCode()).thenReturn(VENMO); + + VenmoClient venmoClient = mock(VenmoClient.class); + when(venmoClient.getBrowserSwitchResultFromNewTask(activity)).thenReturn(browserSwitchResult); + when(venmoClient.deliverBrowserSwitchResultFromNewTask(activity)).thenReturn(browserSwitchResult); + + VenmoLifecycleObserver sut = new VenmoLifecycleObserver(activityResultRegistry, venmoClient); + sut.onStateChanged(fragment, Lifecycle.Event.ON_RESUME); + + shadowOf(getMainLooper()).idle(); + verify(venmoClient).onBrowserSwitchResult(same(browserSwitchResult)); + } + + @Test + public void onResume_whenLifeCycleObserverIsActivity_venmoClientDeliversResultFromCacheWithSameActivity() { + ActivityResultRegistry activityResultRegistry = mock(ActivityResultRegistry.class); + FragmentActivity activity = mock(FragmentActivity.class); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getRequestCode()).thenReturn(VENMO); + + VenmoClient venmoClient = mock(VenmoClient.class); + when(venmoClient.getBrowserSwitchResultFromNewTask(activity)).thenReturn(browserSwitchResult); + when(venmoClient.deliverBrowserSwitchResultFromNewTask(activity)).thenReturn(browserSwitchResult); + + VenmoLifecycleObserver sut = new VenmoLifecycleObserver(activityResultRegistry, venmoClient); + sut.onStateChanged(activity, Lifecycle.Event.ON_RESUME); + + shadowOf(getMainLooper()).idle(); + verify(venmoClient).onBrowserSwitchResult(same(browserSwitchResult)); + } + + @Test + public void onResume_whenPendingBrowserSwitchResultExists_andRequestCodeNotVenmo_doesNothing() { + ActivityResultRegistry activityResultRegistry = mock(ActivityResultRegistry.class); + FragmentActivity activity = mock(FragmentActivity.class); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getRequestCode()).thenReturn(THREE_D_SECURE); + + VenmoClient venmoClient = mock(VenmoClient.class); + when(venmoClient.getBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + when(venmoClient.deliverBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + + VenmoLifecycleObserver sut = new VenmoLifecycleObserver(activityResultRegistry, venmoClient); + sut.onStateChanged(activity, Lifecycle.Event.ON_RESUME); + + shadowOf(getMainLooper()).idle(); + verify(venmoClient, never()).onBrowserSwitchResult(any(BrowserSwitchResult.class)); + } + + @Test + public void onResume_whenCachedBrowserSwitchResultExists_andRequestCodeNotVenmo_doesNothing() { + ActivityResultRegistry activityResultRegistry = mock(ActivityResultRegistry.class); + FragmentActivity activity = mock(FragmentActivity.class); + + BrowserSwitchResult browserSwitchResult = mock(BrowserSwitchResult.class); + when(browserSwitchResult.getRequestCode()).thenReturn(BraintreeRequestCodes.PAYPAL); + + VenmoClient venmoClient = mock(VenmoClient.class); + when(venmoClient.getBrowserSwitchResult(activity)).thenReturn(browserSwitchResult); + + VenmoLifecycleObserver sut = new VenmoLifecycleObserver(activityResultRegistry, venmoClient); + + sut.onStateChanged(activity, Lifecycle.Event.ON_RESUME); + + verify(venmoClient, never()).onBrowserSwitchResult(browserSwitchResult); + } } diff --git a/Venmo/src/test/java/com/braintreepayments/api/VenmoRequestUnitTest.java b/Venmo/src/test/java/com/braintreepayments/api/VenmoRequestUnitTest.java index 09921e51e4..bac39fd12c 100644 --- a/Venmo/src/test/java/com/braintreepayments/api/VenmoRequestUnitTest.java +++ b/Venmo/src/test/java/com/braintreepayments/api/VenmoRequestUnitTest.java @@ -47,6 +47,26 @@ public void getCollectCustomerBillingAddressAsString_returnsStringEquivalent() { assertEquals("false", request.getCollectCustomerBillingAddressAsString()); } + @Test + public void getFallbackToWeb_whenTrue_returnsTrue() { + VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.SINGLE_USE); + request.setFallbackToWeb(true); + assertEquals(true, request.getFallbackToWeb()); + } + + @Test + public void getFallbackToWeb_whenFalse_returnsFalse() { + VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.SINGLE_USE); + request.setFallbackToWeb(false); + assertEquals(false, request.getFallbackToWeb()); + } + + @Test + public void getFallbackToWeb_whenNoValuePassed_defaultsToFalse() { + VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.SINGLE_USE); + assertEquals(false, request.getFallbackToWeb()); + } + @Test public void getIsFinalAmountAsString_returnsStringEquivalent() { VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.MULTI_USE); @@ -71,6 +91,7 @@ public void parcelsCorrectly() { request.setShippingAmount("1.00"); request.setTotalAmount("10.00"); request.setIsFinalAmount(true); + request.setFallbackToWeb(true); ArrayList lineItems = new ArrayList<>(); lineItems.add(new VenmoLineItem(VenmoLineItem.KIND_DEBIT, "An Item", 1, "10.00")); @@ -95,5 +116,6 @@ public void parcelsCorrectly() { assertEquals(1, result.getLineItems().size()); assertEquals("An Item", result.getLineItems().get(0).getName()); assertTrue(result.getIsFinalAmount()); + assertEquals(true, result.getFallbackToWeb()); } } \ No newline at end of file