Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Critical Crash on Payment Success Handling in Google Pay Flutter Plugin Affecting 350+ Users #274

Open
ViniciusDeep opened this issue Aug 26, 2024 · 11 comments

Comments

@ViniciusDeep
Copy link

ViniciusDeep commented Aug 26, 2024

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.

@ViniciusDeep
Copy link
Author

This PR should fix: #276 (review)

@SamuelMTDavies
Copy link

@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.

@JlUgia
Copy link
Member

JlUgia commented Nov 27, 2024

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.

@SamuelMTDavies
Copy link

@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?

@JlUgia
Copy link
Member

JlUgia commented Nov 29, 2024

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.

@SamuelMTDavies
Copy link

SamuelMTDavies commented Dec 2, 2024

@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.

@JlUgia
Copy link
Member

JlUgia commented Dec 3, 2024

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

@SamuelMTDavies
Copy link

SamuelMTDavies commented Dec 4, 2024

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.

@JlUgia
Copy link
Member

JlUgia commented Dec 4, 2024

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.

@SamuelMTDavies
Copy link

SamuelMTDavies commented Dec 5, 2024

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.

@SamuelMTDavies
Copy link

SamuelMTDavies commented Dec 5, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants