[IAP] Check if the payment processor is available#1057
[IAP] Check if the payment processor is available#1057mklim merged 7 commits intoflutter:masterfrom mklim:iap_store_open
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.
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.
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.
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.
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.
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.
Just making sure - this have no side effects right?
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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...
| class GooglePlayConnection | ||
| with WidgetsBindingObserver | ||
| implements InAppPurchaseConnection { | ||
| GooglePlayConnection() : _billingClient = BillingClient() { |
There was a problem hiding this comment.
should we make this a singleton as well? or is there a legit reason to have multiple of these?
...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.
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.
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.
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()`. |
| /// [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.
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() { |
| void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| switch (state) { | ||
| case AppLifecycleState.paused: | ||
| case AppLifecycleState.suspending: |
| /// 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.
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.
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( |
| /// 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.
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.
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.
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