Critical Crash on Payment Success Handling in Google Pay Flutter Plugin Affecting 350+ Users
Crash on Payment Success Handling in Google Pay Flutter Plugin
Description:
We have encountered a critical issue in the Google Pay Flutter plugin that is causing crashes during the payment success handling process. This issue has been reported by multiple clients and is currently affecting over 350 users.
Stack Trace:
io.flutter.plugins.pay_android.GooglePayHandler.handlePaymentSuccess (GooglePayHandler.java:6)
io.flutter.plugins.pay_android.GooglePayHandler.onActivityResult (GooglePayHandler.java:6)
io.flutter.embedding.engine.FlutterEngineConnectionRegistry$FlutterEngineActivityPluginBinding.onActivityResult (FlutterEngineConnectionRegistry.java:25)
io.flutter.embedding.engine.FlutterEngineConnectionRegistry.onActivityResult (FlutterEngineConnectionRegistry.java:13)
io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.m (FlutterActivityAndFragmentDelegate.java:16)
io.flutter.embedding.android.FlutterFragment.onActivityResult (FlutterFragment.java:10)
io.flutter.embedding.android.FlutterFragmentActivity.onActivityResult (FlutterFragmentActivity.java:5)
Impact:
- Affected Users: 350+
- Severity: High – This crash is significantly impacting the user experience and potentially leading to loss of transactions.
Request for Update:
Is there any progress or update on this issue? Given the severity and impact on our users, a prompt resolution or workaround would be greatly appreciated.
Additional Information:
If any further details or logs are needed, please let us know. We're eager to collaborate to resolve this issue as quickly as possible.
This PR should fix: https://github.com/google-pay/flutter-plugin/pull/276#pullrequestreview-2281010300
@ViniciusDeep has this fix been pushed at all?
I see there were some fixes in the android package but have these been filtered through to this package?
I ask as I am getting fatal crashes on receiving a payment result from Google Pay.
Hi @SamuelMTDavies, the update is available in beta. Check out version 3.0.0-beta.2. Feel free to share insights about your experience with it.
@JIUgia - I have upgraded and its not crashing on Android but i may have found a bug. I will post it here and if deemed necessary i will put it n a separate issue.
This function I took from the advanced example:
void _showPaymentSelectorForProvider(PayProvider provider) async {
try {
debugPrint('Starting payment selector...');
final result = await _payClient.showPaymentSelector(provider, [
PaymentItem(
label: 'into your ---- wallet',
amount: validAmount,
status: PaymentItemStatus.final_price,
)
]);
debugPrint('Payment selector result: $result');
// The error might occur here
context.read<PaymentsBloc>().add(GooglePayResponseSelected());
} catch (error) {
debugPrint("Caught an error: ${error.toString()}");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
contentType: ContentType.failure,
message: 'There was a problem setting up your payment.',
),
);
}
}
This try block errors and the SnackBar is shown before the payment selector appears. The payment selector appears and I can get the result.
The error logs:
I/flutter (20680): Starting payment selector...
E/FrameEvents(20680): updateAcquireFence: Did not find frame.
I/flutter (20680): Payment selector result: {}
I/flutter (20680): Caught an error: type 'Null' is not a subtype of type 'String'
I assume this is to do with the _googlePayResultSubscription Stream receiving Null before the paymentSelector is shown? I haven't had any chance to look into this mechanism and I don't have much android specific knowledge.
// A method to listen to events coming from the event channel on Android
void _startListeningForPaymentResults() {
_googlePayResultSubscription = eventChannel
.receiveBroadcastStream()
.map((result) => result.toString())
.listen(debugPrint, onError: (error) => debugPrint(error.toString()));
}
I would like to know if this code smell is right and we can account for it in the next beta version? Do you want me to open this in a separate issue?
Hi @SamuelMTDavies, the error happens exactly at the line you are pointing out, very likely triggered by the logic called by Provider when you receiving a result. That logic is surely expecting a populated object and encountering an empty JSON string, such that when you look for certain properties in there, a null result is returned, hence the exception.
Note that for platforms that use asynchronous means to return the result, calling showPaymentSelector only returns an empty string if the subscription got created without errors. In other words, you won't get a Google Pay result. The readme has a detailed excerpt on the topic in the Advanced section.
I've also added a few extra lines in the advanced.dart example to make this a little more explicit.
Let me know if that works for you.
@JlUgia Just for my own clarity.
This code always returns null if the payment selector is successfully shown? (for android)
final result = await _payClient.showPaymentSelector(provider, [
PaymentItem(
label: 'into your ---- wallet',
amount: validAmount,
status: PaymentItemStatus.final_price,
)
]);
and that any result from the payment selector will be received here:
void _startListeningForPaymentResults() {
_googlePayResultSubscription = eventChannel
.receiveBroadcastStream()
.map((result) => result.toString())
.listen(debugPrint, onError: (error) => debugPrint(error.toString()));
}
I can follow this logic but would suggest that the advanced example is updated to make this clearer and stop it from trying to Cast Null.
Hi @SamuelMTDavies, that's exactly right. Any platform that uses asynchronous mechanisms to return a result will see showPaymentSelector returning null.
I think that the null casting was never part of the advanced.dart example. It looks like that was only present in your code. In any case, we've updated the advanced sample to make it more obvious.
There are other options to handle this, which I'll note here for potential future use based on feedback from you and other developers:
| Solution | Pros | Cons |
|---|---|---|
| Use a stream to receive payment information only on platforms that need it (current solution) | Same API signature | The same call returns different values depending on whether the platform issues payment results synchronously or asynchronously |
| Use a stream to receive payment information on all platforms | Same API signature and behavior | Unnecessary complexity on platforms that can't respond synchronously |
| Introduce separate API/methods to handle different platforms based on their synchronicity requirements | Integrating each platform separately is more straightforward | Having shared logic across platform becomes more challenging |
Update the showPaymentSelector method to return a composite object that has information about what happened during the call (eg.: a stream was started, a result is returned) |
Keeps APIs consistent across platform and reduces confusion | Breaking change |
I've got it working in the test environments but it does seem a little temperamental. It's a pain having to test on real iOS devices and that the results come in two places depending on the device but it will have to do.
@JlUgia
Does the bool the canUserPay function returns change based on the Payment Configuration? I'm trying to understand why a Google Pay button shows up on a device with testing config and then doesn't show on the same device with production config.
Hey @SamuelMTDavies, what feels temperamental? Are you referring to something specific? Also, is there any plugin-specific functionality that requires testing on a real device, or are you referring to the Apple Pay API? I hear you about needing to have logic in two places. Have you seen the table? Would you prefer one or more of the alternatives proposed there?
Remember that both APIs at Google and Apple Pay follow different dynamics to decide whether a user can pay in the platform. For Google Pay, these conditions are mostly structural and relate to aspects like the country where the account is operating from, the existence (and version) of Google Play services on the device, an being logged in with a valid @gmail account. Take a look at the isReadyToPay reference for more info.
The temprementality is whether the Google Pay or Apple Pay button shows or not. I am testing across the simulator, emulator, real iPhone and android devices.
Sometimes it works fine on the emulator then wont show on the real device. I have an android device that had the full flow working in the test environment and then when we tried to test a production setup the button wouldn't show.
I am beginning to wonder if the string implementation of the config is too prone to formatting issues as I can see no other reason for the button to not show when removing a single line of config only. This seems to be the root of many of our issues.
As for your question on implementation i personally would lean towards the first or last option. The first option feels disconnected from the package to me having the traditional _onButtonPressed, _onResultReceived, _onError is clear and clean, similarly if you can subscribe to a single stream and understand the errors that can be thrown and why thats equally workable.
It's also odd to pass the Google Configuration the the Pay client instance and then provide the configuration a second time on the RawGoogleButton - which one is used? Its easy for people to update one and not the other, which could be easily missed.
That's a fair point @SamuelMTDavies.
The need to pass the configuration to the Google Pay button and the Pay client is a current requirement of the PayButton API. The configuration in the button is used to decide the dynamic content included in the button (if any of the button types that support dynamic content are used – read more).
The configuration in the Pay client is used to configure the request and payment information returned by the API.
Hope this helps clarify.