braintree / braintree_android

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

(URGENT!!!) PayPal Activity doesn't open on Samsung Devices #1040

Closed TheArchitect123 closed 4 weeks ago

TheArchitect123 commented 1 month ago

Braintree SDK Version

4.47.0

Environment

Sandbox

Android Version & Device

Samsung S21 (Android 14)

Braintree dependencies

com.braintreepayments.api:paypal:4.47.0

Describe the bug

Currently I have a service that opens a PayPal activity, which allows users to onboard their PayPal wallet into my company's app.

    val braintreeToken = "" // my remote token 
    val  brainTreeClient =
                    BraintreeClient(CurrentActivity, braintreeToken)
                val     payPalClient =
                    PayPalClient(CurrentActivity, brainTreeClient)
                payPalClient!!.setListener(object : PayPalListener {
                    override fun onPayPalSuccess(payPalAccountNonce: PayPalAccountNonce) {
                          // pass payment token back to app
                    }

                    override fun onPayPalFailure(error: java.lang.Exception) {
                            // PAYPal Gets Cancelled on Samsung
                    }
                })

                val request = PayPalVaultRequest()
                    .apply {
                        localeCode = "" // My Country code 
                        billingAgreementDescription =
                            "Billing Agreement"
                    }

                payPalClient!!.tokenizePayPalAccount(CurrentActivity, request)

When invoking the above, onPayPalFailure gets triggered, and I get the following error. error = {UserCanceledException@38573} com.braintreepayments.api.UserCanceledException: User canceled PayPal. isExplicitCancelation = false isExplicitCancelation {boolean} backtrace = {Object[13]@38579} 0 = {long[24]@38658} [481158122528, 481158122080, 481158122496, 468213704320, 1898905344, 1898905408, 1898909864, 1898909832, 1901789312, 1901576224, 1900247616, 1903048448, 127, 5, 8, 65, 2, 4, 185, 83, 107, 4294967295, 11, 334] 1 = {Class@30537} "class com.braintreepayments.api.PayPalClient" 2 = {Class@30537} "class com.braintreepayments.api.PayPalClient" 3 = {Class@30537} "class com.braintreepayments.api.PayPalClient" 4 = {Class@38563} "class com.braintreepayments.api.PayPalLifecycleObserver$1" 5 = {Class@1259} "class android.os.Handler" 6 = {Class@1259} "class android.os.Handler" 7 = {Class@16545} "class android.os.Looper" 8 = {Class@16545} "class android.os.Looper" 9 = {Class@7695} "class android.app.ActivityThread" 10 = {Class@10634} "class java.lang.reflect.Method" 11 = {Class@11255} "class com.android.internal.os.RuntimeInit$MethodAndArgsCaller" 12 = {Class@1339} "class com.android.internal.os.ZygoteInit" cause = null detailMessage = "User canceled PayPal." stackTrace = {StackTraceElement[12]@38584} 0 = {StackTraceElement@38586} "com.braintreepayments.api.PayPalClient.onBrowserSwitchResult(PayPalClient.java:416)" declaringClass = "com.braintreepayments.api.PayPalClient" fileName = "PayPalClient.java" lineNumber = 416 methodName = "onBrowserSwitchResult" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 1 = {StackTraceElement@38587} "com.braintreepayments.api.PayPalClient.deliverBrowserSwitchResultToListener(PayPalClient.java:351)" declaringClass = "com.braintreepayments.api.PayPalClient" fileName = "PayPalClient.java" lineNumber = 351 methodName = "deliverBrowserSwitchResultToListener" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 2 = {StackTraceElement@38588} "com.braintreepayments.api.PayPalClient.onBrowserSwitchResult(PayPalClient.java:346)" declaringClass = "com.braintreepayments.api.PayPalClient" fileName = "PayPalClient.java" lineNumber = 346 methodName = "onBrowserSwitchResult" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 3 = {StackTraceElement@38589} "com.braintreepayments.api.PayPalLifecycleObserver$1.run(PayPalLifeCycleObserver.java:75)" declaringClass = "com.braintreepayments.api.PayPalLifecycleObserver$1" fileName = "PayPalLifeCycleObserver.java" lineNumber = 75 methodName = "run" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 4 = {StackTraceElement@38590} "android.os.Handler.handleCallback(Handler.java:958)" declaringClass = "android.os.Handler" fileName = "Handler.java" lineNumber = 958 methodName = "handleCallback" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 5 = {StackTraceElement@38591} "android.os.Handler.dispatchMessage(Handler.java:99)" declaringClass = "android.os.Handler" fileName = "Handler.java" lineNumber = 99 methodName = "dispatchMessage" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 6 = {StackTraceElement@38592} "android.os.Looper.loopOnce(Looper.java:230)" declaringClass = "android.os.Looper" fileName = "Looper.java" lineNumber = 230 methodName = "loopOnce" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 7 = {StackTraceElement@38593} "android.os.Looper.loop(Looper.java:319)" declaringClass = "android.os.Looper" fileName = "Looper.java" lineNumber = 319 methodName = "loop" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 8 = {StackTraceElement@38594} "android.app.ActivityThread.main(ActivityThread.java:8893)" declaringClass = "android.app.ActivityThread" fileName = "ActivityThread.java" lineNumber = 8893 methodName = "main" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 9 = {StackTraceElement@38595} "java.lang.reflect.Method.invoke(Native Method)" declaringClass = "java.lang.reflect.Method" fileName = "Method.java" lineNumber = -2 methodName = "invoke" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 10 = {StackTraceElement@38596} "com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:608)" declaringClass = "com.android.internal.os.RuntimeInit$MethodAndArgsCaller" fileName = "RuntimeInit.java" lineNumber = 608 methodName = "run" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 11 = {StackTraceElement@38597} "com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)" declaringClass = "com.android.internal.os.ZygoteInit" fileName = "ZygoteInit.java" lineNumber = 1103 methodName = "main" shadow$klass = {Class@12900} "class java.lang.StackTraceElement" shadow$monitor = 0 suppressedExceptions = {Collections$EmptyList@38582} size = 0 shadow$klass = {Class@38506} "class com.braintreepayments.api.UserCanceledException" shadow$monitor = 0

To reproduce

Follow the steps above and run on a Samsung S21 (or an Android device that doesn't require managing responses via Activity Lifecycles)

Expected behavior

That PayPal activity opens up successfully on all Android devices, and users can add their PayPal wallets without issues.

Screenshots

No response

TheArchitect123 commented 1 month ago

@sarahkoop

sarahkoop commented 1 month ago

Hi @TheArchitect123 - Can you share a screen recording of the behavior your a seeing? Are you calling tokenize from within an Android activity or fragment (it's unclear from the code snippet provided)?

Your integration may be a good candidate to use the manual browser switching pattern which allows you to control delivery of the browser switch result and doesn't encapsulate management of the result via the activity lifecycle. This requires you to call parseBrowserSwitchResult on resume of your app.

TheArchitect123 commented 1 month ago

Hi @sarahkoop, I'm calling the tokenize method from a fragment. I'm using Google's Navigation Library to manage fragment transactions within a single Core Activity (Single activity Architecture).

I'm using both Manual Configuration (on devices that can receive intents from Paypal), but the default is to use Paypal tokenisation via function call directly (which is what the Samsung device does).

Here's the expected behaviour (when tested against a Google Pixel Device), which uses Manual Configuration. https://drive.google.com/file/d/1vrZW4IhxuLHoT-QOHoT-k28_8GvhrEju/view?usp=sharing

And here's the behaviour when running against my Samsung S21. It just cancels the Paypal request, and gets stuck. https://drive.google.com/file/d/1YgKvxDskRDyyp_O5LhRYIMRjNZFQWzaE/view?usp=sharing

sarahkoop commented 1 month ago

Based on the code snippet provided, it does not look like you are using the manual browser switching integration. Our docs for this are somewhat confusing, and we are working to simplify the integration patterns in the next major version. I think there could be unexpected behavior when mixing both integration patterns - so we would recommend only using the manual pattern if it's required for your architecture.

For manual integration you should use PayPalClient(BraintreeClient) and call tokenizePayPalAccount(FragmentActivity, PayPalRequest, PayPalFlowStartedCallback) without use of a PayPalListener. You must also call parseBrowserSwitchResult and onBrowserSwitchResult to receive results.

TheArchitect123 commented 1 month ago

@sarahkoop

The manual documentation is a bit outdated (https://github.com/braintree/braintree_android/blob/main/v4.9.0%2B_MIGRATION_GUIDE.md#manual-browser-switching-for-browser-based-flows)

I'm using the latest stable 4.47.0, and it doesn't have a tokenizePayPalRequest with only a request as a parameter.

Here's an example of what I'm doing with my activity (I'm now using Manual Configuration Only). It still doesn't work for Samsung Devices. The activity just freezes now


class MainActivity : AppCompatActivity{
lateinit var payPalClient : PayPalClient
override fun onResume() {
    super.onResume()

    payPalClient.parseBrowserSwitchResult(this, intent)?.let {
        handleBrowserSwitchResult(it)
    }
}

override fun onNewIntent(newIntent: Intent?) {
    super.onNewIntent(newIntent)

    intent = newIntent
    payPalClient.parseBrowserSwitchResult(this, newIntent)?.let {
        handleBrowserSwitchResult(it)
    }
}

private fun handleBrowserSwitchResult(result: BrowserSwitchResult) {
   payPalClient.onBrowserSwitchResult(result) { payPalAccountNonce, error ->
        payPalAccountNonce?.let {
            lifecycleScope.launch {
                 // process PayPal results
                //    it.payerId,
                //    it.string
            }
        } ?: error?.let {
            // handle error
        }
    }

    // clear pending request to guard against additional browser switch result invocations
    payPalClient.clearActiveBrowserSwitchRequests(this)
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // paypal configuration
    val brainTreeClient =
        BraintreeClient(
            this,
            "braintree_token"
        )
     payPalClient =
        PayPalClient(this, brainTreeClient)

    val browserSwitchResult =
        PayPalService.payPalClient.parseBrowserSwitchResult(this, intent)
    if (browserSwitchResult != null) {
        // process kill scenario
        handleBrowserSwitchResult(browserSwitchResult)
    }
}
}
TheArchitect123 commented 1 month ago

Hi @sarahkoop,

Do you have any updates on this issue? My company's deploying our product in the next couple of weeks, and we need this issue resolved ASAP. Thank you.

ahmet12 commented 1 month ago

Can you try adding some delay before launching web payment? onResume lifecycle dependency may cause user canceled problem. @TheArchitect123

TheArchitect123 commented 4 weeks ago

It doesn't work unfortunately @ahmet12 , but thanks for the suggestion.

TheArchitect123 commented 4 weeks ago

I found these logs as well @sarahkoop

PayPalClient.java com.braintreepayments.api.PayPalClient onBrowserSwitchResult 416 null E PayPalClient.java com.braintreepayments.api.PayPalClient deliverBrowserSwitchResultToListener 351 null E PayPalClient.java com.braintreepayments.api.PayPalClient onBrowserSwitchResult 346 null E PayPalLifeCycleObserver.java com.braintreepayments.api.PayPalLifecycleObserver$1 run 75 null E Handler.java android.os.Handler handleCallback 958 null E Handler.java android.os.Handler dispatchMessage 99 null E Looper.java android.os.Looper loopOnce 230 null E Looper.java android.os.Looper loop 319 null E ActivityThread.java android.app.ActivityThread main 8893 null E Method.java java.lang.reflect.Method invoke -2 null E RuntimeInit.java com.android.internal.os.RuntimeInit$MethodAndArgsCaller run 608 null E ZygoteInit.java com.android.internal.os.ZygoteInit main 1103 null E PAYPAL RESULT : kotlin.Unit null

TheArchitect123 commented 4 weeks ago

I've FINALLY FOUND IT!!!

Turns out it was my local antivirus the whole time. I'm closing this issue.

SurfaceFlinger: id=310592 createSurf, flag=44004, com.antivirus/com.avast.android.one.base.ui.scamprotection.scan.UrlScanActivity$_23967#310592 06-24 00:43:22.097 1656 5305 D WindowManager: makeSurface duration=1 name=com.antivirus/com.avast.android.one.base.ui.scamprotection.scan.UrlScanActivity$_23967

sshropshire commented 3 weeks ago

Hey @TheArchitect123 thanks for closing this. Out of curiosity, what antivirus software are you using? We're offering support for app links in the near future, and I have a feeling that will fix this issue without having to turn off the antivirus program. It'd be nice to be able to reproduce on our end so we can verify the potential fix.

TheArchitect123 commented 3 weeks ago

@sshropshire AVG antivirus

sshropshire commented 3 weeks ago

@TheArchitect123 thanks!