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

NullPointerException upon returning to app #441

Open Buakenze opened 10 months ago

Buakenze commented 10 months ago

Braintree SDK Version

4.38.2

Environment

Sandbox

Android Version & Device

Samsung Galaxy A34, Android 13

Braintree dependencies

com.braintreepayments.api:drop-in:6.13.0

Describe the bug

Upon returning to app after selecting a paypal payment method I get a NullPointerException:

java.lang.RuntimeException: Unable to start activity ComponentInfo{it.elbuild.ilceppo/com.braintreepayments.api.DropInActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.os.Bundle.setClassLoader(java.lang.ClassLoader)' on a null object reference
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4169)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4325)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2574)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:226)
        at android.os.Looper.loop(Looper.java:313)
        at android.app.ActivityThread.main(ActivityThread.java:8757)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)
     Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.os.Bundle.setClassLoader(java.lang.ClassLoader)' on a null object reference
        at com.braintreepayments.api.DropInActivity.getDropInRequest(DropInActivity.java:124)
        at com.braintreepayments.api.DropInActivity.onCreate(DropInActivity.java:78)
        at android.app.Activity.performCreate(Activity.java:8591)
        at android.app.Activity.performCreate(Activity.java:8570)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1384)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4150)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4325) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2574) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loopOnce(Looper.java:226) 
        at android.os.Looper.loop(Looper.java:313) 
        at android.app.ActivityThread.main(ActivityThread.java:8757) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)

I found other issues with this error namely #403 and #404 but no solution was given. In my case, it happens everytime.

The crash happens both with an emulator and physical device. I can't understand what I'm doing wrong, it should be a very basic implementation. I have a single activity architecture where I needed the DropInClient inside a fragment, and I even tried moving the logic to the activity to no avail. I am only using the DropInClient, not the BraintreeClient. I also tried overriding onNewIntent as stated here, even though this refers to PayPalClient.

In my activity I have:

private fun initializeDropInClient() {
    if(dropInClient == null) {
        val provider = viewmodel.getClientTokenProvider()
        dropInClient = DropInClient(this, provider)
        dropInClient!!.setListener(this)
    }
}
fun openDropInUI(amount: Double) {
    dropInClient!!.launchDropIn(DropInRequest().apply {
        isCardDisabled = true
        isVenmoDisabled = true
        isGooglePayDisabled = true
        payPalRequest = PayPalCheckoutRequest(formatPrice(amount))
    })
}
override fun onDropInSuccess(dropInResult: DropInResult) {
    val nonce = dropInResult.paymentMethodNonce?.string
    if(nonce != null) {
        (currentFragment() as CarrelloFragment).braintreePayment(nonce)
    }
}

override fun onDropInFailure(error: Exception) {
    if(error !is UserCanceledException) {
        showInfoDialogIfPossible(getString(R.string.errore),
            getString(R.string.error_during_payment) + " " + error.message, getString(android.R.string.ok)) {}
    }
}

I tried fiddling a bit with the intent filters since I read here https://github.com/braintree/braintree-android-drop-in/issues/419#issuecomment-1686825003 that I shouldn't declare it as stated in the documentation (hence the commented part). As for the DropInActivity intent filter, I found it somewhere else in another issue, but can't understand if it's needed or not. I'm still getting the same null pointer either if I declare the intent filter or not. If I declare the intent filter in my activity, I get two choices to get back to the app (duplicated intent filters it seems): one gets me a UserCanceledException, the other gives me the NullPointerException mentioned above.

<activity
    android:name="it.elbuild.ui.MainActivity"
    android:exported="true"
    android:screenOrientation="portrait"
    android:launchMode="singleInstance"
    android:taskAffinity="">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!--
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="it.elbuild.ilceppo.braintree" />
    </intent-filter>
    -->
</activity>

<activity
    android:name="com.braintreepayments.api.DropInActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="it.elbuild.ilceppo.braintree" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
</activity>

I'm also getting the same exception with drop in versions 6.10 and 6.12. I upgraded in hope a later version would solve the issue.

Thank you in advance for your attention.

To reproduce

  1. Open Drop In UI
  2. Select Paypal payment method
  3. login
  4. review order
  5. back to app, crash 100%

Expected behavior

getting a callback to onDropInSuccess or onDropInFailure

Screenshots

No response

sshropshire commented 10 months ago

Hi @Buakenze thanks for using the Braintree SDK for Android. I agree this is strange behavior, but the fact that it's reproducible is promising.

The stack trace references this line in DropInActivity.java. Here, we're looking for an intent extra that contains the original DropInRequest. This gets set by the SDK when your app calls DropInClient.launchDropIn().

The only way I could see this value not being present is if DropInActivity is launched manually without any intent extras. Is it possible for you to set a breakpoint here in DropInActivityResultContract.java to see if the intent is being created correctly?

Buakenze commented 10 months ago

Hi @sshropshire, thank you for your reply. I tried and it appears the intent is created correctly, with my DropInRequest inside. Only weird thing I noticed is that this method is called twice in quick succession after I call launchDropIn. The sessionid inside DropInIntentData is the same, so maybe that's intended behavior.

sshropshire commented 10 months ago

Actually that is a good observation. Is there something in the UI layer that may cause it to launch twice?

Buakenze commented 10 months ago

I believe there isn't, I confirmed I have only one DropInClient instance and launchDropIn is called only once. What else could cause it to fire twice?

As I stated in the main post I had tried overriding onNewIntent as stated here, but currently that code is commented and I'm getting the same result.