-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[in_app_purchase_android] Implement BillingClient connection management and introduce BillingClientManager
#3303
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
Changes from 2 commits
bf467c0
362756d
d41bc53
e3b1e5d
3730512
52b5a54
7ad117a
a525c4e
96b6eef
6257582
e321015
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,156 @@ | ||||||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||||||
| // Use of this source code is governed by a BSD-style license that can be | ||||||
| // found in the LICENSE file. | ||||||
|
|
||||||
| import 'dart:async'; | ||||||
|
|
||||||
| import 'package:flutter/widgets.dart'; | ||||||
|
|
||||||
| import 'billing_client_wrapper.dart'; | ||||||
| import 'purchase_wrapper.dart'; | ||||||
|
|
||||||
| /// Abstraction of result of [BillingClient] operation that includes | ||||||
| /// a [BillingResponse]. | ||||||
| abstract class HasBillingResponse { | ||||||
| /// The status of the operation. | ||||||
| abstract final BillingResponse responseCode; | ||||||
| } | ||||||
|
|
||||||
| /// Utility class that manages a [BillingClient] connection. | ||||||
| /// | ||||||
| /// Connection is initialized on creation of [BillingClientManager]. | ||||||
| /// If [BillingClient] sends `onBillingServiceDisconnected` event or any | ||||||
| /// operation returns [BillingResponse.serviceDisconnected], connection is | ||||||
| /// re-initialized. | ||||||
| /// | ||||||
| /// [BillingClient] instance is not exposed directly. It can be accessed via | ||||||
| /// [run] and [runRaw] methods that handle the connection management. | ||||||
| /// | ||||||
| /// Consider calling [dispose] after the [BillingClient] is no longer needed. | ||||||
| class BillingClientManager { | ||||||
| /// Creates the [BillingClientManager]. | ||||||
| /// | ||||||
| /// Immediately initializes connection to the underlying [BillingClient]. | ||||||
| BillingClientManager() { | ||||||
| _connect(); | ||||||
| } | ||||||
|
|
||||||
| /// Stream of `onPurchasesUpdated` events from the [BillingClient]. | ||||||
| /// | ||||||
| /// This is a broadcast stream, so it can be listened to multiple times. | ||||||
| /// A "done" event will be sent after [dispose] is called. | ||||||
| late final Stream<PurchasesResultWrapper> purchasesUpdatedStream = | ||||||
| _purchasesUpdatedController.stream; | ||||||
|
|
||||||
| /// [BillingClient] instance managed by this [BillingClientManager]. | ||||||
| /// | ||||||
| /// In order to access the [BillingClient], consider using [run] and [runRaw] | ||||||
| /// methods. | ||||||
| @visibleForTesting | ||||||
| late final BillingClient client = BillingClient(_onPurchasesUpdated); | ||||||
|
|
||||||
| final StreamController<PurchasesResultWrapper> _purchasesUpdatedController = | ||||||
| StreamController<PurchasesResultWrapper>.broadcast(); | ||||||
|
|
||||||
| bool _isConnecting = false; | ||||||
| bool _isDisposed = false; | ||||||
|
|
||||||
| // Initialized immediately in the constructor, so it's always safe to access. | ||||||
| late Future<void> _readyFuture; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This is optional, but a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case this wouldn't make much difference - new |
||||||
|
|
||||||
| /// Executes the given [block] with access to the underlying [BillingClient]. | ||||||
| /// | ||||||
| /// If necessary, waits for the underlying [BillingClient] to connect. | ||||||
| /// If given [block] returns [BillingResponse.serviceDisconnected], it will | ||||||
| /// be transparently retried after the connection is restored. Because | ||||||
| /// of this, [block] may be called multiple times. | ||||||
| /// | ||||||
| /// A response with [BillingResponse.serviceDisconnected] may be returned | ||||||
| /// in case of [dispose] being called during the operation. | ||||||
| /// | ||||||
| /// See [runRaw] for operations that do not return a subclass | ||||||
| /// of [HasBillingResponse]. | ||||||
| Future<R> run<R extends HasBillingResponse>( | ||||||
|
||||||
| Future<R> Function(BillingClient client) block, | ||||||
|
||||||
| ) async { | ||||||
| assert(_debugAssertNotDisposed()); | ||||||
|
||||||
| await _readyFuture; | ||||||
| final R result = await block(client); | ||||||
| if (result.responseCode == BillingResponse.serviceDisconnected && | ||||||
| !_isDisposed) { | ||||||
| await _connect(); | ||||||
| return run(block); | ||||||
| } else { | ||||||
| return result; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Executes the given [block] with access to the underlying [BillingClient]. | ||||||
| /// | ||||||
| /// If necessary, waits for the underlying [BillingClient] to connect. | ||||||
| /// Designed only for operations that do not return a subclass | ||||||
| /// of [HasBillingResponse] (e.g. [BillingClient.isReady], | ||||||
| /// [BillingClient.isFeatureSupported]). | ||||||
| /// | ||||||
| /// See [runRaw] for operations that return a subclass | ||||||
| /// of [HasBillingResponse]. | ||||||
| Future<R> runRaw<R>(Future<R> Function(BillingClient client) block) async { | ||||||
|
||||||
| assert(_debugAssertNotDisposed()); | ||||||
|
||||||
| await _readyFuture; | ||||||
| return block(client); | ||||||
| } | ||||||
|
|
||||||
| /// Ends connection to the [BillingClient]. | ||||||
| /// | ||||||
| /// Consider calling [dispose] after you no longer need the [BillingClient] | ||||||
| /// API to free up the resources. | ||||||
| /// | ||||||
| /// After calling [dispose] : | ||||||
|
||||||
| /// After calling [dispose] : | |
| /// After calling [dispose]: |
Outdated
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.
nit: To be consistent
| /// - Further connection attempts will not be made; | |
| /// - Further connection attempts will not be made. |
Outdated
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.
nit:
| /// - [purchasesUpdatedStream] will be closed; | |
| /// - [purchasesUpdatedStream] will be closed. |
Outdated
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 understand why one would throw a FlutterError inside of an assert. It defeats the point of having an assert. Would it not be better to make it this:
assert(!_isDisposed, 'my error message');
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.
nit: This should include how the management is being fixed.