braintree / braintree_android

Braintree SDK for Android
https://developer.paypal.com/braintree/docs/start/hello-client/android/v4
MIT License
409 stars 234 forks source link

braintreeClient.deliverBrowserSwitchResult set the Activity intent to 'null' #634

Closed skauss closed 1 year ago

skauss commented 1 year ago

Integration Details (please complete the following information):

Hi I have to implement Braintree with browser switch. Code to start the browser switch

     browserSwitchOptions.returnUrlScheme(urlScheme);
     browserSwitchOptions.url( Uri.parse(urlToLoad));  
     BrowserSwitchClient browserSwitchClient = new BrowserSwitchClient();
     browserSwitchClient.start(activity,browserSwitchOptions);

To get the result intent

   @Override
    protected void onNewIntent(Intent newIntent) {
        setIntent(newIntent);
        super.onNewIntent(newIntent);
    }

In onResume I check the BrowserSwitch result

BrowserSwitchResult browserSwitchResult = braintreeClient.deliverBrowserSwitchResult(activity);
 // now the intent is null

if (browserSwitchResult != null) {
    if (browserSwitchResult.getStatus() == BrowserSwitchStatus.SUCCESS) {
        String returnUrl = browserSwitchResult.getDeepLinkUrl() != null ? browserSwitchResult.getDeepLinkUrl().toString() : "";
         onPayPalPlusSuccess( activity, null, returnUrl);
     } else {
         onPayPalPlusFailure(activity,null);
      }

Inside the BraintreeClient deliverBrowserSwitchResult line 123 the intent of the activity is set to null

 public BrowserSwitchResult deliverResult(@NonNull FragmentActivity activity) {
        BrowserSwitchResult result = getResult(activity);
        if (result != null) {

            Context appContext = activity.getApplicationContext();
            BrowserSwitchRequest request = persistentStore.getActiveRequest(appContext);

            @BrowserSwitchStatus int status = result.getStatus();
            switch (status) {
                case BrowserSwitchStatus.SUCCESS:
                    // ensure that success result is delivered exactly once
                    persistentStore.clearActiveRequest(appContext);

                    // clear activity intent to prevent deep links from being parsed multiple times
                    activity.setIntent(null);
                    break;
                case BrowserSwitchStatus.CANCELED:
                    // ensure that cancellation result is delivered exactly once, but allow for
                    // a cancellation result to remain in shared storage in case it
                    // later becomes successful
                    request.setShouldNotifyCancellation(false);
                    persistentStore.putActiveRequest(request, activity);
                    break;
            }
        }
        return result;
    }

The following code which checks the intent now is missing his information. The BraintreeClient should only remove the intent data it needs.

sshropshire commented 1 year ago

@skauss thanks for using the Braintree SDK for Android. The browser switch happens internally in DropIn. We clear the intent to make sure a browser switch SUCCESS result is delivered only once.

I'd like to know more about your use case. Is there a reason you need your own BrowserSwitchClient instead of allowing DropInClient to use the one it has internally?

skauss commented 1 year ago

I need to do a setIntent in onNewIntent to get the intent to braintreeClient.deliverBrowserSwitchResult in onResume

All data which start my activity are gone if the deliverResult do an activity.setIntent(null);

The BrowserSwitchClient getResult (around line 160) check only the UrlScheme

 Uri deepLinkUrl = intent.getData();
        if (deepLinkUrl != null && request.matchesDeepLinkUrlScheme(deepLinkUrl)) {
            result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, deepLinkUrl);
        } else if (request.getShouldNotifyCancellation()) {
            result = new BrowserSwitchResult(BrowserSwitchStatus.CANCELED, request);
        }

To work only with scheme in not enough.

In the old BrainTree client we have a succes and cancel path on the return path like ${lowerApplicationId}.ppp://return_url ${lowerApplicationId}.ppp://cancel_url

In your case you it would help if you reset the data intent.setData(null);

Then all other data from the Activity survive an payment.

Our use case

PayPalPlus

skauss commented 1 year ago

For this I need a clientMetaId

sshropshire commented 1 year ago

Hey @skauss I see your point. Setting the activity's Intent to null may be a bug in this case. I've made a PR for this to update the library. Does this resolve your issue? https://github.com/braintree/browser-switch-android/pull/59

sshropshire commented 1 year ago

For this I need a clientMetaId

We can figure out a separate fix for clientMetadataId we can add a getter for this information if needed.

skauss commented 1 year ago

Hi @sshropshire I check the fix tomorrow Thanks Stephan

skauss commented 1 year ago

I am looking forward for the clientMetadataId Fix :-)

skauss commented 1 year ago

@sshropshire Is there any release plan of the drop-in SDK ? I ask this because we have to update the drop-in:5.2.1 SDK until end of January in case of Cardinal Mobile SDK which is used inside of version 5.2.1

sshropshire commented 1 year ago

@skauss this is now available in version 4.23.0 of the Braintree SDK and version 6.6.0 of our DropIn SDK.

I did draft a PR for adding a clientMetadataId accessor, but I would like to get some more testing done internally to make sure this is the best approach.

In the meantime, you can use the following code snippet to extract the clientMetadataId from DropInResult:

fun onDropInSuccess(dropInResult: DropInResult) {
  val deviceDataJSON = JSONObject(dropInResult.deviceData)
  val clientMetadataId = deviceDataJSON.getString("correlation_id")
}

Let us know if this helps!

skauss commented 1 year ago

@sshropshire I take a short look into the public DropInClient(FragmentActivity activity, String authorization)

To create a DropInClient I need an authorization or clientTokenProvider which I didn't have.

val dropInClient = DropInClient(this, "<#CLIENT_AUTHORIZATION#>")
dropInClient.setListener(this)
dropInClient.launchDropIn(dropInRequest

Or is there some pice of code which I didn't found.

sshropshire commented 1 year ago

@skauss that's correct a ClientTokenProvider allows the SDK to fetch a client token on demand. This allows a DropInClient to be instantiated in onCreate(). Is your integration working now?

skauss commented 1 year ago

@sshropshire Hi I have no "<#CLIENT_AUTHORIZATION#>" and have no idea where to get one.

In the flowchart "Our use case" in point #4 the JS Application ask me for an clientMetaId, which I can't deliver because I have no "<#CLIENT_AUTHORIZATION#>". And it is not an option to change the flow (flowchart)

In my client I have no problems to fetch the clientMetaId for PaypalVault or PayPalCheckout ( in this case I use BraintreeClient with an authorization I get from the JS Application).

Just now for PayPalPlus (BrowserSwitch from my flowchart (PayPalPlus is our name for this payment flow)) I use an clientMetaId like this :

clientMetaDataId = UUID.randomUUID().toString();

The BrowserSwitch is started this way :

     BrowserSwitchOptions browserSwitchOptions = new BrowserSwitchOptions();
     browserSwitchOptions.returnUrlScheme(urlScheme);
     browserSwitchOptions.url( Uri.parse(urlToLoad)); 

     browserSwitchClient = new BrowserSwitchClient();
     browserSwitchClient.start(activity,browserSwitchOptions);

Which work in the SandBox environment.

Would this work also in production ?

We need to deliver a new version of our app at the end of January to the Google Play Store. At the moment I see the risk we have to disable the PayPal payment for this release.

sshropshire commented 1 year ago

@skauss take a look at this doc for a reference on obtaining a client token.

As for the clientMetaDataId, you'll want the same unique id generated by the SDK because it is correlated with each transaction for risk analysis. As a workaround for now, it can be parsed in the onDropInSuccess callback (as mentioned here).

skauss commented 1 year ago

@sshropshire Hi for a onDropInSuccess callback I need some kind of client (BraintreeClient with PayPalCheckoutRequest or BraintreeClient with PayPalVaultRequest or ...)

In our scenario the Java App give a clientMetaDataId to the JavaScript payment application which do some magic stuff and than I get an callback to do a BrowserSwitch.

In case of success I send an JavaScript event to our basket application with the clientMetaId.

As I notice before in my sandbox environment I create an random string (UUID.randomUUID().toString()).

This work in my BrainTree sandbox environment, will this be work also in the production environment ?

sshropshire commented 1 year ago

Hi @skauss a random UUID may not work. I believe the UUID needs to match the one produced by the DropIn SDK.

Also, the fix for this issue is now available if you upgrade to the latest 4.23.1 version. Can you file a separate issue for this in our DropIn repo? I'm going to close this one for now to mark it as done.