-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[IAP] Check if the payment processor is available #1057
Conversation
|
I'm happy to break this up into smaller PRs if that would make it easier to review, let me know. I think there's a couple ways this could be split and it's a bit on the large side right now. |
packages/in_app_purchase/lib/src/in_app_purchase_connection.dart
Outdated
Show resolved
Hide resolved
| // If the widget was removed from the tree while the asynchronous platform | ||
| // message was in flight, we want to discard the reply rather than calling | ||
| // setState to update our non-existent appearance. | ||
| if (!mounted) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDYT about using a FutureBuilder instead of having this kind of logic explicitly here? (it will also makes it more clear where to to handle future error).
[Actually looking at this I'm thinking we might want to update the plugin template to use a FutureBuilder]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed here, filed flutter/flutter#26404 for the more broad issue.
| final Widget storeHeader = buildListCard(ListTile( | ||
| leading: Icon(_storeReady ? Icons.check : Icons.block), | ||
| title: | ||
| Text('The store is ' + (_storeReady ? 'open' : 'closed') + '.'))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a first look this made me think like this is some state we're getting from the IAP server (e.g user can configure their store to be "close")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to "available."
| /// | ||
| /// Throws an [UnsupportedError] when accessed on a platform other than | ||
| /// Android or iOS. | ||
| InAppPurchaseConnection connection = _createConnection(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this connection something an app might want to keep open only part of the time? Does it ever makes sense to close it? Is there a scenario where it would make sense to have multiple different connections?
| public void onPurchasesUpdated( | ||
| int responseCode, @Nullable List<Purchase> purchases) {} | ||
| }) | ||
| .build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just making sure - this have no side effects right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I'm aware of, besides creating the instance of the class in memory.
...app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
Show resolved
Hide resolved
...mple/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java
Show resolved
Hide resolved
packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/in_app_purchase_pluginTests.m
Show resolved
Hide resolved
mklim
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this connection something an app might want to keep open only part of the time? Does it ever makes sense to close it? Is there a scenario where it would make sense to have multiple different connections?
Good catch, I missed this. There's some larger changes in the latest commit related to this. I added handlers to close the connection. Then on second thought I was thinking that the start/stop connection was an Android specific thing that didn't really need to be in the main interface. I'm thinking that feasibly, the Android implementation of the interface could just handle those calls internally. I changed it over in the latest patch but I'd appreciate a second opinion. I think it may be a little bit too much "magic," even for the top layer that's supposed to be really simple to use.
| public void onPurchasesUpdated( | ||
| int responseCode, @Nullable List<Purchase> purchases) {} | ||
| }) | ||
| .build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I'm aware of, besides creating the instance of the class in memory.
| final Widget storeHeader = buildListCard(ListTile( | ||
| leading: Icon(_storeReady ? Icons.check : Icons.block), | ||
| title: | ||
| Text('The store is ' + (_storeReady ? 'open' : 'closed') + '.'))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to "available."
| // If the widget was removed from the tree while the asynchronous platform | ||
| // message was in flight, we want to discard the reply rather than calling | ||
| // setState to update our non-existent appearance. | ||
| if (!mounted) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed here, filed flutter/flutter#26404 for the more broad issue.
| MethodChannel('plugins.flutter.io/in_app_purchase'); | ||
| List<Map<String, Function>> _callbacks = <Map<String, Function>>[]; | ||
|
|
||
| /// Wraps `BillingClient#isReady()`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the native documentation for the native APIs these wrappers are calling through to is going to be much more useful than me trying to write summary Dart docs here. I'd rather just point people to the underlying class documentation with wrapper specific notes when it's worthwhile.
| /// [onBillingServiceConnected] has been converted from a callback parameter | ||
| /// to the Future result returned by this function. This returns the | ||
| /// `BillingClient.BillingResponse` `responseCode` of the connection result. | ||
| Future<BillingResponse> startConnection( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in the latest patch, these are now also linked to BillingClient instance creation and destruction on the native side.
| /// Basic generic API for making in app purchases across multiple platforms. | ||
| abstract class InAppPurchaseConnection { | ||
| /// Returns true if the payment platform is ready and available. | ||
| Future<bool> isAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd want to set up long polling to make sure it didn't go totally stale on the Android side. Maybe I could add it as a configurable option? I'd rather do this in a followup PR. For iOS this is always stable, so I could cache it.
amirh
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably also add Dart unit tests.
| /// Basic generic API for making in app purchases across multiple platforms. | ||
| abstract class InAppPurchaseConnection { | ||
| /// Returns true if the payment platform is ready and available. | ||
| Future<bool> isAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible rely on the BillingClientStateListener callbacks to notify us when we're ready/not ready or is "billing client setup complete" is different than ready?
| void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| switch (state) { | ||
| case AppLifecycleState.paused: | ||
| case AppLifecycleState.suspending: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe paused is currently called on Android's onStop, and that suspending is not currently invoked, just making sure that this is what we want here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, removed.
| class GooglePlayConnection | ||
| with WidgetsBindingObserver | ||
| implements InAppPurchaseConnection { | ||
| GooglePlayConnection() : _billingClient = BillingClient() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we make this a singleton as well? or is there a legit reason to have multiple of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason, changed.
...app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java
Outdated
Show resolved
Hide resolved
| MethodChannel('plugins.flutter.io/in_app_purchase'); | ||
| List<Map<String, Function>> _callbacks = <Map<String, Function>>[]; | ||
|
|
||
| /// Wraps `BillingClient#isReady()`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok.
[nit: lets link to the Javadoc at least to make it easier to jump when you look at these docs]
| /// [onBillingServiceConnected] has been converted from a callback parameter | ||
| /// to the Future result returned by this function. This returns the | ||
| /// `BillingClient.BillingResponse` `responseCode` of the connection result. | ||
| Future<BillingResponse> startConnection( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC there is no reason for 2 startConnection's to be in flight simultaneously?
I'm not sure I understand why do we need this list of callback maps and not just callback set for the current connection?
mklim
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably also add Dart unit tests.
Yeah, originally the Dart classes weren't really doing anything much but now GooglePlayConnection especially should probably be tested. Do you mind if this is done as a followup though? I'd rather unblock the other IAP work sooner, and this patch is already large enough where it's hard to review.
| MethodChannel('plugins.flutter.io/in_app_purchase'); | ||
| List<Map<String, Function>> _callbacks = <Map<String, Function>>[]; | ||
|
|
||
| /// Wraps `BillingClient#isReady()`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| /// [onBillingServiceConnected] has been converted from a callback parameter | ||
| /// to the Future result returned by this function. This returns the | ||
| /// `BillingClient.BillingResponse` `responseCode` of the connection result. | ||
| Future<BillingResponse> startConnection( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would work for now, but there's going to be other methods on BillingClient that require callbacks back up into Dart eventually. I figured this way I'd get the structure out of the way to start with.
| class GooglePlayConnection | ||
| with WidgetsBindingObserver | ||
| implements InAppPurchaseConnection { | ||
| GooglePlayConnection() : _billingClient = BillingClient() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason, changed.
| void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| switch (state) { | ||
| case AppLifecycleState.paused: | ||
| case AppLifecycleState.suspending: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, removed.
| /// Basic generic API for making in app purchases across multiple platforms. | ||
| abstract class InAppPurchaseConnection { | ||
| /// Returns true if the payment platform is ready and available. | ||
| Future<bool> isAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, but I can investigate for a followup. The only guarantee for BillingClientStateListener is that it's talking about setup being complete and successful, specifically. Practically I think it's probably tied to isReady but I'm not totally sure. I'd also still need at least one async call here to set it up.
amirh
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM but I still think we should include Dart unit tests as part of this PR as future work will probably need that unit test skeleton anyway, and as in general we try to avoid submitting untested code.
| /// [onBillingServiceConnected] has been converted from a callback parameter | ||
| /// to the Future result returned by this function. This returns the | ||
| /// `BillingClient.BillingResponse` `responseCode` of the connection result. | ||
| Future<BillingResponse> startConnection( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
| /// Basic generic API for making in app purchases across multiple platforms. | ||
| abstract class InAppPurchaseConnection { | ||
| /// Returns true if the payment platform is ready and available. | ||
| Future<bool> isAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, lets try to verify that before we first publish (we should be able to return false before we set up the callbacks so this could be synchronous)
The actual functionality here is extremely minimal. Most of this patch is establishing the structure of the rest of the plugin code to follow. 1. Add a generic IAP plugin interface with platform-specific implementations (`*_connection.dart`.) 2. Add platform specific wrappers (`*_wrappers.dart`). 3. Add dart end to end tests covering the sample app and unit tests covering the native platform code. These all need to be run manually currently.
It really only applies to Play, and the wrapper layer can manage this state itself. Adds an `endConnection()` call to `BillingClientWrapper`. Also some minor code readability changes.
OK, done. I was going to rearrange the dart files into a more fine grained structure in a followup PR, but I think with adding in dart unit tests it also became important to make sure the structure here made more sense now. d14bac6 moves the dart files around and the new tests are in 1f61bf2. |
| import 'dart:async'; | ||
| import 'package:flutter/services.dart'; | ||
|
|
||
| class FakePlatformViewsController { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect a "FakePlatformViewsController" to be a fake implementation of the engine's PlatformViewsController
|
|
||
| setUpAll(() { | ||
| Channel.override = SystemChannels.platform_views; | ||
| SystemChannels.platform_views.setMockMethodCallHandler( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why override the platform_views system channel? is it used by this plugin at all?
Can we not just set a mock handler for the plugins.flutter.io/in_app_purchase channel? and avoid the indirection added with the Channel class?
|
Following up in #1082. |
The actual functionality here is extremely minimal. Most of this patch is establishing the structure of the rest of the plugin code to follow. 1. Add a generic IAP plugin interface with platform-specific implementations (`*_connection.dart`.) 2. Add platform specific wrappers (`*_wrappers.dart`). 3. Add dart end to end tests covering the sample app and unit tests covering dart and the platform code. These all need to be run manually currently. Fixes flutter/flutter#26321 flutter/flutter#25987
…)" This reverts commit e65f594.
The actual functionality here is extremely minimal. Most of this patch
is establishing the structure of the rest of the plugin code to follow.
implementations (
*_connection.dart.)*_wrappers.dart).covering the native platform code. These all need to be run manually
currently.
Fixes flutter/flutter#26321
flutter/flutter#25987