@mpgd/capacitor-play-billing 0.1.7 → 0.2.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 +125 -0
- package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.java +68 -10
- package/dist/commerce-provider.d.ts +68 -0
- package/dist/commerce-provider.js +91 -0
- package/dist/definitions.d.ts +60 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +15 -2
- package/dist/provider.js +18 -14
- package/dist/store-sdk.d.ts +89 -0
- package/dist/store-sdk.js +613 -0
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -13,6 +13,131 @@ This package is published for opt-in integration, but Kit's mock and compile
|
|
|
13
13
|
checks do not establish live Play purchase behavior. Validate it in a
|
|
14
14
|
game-owned Play Console project before relying on it in a published game.
|
|
15
15
|
|
|
16
|
+
## Commerce v2 provider
|
|
17
|
+
|
|
18
|
+
Commerce v2 ([RFC](../../docs/specs/commerce/commerce-v2-rfc.md), PR B2) adds
|
|
19
|
+
a `StoreSdk` over this plugin and a `CommerceProvider` built from it with
|
|
20
|
+
`createStoreCommerceProvider` from `@mpgd/adapter-capacitor` (a peer
|
|
21
|
+
dependency since 0.2.0). Register the service provider with the Capacitor
|
|
22
|
+
gateway and it serves the six `commerce.v2.*` bridge methods as
|
|
23
|
+
`gateway.commerceV2`, plus the four v1 `commerce.*` methods through
|
|
24
|
+
`toCommerceAdapter`, so v1 callers keep working.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor';
|
|
28
|
+
import { createCapacitorPlayBillingServiceProvider } from '@mpgd/capacitor-play-billing';
|
|
29
|
+
|
|
30
|
+
export function createPlayGateway(getObfuscatedAccountId: () => Promise<string>) {
|
|
31
|
+
return createCapacitorPlatformGateway({
|
|
32
|
+
target: 'android',
|
|
33
|
+
appVersion: '1.0.0',
|
|
34
|
+
buildId: 'game-build',
|
|
35
|
+
providers: [
|
|
36
|
+
createCapacitorPlayBillingServiceProvider({
|
|
37
|
+
products: [
|
|
38
|
+
{ logicalId: 'gems_small', storeSku: 'gems.small', kind: 'consumable' },
|
|
39
|
+
{ logicalId: 'remove_ads', storeSku: 'remove.ads', kind: 'durable', offerId: 'intro' },
|
|
40
|
+
],
|
|
41
|
+
// Must agree with the backend's resolveObfuscatedAccountId.
|
|
42
|
+
accountBinding: async () => ({
|
|
43
|
+
kind: 'obfuscated-account-id',
|
|
44
|
+
value: await getObfuscatedAccountId(),
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`createCapacitorPlayBillingStoreSdk({ products, plugin?, isAndroid? })` is
|
|
53
|
+
the `StoreSdk`: store `google-play`; capabilities consumable and durable
|
|
54
|
+
(no subscriptions, no multi-quantity), `pending: true`, `restore: false`,
|
|
55
|
+
`offers: true`, and `finish: 'server'`. `fetchProducts` is `getProducts`
|
|
56
|
+
with the selected one-time offer as the price (`PLAY_BILLING_OFFER_REQUIRED`
|
|
57
|
+
when several offers are eligible and none is configured) and every eligible
|
|
58
|
+
offer listed as `offers[].offerId` (the Play `offerToken`). `requestPurchase`
|
|
59
|
+
is `purchase` with the obfuscated account id from the request's
|
|
60
|
+
`accountBinding` (`kind: 'obfuscated-account-id'`; anything else is
|
|
61
|
+
`PLAY_BILLING_ACCOUNT_REQUIRED`) and `offerToken` from `offerId`: a
|
|
62
|
+
`purchased` outcome is `purchased`, a `pending` outcome is `launched` because
|
|
63
|
+
Play resolves it later through `purchaseUpdated` or `getPurchases`, a
|
|
64
|
+
`cancelled` outcome is `cancelled`, and a plugin rejection is `failed` with
|
|
65
|
+
the plugin code. `getAvailablePurchases` is `getPurchases` filtered to
|
|
66
|
+
unfinished entries: `purchased` with `isAcknowledged: false`, plus `pending`
|
|
67
|
+
when `includePending` is set; each carries `token = purchaseToken` and
|
|
68
|
+
`transactionId = orderId`, or the `play-token-sha256:<hex>` token key until
|
|
69
|
+
Play assigns an order id, so ledgers written by v1 deduplicate. A purchased
|
|
70
|
+
entry from a 0.1 native build carries no `isAcknowledged`; an absent flag
|
|
71
|
+
counts as unfinished so a paid purchase is never dropped during a staged
|
|
72
|
+
upgrade. A purchase for a SKU that is not in `products` (a retired product)
|
|
73
|
+
or a multi-line purchase has no logical id for the reconciler, so it is left
|
|
74
|
+
out of the list and of events and handed to the optional `onUnmappedPurchase`
|
|
75
|
+
diagnostics hook instead; the backend finalizer still sees it through the
|
|
76
|
+
Play Developer API. The plugin's `purchaseUpdated` listener backs
|
|
77
|
+
`addListener`: deliveries leave in the order the plugin reported them (a
|
|
78
|
+
purchase without an order id waits for the token digest, and later updates
|
|
79
|
+
queue behind it), and a `cancelled` or `failed` update without a purchase is
|
|
80
|
+
the plugin's late terminal result for a purchase call that already timed out
|
|
81
|
+
(`PLAY_BILLING_TIMEOUT`), emitted as a `purchaseError` for that product so
|
|
82
|
+
the attempt the timeout left open closes. `getAvailability` is the v1 logic
|
|
83
|
+
(Android check, offer configuration, service state).
|
|
84
|
+
`playBillingStoreSdkErrorCodeMap` extends `defaultStoreSdkErrorCodeMap` with
|
|
85
|
+
every other `PLAY_BILLING_*` code (`BUSY` is `IN_PROGRESS`, `OFFER_REQUIRED`
|
|
86
|
+
and the invalid-product codes are `CONFIGURATION_REQUIRED`,
|
|
87
|
+
`ACTIVITY_REQUIRED` is `NOT_PREPARED`, `ANDROID_REQUIRED` is
|
|
88
|
+
`STORE_UNAVAILABLE`); `PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`,
|
|
89
|
+
`PLAY_BILLING_EMPTY_PURCHASE`, and `PLAY_BILLING_ALREADY_OWNED` stay
|
|
90
|
+
recoverable, so the provider answers `launched` and the reconciler resolves
|
|
91
|
+
the attempt from `getAvailablePurchases` without reopening the store UI.
|
|
92
|
+
|
|
93
|
+
`createCapacitorPlayBillingCommerceProvider({ products, accountBinding,
|
|
94
|
+
plugin?, isAndroid?, store?, mapError? })` composes the provider: evidence
|
|
95
|
+
schema `google-play.product-purchase.v2` with payload `{ purchaseToken }`,
|
|
96
|
+
byte-identical to the v1 provider's evidence, so
|
|
97
|
+
`createGooglePlayProductPurchaseBoundary` verifies both; authority
|
|
98
|
+
`{ evidence: 'client-token', accountBinding: 'required' }`; no
|
|
99
|
+
`finishTransaction` and no `restorePurchases`. A thrown `accountBinding`
|
|
100
|
+
lookup is `ACCOUNT_BINDING_REQUIRED` with `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED`
|
|
101
|
+
kept as the store code. `store` defaults to `google-play`; only a conformance
|
|
102
|
+
harness overrides it, because the derived idempotency key follows the store
|
|
103
|
+
id. The conformance test runs every store vector whose profile the Play
|
|
104
|
+
capabilities can serve (`purchase-server-finish`) against a fake plugin,
|
|
105
|
+
directly and through the gateway bridge path, and lists the skipped vectors
|
|
106
|
+
with their reason: the client-finish, host-finish, adapter-settled, and
|
|
107
|
+
server-push profiles need a finish owner or evidence authority Play does not
|
|
108
|
+
have.
|
|
109
|
+
|
|
110
|
+
The plugin surface gained the Billing Library 9.1.0 `Purchase` getters on
|
|
111
|
+
every purchase (`getPurchases()`, `purchase()`, and `purchaseUpdated`):
|
|
112
|
+
`products`, `purchaseState`, `purchaseTime`, `quantity`, `isAcknowledged`,
|
|
113
|
+
and `obfuscatedAccountId`, with `priceAmountMicros` on offers. The 0.1 names
|
|
114
|
+
`productIds`, `state`, and `purchaseTimeMillis` carry the same values and are
|
|
115
|
+
deprecated until the next major version. `purchaseUpdated` is now retained
|
|
116
|
+
until a listener consumes it, so a purchase that completes after the
|
|
117
|
+
`purchase` call timed out, or before the web view attached its listener,
|
|
118
|
+
still reaches the provider, and a cancel or non-OK result that arrives after
|
|
119
|
+
the 180-second `purchase` timeout is delivered as `{ status: 'cancelled' |
|
|
120
|
+
'failed', productId, code? }` for the timed-out product instead of being
|
|
121
|
+
discarded. The plugin still never acknowledges or consumes;
|
|
122
|
+
`isAcknowledged: false` on a purchased entry marks it unfinished for the
|
|
123
|
+
backend finalizer.
|
|
124
|
+
|
|
125
|
+
### Migrating from the v1 provider
|
|
126
|
+
|
|
127
|
+
`createCapacitorPlayBillingProvider` and `recoverOwnedPlayPurchases` are
|
|
128
|
+
unchanged and keep working; both providers can share the plugin. Games that
|
|
129
|
+
move to v2 replace the v1 provider in `providers` with
|
|
130
|
+
`createCapacitorPlayBillingServiceProvider`, turn `getObfuscatedAccountId`
|
|
131
|
+
into an `accountBinding` resolver returning `{ kind: 'obfuscated-account-id',
|
|
132
|
+
value }`, rename `id`/`storeId`/`type` to `logicalId`/`storeSku`/`kind`
|
|
133
|
+
(`non_consumable` becomes `durable`) and `offerToken` to `offerId`, and stop
|
|
134
|
+
calling `recoverOwnedPlayPurchases`: it is deprecated in favour of the single
|
|
135
|
+
reconciler landing in `@mpgd/game-services` (PR C1), which reads
|
|
136
|
+
`getAvailablePurchases` and `purchaseUpdated` from `gateway.commerceV2`. The
|
|
137
|
+
backend contract is unchanged, since the evidence payload is the same.
|
|
138
|
+
|
|
139
|
+
## v1 provider
|
|
140
|
+
|
|
16
141
|
The provider maps game logical product IDs to Play product IDs and requires a
|
|
17
142
|
stable obfuscated account ID that agrees with the backend account resolver.
|
|
18
143
|
When Play returns multiple eligible one-time offers, map an explicit
|
package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.java
CHANGED
|
@@ -2,6 +2,7 @@ package dev.mpgd.capacitor.playbilling;
|
|
|
2
2
|
|
|
3
3
|
import android.os.Handler;
|
|
4
4
|
import android.os.Looper;
|
|
5
|
+
import com.android.billingclient.api.AccountIdentifiers;
|
|
5
6
|
import com.android.billingclient.api.BillingClient;
|
|
6
7
|
import com.android.billingclient.api.BillingClientStateListener;
|
|
7
8
|
import com.android.billingclient.api.BillingFlowParams;
|
|
@@ -22,7 +23,11 @@ import java.util.Collections;
|
|
|
22
23
|
import java.util.List;
|
|
23
24
|
import org.json.JSONException;
|
|
24
25
|
|
|
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
|
+
*/
|
|
26
31
|
@CapacitorPlugin(name = "CapacitorPlayBilling")
|
|
27
32
|
public class CapacitorPlayBillingPlugin extends Plugin {
|
|
28
33
|
private BillingClient billingClient;
|
|
@@ -32,6 +37,12 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
32
37
|
private PluginCall activePurchase;
|
|
33
38
|
private String activeProductId;
|
|
34
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;
|
|
35
46
|
|
|
36
47
|
@Override
|
|
37
48
|
public void load() {
|
|
@@ -107,7 +118,8 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
107
118
|
eligibleOffers.put(new JSObject()
|
|
108
119
|
.put("offerToken", offer.getOfferToken())
|
|
109
120
|
.put("formattedPrice", offer.getFormattedPrice())
|
|
110
|
-
.put("currencyCode", offer.getPriceCurrencyCode())
|
|
121
|
+
.put("currencyCode", offer.getPriceCurrencyCode())
|
|
122
|
+
.put("priceAmountMicros", offer.getPriceAmountMicros()));
|
|
111
123
|
}
|
|
112
124
|
}
|
|
113
125
|
if (eligibleOffers.length() == 0) {
|
|
@@ -144,8 +156,10 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
144
156
|
}
|
|
145
157
|
activePurchase = call;
|
|
146
158
|
activeProductId = productId;
|
|
159
|
+
timedOutProductId = null;
|
|
147
160
|
purchaseTimeout = () -> {
|
|
148
161
|
if (activePurchase == call) {
|
|
162
|
+
timedOutProductId = activeProductId;
|
|
149
163
|
clearActivePurchase(call);
|
|
150
164
|
call.reject("Purchase confirmation timed out; requery owned purchases.",
|
|
151
165
|
"PLAY_BILLING_TIMEOUT");
|
|
@@ -248,6 +262,8 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
248
262
|
if (pending != null) {
|
|
249
263
|
clearActivePurchase(pending);
|
|
250
264
|
pending.resolve(new JSObject().put("status", "cancelled"));
|
|
265
|
+
} else {
|
|
266
|
+
notifyLateTerminalResult("cancelled", null);
|
|
251
267
|
}
|
|
252
268
|
return;
|
|
253
269
|
}
|
|
@@ -255,6 +271,8 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
255
271
|
if (pending != null) {
|
|
256
272
|
clearActivePurchase(pending);
|
|
257
273
|
rejectBilling(pending, result);
|
|
274
|
+
} else {
|
|
275
|
+
notifyLateTerminalResult("failed", billingErrorCode(result));
|
|
258
276
|
}
|
|
259
277
|
return;
|
|
260
278
|
}
|
|
@@ -272,9 +290,15 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
272
290
|
continue;
|
|
273
291
|
}
|
|
274
292
|
JSObject outcome = new JSObject()
|
|
275
|
-
.put("status", encoded.optString("
|
|
293
|
+
.put("status", encoded.optString("purchaseState"))
|
|
276
294
|
.put("purchase", encoded);
|
|
277
|
-
|
|
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
|
+
}
|
|
278
302
|
if (pending != null && purchase.getProducts().contains(expectedProductId)) {
|
|
279
303
|
clearActivePurchase(pending);
|
|
280
304
|
pending.resolve(outcome);
|
|
@@ -283,6 +307,24 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
283
307
|
}
|
|
284
308
|
}
|
|
285
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
|
+
|
|
286
328
|
private JSObject encodePurchase(Purchase purchase) {
|
|
287
329
|
int state = purchase.getPurchaseState();
|
|
288
330
|
if (state != Purchase.PurchaseState.PURCHASED
|
|
@@ -293,20 +335,32 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
293
335
|
if (token == null || token.trim().isEmpty() || purchase.getProducts().isEmpty()) {
|
|
294
336
|
return null;
|
|
295
337
|
}
|
|
296
|
-
JSArray
|
|
338
|
+
JSArray products = new JSArray();
|
|
297
339
|
for (String productId : purchase.getProducts()) {
|
|
298
|
-
|
|
340
|
+
products.put(productId);
|
|
299
341
|
}
|
|
342
|
+
String purchaseState = state == Purchase.PurchaseState.PURCHASED ? "purchased" : "pending";
|
|
343
|
+
// Billing 9.1.0 Purchase getters under their own names, plus the 0.1 names
|
|
344
|
+
// (productIds, state, purchaseTimeMillis) until the next major version.
|
|
300
345
|
JSObject encoded = new JSObject()
|
|
301
|
-
.put("
|
|
346
|
+
.put("products", products)
|
|
347
|
+
.put("productIds", products)
|
|
302
348
|
.put("purchaseToken", token)
|
|
303
|
-
.put("
|
|
349
|
+
.put("purchaseState", purchaseState)
|
|
350
|
+
.put("state", purchaseState)
|
|
351
|
+
.put("purchaseTime", purchase.getPurchaseTime())
|
|
352
|
+
.put("quantity", purchase.getQuantity())
|
|
353
|
+
.put("isAcknowledged", purchase.isAcknowledged());
|
|
304
354
|
if (purchase.getPurchaseTime() > 0) {
|
|
305
355
|
encoded.put("purchaseTimeMillis", purchase.getPurchaseTime());
|
|
306
356
|
}
|
|
307
357
|
if (purchase.getOrderId() != null) {
|
|
308
358
|
encoded.put("orderId", purchase.getOrderId());
|
|
309
359
|
}
|
|
360
|
+
AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
|
|
361
|
+
if (accountIdentifiers != null && accountIdentifiers.getObfuscatedAccountId() != null) {
|
|
362
|
+
encoded.put("obfuscatedAccountId", accountIdentifiers.getObfuscatedAccountId());
|
|
363
|
+
}
|
|
310
364
|
return encoded;
|
|
311
365
|
}
|
|
312
366
|
|
|
@@ -378,6 +432,11 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
378
432
|
}
|
|
379
433
|
|
|
380
434
|
private void rejectBilling(PluginCall call, BillingResult result) {
|
|
435
|
+
call.reject("Google Play Billing is unavailable (" + result.getResponseCode() + ").",
|
|
436
|
+
billingErrorCode(result));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private String billingErrorCode(BillingResult result) {
|
|
381
440
|
int responseCode = result.getResponseCode();
|
|
382
441
|
String code;
|
|
383
442
|
switch (responseCode) {
|
|
@@ -406,8 +465,7 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
406
465
|
default:
|
|
407
466
|
code = "PLAY_BILLING_ERROR";
|
|
408
467
|
}
|
|
409
|
-
|
|
410
|
-
code);
|
|
468
|
+
return code;
|
|
411
469
|
}
|
|
412
470
|
|
|
413
471
|
private void clearActivePurchase(PluginCall call) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type CapacitorCommerceServiceProvider, type StoreCommerceProductMapping, type StoreCommerceProvider, type StoreSdkErrorCodeMap, type StoreSdkPurchase } from '@mpgd/adapter-capacitor';
|
|
2
|
+
import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
|
|
3
|
+
import { type CommerceAuthorityCapabilities, type StoreId, type StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
|
|
4
|
+
import type { CapacitorPlayBillingPlugin, PlayPurchase } from './definitions.js';
|
|
5
|
+
/** The evidence schema the Google Play verifier in `@mpgd/game-services` routes on. */
|
|
6
|
+
export declare const googlePlayProductPurchaseEvidenceSchema: 'google-play.product-purchase.v2';
|
|
7
|
+
/** RFC "Capabilities" profile row for `google-play`: the purchase token is the proof, bound to an obfuscated account id. */
|
|
8
|
+
export declare const playBillingCommerceAuthority: CommerceAuthorityCapabilities;
|
|
9
|
+
/** The default registry id of the Play Billing commerce v2 service provider. */
|
|
10
|
+
export declare const playBillingCommerceServiceProviderId: 'google-play-billing';
|
|
11
|
+
/**
|
|
12
|
+
* Evidence payload of a Play purchase: exactly `{ purchaseToken }`, which is
|
|
13
|
+
* what `createGooglePlayProductPurchaseBoundary` reads and what the v1
|
|
14
|
+
* provider emits, so v1 and v2 evidence verify through the same code path.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createPlayBillingEvidencePayload(purchase: Pick<StoreSdkPurchase, 'token'>): PlatformEvidenceEnvelope['payload'];
|
|
17
|
+
/** One catalog product sold through Play; `offerId` is the default one-time offer token. */
|
|
18
|
+
export interface PlayBillingCommerceProductMapping extends StoreCommerceProductMapping {
|
|
19
|
+
readonly kind: 'consumable' | 'durable';
|
|
20
|
+
}
|
|
21
|
+
export interface CreateCapacitorPlayBillingCommerceProviderInput {
|
|
22
|
+
readonly products: readonly PlayBillingCommerceProductMapping[];
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the player's obfuscated account id (`{ kind:
|
|
25
|
+
* 'obfuscated-account-id', value }`) before a purchase opens. It must agree
|
|
26
|
+
* with the backend's `resolveObfuscatedAccountId`. A thrown lookup becomes
|
|
27
|
+
* `ACCOUNT_BINDING_REQUIRED` with `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED` kept.
|
|
28
|
+
*/
|
|
29
|
+
readonly accountBinding: () => Promise<StorePurchaseAccountBinding | undefined> | StorePurchaseAccountBinding | undefined;
|
|
30
|
+
/** Defaults to the registered `CapacitorPlayBilling` plugin. */
|
|
31
|
+
readonly plugin?: CapacitorPlayBillingPlugin;
|
|
32
|
+
/** Defaults to `Capacitor.getPlatform() === 'android'`. */
|
|
33
|
+
readonly isAndroid?: () => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Store id on purchases and in derived idempotency keys. Defaults to
|
|
36
|
+
* `google-play`, which keeps the `play-token-sha256:<hex>` key v1 ledgers
|
|
37
|
+
* use; a conformance harness sets the vector's fixture store id.
|
|
38
|
+
*/
|
|
39
|
+
readonly store?: StoreId;
|
|
40
|
+
/** Extra plugin codes merged over `playBillingStoreSdkErrorCodeMap`. */
|
|
41
|
+
readonly mapError?: StoreSdkErrorCodeMap;
|
|
42
|
+
/** Diagnostics for plugin purchases left out of recovery (retired SKU, multi-line); see the SDK input. */
|
|
43
|
+
readonly onUnmappedPurchase?: (purchase: PlayPurchase) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Commerce v2 `CommerceProvider` for Google Play: `createStoreCommerceProvider`
|
|
47
|
+
* over `createCapacitorPlayBillingStoreSdk`, with store `google-play`, evidence
|
|
48
|
+
* schema `google-play.product-purchase.v2` carrying `{ purchaseToken }`, and
|
|
49
|
+
* the `client-token` / `required` authority block. Finishing stays on the
|
|
50
|
+
* server (the backend acknowledges or consumes after the ledger grant), so
|
|
51
|
+
* the provider has no `finishTransaction` and no `restorePurchases`. It never
|
|
52
|
+
* grants.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createCapacitorPlayBillingCommerceProvider(input: CreateCapacitorPlayBillingCommerceProviderInput): StoreCommerceProvider;
|
|
55
|
+
export interface CreateCapacitorPlayBillingServiceProviderInput extends CreateCapacitorPlayBillingCommerceProviderInput {
|
|
56
|
+
/** Registry id; defaults to `google-play-billing`. */
|
|
57
|
+
readonly id?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Registry entry for `createCapacitorPlatformGateway({ providers })`: the
|
|
61
|
+
* gateway serves the six `commerce.v2.*` bridge methods and exposes
|
|
62
|
+
* `gateway.commerceV2`, and v1 callers keep working through
|
|
63
|
+
* `toCommerceAdapter`. Only `nativeIap` is declared; Play sells no
|
|
64
|
+
* subscriptions in this release.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createCapacitorPlayBillingServiceProvider(input: CreateCapacitorPlayBillingServiceProviderInput): CapacitorCommerceServiceProvider & {
|
|
67
|
+
readonly commerce: StoreCommerceProvider;
|
|
68
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createCommerceServiceProvider, createStoreCommerceProvider, } from '@mpgd/adapter-capacitor';
|
|
2
|
+
import { CommerceError, } from '@mpgd/platform/commerce';
|
|
3
|
+
import { createCapacitorPlayBillingStoreSdk, googlePlayStoreId, playBillingStoreSdkErrorCodeMap, } from './store-sdk.js';
|
|
4
|
+
/** The evidence schema the Google Play verifier in `@mpgd/game-services` routes on. */
|
|
5
|
+
export const googlePlayProductPurchaseEvidenceSchema = 'google-play.product-purchase.v2';
|
|
6
|
+
/** RFC "Capabilities" profile row for `google-play`: the purchase token is the proof, bound to an obfuscated account id. */
|
|
7
|
+
export const playBillingCommerceAuthority = Object.freeze({
|
|
8
|
+
evidence: 'client-token',
|
|
9
|
+
accountBinding: 'required',
|
|
10
|
+
});
|
|
11
|
+
/** The default registry id of the Play Billing commerce v2 service provider. */
|
|
12
|
+
export const playBillingCommerceServiceProviderId = 'google-play-billing';
|
|
13
|
+
/**
|
|
14
|
+
* Evidence payload of a Play purchase: exactly `{ purchaseToken }`, which is
|
|
15
|
+
* what `createGooglePlayProductPurchaseBoundary` reads and what the v1
|
|
16
|
+
* provider emits, so v1 and v2 evidence verify through the same code path.
|
|
17
|
+
*/
|
|
18
|
+
export function createPlayBillingEvidencePayload(purchase) {
|
|
19
|
+
return { purchaseToken: purchase.token };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Commerce v2 `CommerceProvider` for Google Play: `createStoreCommerceProvider`
|
|
23
|
+
* over `createCapacitorPlayBillingStoreSdk`, with store `google-play`, evidence
|
|
24
|
+
* schema `google-play.product-purchase.v2` carrying `{ purchaseToken }`, and
|
|
25
|
+
* the `client-token` / `required` authority block. Finishing stays on the
|
|
26
|
+
* server (the backend acknowledges or consumes after the ledger grant), so
|
|
27
|
+
* the provider has no `finishTransaction` and no `restorePurchases`. It never
|
|
28
|
+
* grants.
|
|
29
|
+
*/
|
|
30
|
+
export function createCapacitorPlayBillingCommerceProvider(input) {
|
|
31
|
+
const sdkProducts = [];
|
|
32
|
+
for (const mapping of input.products) {
|
|
33
|
+
if (mapping.kind !== 'consumable' && mapping.kind !== 'durable') {
|
|
34
|
+
throw new TypeError(`Play Billing sells one-time products only; ${String(mapping.logicalId)} is ${String(mapping.kind)}.`);
|
|
35
|
+
}
|
|
36
|
+
sdkProducts.push({
|
|
37
|
+
sku: mapping.storeSku,
|
|
38
|
+
kind: mapping.kind,
|
|
39
|
+
...(mapping.offerId === undefined ? {} : { offerToken: mapping.offerId }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const sdk = createCapacitorPlayBillingStoreSdk({
|
|
43
|
+
products: sdkProducts,
|
|
44
|
+
...(input.plugin === undefined ? {} : { plugin: input.plugin }),
|
|
45
|
+
...(input.isAndroid === undefined ? {} : { isAndroid: input.isAndroid }),
|
|
46
|
+
...(input.onUnmappedPurchase === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { onUnmappedPurchase: input.onUnmappedPurchase }),
|
|
49
|
+
});
|
|
50
|
+
const resolveAccountBinding = input.accountBinding;
|
|
51
|
+
return createStoreCommerceProvider({
|
|
52
|
+
sdk,
|
|
53
|
+
store: input.store ?? googlePlayStoreId,
|
|
54
|
+
products: input.products,
|
|
55
|
+
evidenceSchema: googlePlayProductPurchaseEvidenceSchema,
|
|
56
|
+
evidencePayload: createPlayBillingEvidencePayload,
|
|
57
|
+
accountBinding: async () => {
|
|
58
|
+
try {
|
|
59
|
+
return await resolveAccountBinding();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new CommerceError('ACCOUNT_BINDING_REQUIRED', {
|
|
63
|
+
message: 'Play Billing could not resolve the obfuscated account id.',
|
|
64
|
+
storeCode: 'PLAY_BILLING_ACCOUNT_LOOKUP_FAILED',
|
|
65
|
+
cause: error,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
authority: playBillingCommerceAuthority,
|
|
70
|
+
mapError: { ...playBillingStoreSdkErrorCodeMap, ...input.mapError },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Registry entry for `createCapacitorPlatformGateway({ providers })`: the
|
|
75
|
+
* gateway serves the six `commerce.v2.*` bridge methods and exposes
|
|
76
|
+
* `gateway.commerceV2`, and v1 callers keep working through
|
|
77
|
+
* `toCommerceAdapter`. Only `nativeIap` is declared; Play sells no
|
|
78
|
+
* subscriptions in this release.
|
|
79
|
+
*/
|
|
80
|
+
export function createCapacitorPlayBillingServiceProvider(input) {
|
|
81
|
+
const { id, ...providerInput } = input;
|
|
82
|
+
const commerce = createCapacitorPlayBillingCommerceProvider(providerInput);
|
|
83
|
+
return {
|
|
84
|
+
...createCommerceServiceProvider({
|
|
85
|
+
id: id ?? playBillingCommerceServiceProviderId,
|
|
86
|
+
commerce,
|
|
87
|
+
features: ['nativeIap'],
|
|
88
|
+
}),
|
|
89
|
+
commerce,
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/definitions.d.ts
CHANGED
|
@@ -1,26 +1,68 @@
|
|
|
1
1
|
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
2
|
export type PlayPurchaseState = 'purchased' | 'pending';
|
|
3
|
+
export interface PlayOneTimeOffer {
|
|
4
|
+
readonly offerToken: string;
|
|
5
|
+
readonly formattedPrice: string;
|
|
6
|
+
readonly currencyCode: string;
|
|
7
|
+
/** `OneTimePurchaseOfferDetails.getPriceAmountMicros()`; absent on older plugin builds. */
|
|
8
|
+
readonly priceAmountMicros?: number;
|
|
9
|
+
}
|
|
3
10
|
export interface PlayProduct {
|
|
4
11
|
readonly productId: string;
|
|
5
12
|
readonly title: string;
|
|
6
13
|
readonly description: string;
|
|
7
14
|
/** Explicit offer selection is required when Google returns multiple offers. */
|
|
8
|
-
readonly offers: readonly
|
|
9
|
-
readonly offerToken: string;
|
|
10
|
-
readonly formattedPrice: string;
|
|
11
|
-
readonly currencyCode: string;
|
|
12
|
-
}[];
|
|
15
|
+
readonly offers: readonly PlayOneTimeOffer[];
|
|
13
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* One Play purchase as the plugin encodes `com.android.billingclient.api.Purchase`.
|
|
19
|
+
* `products`, `purchaseState`, `purchaseTime`, `quantity`, `isAcknowledged`, and
|
|
20
|
+
* `obfuscatedAccountId` follow the Billing Library 9.1.0 getters; `productIds`,
|
|
21
|
+
* `state`, and `purchaseTimeMillis` are the 0.1 names and carry the same values
|
|
22
|
+
* until the next major version.
|
|
23
|
+
*/
|
|
14
24
|
export interface PlayPurchase {
|
|
25
|
+
/** `Purchase.getProducts()`: the product ids this purchase covers (one for a single-line purchase). */
|
|
26
|
+
readonly products: readonly string[];
|
|
27
|
+
/** @deprecated Same value as `products`; removed in the next major version. */
|
|
15
28
|
readonly productIds: readonly string[];
|
|
16
29
|
readonly purchaseToken: string;
|
|
30
|
+
/** `Purchase.getOrderId()`; absent while Play has not assigned one (for example a pending purchase). */
|
|
17
31
|
readonly orderId?: string;
|
|
32
|
+
/** `Purchase.getPurchaseTime()` in milliseconds since the epoch. */
|
|
33
|
+
readonly purchaseTime: number;
|
|
34
|
+
/** @deprecated Same value as `purchaseTime` when it is positive; removed in the next major version. */
|
|
18
35
|
readonly purchaseTimeMillis?: number;
|
|
36
|
+
/** `Purchase.getPurchaseState()`; the plugin never reports `UNSPECIFIED_STATE`. */
|
|
37
|
+
readonly purchaseState: PlayPurchaseState;
|
|
38
|
+
/** @deprecated Same value as `purchaseState`; removed in the next major version. */
|
|
19
39
|
readonly state: PlayPurchaseState;
|
|
40
|
+
/**
|
|
41
|
+
* `Purchase.isAcknowledged()`. The plugin never acknowledges or consumes; the
|
|
42
|
+
* backend finalizer does after the ledger grant, so `false` on a purchased
|
|
43
|
+
* entry means the purchase is still unfinished.
|
|
44
|
+
*/
|
|
45
|
+
readonly isAcknowledged: boolean;
|
|
46
|
+
/** `Purchase.getQuantity()`; 1 unless multi-quantity purchases are enabled in Play Console. */
|
|
47
|
+
readonly quantity: number;
|
|
48
|
+
/** `Purchase.getAccountIdentifiers().getObfuscatedAccountId()` when the purchase flow set one. */
|
|
49
|
+
readonly obfuscatedAccountId?: string;
|
|
20
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Result of a `purchase` call (`purchased`, `pending`, or `cancelled`) and
|
|
53
|
+
* payload of every `purchaseUpdated` event. A `purchase` call never resolves
|
|
54
|
+
* `failed`: it rejects with the plugin code instead. On `purchaseUpdated`, a
|
|
55
|
+
* `cancelled` or `failed` outcome without a `purchase` is a terminal result
|
|
56
|
+
* that arrived after the `purchase` call for `productId` already timed out
|
|
57
|
+
* (`PLAY_BILLING_TIMEOUT`), so the attempt the timeout left open can close.
|
|
58
|
+
*/
|
|
21
59
|
export interface PlayPurchaseOutcome {
|
|
22
|
-
readonly status: 'purchased' | 'pending' | 'cancelled';
|
|
60
|
+
readonly status: 'purchased' | 'pending' | 'cancelled' | 'failed';
|
|
23
61
|
readonly purchase?: PlayPurchase;
|
|
62
|
+
/** The timed-out product a late `cancelled` or `failed` result belongs to. */
|
|
63
|
+
readonly productId?: string;
|
|
64
|
+
/** The `PLAY_BILLING_*` code of a late `failed` result. */
|
|
65
|
+
readonly code?: string;
|
|
24
66
|
}
|
|
25
67
|
export interface CapacitorPlayBillingPlugin {
|
|
26
68
|
getProducts(input: {
|
|
@@ -33,9 +75,20 @@ export interface CapacitorPlayBillingPlugin {
|
|
|
33
75
|
readonly offerToken?: string;
|
|
34
76
|
readonly obfuscatedAccountId: string;
|
|
35
77
|
}): Promise<PlayPurchaseOutcome>;
|
|
36
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Requeries every purchased or pending in-app purchase Play still reports
|
|
80
|
+
* for this user and device (`queryPurchasesAsync`): unconsumed consumables,
|
|
81
|
+
* acknowledged and unacknowledged durables, and pending purchases. The
|
|
82
|
+
* `isAcknowledged` flag separates finished durables from unfinished ones.
|
|
83
|
+
*/
|
|
37
84
|
getPurchases(): Promise<{
|
|
38
85
|
readonly purchases: readonly PlayPurchase[];
|
|
39
86
|
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Every purchase update Play delivers (`PurchasesUpdatedListener`), including
|
|
89
|
+
* one that completes after the `purchase` call timed out or the app
|
|
90
|
+
* restarted. Updates delivered before a listener attaches are retained
|
|
91
|
+
* until the first listener consumes them.
|
|
92
|
+
*/
|
|
40
93
|
addListener(eventName: 'purchaseUpdated', listener: (event: PlayPurchaseOutcome) => void): Promise<PluginListenerHandle>;
|
|
41
94
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/provider.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface PlayPurchaseRecoveryBackend {
|
|
|
66
66
|
/**
|
|
67
67
|
* Resubmit owned purchases after a callback was missed or the app restarted.
|
|
68
68
|
* A native purchase is never credited locally; only the backend response grants it.
|
|
69
|
+
*
|
|
70
|
+
* @deprecated v1 recovery driver. Commerce v2 games register
|
|
71
|
+
* `createCapacitorPlayBillingServiceProvider` and let the single reconciler
|
|
72
|
+
* in `@mpgd/game-services` (PR C1) resolve unfinished purchases from
|
|
73
|
+
* `getAvailablePurchases` and `purchaseUpdated`; this driver is removed with
|
|
74
|
+
* the v1 adapter in the next major version.
|
|
69
75
|
*/
|
|
70
76
|
export declare function recoverOwnedPlayPurchases(input: {
|
|
71
77
|
readonly provider: CapacitorPlayBillingProvider;
|
|
@@ -75,5 +81,12 @@ export declare function recoverOwnedPlayPurchases(input: {
|
|
|
75
81
|
readonly now?: () => string;
|
|
76
82
|
}): Promise<readonly RecoveredPlayPurchase[]>;
|
|
77
83
|
export declare function createCapacitorPlayBillingProvider(options: PlayBillingProviderOptions): CapacitorPlayBillingProvider;
|
|
78
|
-
/**
|
|
79
|
-
|
|
84
|
+
/**
|
|
85
|
+
* The server's token transaction ID, computed by the one shared helper so the
|
|
86
|
+
* plugin and `@mpgd/game-services` can never drift.
|
|
87
|
+
*
|
|
88
|
+
* @deprecated Use `createGooglePlayTokenPurchaseKey` from `@mpgd/platform/commerce`;
|
|
89
|
+
* this alias is the same function (same key, same `TypeError` for an invalid
|
|
90
|
+
* token) and will be removed in the next major version.
|
|
91
|
+
*/
|
|
92
|
+
export declare const createGooglePlayTokenTransactionId: (purchaseToken: string) => Promise<string>;
|