-
Notifications
You must be signed in to change notification settings - Fork 3.8k
[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 8 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,154 @@ | ||
| // 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 | ||
| /// [runWithClient] and [runWithClientNonRetryable] 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], use [runWithClient] | ||
| /// and [runWithClientNonRetryable] 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 [action] with access to the underlying [BillingClient]. | ||
| /// | ||
| /// If necessary, waits for the underlying [BillingClient] to connect. | ||
| /// If given [action] returns [BillingResponse.serviceDisconnected], it will | ||
| /// be transparently retried after the connection is restored. Because | ||
| /// of this, [action] may be called multiple times. | ||
| /// | ||
| /// A response with [BillingResponse.serviceDisconnected] may be returned | ||
| /// in case of [dispose] being called during the operation. | ||
| /// | ||
| /// See [runWithClientNonRetryable] for operations that do not return | ||
| /// a subclass of [HasBillingResponse]. | ||
| Future<R> runWithClient<R extends HasBillingResponse>( | ||
| Future<R> Function(BillingClient client) action, | ||
| ) async { | ||
| _debugAssertNotDisposed(); | ||
| await _readyFuture; | ||
| final R result = await action(client); | ||
| if (result.responseCode == BillingResponse.serviceDisconnected && | ||
| !_isDisposed) { | ||
| await _connect(); | ||
| return runWithClient(action); | ||
| } else { | ||
| return result; | ||
| } | ||
| } | ||
|
|
||
| /// Executes the given [action] 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 [runWithClient] for operations that return a subclass | ||
| /// of [HasBillingResponse]. | ||
| Future<R> runWithClientNonRetryable<R>( | ||
| Future<R> Function(BillingClient client) action, | ||
| ) async { | ||
| _debugAssertNotDisposed(); | ||
| await _readyFuture; | ||
| return action(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]: | ||
| /// - Further connection attempts will not be made. | ||
| /// - [purchasesUpdatedStream] will be closed. | ||
| /// - Calls to [runWithClient] and [runWithClientNonRetryable] will throw. | ||
| void dispose() { | ||
| _debugAssertNotDisposed(); | ||
| _isDisposed = true; | ||
| client.endConnection(); | ||
| _purchasesUpdatedController.close(); | ||
| } | ||
|
|
||
| // If disposed, does nothing. | ||
| // If currently connecting, waits for it to complete. | ||
| // Otherwise, starts a new connection. | ||
| Future<void> _connect() { | ||
| if (_isDisposed) { | ||
| return Future<void>.value(); | ||
| } | ||
| if (_isConnecting) { | ||
| return _readyFuture; | ||
| } | ||
| _isConnecting = true; | ||
| _readyFuture = Future<void>.sync(() async { | ||
| await client.startConnection(onBillingServiceDisconnected: _connect); | ||
| _isConnecting = false; | ||
| }); | ||
| return _readyFuture; | ||
| } | ||
|
|
||
| void _onPurchasesUpdated(PurchasesResultWrapper event) { | ||
| if (_isDisposed) { | ||
| return; | ||
| } | ||
| _purchasesUpdatedController.add(event); | ||
| } | ||
|
|
||
| void _debugAssertNotDisposed() { | ||
| assert( | ||
| !_isDisposed, | ||
| 'A BillingClientManager was used after being disposed. Once you have ' | ||
| 'called dispose() on a BillingClientManager, it can no longer be used.', | ||
| ); | ||
| } | ||
| } | ||
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.