@mpgd/capacitor-play-billing 0.4.4 → 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 +10 -2
- package/dist/commerce-provider.d.ts +11 -2
- package/dist/commerce-provider.js +8 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/purchase-gate-support.d.ts +50 -0
- package/dist/purchase-gate-support.js +50 -0
- package/package.json +9 -9
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
|
|
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
|
|
24
|
-
*
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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.
|
|
42
|
-
"@mpgd/bridge": "^0.
|
|
43
|
-
"@mpgd/platform": "^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.
|
|
48
|
+
"ttsc": "0.34.2",
|
|
49
49
|
"typescript": "7.0.2",
|
|
50
50
|
"vitest": "^4.0.0",
|
|
51
|
-
"@mpgd/adapter-capacitor": "0.
|
|
52
|
-
"@mpgd/
|
|
53
|
-
"@mpgd/platform": "0.
|
|
54
|
-
"@mpgd/
|
|
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",
|