chargebee / chargebee-flutter

MIT License
5 stars 8 forks source link

Error on Android: Reply already submitted #46

Closed ciriousjoker closed 1 year ago

ciriousjoker commented 1 year ago

Our production app occasionally throws an error.

Stacktrace:

Fatal Exception: java.lang.IllegalStateException: Reply already submitted
       at ff.c$g.a(:35)
       at rf.k$a$a.b(:14)
       at q2.a.d(:16)
       at q2.a.a()
       at q2.a$g.a(:9)
       at j2.a$c.a(:245)
       at com.android.billingclient.api.a0.run(:5)
       at d1.g.run(:29)
       at android.os.Handler.handleCallback(Handler.java:942)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loopOnce(Looper.java:226)
       at android.os.Looper.loop(Looper.java:313)
       at android.app.ActivityThread.main(ActivityThread.java:8741)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)

This seems like some sort of a race condition.

Reply already submitted

This often happens when trying to call .success() multiple times in the Android implementation of a Flutter plugin.

Since billingclient shows up and we didn't have this error before switching from our custom chargebee sdk wrapper to this official one, I opened the issue here.

ciriousjoker commented 1 year ago

Here are some notes based on our crash logs:

I suspect the error is somewhere in here: https://github.com/chargebee/chargebee-flutter/blob/2d220293eec3aab3462b153ef23d6530804d606a/android/src/main/kotlin/com/chargebee/flutter/sdk/ChargebeeFlutterSdkPlugin.kt#L141-L196

The code in purchaseProduct() seems convoluted and messy and it's really hard to guarantee that only a single call to .success() or onError() goes through.

When we created our own Chargebee sdk wrapper because this one doesn't exist, I ran into the same issue. @cb-amutha I invited you to the repository, check it out if you want.

Here's the excerpt of the purchaseProduct():

private fun purchaseProduct(@NonNull call: MethodCall, @NonNull result: Result) {
        val customerID = call.argument<String>("customerID")!!

        CBPurchase.retrieveProducts(
            this.activity!!, arrayListOf(call.argument<String>("productId")!!),
            object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
                override fun onSuccess(productIDs: ArrayList<CBProduct>) {
                    // Error: no products found
                    if (productIDs.size == 0) {
                        result.error(
                            "no_products_found",
                            "No products found. Perhaps the product ID you specified doesn't exist.",
                            null,
                        )
                        return
                    }

                    CBPurchase.purchaseProduct(
                        product = productIDs[0],
                        customerID = customerID,
                        object : CBCallback.PurchaseCallback<String> {
                            override fun onSuccess(subscriptionID: String, status: Boolean) {
                                result.success(
                                    hashMapOf(
                                        "subscriptionId" to subscriptionID,
                                        "status" to status,
                                    )
                                )
                            }

                            override fun onError(error: CBException) {
                                sendError(result, error)
                            }
                        }
                    )
                }

                override fun onError(error: CBException) {
                    sendError(result, error)
                }
            }
        )
    }

The main difference seems to be that there's another catch around the whole thing. I'm not sure if the native sdk can throw, but if it does, this would mean .error() is called twice for the same error.

cb-amutha commented 1 year ago

thanks @ciriousjoker, Let’s do some more sanity test around the purchaseProduct and will update it.