@mpgd/capacitor-play-billing 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -13
- package/android/build.gradle +42 -1
- package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.kt +378 -0
- package/android/src/main/java/dev/mpgd/capacitor/playbilling/OpenIapPlayConnection.kt +146 -0
- package/android/src/main/java/dev/mpgd/capacitor/playbilling/PlayBillingPolicy.kt +153 -0
- package/dist/store-sdk.d.ts +3 -1
- package/dist/store-sdk.js +19 -2
- package/package.json +8 -8
- package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.java +0 -485
package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.java
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
package dev.mpgd.capacitor.playbilling;
|
|
2
|
-
|
|
3
|
-
import android.os.Handler;
|
|
4
|
-
import android.os.Looper;
|
|
5
|
-
import com.android.billingclient.api.AccountIdentifiers;
|
|
6
|
-
import com.android.billingclient.api.BillingClient;
|
|
7
|
-
import com.android.billingclient.api.BillingClientStateListener;
|
|
8
|
-
import com.android.billingclient.api.BillingFlowParams;
|
|
9
|
-
import com.android.billingclient.api.BillingResult;
|
|
10
|
-
import com.android.billingclient.api.PendingPurchasesParams;
|
|
11
|
-
import com.android.billingclient.api.ProductDetails;
|
|
12
|
-
import com.android.billingclient.api.Purchase;
|
|
13
|
-
import com.android.billingclient.api.QueryProductDetailsParams;
|
|
14
|
-
import com.android.billingclient.api.QueryPurchasesParams;
|
|
15
|
-
import com.getcapacitor.JSArray;
|
|
16
|
-
import com.getcapacitor.JSObject;
|
|
17
|
-
import com.getcapacitor.Plugin;
|
|
18
|
-
import com.getcapacitor.PluginCall;
|
|
19
|
-
import com.getcapacitor.PluginMethod;
|
|
20
|
-
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
21
|
-
import java.util.ArrayList;
|
|
22
|
-
import java.util.Collections;
|
|
23
|
-
import java.util.List;
|
|
24
|
-
import org.json.JSONException;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Purchase collection only. Finalization is server-owned after the mpgd ledger grant: this
|
|
28
|
-
* plugin never calls acknowledgePurchase or consumeAsync, and the encoded isAcknowledged flag
|
|
29
|
-
* lets the TypeScript provider list unfinished purchases for the backend finalizer.
|
|
30
|
-
*/
|
|
31
|
-
@CapacitorPlugin(name = "CapacitorPlayBilling")
|
|
32
|
-
public class CapacitorPlayBillingPlugin extends Plugin {
|
|
33
|
-
private BillingClient billingClient;
|
|
34
|
-
private final List<PendingOperation> afterConnection = new ArrayList<>();
|
|
35
|
-
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
36
|
-
private boolean connecting;
|
|
37
|
-
private PluginCall activePurchase;
|
|
38
|
-
private String activeProductId;
|
|
39
|
-
private Runnable purchaseTimeout;
|
|
40
|
-
/**
|
|
41
|
-
* Product of a purchase call that timed out while the Play UI was still up. A later
|
|
42
|
-
* terminal callback (cancel or non-OK result) is delivered for it as a purchaseUpdated
|
|
43
|
-
* event, so the JavaScript attempt the timeout left open can close.
|
|
44
|
-
*/
|
|
45
|
-
private String timedOutProductId;
|
|
46
|
-
|
|
47
|
-
@Override
|
|
48
|
-
public void load() {
|
|
49
|
-
billingClient = BillingClient.newBuilder(getContext())
|
|
50
|
-
.setListener((result, purchases) ->
|
|
51
|
-
mainHandler.post(() -> onPurchasesUpdated(result, purchases)))
|
|
52
|
-
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
|
53
|
-
.enableAutoServiceReconnection()
|
|
54
|
-
.build();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
@Override
|
|
58
|
-
protected void handleOnDestroy() {
|
|
59
|
-
if (activePurchase != null) {
|
|
60
|
-
activePurchase.reject("Purchase flow was interrupted.", "PLAY_BILLING_INTERRUPTED");
|
|
61
|
-
clearActivePurchase(activePurchase);
|
|
62
|
-
}
|
|
63
|
-
for (PendingOperation operation : afterConnection) {
|
|
64
|
-
operation.call.reject("Billing connection was interrupted.", "PLAY_BILLING_INTERRUPTED");
|
|
65
|
-
}
|
|
66
|
-
afterConnection.clear();
|
|
67
|
-
if (billingClient != null) {
|
|
68
|
-
billingClient.endConnection();
|
|
69
|
-
}
|
|
70
|
-
super.handleOnDestroy();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@PluginMethod
|
|
74
|
-
public void getProducts(PluginCall call) {
|
|
75
|
-
mainHandler.post(() -> getProductsOnMain(call));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private void getProductsOnMain(PluginCall call) {
|
|
79
|
-
JSArray productIds = call.getArray("productIds");
|
|
80
|
-
if (productIds == null || productIds.length() == 0 || productIds.length() > 100) {
|
|
81
|
-
call.reject("Provide 1-100 product IDs.", "PLAY_BILLING_INVALID_PRODUCTS");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
List<QueryProductDetailsParams.Product> products = new ArrayList<>();
|
|
85
|
-
try {
|
|
86
|
-
for (int index = 0; index < productIds.length(); index++) {
|
|
87
|
-
String productId = productIds.getString(index);
|
|
88
|
-
if (productId == null || productId.trim().isEmpty()) {
|
|
89
|
-
call.reject("Product ID is invalid.", "PLAY_BILLING_INVALID_PRODUCT");
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
products.add(QueryProductDetailsParams.Product.newBuilder()
|
|
93
|
-
.setProductId(productId)
|
|
94
|
-
.setProductType(BillingClient.ProductType.INAPP)
|
|
95
|
-
.build());
|
|
96
|
-
}
|
|
97
|
-
} catch (JSONException error) {
|
|
98
|
-
call.reject("Product IDs are invalid.", "PLAY_BILLING_INVALID_PRODUCTS");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
connected(call, () -> billingClient.queryProductDetailsAsync(
|
|
102
|
-
QueryProductDetailsParams.newBuilder().setProductList(products).build(),
|
|
103
|
-
(result, detailsResult) -> {
|
|
104
|
-
if (!isOk(result)) {
|
|
105
|
-
rejectBilling(call, result);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
JSArray found = new JSArray();
|
|
109
|
-
for (ProductDetails details : detailsResult.getProductDetailsList()) {
|
|
110
|
-
List<ProductDetails.OneTimePurchaseOfferDetails> offers =
|
|
111
|
-
details.getOneTimePurchaseOfferDetailsList();
|
|
112
|
-
if (offers == null || offers.isEmpty()) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
JSArray eligibleOffers = new JSArray();
|
|
116
|
-
for (ProductDetails.OneTimePurchaseOfferDetails offer : offers) {
|
|
117
|
-
if (offer.getRentalDetails() == null && offer.getPreorderDetails() == null) {
|
|
118
|
-
eligibleOffers.put(new JSObject()
|
|
119
|
-
.put("offerToken", offer.getOfferToken())
|
|
120
|
-
.put("formattedPrice", offer.getFormattedPrice())
|
|
121
|
-
.put("currencyCode", offer.getPriceCurrencyCode())
|
|
122
|
-
.put("priceAmountMicros", offer.getPriceAmountMicros()));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (eligibleOffers.length() == 0) {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
found.put(new JSObject()
|
|
129
|
-
.put("productId", details.getProductId())
|
|
130
|
-
.put("title", details.getTitle())
|
|
131
|
-
.put("description", details.getDescription())
|
|
132
|
-
.put("offers", eligibleOffers));
|
|
133
|
-
}
|
|
134
|
-
call.resolve(new JSObject().put("products", found));
|
|
135
|
-
}
|
|
136
|
-
));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
@PluginMethod
|
|
140
|
-
public void purchase(PluginCall call) {
|
|
141
|
-
mainHandler.post(() -> purchaseOnMain(call));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private void purchaseOnMain(PluginCall call) {
|
|
145
|
-
String productId = call.getString("productId");
|
|
146
|
-
String accountId = call.getString("obfuscatedAccountId");
|
|
147
|
-
String selectedOfferToken = call.getString("offerToken");
|
|
148
|
-
if (productId == null || productId.trim().isEmpty() || accountId == null
|
|
149
|
-
|| accountId.trim().isEmpty() || accountId.length() > 64) {
|
|
150
|
-
call.reject("Product and account identifiers are required.", "PLAY_BILLING_INVALID_REQUEST");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (activePurchase != null) {
|
|
154
|
-
call.reject("Another purchase is already in progress.", "PLAY_BILLING_BUSY");
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
activePurchase = call;
|
|
158
|
-
activeProductId = productId;
|
|
159
|
-
timedOutProductId = null;
|
|
160
|
-
purchaseTimeout = () -> {
|
|
161
|
-
if (activePurchase == call) {
|
|
162
|
-
timedOutProductId = activeProductId;
|
|
163
|
-
clearActivePurchase(call);
|
|
164
|
-
call.reject("Purchase confirmation timed out; requery owned purchases.",
|
|
165
|
-
"PLAY_BILLING_TIMEOUT");
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
mainHandler.postDelayed(purchaseTimeout, 180_000);
|
|
169
|
-
connected(call, () -> {
|
|
170
|
-
if (activePurchase != call) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
billingClient.queryProductDetailsAsync(
|
|
174
|
-
QueryProductDetailsParams.newBuilder().setProductList(Collections.singletonList(
|
|
175
|
-
QueryProductDetailsParams.Product.newBuilder()
|
|
176
|
-
.setProductId(productId)
|
|
177
|
-
.setProductType(BillingClient.ProductType.INAPP)
|
|
178
|
-
.build()
|
|
179
|
-
)).build(),
|
|
180
|
-
(result, detailsResult) -> {
|
|
181
|
-
if (activePurchase != call) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (!isOk(result)) {
|
|
185
|
-
clearActivePurchase(call);
|
|
186
|
-
rejectBilling(call, result);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (detailsResult.getProductDetailsList().size() != 1) {
|
|
190
|
-
clearActivePurchase(call);
|
|
191
|
-
call.reject("Product is unavailable.", "PLAY_BILLING_PRODUCT_UNAVAILABLE");
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
ProductDetails details = detailsResult.getProductDetailsList().get(0);
|
|
195
|
-
List<ProductDetails.OneTimePurchaseOfferDetails> offers =
|
|
196
|
-
details.getOneTimePurchaseOfferDetailsList();
|
|
197
|
-
ProductDetails.OneTimePurchaseOfferDetails selected = selectOffer(
|
|
198
|
-
offers, selectedOfferToken);
|
|
199
|
-
if (selected == null) {
|
|
200
|
-
clearActivePurchase(call);
|
|
201
|
-
call.reject("Select an eligible one-time offer.", "PLAY_BILLING_OFFER_REQUIRED");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
BillingFlowParams.ProductDetailsParams productParams =
|
|
205
|
-
BillingFlowParams.ProductDetailsParams.newBuilder()
|
|
206
|
-
.setProductDetails(details)
|
|
207
|
-
.setOfferToken(selected.getOfferToken())
|
|
208
|
-
.build();
|
|
209
|
-
BillingFlowParams flow = BillingFlowParams.newBuilder()
|
|
210
|
-
.setProductDetailsParamsList(Collections.singletonList(productParams))
|
|
211
|
-
.setObfuscatedAccountId(accountId)
|
|
212
|
-
.build();
|
|
213
|
-
if (getActivity() == null) {
|
|
214
|
-
clearActivePurchase(call);
|
|
215
|
-
call.reject("Billing requires a foreground activity.",
|
|
216
|
-
"PLAY_BILLING_ACTIVITY_REQUIRED");
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
BillingResult launched = billingClient.launchBillingFlow(getActivity(), flow);
|
|
220
|
-
if (!isOk(launched)) {
|
|
221
|
-
clearActivePurchase(call);
|
|
222
|
-
if (launched.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
|
|
223
|
-
call.resolve(new JSObject().put("status", "cancelled"));
|
|
224
|
-
} else {
|
|
225
|
-
rejectBilling(call, launched);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
@PluginMethod
|
|
234
|
-
public void getPurchases(PluginCall call) {
|
|
235
|
-
mainHandler.post(() -> getPurchasesOnMain(call));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private void getPurchasesOnMain(PluginCall call) {
|
|
239
|
-
connected(call, () -> billingClient.queryPurchasesAsync(
|
|
240
|
-
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build(),
|
|
241
|
-
(result, purchases) -> {
|
|
242
|
-
if (!isOk(result)) {
|
|
243
|
-
rejectBilling(call, result);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
JSArray found = new JSArray();
|
|
247
|
-
for (Purchase purchase : purchases) {
|
|
248
|
-
JSObject encoded = encodePurchase(purchase);
|
|
249
|
-
if (encoded != null) {
|
|
250
|
-
found.put(encoded);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
call.resolve(new JSObject().put("purchases", found));
|
|
254
|
-
}
|
|
255
|
-
));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private void onPurchasesUpdated(BillingResult result, List<Purchase> purchases) {
|
|
259
|
-
PluginCall pending = activePurchase;
|
|
260
|
-
String expectedProductId = activeProductId;
|
|
261
|
-
if (result.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
|
|
262
|
-
if (pending != null) {
|
|
263
|
-
clearActivePurchase(pending);
|
|
264
|
-
pending.resolve(new JSObject().put("status", "cancelled"));
|
|
265
|
-
} else {
|
|
266
|
-
notifyLateTerminalResult("cancelled", null);
|
|
267
|
-
}
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (!isOk(result)) {
|
|
271
|
-
if (pending != null) {
|
|
272
|
-
clearActivePurchase(pending);
|
|
273
|
-
rejectBilling(pending, result);
|
|
274
|
-
} else {
|
|
275
|
-
notifyLateTerminalResult("failed", billingErrorCode(result));
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (purchases == null || purchases.isEmpty()) {
|
|
280
|
-
if (pending != null) {
|
|
281
|
-
clearActivePurchase(pending);
|
|
282
|
-
pending.reject("Purchase callback was empty; requery owned purchases.",
|
|
283
|
-
"PLAY_BILLING_EMPTY_PURCHASE");
|
|
284
|
-
}
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
for (Purchase purchase : purchases) {
|
|
288
|
-
JSObject encoded = encodePurchase(purchase);
|
|
289
|
-
if (encoded == null) {
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
JSObject outcome = new JSObject()
|
|
293
|
-
.put("status", encoded.optString("purchaseState"))
|
|
294
|
-
.put("purchase", encoded);
|
|
295
|
-
// Retained until a listener consumes it: a purchase that completes after the
|
|
296
|
-
// purchase call timed out, or before the web view attached its listener, must
|
|
297
|
-
// still reach the provider's purchaseUpdated subscription.
|
|
298
|
-
notifyListeners("purchaseUpdated", outcome, true);
|
|
299
|
-
if (timedOutProductId != null && purchase.getProducts().contains(timedOutProductId)) {
|
|
300
|
-
timedOutProductId = null;
|
|
301
|
-
}
|
|
302
|
-
if (pending != null && purchase.getProducts().contains(expectedProductId)) {
|
|
303
|
-
clearActivePurchase(pending);
|
|
304
|
-
pending.resolve(outcome);
|
|
305
|
-
pending = null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* A cancel or non-OK result that arrives after the purchase call already timed out has no
|
|
312
|
-
* call to answer; without this event the JavaScript attempt would stay open forever and
|
|
313
|
-
* block another purchase of the product. Only the timed-out product is correlated.
|
|
314
|
-
*/
|
|
315
|
-
private void notifyLateTerminalResult(String status, String code) {
|
|
316
|
-
String productId = timedOutProductId;
|
|
317
|
-
if (productId == null) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
timedOutProductId = null;
|
|
321
|
-
JSObject outcome = new JSObject().put("status", status).put("productId", productId);
|
|
322
|
-
if (code != null) {
|
|
323
|
-
outcome.put("code", code);
|
|
324
|
-
}
|
|
325
|
-
notifyListeners("purchaseUpdated", outcome, true);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private JSObject encodePurchase(Purchase purchase) {
|
|
329
|
-
int state = purchase.getPurchaseState();
|
|
330
|
-
if (state != Purchase.PurchaseState.PURCHASED
|
|
331
|
-
&& state != Purchase.PurchaseState.PENDING) {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
String token = purchase.getPurchaseToken();
|
|
335
|
-
if (token == null || token.trim().isEmpty() || purchase.getProducts().isEmpty()) {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
JSArray products = new JSArray();
|
|
339
|
-
for (String productId : purchase.getProducts()) {
|
|
340
|
-
products.put(productId);
|
|
341
|
-
}
|
|
342
|
-
String purchaseState = state == Purchase.PurchaseState.PURCHASED ? "purchased" : "pending";
|
|
343
|
-
// Billing 9.1.0 Purchase getters under their own names.
|
|
344
|
-
JSObject encoded = new JSObject()
|
|
345
|
-
.put("products", products)
|
|
346
|
-
.put("purchaseToken", token)
|
|
347
|
-
.put("purchaseState", purchaseState)
|
|
348
|
-
.put("purchaseTime", purchase.getPurchaseTime())
|
|
349
|
-
.put("quantity", purchase.getQuantity())
|
|
350
|
-
.put("isAcknowledged", purchase.isAcknowledged());
|
|
351
|
-
if (purchase.getOrderId() != null) {
|
|
352
|
-
encoded.put("orderId", purchase.getOrderId());
|
|
353
|
-
}
|
|
354
|
-
AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
|
|
355
|
-
if (accountIdentifiers != null && accountIdentifiers.getObfuscatedAccountId() != null) {
|
|
356
|
-
encoded.put("obfuscatedAccountId", accountIdentifiers.getObfuscatedAccountId());
|
|
357
|
-
}
|
|
358
|
-
return encoded;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private ProductDetails.OneTimePurchaseOfferDetails selectOffer(
|
|
362
|
-
List<ProductDetails.OneTimePurchaseOfferDetails> offers,
|
|
363
|
-
String offerToken
|
|
364
|
-
) {
|
|
365
|
-
if (offers == null) {
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
ProductDetails.OneTimePurchaseOfferDetails only = null;
|
|
369
|
-
int eligibleCount = 0;
|
|
370
|
-
for (ProductDetails.OneTimePurchaseOfferDetails offer : offers) {
|
|
371
|
-
if (offer.getRentalDetails() != null || offer.getPreorderDetails() != null) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (offerToken != null && offerToken.equals(offer.getOfferToken())) {
|
|
375
|
-
return offer;
|
|
376
|
-
}
|
|
377
|
-
eligibleCount++;
|
|
378
|
-
only = offer;
|
|
379
|
-
}
|
|
380
|
-
return offerToken == null && eligibleCount == 1 ? only : null;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private void connected(PluginCall call, Runnable task) {
|
|
384
|
-
if (billingClient.isReady()) {
|
|
385
|
-
task.run();
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
afterConnection.add(new PendingOperation(call, task));
|
|
389
|
-
if (connecting) {
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
connecting = true;
|
|
393
|
-
billingClient.startConnection(new BillingClientStateListener() {
|
|
394
|
-
@Override
|
|
395
|
-
public void onBillingSetupFinished(BillingResult result) {
|
|
396
|
-
connecting = false;
|
|
397
|
-
List<PendingOperation> queued = new ArrayList<>(afterConnection);
|
|
398
|
-
afterConnection.clear();
|
|
399
|
-
if (!isOk(result)) {
|
|
400
|
-
for (PendingOperation operation : queued) {
|
|
401
|
-
clearActivePurchase(operation.call);
|
|
402
|
-
rejectBilling(operation.call, result);
|
|
403
|
-
}
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
for (PendingOperation operation : queued) {
|
|
407
|
-
operation.task.run();
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
@Override
|
|
412
|
-
public void onBillingServiceDisconnected() {
|
|
413
|
-
connecting = false;
|
|
414
|
-
List<PendingOperation> queued = new ArrayList<>(afterConnection);
|
|
415
|
-
afterConnection.clear();
|
|
416
|
-
for (PendingOperation operation : queued) {
|
|
417
|
-
clearActivePurchase(operation.call);
|
|
418
|
-
operation.call.reject("Billing service disconnected.", "PLAY_BILLING_DISCONNECTED");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private boolean isOk(BillingResult result) {
|
|
425
|
-
return result.getResponseCode() == BillingClient.BillingResponseCode.OK;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
private void rejectBilling(PluginCall call, BillingResult result) {
|
|
429
|
-
call.reject("Google Play Billing is unavailable (" + result.getResponseCode() + ").",
|
|
430
|
-
billingErrorCode(result));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private String billingErrorCode(BillingResult result) {
|
|
434
|
-
int responseCode = result.getResponseCode();
|
|
435
|
-
String code;
|
|
436
|
-
switch (responseCode) {
|
|
437
|
-
case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
|
|
438
|
-
code = "PLAY_BILLING_DISCONNECTED";
|
|
439
|
-
break;
|
|
440
|
-
case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
|
|
441
|
-
case BillingClient.BillingResponseCode.SERVICE_TIMEOUT:
|
|
442
|
-
code = "PLAY_BILLING_SERVICE_UNAVAILABLE";
|
|
443
|
-
break;
|
|
444
|
-
case BillingClient.BillingResponseCode.NETWORK_ERROR:
|
|
445
|
-
code = "PLAY_BILLING_NETWORK_ERROR";
|
|
446
|
-
break;
|
|
447
|
-
case BillingClient.BillingResponseCode.ERROR:
|
|
448
|
-
code = "PLAY_BILLING_TRANSIENT_ERROR";
|
|
449
|
-
break;
|
|
450
|
-
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
|
|
451
|
-
code = "PLAY_BILLING_ALREADY_OWNED";
|
|
452
|
-
break;
|
|
453
|
-
case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
|
|
454
|
-
code = "PLAY_BILLING_PRODUCT_UNAVAILABLE";
|
|
455
|
-
break;
|
|
456
|
-
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
|
|
457
|
-
code = "PLAY_BILLING_CONFIGURATION_ERROR";
|
|
458
|
-
break;
|
|
459
|
-
default:
|
|
460
|
-
code = "PLAY_BILLING_ERROR";
|
|
461
|
-
}
|
|
462
|
-
return code;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
private void clearActivePurchase(PluginCall call) {
|
|
466
|
-
if (activePurchase == call) {
|
|
467
|
-
activePurchase = null;
|
|
468
|
-
activeProductId = null;
|
|
469
|
-
if (purchaseTimeout != null) {
|
|
470
|
-
mainHandler.removeCallbacks(purchaseTimeout);
|
|
471
|
-
purchaseTimeout = null;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
private static final class PendingOperation {
|
|
477
|
-
final PluginCall call;
|
|
478
|
-
final Runnable task;
|
|
479
|
-
|
|
480
|
-
PendingOperation(PluginCall call, Runnable task) {
|
|
481
|
-
this.call = call;
|
|
482
|
-
this.task = task;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|