Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Conversation

@mklim
Copy link
Contributor

@mklim mklim commented Jan 9, 2019

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.

Fixes flutter/flutter#26321
flutter/flutter#25987

@mklim mklim requested review from amirh and cyanglaz January 9, 2019 22:16
@mklim
Copy link
Contributor Author

mklim commented Jan 9, 2019

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.

// 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;
Copy link
Contributor

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]

Copy link
Contributor Author

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') + '.')));
Copy link
Contributor

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")

Copy link
Contributor Author

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();
Copy link
Contributor

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@mklim mklim left a 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();
Copy link
Contributor Author

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') + '.')));
Copy link
Contributor Author

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;
Copy link
Contributor Author

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()`.
Copy link
Contributor Author

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(
Copy link
Contributor Author

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();
Copy link
Contributor Author

@mklim mklim Jan 11, 2019

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.

Copy link
Contributor

@amirh amirh left a 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();
Copy link
Contributor

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:
Copy link
Contributor

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

Copy link
Contributor Author

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() {
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, changed.

MethodChannel('plugins.flutter.io/in_app_purchase');
List<Map<String, Function>> _callbacks = <Map<String, Function>>[];

/// Wraps `BillingClient#isReady()`.
Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Contributor Author

@mklim mklim left a 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()`.
Copy link
Contributor Author

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(
Copy link
Contributor Author

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() {
Copy link
Contributor Author

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:
Copy link
Contributor Author

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();
Copy link
Contributor Author

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.

Copy link
Contributor

@amirh amirh left a 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(
Copy link
Contributor

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();
Copy link
Contributor

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)

Michael Klimushyn added 4 commits January 14, 2019 09:58
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.
@mklim mklim mentioned this pull request Jan 14, 2019
@mklim
Copy link
Contributor Author

mklim commented Jan 14, 2019

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.

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.

@mklim mklim merged commit fe03e40 into flutter:master Jan 14, 2019
@mklim mklim deleted the iap_store_open branch January 14, 2019 22:30
import 'dart:async';
import 'package:flutter/services.dart';

class FakePlatformViewsController {
Copy link
Contributor

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(
Copy link
Contributor

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?

@mklim
Copy link
Contributor Author

mklim commented Jan 15, 2019

Following up in #1082.

andreidiaconu pushed a commit to andreidiaconu/plugins that referenced this pull request Feb 17, 2019
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
andreidiaconu added a commit to andreidiaconu/plugins that referenced this pull request Feb 17, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants