@mpgd/capacitor-play-billing 0.2.3 → 0.3.1
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 +45 -56
- package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.java +1 -7
- package/dist/commerce-profile.d.ts +24 -0
- package/dist/commerce-profile.js +51 -0
- package/dist/commerce-provider.d.ts +9 -13
- package/dist/commerce-provider.js +8 -14
- package/dist/definitions.d.ts +1 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/store-sdk.d.ts +6 -14
- package/dist/store-sdk.js +22 -27
- package/package.json +10 -10
- package/dist/provider.d.ts +0 -92
- package/dist/provider.js +0 -303
package/README.md
CHANGED
|
@@ -13,15 +13,16 @@ 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
|
|
16
|
+
## Commerce provider
|
|
17
17
|
|
|
18
|
-
Commerce
|
|
19
|
-
|
|
18
|
+
Commerce ([RFC](../../docs/specs/commerce/commerce-v2-rfc.md)) is a `StoreSdk`
|
|
19
|
+
over this plugin and a `CommerceProvider` built from it with
|
|
20
20
|
`createStoreCommerceProvider` from `@mpgd/adapter-capacitor` (a peer
|
|
21
|
-
dependency
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`
|
|
21
|
+
dependency). Register the service provider with the Capacitor gateway and it
|
|
22
|
+
serves the six `commerce.v2.*` bridge methods as `gateway.commerce`, reports
|
|
23
|
+
its capability block in `getCapabilities().commerce`, and declares no
|
|
24
|
+
`features` or `methods` of its own. The `@mpgd/game-services` reconciler
|
|
25
|
+
drives it; a scene never calls it.
|
|
25
26
|
|
|
26
27
|
```ts
|
|
27
28
|
import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor';
|
|
@@ -66,10 +67,9 @@ the plugin code. `getAvailablePurchases` is `getPurchases` filtered to
|
|
|
66
67
|
unfinished entries: `purchased` with `isAcknowledged: false`, plus `pending`
|
|
67
68
|
when `includePending` is set; each carries `token = purchaseToken` and
|
|
68
69
|
`transactionId = orderId`, or the `play-token-sha256:<hex>` token key until
|
|
69
|
-
Play assigns an order id, so
|
|
70
|
-
entry
|
|
71
|
-
|
|
72
|
-
upgrade. A purchase for a SKU that is not in `products` (a retired product)
|
|
70
|
+
Play assigns an order id, so the same purchase deduplicates in the ledger. A
|
|
71
|
+
purchased entry whose `isAcknowledged` flag is absent counts as unfinished so a
|
|
72
|
+
paid purchase is never dropped. A purchase for a SKU that is not in `products` (a retired product)
|
|
73
73
|
or a multi-line purchase has no logical id for the reconciler, so it is left
|
|
74
74
|
out of the list and of events and handed to the optional `onUnmappedPurchase`
|
|
75
75
|
diagnostics hook instead; the backend finalizer still sees it through the
|
|
@@ -79,8 +79,8 @@ purchase without an order id waits for the token digest, and later updates
|
|
|
79
79
|
queue behind it), and a `cancelled` or `failed` update without a purchase is
|
|
80
80
|
the plugin's late terminal result for a purchase call that already timed out
|
|
81
81
|
(`PLAY_BILLING_TIMEOUT`), emitted as a `purchaseError` for that product so
|
|
82
|
-
the attempt the timeout left open closes. `getAvailability`
|
|
83
|
-
|
|
82
|
+
the attempt the timeout left open closes. `getAvailability` reports the Android
|
|
83
|
+
check, offer configuration, and service state.
|
|
84
84
|
`playBillingStoreSdkErrorCodeMap` extends `defaultStoreSdkErrorCodeMap` with
|
|
85
85
|
every other `PLAY_BILLING_*` code (`BUSY` is `IN_PROGRESS`, `OFFER_REQUIRED`
|
|
86
86
|
and the invalid-product codes are `CONFIGURATION_REQUIRED`,
|
|
@@ -93,26 +93,32 @@ the attempt from `getAvailablePurchases` without reopening the store UI.
|
|
|
93
93
|
`createCapacitorPlayBillingCommerceProvider({ products, accountBinding,
|
|
94
94
|
plugin?, isAndroid?, store?, mapError? })` composes the provider: evidence
|
|
95
95
|
schema `google-play.product-purchase.v2` with payload `{ purchaseToken }`,
|
|
96
|
-
|
|
97
|
-
`createGooglePlayProductPurchaseBoundary` verifies both; authority
|
|
96
|
+
which `createGooglePlayProductPurchaseBoundary` verifies; authority
|
|
98
97
|
`{ evidence: 'client-token', accountBinding: 'required' }`; no
|
|
99
98
|
`finishTransaction` and no `restorePurchases`. A thrown `accountBinding`
|
|
100
99
|
lookup is `ACCOUNT_BINDING_REQUIRED` with `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED`
|
|
101
100
|
kept as the store code. `store` defaults to `google-play`; only a conformance
|
|
102
101
|
harness overrides it, because the derived idempotency key follows the store
|
|
103
|
-
id.
|
|
102
|
+
id. `createPlayBillingCommerceCapabilities(availability)` (from
|
|
103
|
+
`src/commerce-profile.ts`, which imports types only and no Capacitor code)
|
|
104
|
+
returns the full `CommerceCapabilities` block this provider declares while the
|
|
105
|
+
store reports `availability`, so a validator or server can read the profile
|
|
106
|
+
without loading the plugin; a unit test keeps it equal to
|
|
107
|
+
`provider.getCapabilities()`. The conformance test runs every store vector whose profile the Play
|
|
104
108
|
capabilities can serve (`purchase-server-finish`) against a fake plugin,
|
|
105
109
|
directly and through the gateway bridge path, and lists the skipped vectors
|
|
106
110
|
with their reason: the client-finish, host-finish, adapter-settled, and
|
|
107
111
|
server-push profiles need a finish owner or evidence authority Play does not
|
|
108
112
|
have.
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
Every purchase the plugin reports (`getPurchases()`, `purchase()`, and
|
|
115
|
+
`purchaseUpdated`) carries the Billing Library 9.1.0 `Purchase` getters
|
|
112
116
|
`products`, `purchaseState`, `purchaseTime`, `quantity`, `isAcknowledged`,
|
|
113
117
|
and `obfuscatedAccountId`, with `priceAmountMicros` on offers. The 0.1 names
|
|
114
|
-
`productIds`, `state`, and `purchaseTimeMillis`
|
|
115
|
-
|
|
118
|
+
`productIds`, `state`, and `purchaseTimeMillis` were removed from the payload
|
|
119
|
+
and the type. The TypeScript readers still accept them, so a web bundle that
|
|
120
|
+
reaches a 0.1 native build first (a live update) keeps paid purchases
|
|
121
|
+
readable; run `cap sync` after upgrading all the same. `purchaseUpdated` is retained
|
|
116
122
|
until a listener consumes it, so a purchase that completes after the
|
|
117
123
|
`purchase` call timed out, or before the web view attached its listener,
|
|
118
124
|
still reaches the provider, and a cancel or non-OK result that arrives after
|
|
@@ -143,42 +149,25 @@ A build whose shell lacks the native module still runs: the plugin answers
|
|
|
143
149
|
Capacitor's `UNIMPLEMENTED`, which the SDK reports as
|
|
144
150
|
`configuration-required`, so no purchase UI opens.
|
|
145
151
|
|
|
146
|
-
### Migrating from
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
##
|
|
165
|
-
|
|
166
|
-
The provider maps game logical product IDs to Play product IDs and requires a
|
|
167
|
-
stable obfuscated account ID that agrees with the backend account resolver.
|
|
168
|
-
When Play returns multiple eligible one-time offers, map an explicit
|
|
169
|
-
`offerToken`; otherwise product lookup reports `PLAY_BILLING_OFFER_REQUIRED`
|
|
170
|
-
instead of displaying a price for the wrong offer.
|
|
171
|
-
`getOwnedPurchases()` returns provisional evidence for recovery after a missed
|
|
172
|
-
callback or process restart. `recoverOwnedPlayPurchases({ provider, backend:
|
|
173
|
-
backend.purchases, playerId })` submits completed owned purchases to the same
|
|
174
|
-
authenticated backend using stable token-derived idempotency keys. Backend
|
|
175
|
-
uncertainty stays pending for retry; `commerce.restore` deliberately returns
|
|
176
|
-
no local entitlements. Pending Play purchases remain pending until Google
|
|
177
|
-
reports purchased. The caller must not grant based on a native callback,
|
|
178
|
-
`getOwnedPurchases()` result, or `commerce.restore` alone.
|
|
179
|
-
If a launched purchase times out, is interrupted, or reports an already-owned
|
|
180
|
-
item without a matching callback, the provider returns `pending`. Requery and
|
|
181
|
-
recover owned purchases before offering another billing flow.
|
|
152
|
+
### Migrating from v1
|
|
153
|
+
|
|
154
|
+
Commerce v2 PR C4 removed the v1 provider and its recovery driver.
|
|
155
|
+
|
|
156
|
+
| Removed | Replacement |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `createCapacitorPlayBillingProvider`, `PlayBillingProviderOptions`, `CapacitorPlayBillingProvider`, `PlayBillingProductMapping`, `PlayBillingAvailability` | `createCapacitorPlayBillingServiceProvider` (registry entry) or `createCapacitorPlayBillingCommerceProvider`; rename `id`/`storeId`/`type` to `logicalId`/`storeSku`/`kind` (`non_consumable` becomes `durable`) and `offerToken` to `offerId`; `getObfuscatedAccountId` becomes an `accountBinding` resolver returning `{ kind: 'obfuscated-account-id', value }` |
|
|
159
|
+
| `recoverOwnedPlayPurchases`, `getOwnedPurchases`, `OwnedPlayPurchase`, `RecoveredPlayPurchase`, `PlayPurchaseVerification`, `PlayPurchaseRecoveryBackend` | the reconciler in `@mpgd/game-services` (`createCommerceReconciler`, behind `createGameServicesClient` and `monetizationRecovery.reconcile()`), which reads `getAvailablePurchases` and `purchaseUpdated` from the provider and verifies each purchase under the same `play-token-sha256` key; the backend contract is unchanged because the evidence payload is the same |
|
|
160
|
+
| `createGooglePlayTokenTransactionId` | `createGooglePlayTokenPurchaseKey` from `@mpgd/platform/commerce` (the same function) |
|
|
161
|
+
| `gateway.commerceV2`, the v1 `commerce.*` bridge methods | `gateway.commerce` and the `commerce.v2.*` methods |
|
|
162
|
+
| `features: ['nativeIap']` on the registry entry | none: the entry declares no `features` and its readiness is `getCapabilities().commerce` |
|
|
163
|
+
| `PlayPurchase.productIds`, `.state`, `.purchaseTimeMillis` (plugin payload and TypeScript type) | `products`, `purchaseState`, `purchaseTime` |
|
|
164
|
+
|
|
165
|
+
`src/commerce-reconciler-play-smoke.test.ts` shows the reconciler resubmitting
|
|
166
|
+
owned purchases over the fake plugin: a purchased token is verified once, a
|
|
167
|
+
pending purchase is never sent, a rejection is terminal, and an outage stays
|
|
168
|
+
retryable.
|
|
169
|
+
|
|
170
|
+
## Native compile check
|
|
182
171
|
|
|
183
172
|
The Kit reference app includes an opt-in Gradle compile target, without
|
|
184
173
|
bundling Billing into the base app:
|
package/android/src/main/java/dev/mpgd/capacitor/playbilling/CapacitorPlayBillingPlugin.java
CHANGED
|
@@ -340,20 +340,14 @@ public class CapacitorPlayBillingPlugin extends Plugin {
|
|
|
340
340
|
products.put(productId);
|
|
341
341
|
}
|
|
342
342
|
String purchaseState = state == Purchase.PurchaseState.PURCHASED ? "purchased" : "pending";
|
|
343
|
-
// Billing 9.1.0 Purchase getters under their own names
|
|
344
|
-
// (productIds, state, purchaseTimeMillis) until the next major version.
|
|
343
|
+
// Billing 9.1.0 Purchase getters under their own names.
|
|
345
344
|
JSObject encoded = new JSObject()
|
|
346
345
|
.put("products", products)
|
|
347
|
-
.put("productIds", products)
|
|
348
346
|
.put("purchaseToken", token)
|
|
349
347
|
.put("purchaseState", purchaseState)
|
|
350
|
-
.put("state", purchaseState)
|
|
351
348
|
.put("purchaseTime", purchase.getPurchaseTime())
|
|
352
349
|
.put("quantity", purchase.getQuantity())
|
|
353
350
|
.put("isAcknowledged", purchase.isAcknowledged());
|
|
354
|
-
if (purchase.getPurchaseTime() > 0) {
|
|
355
|
-
encoded.put("purchaseTimeMillis", purchase.getPurchaseTime());
|
|
356
|
-
}
|
|
357
351
|
if (purchase.getOrderId() != null) {
|
|
358
352
|
encoded.put("orderId", purchase.getOrderId());
|
|
359
353
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StoreSdkCapabilities } from '@mpgd/adapter-capacitor';
|
|
2
|
+
import type { CommerceAuthorityCapabilities, CommerceCapabilities, PlatformProviderAvailability } from '@mpgd/platform';
|
|
3
|
+
/** The store id of the Play Billing `StoreSdk` (RFC "Capabilities", `google-play`). */
|
|
4
|
+
export declare const googlePlayStoreId: 'google-play';
|
|
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
|
+
/**
|
|
8
|
+
* What the Play Billing plugin supports (RFC "Capabilities" profile row for
|
|
9
|
+
* `google-play`): one-time consumables and durables, no subscriptions, no
|
|
10
|
+
* multi-quantity, pending purchases, no restore call, one-time offers, and a
|
|
11
|
+
* server-owned finish (the backend acknowledges or consumes after the grant).
|
|
12
|
+
*/
|
|
13
|
+
export declare const playBillingStoreSdkCapabilities: StoreSdkCapabilities;
|
|
14
|
+
/** RFC "Capabilities" profile row for `google-play`: the purchase token is the proof, bound to an obfuscated account id. */
|
|
15
|
+
export declare const playBillingCommerceAuthority: CommerceAuthorityCapabilities;
|
|
16
|
+
/**
|
|
17
|
+
* The `CommerceCapabilities` block `createCapacitorPlayBillingCommerceProvider`
|
|
18
|
+
* declares while the store reports `availability`: composed from
|
|
19
|
+
* `playBillingStoreSdkCapabilities` and `playBillingCommerceAuthority` exactly
|
|
20
|
+
* as `createStoreCommerceProvider` composes it (the provider has no capability
|
|
21
|
+
* overrides, and the Play SDK has no `restorePurchases`). Returns a fresh object
|
|
22
|
+
* on every call and has no side effects.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createPlayBillingCommerceCapabilities(availability: PlatformProviderAvailability): CommerceCapabilities;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** The store id of the Play Billing `StoreSdk` (RFC "Capabilities", `google-play`). */
|
|
2
|
+
export const googlePlayStoreId = 'google-play';
|
|
3
|
+
/** The evidence schema the Google Play verifier in `@mpgd/game-services` routes on. */
|
|
4
|
+
export const googlePlayProductPurchaseEvidenceSchema = 'google-play.product-purchase.v2';
|
|
5
|
+
/**
|
|
6
|
+
* What the Play Billing plugin supports (RFC "Capabilities" profile row for
|
|
7
|
+
* `google-play`): one-time consumables and durables, no subscriptions, no
|
|
8
|
+
* multi-quantity, pending purchases, no restore call, one-time offers, and a
|
|
9
|
+
* server-owned finish (the backend acknowledges or consumes after the grant).
|
|
10
|
+
*/
|
|
11
|
+
export const playBillingStoreSdkCapabilities = Object.freeze({
|
|
12
|
+
purchase: Object.freeze({
|
|
13
|
+
consumable: true,
|
|
14
|
+
durable: true,
|
|
15
|
+
subscription: false,
|
|
16
|
+
quantity: false,
|
|
17
|
+
}),
|
|
18
|
+
offers: true,
|
|
19
|
+
pending: true,
|
|
20
|
+
restore: false,
|
|
21
|
+
finish: 'server',
|
|
22
|
+
});
|
|
23
|
+
/** RFC "Capabilities" profile row for `google-play`: the purchase token is the proof, bound to an obfuscated account id. */
|
|
24
|
+
export const playBillingCommerceAuthority = Object.freeze({
|
|
25
|
+
evidence: 'client-token',
|
|
26
|
+
accountBinding: 'required',
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* The `CommerceCapabilities` block `createCapacitorPlayBillingCommerceProvider`
|
|
30
|
+
* declares while the store reports `availability`: composed from
|
|
31
|
+
* `playBillingStoreSdkCapabilities` and `playBillingCommerceAuthority` exactly
|
|
32
|
+
* as `createStoreCommerceProvider` composes it (the provider has no capability
|
|
33
|
+
* overrides, and the Play SDK has no `restorePurchases`). Returns a fresh object
|
|
34
|
+
* on every call and has no side effects.
|
|
35
|
+
*/
|
|
36
|
+
export function createPlayBillingCommerceCapabilities(availability) {
|
|
37
|
+
const sdk = playBillingStoreSdkCapabilities;
|
|
38
|
+
return {
|
|
39
|
+
purchase: { ...sdk.purchase },
|
|
40
|
+
catalog: { products: true, offers: sdk.offers ?? false, localizedPrice: true },
|
|
41
|
+
lifecycle: {
|
|
42
|
+
availablePurchases: true,
|
|
43
|
+
pending: sdk.pending,
|
|
44
|
+
restore: sdk.restore,
|
|
45
|
+
events: true,
|
|
46
|
+
finish: sdk.finish,
|
|
47
|
+
},
|
|
48
|
+
authority: { ...playBillingCommerceAuthority },
|
|
49
|
+
availability,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { type CapacitorCommerceServiceProvider, type StoreCommerceProductMapping, type StoreCommerceProvider, type StoreSdkErrorCodeMap, type StoreSdkPurchase } from '@mpgd/adapter-capacitor';
|
|
2
2
|
import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
|
|
3
|
-
import { type
|
|
3
|
+
import { type StoreId, type StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
|
|
4
|
+
import { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority } from './commerce-profile.js';
|
|
4
5
|
import type { CapacitorPlayBillingPlugin, PlayPurchase } from './definitions.js';
|
|
5
|
-
|
|
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;
|
|
6
|
+
export { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority };
|
|
9
7
|
/** The default registry id of the Play Billing commerce v2 service provider. */
|
|
10
8
|
export declare const playBillingCommerceServiceProviderId: 'google-play-billing';
|
|
11
9
|
/**
|
|
12
10
|
* Evidence payload of a Play purchase: exactly `{ purchaseToken }`, which is
|
|
13
|
-
* what `createGooglePlayProductPurchaseBoundary` reads
|
|
14
|
-
* provider emits, so v1 and v2 evidence verify through the same code path.
|
|
11
|
+
* what `createGooglePlayProductPurchaseBoundary` reads.
|
|
15
12
|
*/
|
|
16
13
|
export declare function createPlayBillingEvidencePayload(purchase: Pick<StoreSdkPurchase, 'token'>): PlatformEvidenceEnvelope['payload'];
|
|
17
14
|
/** One catalog product sold through Play; `offerId` is the default one-time offer token. */
|
|
@@ -33,8 +30,8 @@ export interface CreateCapacitorPlayBillingCommerceProviderInput {
|
|
|
33
30
|
readonly isAndroid?: () => boolean;
|
|
34
31
|
/**
|
|
35
32
|
* Store id on purchases and in derived idempotency keys. Defaults to
|
|
36
|
-
* `google-play`, which keeps the `play-token-sha256:<hex>` key
|
|
37
|
-
*
|
|
33
|
+
* `google-play`, which keeps the `play-token-sha256:<hex>` key existing ledgers
|
|
34
|
+
* hold; a conformance harness sets the vector's fixture store id.
|
|
38
35
|
*/
|
|
39
36
|
readonly store?: StoreId;
|
|
40
37
|
/** Extra plugin codes merged over `playBillingStoreSdkErrorCodeMap`. */
|
|
@@ -58,10 +55,9 @@ export interface CreateCapacitorPlayBillingServiceProviderInput extends CreateCa
|
|
|
58
55
|
}
|
|
59
56
|
/**
|
|
60
57
|
* Registry entry for `createCapacitorPlatformGateway({ providers })`: the
|
|
61
|
-
* gateway serves the six `commerce.v2.*` bridge methods
|
|
62
|
-
* `gateway.
|
|
63
|
-
*
|
|
64
|
-
* subscriptions in this release.
|
|
58
|
+
* gateway serves the six `commerce.v2.*` bridge methods, exposes the provider
|
|
59
|
+
* as `gateway.commerce`, and reports its capability block (Play sells no
|
|
60
|
+
* subscriptions in this release).
|
|
65
61
|
*/
|
|
66
62
|
export declare function createCapacitorPlayBillingServiceProvider(input: CreateCapacitorPlayBillingServiceProviderInput): CapacitorCommerceServiceProvider & {
|
|
67
63
|
readonly commerce: StoreCommerceProvider;
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { createCommerceServiceProvider, createStoreCommerceProvider, } from '@mpgd/adapter-capacitor';
|
|
2
2
|
import { CommerceError, } from '@mpgd/platform/commerce';
|
|
3
|
+
import { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority, } from './commerce-profile.js';
|
|
3
4
|
import { createCapacitorPlayBillingStoreSdk, googlePlayStoreId, playBillingStoreSdkErrorCodeMap, } from './store-sdk.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const playBillingCommerceAuthority = Object.freeze({
|
|
8
|
-
evidence: 'client-token',
|
|
9
|
-
accountBinding: 'required',
|
|
10
|
-
});
|
|
5
|
+
// The profile constants live in the SDK-free `commerce-profile.ts` so a tools
|
|
6
|
+
// project can read them without loading Capacitor; they stay exported here.
|
|
7
|
+
export { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority };
|
|
11
8
|
/** The default registry id of the Play Billing commerce v2 service provider. */
|
|
12
9
|
export const playBillingCommerceServiceProviderId = 'google-play-billing';
|
|
13
10
|
/**
|
|
14
11
|
* Evidence payload of a Play purchase: exactly `{ purchaseToken }`, which is
|
|
15
|
-
* what `createGooglePlayProductPurchaseBoundary` reads
|
|
16
|
-
* provider emits, so v1 and v2 evidence verify through the same code path.
|
|
12
|
+
* what `createGooglePlayProductPurchaseBoundary` reads.
|
|
17
13
|
*/
|
|
18
14
|
export function createPlayBillingEvidencePayload(purchase) {
|
|
19
15
|
return { purchaseToken: purchase.token };
|
|
@@ -72,10 +68,9 @@ export function createCapacitorPlayBillingCommerceProvider(input) {
|
|
|
72
68
|
}
|
|
73
69
|
/**
|
|
74
70
|
* Registry entry for `createCapacitorPlatformGateway({ providers })`: the
|
|
75
|
-
* gateway serves the six `commerce.v2.*` bridge methods
|
|
76
|
-
* `gateway.
|
|
77
|
-
*
|
|
78
|
-
* subscriptions in this release.
|
|
71
|
+
* gateway serves the six `commerce.v2.*` bridge methods, exposes the provider
|
|
72
|
+
* as `gateway.commerce`, and reports its capability block (Play sells no
|
|
73
|
+
* subscriptions in this release).
|
|
79
74
|
*/
|
|
80
75
|
export function createCapacitorPlayBillingServiceProvider(input) {
|
|
81
76
|
const { id, ...providerInput } = input;
|
|
@@ -84,7 +79,6 @@ export function createCapacitorPlayBillingServiceProvider(input) {
|
|
|
84
79
|
...createCommerceServiceProvider({
|
|
85
80
|
id: id ?? playBillingCommerceServiceProviderId,
|
|
86
81
|
commerce,
|
|
87
|
-
features: ['nativeIap'],
|
|
88
82
|
}),
|
|
89
83
|
commerce,
|
|
90
84
|
};
|
package/dist/definitions.d.ts
CHANGED
|
@@ -17,26 +17,18 @@ export interface PlayProduct {
|
|
|
17
17
|
/**
|
|
18
18
|
* One Play purchase as the plugin encodes `com.android.billingclient.api.Purchase`.
|
|
19
19
|
* `products`, `purchaseState`, `purchaseTime`, `quantity`, `isAcknowledged`, and
|
|
20
|
-
* `obfuscatedAccountId` follow the Billing Library 9.1.0 getters
|
|
21
|
-
* `state`, and `purchaseTimeMillis` are the 0.1 names and carry the same values
|
|
22
|
-
* until the next major version.
|
|
20
|
+
* `obfuscatedAccountId` follow the Billing Library 9.1.0 getters.
|
|
23
21
|
*/
|
|
24
22
|
export interface PlayPurchase {
|
|
25
23
|
/** `Purchase.getProducts()`: the product ids this purchase covers (one for a single-line purchase). */
|
|
26
24
|
readonly products: readonly string[];
|
|
27
|
-
/** @deprecated Same value as `products`; removed in the next major version. */
|
|
28
|
-
readonly productIds: readonly string[];
|
|
29
25
|
readonly purchaseToken: string;
|
|
30
26
|
/** `Purchase.getOrderId()`; absent while Play has not assigned one (for example a pending purchase). */
|
|
31
27
|
readonly orderId?: string;
|
|
32
28
|
/** `Purchase.getPurchaseTime()` in milliseconds since the epoch. */
|
|
33
29
|
readonly purchaseTime: number;
|
|
34
|
-
/** @deprecated Same value as `purchaseTime` when it is positive; removed in the next major version. */
|
|
35
|
-
readonly purchaseTimeMillis?: number;
|
|
36
30
|
/** `Purchase.getPurchaseState()`; the plugin never reports `UNSPECIFIED_STATE`. */
|
|
37
31
|
readonly purchaseState: PlayPurchaseState;
|
|
38
|
-
/** @deprecated Same value as `purchaseState`; removed in the next major version. */
|
|
39
|
-
readonly state: PlayPurchaseState;
|
|
40
32
|
/**
|
|
41
33
|
* `Purchase.isAcknowledged()`. The plugin never acknowledges or consumes; the
|
|
42
34
|
* backend finalizer does after the ledger grant, so `false` on a purchased
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/store-sdk.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { PlatformProviderAvailability } from '@mpgd/platform';
|
|
2
|
-
import { type StoreSdk, type
|
|
2
|
+
import { type StoreSdk, type StoreSdkError, type StoreSdkErrorCodeMap, type StoreSdkPurchase } from '@mpgd/adapter-capacitor';
|
|
3
3
|
import type { CapacitorPlayBillingPlugin, PlayPurchase } from './definitions.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
/**
|
|
7
|
-
* What the Play Billing plugin supports (RFC "Capabilities" profile row for
|
|
8
|
-
* `google-play`): one-time consumables and durables, no subscriptions, no
|
|
9
|
-
* multi-quantity, pending purchases, no restore call, one-time offers, and a
|
|
10
|
-
* server-owned finish (the backend acknowledges or consumes after the grant).
|
|
11
|
-
*/
|
|
12
|
-
export declare const playBillingStoreSdkCapabilities: StoreSdkCapabilities;
|
|
4
|
+
import { googlePlayStoreId, playBillingStoreSdkCapabilities } from './commerce-profile.js';
|
|
5
|
+
export { googlePlayStoreId, playBillingStoreSdkCapabilities };
|
|
13
6
|
/**
|
|
14
7
|
* `defaultStoreSdkErrorCodeMap` plus every `PLAY_BILLING_*` code the plugin or
|
|
15
8
|
* this SDK emits that the default map does not already normalize.
|
|
@@ -71,19 +64,18 @@ export declare class PlayBillingStoreSdkError extends Error implements StoreSdkE
|
|
|
71
64
|
* provider normalizes (`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`, and
|
|
72
65
|
* `PLAY_BILLING_EMPTY_PURCHASE` become `OUTCOME_UNKNOWN` and stay recoverable).
|
|
73
66
|
* `transactionId` is the order id or, until Play assigns one, the token key
|
|
74
|
-
* `play-token-sha256:<hex>`
|
|
67
|
+
* `play-token-sha256:<hex>` existing ledgers already use. Nothing here grants, and
|
|
75
68
|
* nothing here acknowledges or consumes: the backend finalizer owns both.
|
|
76
69
|
*/
|
|
77
70
|
export declare function createCapacitorPlayBillingStoreSdk(input: CreateCapacitorPlayBillingStoreSdkInput): CapacitorPlayBillingStoreSdk;
|
|
78
|
-
/** Validates a purchase token
|
|
71
|
+
/** Validates a Play purchase token: non-empty, at most 4096 characters, no padding or control characters. */
|
|
79
72
|
export declare function isValidPlayPurchaseToken(value: unknown): value is string;
|
|
80
73
|
/**
|
|
81
74
|
* `StoreSdkPurchase` of one plugin purchase: `token` is the purchase token,
|
|
82
75
|
* `transactionId` is the order id or the token key when Play has not assigned
|
|
83
|
-
* one yet (the
|
|
76
|
+
* one yet (so a purchase deduplicates in the ledger before and after Play assigns an order), `purchasedAt`
|
|
84
77
|
* is `purchaseTime` as ISO 8601 when it is a valid instant, and the account
|
|
85
78
|
* binding is the obfuscated account id the purchase flow recorded. Throws
|
|
86
79
|
* `PLAY_BILLING_EVIDENCE_INVALID` for a shape the plugin never produces.
|
|
87
80
|
*/
|
|
88
81
|
export declare function toPlayBillingSdkPurchase(purchase: PlayPurchase): Promise<StoreSdkPurchase>;
|
|
89
|
-
export {};
|
package/dist/store-sdk.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
import { Capacitor } from '@capacitor/core';
|
|
2
2
|
import { defaultStoreSdkErrorCodeMap, } from '@mpgd/adapter-capacitor';
|
|
3
3
|
import { createGooglePlayTokenPurchaseKey } from '@mpgd/platform/commerce';
|
|
4
|
+
import { googlePlayStoreId, playBillingStoreSdkCapabilities } from './commerce-profile.js';
|
|
4
5
|
import { CapacitorPlayBilling } from './plugin.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* What the Play Billing plugin supports (RFC "Capabilities" profile row for
|
|
9
|
-
* `google-play`): one-time consumables and durables, no subscriptions, no
|
|
10
|
-
* multi-quantity, pending purchases, no restore call, one-time offers, and a
|
|
11
|
-
* server-owned finish (the backend acknowledges or consumes after the grant).
|
|
12
|
-
*/
|
|
13
|
-
export const playBillingStoreSdkCapabilities = Object.freeze({
|
|
14
|
-
purchase: Object.freeze({
|
|
15
|
-
consumable: true,
|
|
16
|
-
durable: true,
|
|
17
|
-
subscription: false,
|
|
18
|
-
quantity: false,
|
|
19
|
-
}),
|
|
20
|
-
offers: true,
|
|
21
|
-
pending: true,
|
|
22
|
-
restore: false,
|
|
23
|
-
finish: 'server',
|
|
24
|
-
});
|
|
6
|
+
// The profile constants live in the SDK-free `commerce-profile.ts` so a tools
|
|
7
|
+
// project can read them without loading Capacitor; they stay exported here.
|
|
8
|
+
export { googlePlayStoreId, playBillingStoreSdkCapabilities };
|
|
25
9
|
/**
|
|
26
10
|
* `defaultStoreSdkErrorCodeMap` plus every `PLAY_BILLING_*` code the plugin or
|
|
27
11
|
* this SDK emits that the default map does not already normalize.
|
|
@@ -84,7 +68,7 @@ export class PlayBillingStoreSdkError extends Error {
|
|
|
84
68
|
* provider normalizes (`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`, and
|
|
85
69
|
* `PLAY_BILLING_EMPTY_PURCHASE` become `OUTCOME_UNKNOWN` and stay recoverable).
|
|
86
70
|
* `transactionId` is the order id or, until Play assigns one, the token key
|
|
87
|
-
* `play-token-sha256:<hex>`
|
|
71
|
+
* `play-token-sha256:<hex>` existing ledgers already use. Nothing here grants, and
|
|
88
72
|
* nothing here acknowledges or consumes: the backend finalizer owns both.
|
|
89
73
|
*/
|
|
90
74
|
export function createCapacitorPlayBillingStoreSdk(input) {
|
|
@@ -416,7 +400,7 @@ export function createCapacitorPlayBillingStoreSdk(input) {
|
|
|
416
400
|
};
|
|
417
401
|
return sdk;
|
|
418
402
|
}
|
|
419
|
-
/** Validates a purchase token
|
|
403
|
+
/** Validates a Play purchase token: non-empty, at most 4096 characters, no padding or control characters. */
|
|
420
404
|
export function isValidPlayPurchaseToken(value) {
|
|
421
405
|
return typeof value === 'string'
|
|
422
406
|
&& value.length > 0
|
|
@@ -427,7 +411,7 @@ export function isValidPlayPurchaseToken(value) {
|
|
|
427
411
|
/**
|
|
428
412
|
* `StoreSdkPurchase` of one plugin purchase: `token` is the purchase token,
|
|
429
413
|
* `transactionId` is the order id or the token key when Play has not assigned
|
|
430
|
-
* one yet (the
|
|
414
|
+
* one yet (so a purchase deduplicates in the ledger before and after Play assigns an order), `purchasedAt`
|
|
431
415
|
* is `purchaseTime` as ISO 8601 when it is a valid instant, and the account
|
|
432
416
|
* binding is the obfuscated account id the purchase flow recorded. Throws
|
|
433
417
|
* `PLAY_BILLING_EVIDENCE_INVALID` for a shape the plugin never produces.
|
|
@@ -446,7 +430,7 @@ function convertPurchase(purchase) {
|
|
|
446
430
|
if (!isRecord(purchase)) {
|
|
447
431
|
throw evidenceInvalid('Play purchase is not an object.');
|
|
448
432
|
}
|
|
449
|
-
const state = purchase
|
|
433
|
+
const state = purchaseStateOf(purchase);
|
|
450
434
|
if (state !== 'pending' && state !== 'purchased') {
|
|
451
435
|
throw evidenceInvalid('Play purchase state is invalid.');
|
|
452
436
|
}
|
|
@@ -502,11 +486,22 @@ function skuOf(purchase) {
|
|
|
502
486
|
const sku = Array.isArray(products) && products.length === 1 ? products[0] : undefined;
|
|
503
487
|
return typeof sku === 'string' && productIdPattern.test(sku) ? sku : undefined;
|
|
504
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* The purchase state under its Billing 9.1.0 name, or under the 0.1 name
|
|
491
|
+
* (`state`). The plugin's own native code sends only the current names; the
|
|
492
|
+
* 0.1 names (`state`, `productIds`, `purchaseTimeMillis`) are still read so a
|
|
493
|
+
* web bundle shipped ahead of its native binary, for example by a live
|
|
494
|
+
* update onto a 0.1 build, keeps every paid purchase readable and
|
|
495
|
+
* recoverable instead of rejecting it as invalid evidence.
|
|
496
|
+
*/
|
|
497
|
+
function purchaseStateOf(purchase) {
|
|
498
|
+
return purchase.purchaseState ?? purchase.state;
|
|
499
|
+
}
|
|
505
500
|
function isUnfinished(purchase, includePending) {
|
|
506
|
-
const state = purchase
|
|
501
|
+
const state = purchaseStateOf(purchase);
|
|
507
502
|
if (state === 'purchased') {
|
|
508
503
|
// A 0.1 native build reports no flag; an absent flag is unfinished so a
|
|
509
|
-
// paid purchase is never dropped from recovery
|
|
504
|
+
// paid purchase is never dropped from recovery.
|
|
510
505
|
return purchase.isAcknowledged !== true;
|
|
511
506
|
}
|
|
512
507
|
return state === 'pending' && includePending;
|
|
@@ -519,7 +514,7 @@ function assertOutcomePurchase(outcome, expectedSku, expectedState) {
|
|
|
519
514
|
if (skuOf(purchase) !== expectedSku) {
|
|
520
515
|
throw evidenceInvalid('Play purchase did not match the requested product.');
|
|
521
516
|
}
|
|
522
|
-
if ((purchase
|
|
517
|
+
if (purchaseStateOf(purchase) !== expectedState) {
|
|
523
518
|
throw evidenceInvalid('Play purchase state is inconsistent.');
|
|
524
519
|
}
|
|
525
520
|
return purchase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/capacitor-play-billing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Opt-in Android Google Play Billing purchase collector for mpgd Capacitor games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,20 +38,20 @@
|
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@capacitor/core": "^8.5.1",
|
|
41
|
-
"@mpgd/adapter-capacitor": "^0.
|
|
42
|
-
"@mpgd/bridge": "^0.
|
|
43
|
-
"@mpgd/platform": "^0.
|
|
41
|
+
"@mpgd/adapter-capacitor": "^0.10.1",
|
|
42
|
+
"@mpgd/bridge": "^0.13.1",
|
|
43
|
+
"@mpgd/platform": "^0.23.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@capacitor/core": "8.5.2",
|
|
47
47
|
"@types/node": "^24.0.0",
|
|
48
|
-
"ttsc": "0.
|
|
48
|
+
"ttsc": "0.32.1",
|
|
49
49
|
"typescript": "7.0.2",
|
|
50
50
|
"vitest": "^4.0.0",
|
|
51
|
-
"@mpgd/adapter-capacitor": "0.
|
|
52
|
-
"@mpgd/bridge": "0.
|
|
53
|
-
"@mpgd/
|
|
54
|
-
"@mpgd/
|
|
51
|
+
"@mpgd/adapter-capacitor": "0.10.1",
|
|
52
|
+
"@mpgd/bridge": "0.13.1",
|
|
53
|
+
"@mpgd/game-services": "0.24.1",
|
|
54
|
+
"@mpgd/platform": "0.23.1"
|
|
55
55
|
},
|
|
56
56
|
"main": "./dist/index.js",
|
|
57
57
|
"types": "./dist/index.d.ts",
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
"scripts": {
|
|
62
62
|
"check": "ttsc --noEmit",
|
|
63
63
|
"lint": "ttsc --noEmit",
|
|
64
|
-
"test": "vitest run
|
|
64
|
+
"test": "vitest run"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/dist/provider.d.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import type { BridgeRequest, BridgeResponse } from '@mpgd/bridge';
|
|
2
|
-
import type { PurchaseResult } from '@mpgd/platform';
|
|
3
|
-
import type { CapacitorPlayBillingPlugin } from './definitions.js';
|
|
4
|
-
export interface PlayBillingProductMapping {
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly type: 'consumable' | 'non_consumable';
|
|
7
|
-
readonly storeId: string;
|
|
8
|
-
/** Required when Play lists multiple eligible purchase options or offers. */
|
|
9
|
-
readonly offerToken?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface PlayBillingProviderOptions {
|
|
12
|
-
readonly products: readonly PlayBillingProductMapping[];
|
|
13
|
-
/** Must agree with the server's resolveObfuscatedAccountId mapping. */
|
|
14
|
-
readonly getObfuscatedAccountId: () => Promise<string> | string;
|
|
15
|
-
readonly sdk?: Pick<CapacitorPlayBillingPlugin, 'getProducts' | 'purchase' | 'getPurchases'>;
|
|
16
|
-
readonly isAndroid?: () => boolean;
|
|
17
|
-
}
|
|
18
|
-
export interface OwnedPlayPurchase {
|
|
19
|
-
readonly productId: string;
|
|
20
|
-
readonly result: PurchaseResult;
|
|
21
|
-
readonly purchasedAt?: string;
|
|
22
|
-
}
|
|
23
|
-
export type PlayBillingAvailability = 'available' | 'configuration-required' | 'temporarily-unavailable' | 'unsupported';
|
|
24
|
-
export interface CapacitorPlayBillingProvider {
|
|
25
|
-
readonly id: 'google-play-billing';
|
|
26
|
-
readonly features: readonly ['nativeIap'];
|
|
27
|
-
readonly methods: readonly [
|
|
28
|
-
'commerce.getProducts',
|
|
29
|
-
'commerce.purchase',
|
|
30
|
-
'commerce.restore',
|
|
31
|
-
'commerce.getEntitlements'
|
|
32
|
-
];
|
|
33
|
-
readonly bridge: {
|
|
34
|
-
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
35
|
-
};
|
|
36
|
-
getAvailability(): Promise<Readonly<{
|
|
37
|
-
nativeIap: PlayBillingAvailability;
|
|
38
|
-
}>>;
|
|
39
|
-
/** Requery owned, non-consumed transactions for game-owned backend recovery. */
|
|
40
|
-
getOwnedPurchases(): Promise<readonly OwnedPlayPurchase[]>;
|
|
41
|
-
}
|
|
42
|
-
export interface RecoveredPlayPurchase {
|
|
43
|
-
readonly productId: string;
|
|
44
|
-
readonly status: 'granted' | 'pending' | 'rejected';
|
|
45
|
-
readonly verification?: PlayPurchaseVerification;
|
|
46
|
-
}
|
|
47
|
-
export interface PlayPurchaseVerification {
|
|
48
|
-
readonly verified: boolean;
|
|
49
|
-
readonly disposition?: 'pending' | 'rejected';
|
|
50
|
-
readonly alreadyProcessed: boolean;
|
|
51
|
-
readonly ledgerEntryId?: string;
|
|
52
|
-
readonly reason?: string;
|
|
53
|
-
}
|
|
54
|
-
export interface PlayPurchaseRecoveryBackend {
|
|
55
|
-
verifyPurchase(input: {
|
|
56
|
-
readonly target: 'android';
|
|
57
|
-
readonly deploymentTarget?: string;
|
|
58
|
-
readonly playerId: string;
|
|
59
|
-
readonly productId: string;
|
|
60
|
-
readonly platformTransactionId: string;
|
|
61
|
-
readonly idempotencyKey: string;
|
|
62
|
-
readonly purchasedAt: string;
|
|
63
|
-
readonly evidence: NonNullable<PurchaseResult['evidence']>;
|
|
64
|
-
}): Promise<PlayPurchaseVerification>;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Resubmit owned purchases after a callback was missed or the app restarted.
|
|
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.
|
|
75
|
-
*/
|
|
76
|
-
export declare function recoverOwnedPlayPurchases(input: {
|
|
77
|
-
readonly provider: CapacitorPlayBillingProvider;
|
|
78
|
-
readonly backend: PlayPurchaseRecoveryBackend;
|
|
79
|
-
readonly playerId: string;
|
|
80
|
-
readonly deploymentTarget?: string;
|
|
81
|
-
readonly now?: () => string;
|
|
82
|
-
}): Promise<readonly RecoveredPlayPurchase[]>;
|
|
83
|
-
export declare function createCapacitorPlayBillingProvider(options: PlayBillingProviderOptions): CapacitorPlayBillingProvider;
|
|
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>;
|
package/dist/provider.js
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { Capacitor } from '@capacitor/core';
|
|
2
|
-
import { createGooglePlayTokenPurchaseKey } from '@mpgd/platform/commerce';
|
|
3
|
-
import { CapacitorPlayBilling } from './plugin.js';
|
|
4
|
-
const googlePlayProductPurchaseEvidenceSchema = 'google-play.product-purchase.v2';
|
|
5
|
-
class PlayPurchaseIntegrityError extends Error {
|
|
6
|
-
}
|
|
7
|
-
function isValidPurchaseToken(value) {
|
|
8
|
-
return typeof value === 'string'
|
|
9
|
-
&& value.length > 0
|
|
10
|
-
&& value.length <= 4096
|
|
11
|
-
&& value.trim() === value
|
|
12
|
-
&& !/[\u0000-\u001f\u007f]/u.test(value);
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Resubmit owned purchases after a callback was missed or the app restarted.
|
|
16
|
-
* A native purchase is never credited locally; only the backend response grants it.
|
|
17
|
-
*
|
|
18
|
-
* @deprecated v1 recovery driver. Commerce v2 games register
|
|
19
|
-
* `createCapacitorPlayBillingServiceProvider` and let the single reconciler
|
|
20
|
-
* in `@mpgd/game-services` (PR C1) resolve unfinished purchases from
|
|
21
|
-
* `getAvailablePurchases` and `purchaseUpdated`; this driver is removed with
|
|
22
|
-
* the v1 adapter in the next major version.
|
|
23
|
-
*/
|
|
24
|
-
export async function recoverOwnedPlayPurchases(input) {
|
|
25
|
-
if (input.playerId.trim() === '') {
|
|
26
|
-
throw new TypeError('Recovery requires an authenticated player ID.');
|
|
27
|
-
}
|
|
28
|
-
const owned = await input.provider.getOwnedPurchases();
|
|
29
|
-
return Promise.all(owned.map(async ({ productId, result, purchasedAt }) => {
|
|
30
|
-
if (result.status === 'pending') {
|
|
31
|
-
return { productId, status: 'pending' };
|
|
32
|
-
}
|
|
33
|
-
if (result.status !== 'completed' || result.transactionId === undefined
|
|
34
|
-
|| result.evidence === undefined) {
|
|
35
|
-
return { productId, status: 'rejected' };
|
|
36
|
-
}
|
|
37
|
-
const token = result.evidence.payload.purchaseToken;
|
|
38
|
-
if (typeof token !== 'string') {
|
|
39
|
-
return { productId, status: 'rejected' };
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
const verification = await input.backend.verifyPurchase({
|
|
43
|
-
target: 'android',
|
|
44
|
-
...(input.deploymentTarget === undefined ? {} : { deploymentTarget: input.deploymentTarget }),
|
|
45
|
-
playerId: input.playerId,
|
|
46
|
-
productId,
|
|
47
|
-
platformTransactionId: result.transactionId,
|
|
48
|
-
idempotencyKey: await createGooglePlayTokenPurchaseKey(token),
|
|
49
|
-
purchasedAt: purchasedAt ?? input.now?.() ?? new Date().toISOString(),
|
|
50
|
-
evidence: result.evidence,
|
|
51
|
-
});
|
|
52
|
-
return {
|
|
53
|
-
productId,
|
|
54
|
-
status: verification.verified ? 'granted' : (verification.disposition ?? 'pending'),
|
|
55
|
-
verification,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// Network or backend uncertainty must remain retryable, never rejected.
|
|
60
|
-
return { productId, status: 'pending' };
|
|
61
|
-
}
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
export function createCapacitorPlayBillingProvider(options) {
|
|
65
|
-
const sdk = options.sdk ?? CapacitorPlayBilling;
|
|
66
|
-
const isAndroid = options.isAndroid ?? (() => Capacitor.getPlatform() === 'android');
|
|
67
|
-
const byLogicalId = new Map();
|
|
68
|
-
const byStoreId = new Map();
|
|
69
|
-
for (const product of options.products) {
|
|
70
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(product.id)
|
|
71
|
-
|| !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(product.storeId)
|
|
72
|
-
|| byLogicalId.has(product.id)
|
|
73
|
-
|| byStoreId.has(product.storeId)) {
|
|
74
|
-
throw new TypeError('Play Billing product mappings must be unique and valid.');
|
|
75
|
-
}
|
|
76
|
-
byLogicalId.set(product.id, product);
|
|
77
|
-
byStoreId.set(product.storeId, product);
|
|
78
|
-
}
|
|
79
|
-
if (byLogicalId.size === 0) {
|
|
80
|
-
throw new TypeError('Play Billing requires at least one one-time product.');
|
|
81
|
-
}
|
|
82
|
-
if (byLogicalId.size > 100) {
|
|
83
|
-
throw new TypeError('Play Billing supports at most 100 products per provider.');
|
|
84
|
-
}
|
|
85
|
-
async function getOwnedPurchases() {
|
|
86
|
-
if (!isAndroid()) {
|
|
87
|
-
throw new Error('Play Billing is only available on Android.');
|
|
88
|
-
}
|
|
89
|
-
const response = await sdk.getPurchases();
|
|
90
|
-
const owned = [];
|
|
91
|
-
for (const purchase of response.purchases) {
|
|
92
|
-
if (purchase.productIds.length !== 1) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const mapping = byStoreId.get(purchase.productIds[0] ?? '');
|
|
96
|
-
if (mapping !== undefined) {
|
|
97
|
-
const timestamp = purchase.purchaseTimeMillis;
|
|
98
|
-
const purchasedAt = typeof timestamp === 'number'
|
|
99
|
-
&& Number.isSafeInteger(timestamp)
|
|
100
|
-
&& timestamp > 0
|
|
101
|
-
&& timestamp <= 8_640_000_000_000_000
|
|
102
|
-
? new Date(timestamp).toISOString()
|
|
103
|
-
: undefined;
|
|
104
|
-
owned.push({
|
|
105
|
-
productId: mapping.id,
|
|
106
|
-
result: await convertPurchase(purchase),
|
|
107
|
-
...(purchasedAt === undefined ? {} : { purchasedAt }),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return owned;
|
|
112
|
-
}
|
|
113
|
-
function selectOffer(mapping, found) {
|
|
114
|
-
if (mapping.offerToken === undefined) {
|
|
115
|
-
return found.offers.length === 1 ? found.offers[0] : undefined;
|
|
116
|
-
}
|
|
117
|
-
return found.offers.find((item) => item.offerToken === mapping.offerToken);
|
|
118
|
-
}
|
|
119
|
-
async function request(input) {
|
|
120
|
-
try {
|
|
121
|
-
if (!isAndroid()) {
|
|
122
|
-
return failure(input.id, 'PLAY_BILLING_ANDROID_REQUIRED');
|
|
123
|
-
}
|
|
124
|
-
switch (input.method) {
|
|
125
|
-
case 'commerce.getProducts': {
|
|
126
|
-
const response = await sdk.getProducts({
|
|
127
|
-
productIds: [...byStoreId.keys()],
|
|
128
|
-
});
|
|
129
|
-
const products = [];
|
|
130
|
-
for (const found of response.products) {
|
|
131
|
-
const mapping = byStoreId.get(found.productId);
|
|
132
|
-
if (mapping === undefined) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const offer = selectOffer(mapping, found);
|
|
136
|
-
if (offer === undefined) {
|
|
137
|
-
return failure(input.id, 'PLAY_BILLING_OFFER_REQUIRED');
|
|
138
|
-
}
|
|
139
|
-
products.push({
|
|
140
|
-
id: mapping.id,
|
|
141
|
-
type: mapping.type,
|
|
142
|
-
title: found.title,
|
|
143
|
-
description: found.description,
|
|
144
|
-
price: {
|
|
145
|
-
formatted: offer.formattedPrice,
|
|
146
|
-
currencyCode: offer.currencyCode,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
return success(input.id, products);
|
|
151
|
-
}
|
|
152
|
-
case 'commerce.purchase': {
|
|
153
|
-
const payload = input.payload;
|
|
154
|
-
const productId = isRecord(payload) ? payload.productId : undefined;
|
|
155
|
-
const mapping = typeof productId === 'string' ? byLogicalId.get(productId) : undefined;
|
|
156
|
-
if (mapping === undefined) {
|
|
157
|
-
return failure(input.id, 'PLAY_BILLING_PRODUCT_UNKNOWN');
|
|
158
|
-
}
|
|
159
|
-
let obfuscatedAccountId;
|
|
160
|
-
try {
|
|
161
|
-
obfuscatedAccountId = await options.getObfuscatedAccountId();
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return failure(input.id, 'PLAY_BILLING_ACCOUNT_LOOKUP_FAILED');
|
|
165
|
-
}
|
|
166
|
-
if (!/^[A-Za-z0-9._-]{1,64}$/u.test(obfuscatedAccountId)) {
|
|
167
|
-
return failure(input.id, 'PLAY_BILLING_ACCOUNT_REQUIRED');
|
|
168
|
-
}
|
|
169
|
-
const outcome = await sdk.purchase({
|
|
170
|
-
productId: mapping.storeId,
|
|
171
|
-
obfuscatedAccountId,
|
|
172
|
-
...(mapping.offerToken === undefined ? {} : { offerToken: mapping.offerToken }),
|
|
173
|
-
});
|
|
174
|
-
return success(input.id, await convertOutcome(outcome, mapping.storeId));
|
|
175
|
-
}
|
|
176
|
-
case 'commerce.restore':
|
|
177
|
-
// Native ownership is not an authoritative entitlement. The game
|
|
178
|
-
// must submit getOwnedPurchases() results to its backend separately.
|
|
179
|
-
return success(input.id, { restoredEntitlements: [] });
|
|
180
|
-
case 'commerce.getEntitlements':
|
|
181
|
-
return success(input.id, []);
|
|
182
|
-
default:
|
|
183
|
-
return failure(input.id, 'PLAY_BILLING_METHOD_UNSUPPORTED');
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
if (error instanceof PlayPurchaseIntegrityError) {
|
|
188
|
-
return failure(input.id, 'PLAY_BILLING_EVIDENCE_INVALID');
|
|
189
|
-
}
|
|
190
|
-
const code = isRecord(error) ? error.code : undefined;
|
|
191
|
-
if (typeof code === 'string' && /^PLAY_BILLING_[A-Z_]+$/u.test(code)) {
|
|
192
|
-
if (input.method === 'commerce.purchase'
|
|
193
|
-
&& (code === 'PLAY_BILLING_TIMEOUT'
|
|
194
|
-
|| code === 'PLAY_BILLING_INTERRUPTED'
|
|
195
|
-
|| code === 'PLAY_BILLING_EMPTY_PURCHASE'
|
|
196
|
-
|| code === 'PLAY_BILLING_ALREADY_OWNED')) {
|
|
197
|
-
// The purchase may have completed without an observed callback.
|
|
198
|
-
// Requery owned purchases; never launch another flow blindly.
|
|
199
|
-
return success(input.id, { status: 'pending', entitlementIds: [] });
|
|
200
|
-
}
|
|
201
|
-
const retryable = code === 'PLAY_BILLING_DISCONNECTED'
|
|
202
|
-
|| code === 'PLAY_BILLING_SERVICE_UNAVAILABLE'
|
|
203
|
-
|| code === 'PLAY_BILLING_NETWORK_ERROR'
|
|
204
|
-
|| code === 'PLAY_BILLING_TRANSIENT_ERROR';
|
|
205
|
-
return failure(input.id, code, retryable);
|
|
206
|
-
}
|
|
207
|
-
return failure(input.id, 'PLAY_BILLING_UNAVAILABLE', true);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
id: 'google-play-billing',
|
|
212
|
-
features: ['nativeIap'],
|
|
213
|
-
methods: [
|
|
214
|
-
'commerce.getProducts',
|
|
215
|
-
'commerce.purchase',
|
|
216
|
-
'commerce.restore',
|
|
217
|
-
'commerce.getEntitlements',
|
|
218
|
-
],
|
|
219
|
-
bridge: { request },
|
|
220
|
-
async getAvailability() {
|
|
221
|
-
if (!isAndroid()) {
|
|
222
|
-
return { nativeIap: 'unsupported' };
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
const response = await sdk.getProducts({ productIds: [...byStoreId.keys()] });
|
|
226
|
-
const hasPurchasableProduct = response.products.some((found) => {
|
|
227
|
-
const mapping = byStoreId.get(found.productId);
|
|
228
|
-
return mapping !== undefined && selectOffer(mapping, found) !== undefined;
|
|
229
|
-
});
|
|
230
|
-
return { nativeIap: hasPurchasableProduct ? 'available' : 'configuration-required' };
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
const code = isRecord(error) ? error.code : undefined;
|
|
234
|
-
const configurationError = code === 'PLAY_BILLING_CONFIGURATION_ERROR'
|
|
235
|
-
|| code === 'PLAY_BILLING_PRODUCT_UNAVAILABLE'
|
|
236
|
-
|| code === 'PLAY_BILLING_OFFER_REQUIRED'
|
|
237
|
-
|| code === 'PLAY_BILLING_INVALID_PRODUCTS'
|
|
238
|
-
|| code === 'PLAY_BILLING_INVALID_PRODUCT';
|
|
239
|
-
return { nativeIap: configurationError
|
|
240
|
-
? 'configuration-required'
|
|
241
|
-
: 'temporarily-unavailable' };
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
getOwnedPurchases,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
async function convertOutcome(outcome, expectedStoreId) {
|
|
248
|
-
if (outcome.status === 'cancelled') {
|
|
249
|
-
return { status: 'cancelled', entitlementIds: [] };
|
|
250
|
-
}
|
|
251
|
-
const purchase = outcome.purchase;
|
|
252
|
-
if (purchase === undefined
|
|
253
|
-
|| purchase.productIds.length !== 1
|
|
254
|
-
|| purchase.productIds[0] !== expectedStoreId) {
|
|
255
|
-
throw new PlayPurchaseIntegrityError('Play purchase did not match the requested product.');
|
|
256
|
-
}
|
|
257
|
-
if ((outcome.status === 'purchased' && purchase.state !== 'purchased')
|
|
258
|
-
|| (outcome.status === 'pending' && purchase.state !== 'pending')) {
|
|
259
|
-
throw new PlayPurchaseIntegrityError('Play purchase state is inconsistent.');
|
|
260
|
-
}
|
|
261
|
-
return convertPurchase(purchase);
|
|
262
|
-
}
|
|
263
|
-
async function convertPurchase(purchase) {
|
|
264
|
-
if (purchase.state !== 'pending' && purchase.state !== 'purchased') {
|
|
265
|
-
throw new PlayPurchaseIntegrityError('Play purchase state is invalid.');
|
|
266
|
-
}
|
|
267
|
-
if (!isValidPurchaseToken(purchase.purchaseToken)) {
|
|
268
|
-
throw new PlayPurchaseIntegrityError('Play purchase token is invalid.');
|
|
269
|
-
}
|
|
270
|
-
const evidence = {
|
|
271
|
-
schema: googlePlayProductPurchaseEvidenceSchema,
|
|
272
|
-
payload: { purchaseToken: purchase.purchaseToken },
|
|
273
|
-
};
|
|
274
|
-
if (purchase.state === 'pending') {
|
|
275
|
-
return { status: 'pending', entitlementIds: [], evidence };
|
|
276
|
-
}
|
|
277
|
-
const transactionId = purchase.orderId?.trim()
|
|
278
|
-
|| await createGooglePlayTokenPurchaseKey(purchase.purchaseToken);
|
|
279
|
-
return {
|
|
280
|
-
status: 'completed',
|
|
281
|
-
transactionId,
|
|
282
|
-
entitlementIds: [],
|
|
283
|
-
evidence,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* The server's token transaction ID, computed by the one shared helper so the
|
|
288
|
-
* plugin and `@mpgd/game-services` can never drift.
|
|
289
|
-
*
|
|
290
|
-
* @deprecated Use `createGooglePlayTokenPurchaseKey` from `@mpgd/platform/commerce`;
|
|
291
|
-
* this alias is the same function (same key, same `TypeError` for an invalid
|
|
292
|
-
* token) and will be removed in the next major version.
|
|
293
|
-
*/
|
|
294
|
-
export const createGooglePlayTokenTransactionId = createGooglePlayTokenPurchaseKey;
|
|
295
|
-
function success(id, data) {
|
|
296
|
-
return { id, ok: true, data };
|
|
297
|
-
}
|
|
298
|
-
function failure(id, code, retryable = false) {
|
|
299
|
-
return { id, ok: false, error: { code, message: code, retryable } };
|
|
300
|
-
}
|
|
301
|
-
function isRecord(value) {
|
|
302
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
303
|
-
}
|