braintree / braintree-android-drop-in

Braintree Drop-In SDK for Android
https://developers.braintreepayments.com/guides/drop-in/android/v2
MIT License
124 stars 79 forks source link

PayPal flow errors with UserCanceled error on returning to the Drop In. #419

Open soarb opened 1 year ago

soarb commented 1 year ago

Braintree SDK Version

4.27.0

Environment

Sandbox

Android Version & Device

Samsung S10 with Android 12

Braintree dependencies

com.braintreepayments.api:drop-in:6.9.0

Describe the bug

On completion of the PayPal flow triggered by the drop-in, the device presents two options. Our App name - "Just Once" and our App name - "Always".

I assume this is the OS asking the user if our App should be the default App for handling all links of a specific type?

Selecting "Just Once" fires the drop-in onFailure callback with a UserCanceled error?

The only way to get an instance of PayPalAccountNonce to charge the customer with (again this is in Sandbox) is to select "Always".

I'm not even sure why these two options get presented at all by the OS?

Completing a similar browser switch to perform 3DS 2FA auth immediately returns to our App on success or cancellation, no prompt to set the default App is presented.

To reproduce

  1. Present the drop-in UI. Include a PayPalCheckoutRequest instance configured with the correct order total and a request to present the PayPal PayIn3 option.

  2. Complete the PayPal sign in flow, select a test payment instrument and select the "Just Once" option when the OS presents it.

  3. Get returned to our App, but our logs clearly show that the drop-in onFailure method was called with a UserCanceledException.

Expected behavior

The drop-in UI doesn't error with a UserCanceled exception. Instead a PayPalAccountNonce is returned with which we can complete the Sandbox transaction.

Also ideally, the OS shouldn't be presenting the user with the two options requesting how supported links should be handled in the future. The 3DS flow doesn't do this.

Screenshots

Here's a link to a video I posted demonstrating the issue.

sshropshire commented 1 year ago

Hey @soarb thanks for filing this. Are there multiple build variants present on this test device? Also does the same thing happen on an emulator?

soarb commented 1 year ago

Thanks for getting back to me @sshropshire ... there's only ever a single build variant on the test device and I tend not to use emulators as I like to see things working on the real thing - especially on Android!

I just don't quite understand how the 3DS browser switch back to the device works, but the PayPal browser switch throws up this dialog and then errors with a UserCanceledException if I tap "Just Once"?

sshropshire commented 1 year ago

@soarb understood! A real device is the best way to test. I want to rule out some possibilities here though, I'm wondering if this issue could have something to do with a specific device. I'm testing on a Samsung Galaxy S21 5G and I don't get the disambiguation dialog.

I'm not sure what would cause "Just Once" to fail either. If possible, I'd ⌘ + Shift + O in Android Studio to do a symbol search for DropInInternalClient, set a breakpoint here, and reproduce the issue to inspect URL from the deep link intent.

soarb commented 1 year ago

Hi @sshropshire,

I can confirm we have the same issue on the following test devices:-

Samsung S22 OS 13 Pixel 3 with Android 12 Pixel 6 pro with Android 13

I'll report back when I've debugged the URL from the deep link intent though :)

soarb commented 1 year ago

So in all cases @sshropshire, when choosing our App and the "Just Once" option the PayPalClient's onBrowserSwitchResult.deepLinkUrl is always null. This is the case on repeated attempts.

The only time the url resolves to a value is when selecting "Always" in which case we get the following:-

{our app's package name}.braintree://onetouch/v1/success?paymentId=PAYID-MRJ66YI8H858885D1798690T&token=EC-07G27203YY045835S&PayerID=UGR2UJXVNXW7W

and everything works.

In order to recreate this, please ensure whatever app you're testing on, is removed from your device's Settings -> Apps -> Choose default apps -> Opening links -> {App Name} by clicking on "Clear defaults". This is on our S10 so the location may differ slightly.

With this setting removed, we get the OS prompt about how the App should handle links in the future. Selecting "Just Once" always returns a null url, selecting "Always" works.

It's worth reiterating, that when we trigger 3DS 2FA via our custom checkout (which performs a similar browser switch) we don't go via the drop-in. Instead we use an instance of ThreeDSecureClient. Once the browser switch is complete, we're returned to our App and everything works as expected (with no OS prompt).

soarb commented 1 year ago

Hi @sshropshire, any update on this please? I can also add that dismissing the OS prompt leaves us "stuck" on the PayPal "One more step" phase in the browser screen. We never get returned to our app unless we just close the screen. I can provide a video if required?

I know this doesn't help much but the drop-in experience on iOS performs without any issue whatsoever.

kevin68 commented 1 year ago

Hi, I'm having the same issue when using the DropInClient to request to vault a PayPal account, here is the code (kotlin) that I used on a test jetpack compose app:

class MainActivity : FragmentActivity(), ClientTokenProvider, DropInListener {

    private lateinit var braintreeDropInClient: DropInClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        braintreeDropInClient = DropInClient(
            this,
            this
        )
        braintreeDropInClient.setListener(this)

        setContent {

            TestTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Text("Hello")
                    Button(content = {
                        Text("test")
                    }, onClick = {
                        launchBraintreeDropIn()
                    })
                }
            }
        }
    }

    fun launchBraintreeDropIn() {
        braintreeDropInClient.invalidateClientToken()
        val request = DropInRequest().apply {
            isCardDisabled = true
            isVaultManagerEnabled = false
            payPalRequest = PayPalVaultRequest().apply {
                billingAgreementDescription = "."
            }
        }
        braintreeDropInClient.launchDropIn(request);
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        setIntent(intent) // Breakpoint here
    }

    override fun getClientToken(callback: ClientTokenCallback) {
        callback.onSuccess("token_from_server")
    }

    override fun onDropInSuccess(dropInResult: DropInResult) {
        TODO("Not yet implemented") // Breakpoint here
    }

    override fun onDropInFailure(error: Exception) {
        TODO("Not yet implemented") // Breakpoint here (execution stop there first)
    }
}

manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test"
        tools:targetApi="34">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleInstance"
            android:label="@string/app_name"
            android:theme="@style/Theme.Test">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="${applicationId}.braintree" />
            </intent-filter>
        </activity>
    </application>
</manifest>

The breakpoint on setIntent shows that the data of the intent contains this url: (app_id).braintree://onetouch/v1/success?ba_token=BA-1234567890ABCDEFG I'm not sure of anything, but from what i understand, it looks like something like this happen:

Versions of what I use:

I hope thank we can find a solution, don't hesitate to tell me how I can help you investigate.

Chaos2805 commented 1 year ago

@sshropshire Hi any update on this? I need to bump braintree SDK in my project

sshropshire commented 1 year ago

Hi @Chaos2805 @kevin68 @soarb apologies for the extreme delay. I'm circling back and I believe I know the root cause of this issue, but will need verification to make sure as I'm unable to reproduce on my end.

There seems to be an error in the documentation. For DropIn, you won't need to register an intent filter. We launch DropInActivity internally, and this activity is registered as the deep link target.

It appears that when the DropIn library's AndroidManifest.xml is merged with the manifest of the host application, it's possible for duplicate intent filters to be created. This will cause the Android OS to disambiguate the next course of action by requesting the user's preference with a system popup dialog.

If this resolves the issue, I'll update the docs to remove this additional unnecessary step.

kevin68 commented 1 year ago

@sshropshire It's working after removing the intent-filter, thanks !

Chaos2805 commented 1 year ago

@sshropshire It didn't solve my issue, if I remove the intent-filter, drop-in activity not receiving the deeplink. For my case, I upgrade drop-in SDK from 6.5.0 to 6.11.0, and i found that previously BraintreeClient will convert default params to lowercase, and now it removed. img_v2_f680de6e-9ecb-4f20-bdd4-bada865326dh In my application the applicationId contains capital letter, I think the scheme is not accepting capital case, since it prompt AppLinkUrlError if I put capital for android:scheme. @sshropshire Can you try with a capitalized applicationId to reproduce this?

I found the BrainTreeClient constructor able to pass the custom url scheme, but on top DropInClient not able to modify the setting of BrainTreeClient image