@mpgd/capacitor-play-billing 0.4.5 → 0.5.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 CHANGED
@@ -105,13 +105,21 @@ recoverable, so the provider answers `launched` and the reconciler resolves
105
105
  the attempt from `getAvailablePurchases` without reopening the store UI.
106
106
 
107
107
  `createCapacitorPlayBillingCommerceProvider({ products, accountBinding,
108
- plugin?, isAndroid?, store?, mapError? })` composes the provider: evidence
108
+ purchaseGate?, plugin?, isAndroid?, store?, mapError? })` composes the provider: evidence
109
109
  schema `google-play.product-purchase.v2` with payload `{ purchaseToken }`,
110
110
  which `createGooglePlayProductPurchaseBoundary` verifies; authority
111
111
  `{ evidence: 'client-token', accountBinding: 'required' }`; no
112
112
  `finishTransaction` and no `restorePurchases`. A thrown `accountBinding`
113
113
  lookup is `ACCOUNT_BINDING_REQUIRED` with `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED`
114
- kept as the store code. `store` defaults to `google-play`; only a conformance
114
+ kept as the store code (a thrown `CommerceError` keeps its own code), answered
115
+ as a `failed` result before the sheet opens. The optional `purchaseGate` is
116
+ the sales switch: it is asked before a new purchase opens the Play sheet,
117
+ never during recovery, and a refusal is `failed` with `ITEM_UNAVAILABLE` and
118
+ `PURCHASES_PAUSED` (see the adapter-capacitor README). With an adapter-capacitor from before
119
+ the gate, which would ignore it, the plugin detects the missing gate support
120
+ and answers every new purchase itself with a `failed` result, `ITEM_UNAVAILABLE` and
121
+ `PURCHASE_GATE_UNSUPPORTED` instead of selling; recovery is unaffected.
122
+ `store` defaults to `google-play`; only a conformance
115
123
  harness overrides it, because the derived idempotency key follows the store
116
124
  id. `createPlayBillingCommerceCapabilities(availability)` (from
117
125
  `src/commerce-profile.ts`, which imports types only and no Capacitor code)
@@ -3,6 +3,7 @@ import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
3
3
  import { type StoreId, type StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
4
4
  import { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority } from './commerce-profile.js';
5
5
  import type { CapacitorPlayBillingPlugin, PlayPurchase } from './definitions.js';
6
+ import { type StorePurchaseGate } from './purchase-gate-support.js';
6
7
  export { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority };
7
8
  /** The default registry id of the Play Billing commerce v2 service provider. */
8
9
  export declare const playBillingCommerceServiceProviderId: 'google-play-billing';
@@ -20,10 +21,18 @@ export interface CreateCapacitorPlayBillingCommerceProviderInput {
20
21
  /**
21
22
  * Resolves the player's obfuscated account id (`{ kind:
22
23
  * 'obfuscated-account-id', value }`) before a purchase opens. It must agree
23
- * with the backend's `resolveObfuscatedAccountId`. A thrown lookup becomes
24
- * `ACCOUNT_BINDING_REQUIRED` with `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED` kept.
24
+ * with the backend's `resolveObfuscatedAccountId`. A thrown `CommerceError`
25
+ * keeps its code; any other thrown lookup becomes `ACCOUNT_BINDING_REQUIRED`
26
+ * with `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED` kept. Either is answered as a
27
+ * `failed` purchase result before the store opens.
25
28
  */
26
29
  readonly accountBinding: () => Promise<StorePurchaseAccountBinding | undefined> | StorePurchaseAccountBinding | undefined;
30
+ /**
31
+ * Optional sales switch, asked before a new purchase opens the Play sheet
32
+ * and never during recovery; a refusal is `ITEM_UNAVAILABLE` with
33
+ * `PURCHASES_PAUSED` (see `StorePurchaseGate` in `@mpgd/adapter-capacitor`).
34
+ */
35
+ readonly purchaseGate?: StorePurchaseGate;
27
36
  /** Defaults to the registered `CapacitorPlayBilling` plugin. */
28
37
  readonly plugin?: CapacitorPlayBillingPlugin;
29
38
  /** Defaults to `Capacitor.getPlatform() === 'android'`. */
@@ -1,6 +1,7 @@
1
1
  import { createCommerceServiceProvider, createStoreCommerceProvider, } from '@mpgd/adapter-capacitor';
2
2
  import { CommerceError, } from '@mpgd/platform/commerce';
3
3
  import { googlePlayProductPurchaseEvidenceSchema, playBillingCommerceAuthority, } from './commerce-profile.js';
4
+ import { refuseNewPurchasesUnlessGated, resolvePurchaseGateSupport, } from './purchase-gate-support.js';
4
5
  import { createCapacitorPlayBillingStoreSdk, googlePlayStoreId, playBillingStoreSdkErrorCodeMap, } from './store-sdk.js';
5
6
  // The profile constants live in the SDK-free `commerce-profile.ts` so a tools
6
7
  // project can read them without loading Capacitor; they stay exported here.
@@ -44,7 +45,8 @@ export function createCapacitorPlayBillingCommerceProvider(input) {
44
45
  : { onUnmappedPurchase: input.onUnmappedPurchase }),
45
46
  });
46
47
  const resolveAccountBinding = input.accountBinding;
47
- return createStoreCommerceProvider({
48
+ const gate = resolvePurchaseGateSupport(input.purchaseGate);
49
+ return refuseNewPurchasesUnlessGated(createStoreCommerceProvider({
48
50
  sdk,
49
51
  store: input.store ?? googlePlayStoreId,
50
52
  products: input.products,
@@ -55,6 +57,9 @@ export function createCapacitorPlayBillingCommerceProvider(input) {
55
57
  return await resolveAccountBinding();
56
58
  }
57
59
  catch (error) {
60
+ if (error instanceof CommerceError) {
61
+ throw error;
62
+ }
58
63
  throw new CommerceError('ACCOUNT_BINDING_REQUIRED', {
59
64
  message: 'Play Billing could not resolve the obfuscated account id.',
60
65
  storeCode: 'PLAY_BILLING_ACCOUNT_LOOKUP_FAILED',
@@ -62,9 +67,10 @@ export function createCapacitorPlayBillingCommerceProvider(input) {
62
67
  });
63
68
  }
64
69
  },
70
+ ...(gate.purchaseGate === undefined ? {} : { purchaseGate: gate.purchaseGate }),
65
71
  authority: playBillingCommerceAuthority,
66
72
  mapError: { ...playBillingStoreSdkErrorCodeMap, ...input.mapError },
67
- });
73
+ }), gate);
68
74
  }
69
75
  /**
70
76
  * Registry entry for `createCapacitorPlatformGateway({ providers })`: the
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from './commerce-provider.js';
3
3
  export * from './definitions.js';
4
4
  export * from './plugin.js';
5
5
  export * from './store-sdk.js';
6
+ export { purchaseGateUnsupportedStoreCode, type StorePurchaseGate, type StorePurchaseGateDecision, type StorePurchaseGateRequest, } from './purchase-gate-support.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export * from './commerce-provider.js';
3
3
  export * from './definitions.js';
4
4
  export * from './plugin.js';
5
5
  export * from './store-sdk.js';
6
+ export { purchaseGateUnsupportedStoreCode, } from './purchase-gate-support.js';
@@ -0,0 +1,50 @@
1
+ import { CommerceError, type CommerceLogicalProductId, type CommerceProvider, type PurchaseKind, type PurchaseSource, type StoreId } from '@mpgd/platform/commerce';
2
+ /** What a purchase gate is asked about: one new purchase, before any store UI opens. */
3
+ export interface StorePurchaseGateRequest {
4
+ readonly store: StoreId;
5
+ readonly logicalId: CommerceLogicalProductId;
6
+ readonly storeSku: string;
7
+ readonly kind: PurchaseKind;
8
+ readonly source: PurchaseSource;
9
+ }
10
+ /** A purchase gate's answer; see `StorePurchaseGateDecision` in `@mpgd/adapter-capacitor`. */
11
+ export type StorePurchaseGateDecision = {
12
+ readonly allowed: true;
13
+ } | {
14
+ readonly allowed: false;
15
+ readonly storeCode?: string;
16
+ readonly message?: string;
17
+ readonly retryable?: boolean;
18
+ };
19
+ /** The game's sales switch for new purchases; see `StorePurchaseGate` in `@mpgd/adapter-capacitor`. */
20
+ export type StorePurchaseGate = (request: StorePurchaseGateRequest) => StorePurchaseGateDecision | Promise<StorePurchaseGateDecision>;
21
+ /** `storeCode` of every new purchase refused because the adapter cannot run the gate. */
22
+ export declare const purchaseGateUnsupportedStoreCode: 'PURCHASE_GATE_UNSUPPORTED';
23
+ /**
24
+ * Whether the installed `@mpgd/adapter-capacitor` runs `purchaseGate`. An
25
+ * adapter from before the gate ignores the option and would keep selling, so
26
+ * the probe reads an export the gate release added (`purchasesPausedStoreCode`)
27
+ * through the module namespace, where a missing export is `undefined` rather
28
+ * than a link error.
29
+ */
30
+ export declare function adapterSupportsPurchaseGate(adapterExports?: object): boolean;
31
+ export interface PurchaseGateSupport {
32
+ /** The gate to hand the adapter, when it runs one. */
33
+ readonly purchaseGate?: StorePurchaseGate;
34
+ /**
35
+ * Set when a gate was given but the adapter cannot run it: the plugin then
36
+ * answers every new purchase itself (`refuseNewPurchasesUnlessGated`).
37
+ */
38
+ readonly refuseNewPurchases: boolean;
39
+ }
40
+ /** Fails closed: a gate the adapter would ignore refuses every new purchase instead. */
41
+ export declare function resolvePurchaseGateSupport(purchaseGate: StorePurchaseGate | undefined, supported?: boolean): PurchaseGateSupport;
42
+ export declare function createPurchaseGateUnsupportedError(): CommerceError;
43
+ /**
44
+ * Refuses every new purchase at the plugin boundary when the adapter cannot
45
+ * run the configured gate, before the adapter's `requestPurchase` (which
46
+ * would open the store) is reached. The refusal is a `failed` result, not a
47
+ * throw, so `PURCHASE_GATE_UNSUPPORTED` survives the bridge whatever the
48
+ * adapter version. Recovery, finishing, restore, and events are untouched.
49
+ */
50
+ export declare function refuseNewPurchasesUnlessGated<TProvider extends CommerceProvider>(provider: TProvider, support: PurchaseGateSupport): TProvider;
@@ -0,0 +1,50 @@
1
+ import * as adapterCapacitor from '@mpgd/adapter-capacitor';
2
+ import { CommerceError, } from '@mpgd/platform/commerce';
3
+ /** `storeCode` of every new purchase refused because the adapter cannot run the gate. */
4
+ export const purchaseGateUnsupportedStoreCode = 'PURCHASE_GATE_UNSUPPORTED';
5
+ /**
6
+ * Whether the installed `@mpgd/adapter-capacitor` runs `purchaseGate`. An
7
+ * adapter from before the gate ignores the option and would keep selling, so
8
+ * the probe reads an export the gate release added (`purchasesPausedStoreCode`)
9
+ * through the module namespace, where a missing export is `undefined` rather
10
+ * than a link error.
11
+ */
12
+ export function adapterSupportsPurchaseGate(adapterExports = adapterCapacitor) {
13
+ return typeof adapterExports.purchasesPausedStoreCode
14
+ === 'string';
15
+ }
16
+ /** Fails closed: a gate the adapter would ignore refuses every new purchase instead. */
17
+ export function resolvePurchaseGateSupport(purchaseGate, supported = adapterSupportsPurchaseGate()) {
18
+ if (purchaseGate === undefined) {
19
+ return { refuseNewPurchases: false };
20
+ }
21
+ return supported ? { purchaseGate, refuseNewPurchases: false } : { refuseNewPurchases: true };
22
+ }
23
+ export function createPurchaseGateUnsupportedError() {
24
+ return new CommerceError('ITEM_UNAVAILABLE', {
25
+ message: 'A purchase gate is configured but @mpgd/adapter-capacitor is too old to run it; '
26
+ + 'new purchases are refused until the adapter is upgraded.',
27
+ storeCode: purchaseGateUnsupportedStoreCode,
28
+ retryable: false,
29
+ });
30
+ }
31
+ /**
32
+ * Refuses every new purchase at the plugin boundary when the adapter cannot
33
+ * run the configured gate, before the adapter's `requestPurchase` (which
34
+ * would open the store) is reached. The refusal is a `failed` result, not a
35
+ * throw, so `PURCHASE_GATE_UNSUPPORTED` survives the bridge whatever the
36
+ * adapter version. Recovery, finishing, restore, and events are untouched.
37
+ */
38
+ export function refuseNewPurchasesUnlessGated(provider, support) {
39
+ if (!support.refuseNewPurchases) {
40
+ return provider;
41
+ }
42
+ return Object.assign(provider, {
43
+ async requestPurchase() {
44
+ return {
45
+ outcome: 'failed',
46
+ error: createPurchaseGateUnsupportedError().toCommerceErrorInfo(),
47
+ };
48
+ },
49
+ });
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/capacitor-play-billing",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
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.16.0",
42
- "@mpgd/bridge": "^0.16.0",
43
- "@mpgd/platform": "^0.32.0"
41
+ "@mpgd/adapter-capacitor": "^0.17.0",
42
+ "@mpgd/bridge": "^0.17.0",
43
+ "@mpgd/platform": "^0.33.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@capacitor/core": "8.5.2",
47
47
  "@types/node": "^24.0.0",
48
- "ttsc": "0.32.1",
48
+ "ttsc": "0.34.2",
49
49
  "typescript": "7.0.2",
50
50
  "vitest": "^4.0.0",
51
- "@mpgd/adapter-capacitor": "0.16.3",
52
- "@mpgd/bridge": "0.16.4",
53
- "@mpgd/game-services": "0.30.5",
54
- "@mpgd/platform": "0.32.0"
51
+ "@mpgd/adapter-capacitor": "0.17.0",
52
+ "@mpgd/bridge": "0.17.0",
53
+ "@mpgd/platform": "0.33.0",
54
+ "@mpgd/game-services": "0.31.0"
55
55
  },
56
56
  "main": "./dist/index.js",
57
57
  "types": "./dist/index.d.ts",