@mpgd/capacitor-storekit 0.1.1 → 0.1.3
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 +13 -0
- package/dist/definitions.d.ts +10 -1
- package/dist/provider.d.ts +13 -0
- package/dist/provider.js +55 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -25,6 +25,19 @@ bundle, product, environment and account binding before the ledger grant.
|
|
|
25
25
|
The recovery helper calls native `finishTransaction` only after a
|
|
26
26
|
verified response with a ledger entry ID. If finish fails, it reports
|
|
27
27
|
`finishPending: true`; repeat recovery with the original purchase identity.
|
|
28
|
+
`provider.finishGrantedTransaction` accepts only a transaction and ledger
|
|
29
|
+
entry pair that `recoverStoreKitPurchases` recorded from a backend answer in
|
|
30
|
+
the current session. The pair is consumed before the native call, so
|
|
31
|
+
overlapping finishes (for example startup recovery and a `transactionUpdated`
|
|
32
|
+
recovery) share one native call, and a failed native finish restores the pair
|
|
33
|
+
for a retry. Calling it, or the native `finishTransaction` plugin method, with
|
|
34
|
+
any other value is rejected so an unfinished purchase keeps its recovery
|
|
35
|
+
signal. This guard prevents arbitrary or accidental finish calls with
|
|
36
|
+
unverified ledger IDs and binds finishing to the recovery flow; it is **not**
|
|
37
|
+
a trust boundary against code that controls the `backend` object it passes to
|
|
38
|
+
`recoverStoreKitPurchases`. The real control is server-side: the authenticated
|
|
39
|
+
`recoverPurchase` implementation must verify Apple evidence before it grants,
|
|
40
|
+
and finishing a transaction never grants anything by itself.
|
|
28
41
|
An unknown backend result stays pending and never triggers a
|
|
29
42
|
new purchase sheet automatically.
|
|
30
43
|
|
package/dist/definitions.d.ts
CHANGED
|
@@ -42,7 +42,16 @@ export interface CapacitorStoreKitPlugin {
|
|
|
42
42
|
sync(): Promise<{
|
|
43
43
|
readonly synced: boolean;
|
|
44
44
|
}>;
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Call only after the authenticated backend confirms the ledger grant.
|
|
47
|
+
* Native validation is intentionally shallow and finishing never grants
|
|
48
|
+
* anything. The TypeScript provider's finishGrantedTransaction binds this
|
|
49
|
+
* call to the recoverStoreKitPurchases flow so games cannot finish a
|
|
50
|
+
* purchase accidentally or with an arbitrary ledger ID; games must not call
|
|
51
|
+
* this plugin method directly. That binding is not a trust boundary against
|
|
52
|
+
* code that controls the recovery backend object: the authenticated
|
|
53
|
+
* server-side recoverPurchase verification is the real control.
|
|
54
|
+
*/
|
|
46
55
|
finishTransaction(input: {
|
|
47
56
|
readonly transactionId: string;
|
|
48
57
|
readonly ledgerEntryId: string;
|
package/dist/provider.d.ts
CHANGED
|
@@ -47,6 +47,19 @@ export interface CapacitorStoreKitProvider {
|
|
|
47
47
|
}>>;
|
|
48
48
|
getAppAccountToken(): Promise<string>;
|
|
49
49
|
getRecoverableTransactions(): Promise<StoreKitRecoverySnapshot>;
|
|
50
|
+
/**
|
|
51
|
+
* Finish a transaction the backend already granted. The provider accepts
|
|
52
|
+
* only a transaction and ledger entry pair that recoverStoreKitPurchases
|
|
53
|
+
* recorded from a backend answer in this session, and consumes the pair
|
|
54
|
+
* before the native call so overlapping finishes share one native call;
|
|
55
|
+
* any other pair is rejected so an unfinished purchase keeps its recovery
|
|
56
|
+
* signal. A failed native finish restores the pair for a retry.
|
|
57
|
+
*
|
|
58
|
+
* The guard prevents arbitrary or accidental finish calls with unverified
|
|
59
|
+
* ledger IDs. It is not a trust boundary against code that controls the
|
|
60
|
+
* backend object given to recoverStoreKitPurchases; the authenticated
|
|
61
|
+
* server-side recoverPurchase implementation is the real control.
|
|
62
|
+
*/
|
|
50
63
|
finishGrantedTransaction(input: {
|
|
51
64
|
readonly transactionId: string;
|
|
52
65
|
readonly ledgerEntryId: string;
|
package/dist/provider.js
CHANGED
|
@@ -3,6 +3,20 @@ import { CapacitorStoreKit } from './plugin.js';
|
|
|
3
3
|
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
4
4
|
const productPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
5
5
|
const transactionPattern = /^[1-9][0-9]{0,19}$/u;
|
|
6
|
+
/**
|
|
7
|
+
* Grants recorded by recoverStoreKitPurchases per provider instance:
|
|
8
|
+
* transaction ID to the ledger entry ID the recovery backend returned. The map
|
|
9
|
+
* binds finishGrantedTransaction to that recovery flow so a game cannot finish
|
|
10
|
+
* an unfinished purchase by accident, or with an arbitrary ledger string, and
|
|
11
|
+
* so each recorded pair reaches the native finish call at most once.
|
|
12
|
+
*
|
|
13
|
+
* This is not a trust boundary against a caller who controls the backend
|
|
14
|
+
* object passed to recoverStoreKitPurchases: such a caller can record any
|
|
15
|
+
* pair. The real control stays server-side, in the authenticated
|
|
16
|
+
* recoverPurchase implementation that verifies Apple evidence before it
|
|
17
|
+
* grants, and in the native plugin never granting anything on finish.
|
|
18
|
+
*/
|
|
19
|
+
const verifiedGrants = new WeakMap();
|
|
6
20
|
/** Submit unfinished purchases to the backend before asking StoreKit to finish. */
|
|
7
21
|
export async function recoverStoreKitPurchases(input) {
|
|
8
22
|
if (input.playerId.trim() === '') {
|
|
@@ -43,6 +57,7 @@ export async function recoverStoreKitPurchases(input) {
|
|
|
43
57
|
if (verification.ledgerEntryId === undefined || verification.ledgerEntryId.trim() === '') {
|
|
44
58
|
return { productId: item.productId, transactionId, status: 'pending', verification };
|
|
45
59
|
}
|
|
60
|
+
verifiedGrants.get(input.provider)?.set(transactionId, verification.ledgerEntryId);
|
|
46
61
|
try {
|
|
47
62
|
const finished = await input.provider.finishGrantedTransaction({
|
|
48
63
|
transactionId,
|
|
@@ -209,7 +224,23 @@ export function createCapacitorStoreKitProvider(options) {
|
|
|
209
224
|
return failure(input.id, 'STOREKIT_UNAVAILABLE', true);
|
|
210
225
|
}
|
|
211
226
|
}
|
|
212
|
-
|
|
227
|
+
const grants = new Map();
|
|
228
|
+
const inFlight = new Map();
|
|
229
|
+
async function finishReserved(transactionId, ledgerEntryId) {
|
|
230
|
+
let finished = false;
|
|
231
|
+
try {
|
|
232
|
+
finished = (await sdk.finishTransaction({ transactionId, ledgerEntryId })).finished;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
// Restore the pair after a rejected or failed native finish so recovery
|
|
236
|
+
// can retry it, unless a newer recovery recorded the transaction again.
|
|
237
|
+
if (!finished && !grants.has(transactionId)) {
|
|
238
|
+
grants.set(transactionId, ledgerEntryId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return finished;
|
|
242
|
+
}
|
|
243
|
+
const provider = {
|
|
213
244
|
id: 'apple-storekit',
|
|
214
245
|
features: ['nativeIap'],
|
|
215
246
|
methods: [
|
|
@@ -247,10 +278,31 @@ export function createCapacitorStoreKitProvider(options) {
|
|
|
247
278
|
|| input.ledgerEntryId.trim() === '') {
|
|
248
279
|
throw new TypeError('StoreKit finish requires a verified grant and transaction ID.');
|
|
249
280
|
}
|
|
250
|
-
const
|
|
251
|
-
|
|
281
|
+
const { transactionId, ledgerEntryId } = input;
|
|
282
|
+
const active = inFlight.get(transactionId);
|
|
283
|
+
if (active !== undefined && active.ledgerEntryId === ledgerEntryId) {
|
|
284
|
+
return active.promise;
|
|
285
|
+
}
|
|
286
|
+
if (grants.get(transactionId) !== ledgerEntryId) {
|
|
287
|
+
throw new TypeError('StoreKit finish requires the ledger entry the backend returned for this transaction via recoverStoreKitPurchases.');
|
|
288
|
+
}
|
|
289
|
+
// Consume the pair synchronously, before the first await, so an
|
|
290
|
+
// overlapping finish for the same transaction cannot reach native twice.
|
|
291
|
+
grants.delete(transactionId);
|
|
292
|
+
const promise = finishReserved(transactionId, ledgerEntryId);
|
|
293
|
+
inFlight.set(transactionId, { ledgerEntryId, promise });
|
|
294
|
+
try {
|
|
295
|
+
return await promise;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (inFlight.get(transactionId)?.promise === promise) {
|
|
299
|
+
inFlight.delete(transactionId);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
252
302
|
},
|
|
253
303
|
};
|
|
304
|
+
verifiedGrants.set(provider, grants);
|
|
305
|
+
return provider;
|
|
254
306
|
}
|
|
255
307
|
function convertOutcome(outcome, mapping, expectedAccountToken) {
|
|
256
308
|
if (outcome.status !== 'purchased') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/capacitor-storekit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Opt-in StoreKit 2 one-time purchase collector for mpgd Capacitor games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"@capacitor/core": "8.5.2",
|
|
46
46
|
"ttsc": "0.30.4",
|
|
47
47
|
"typescript": "7.0.2",
|
|
48
|
-
"@mpgd/adapter-capacitor": "0.6.
|
|
48
|
+
"@mpgd/adapter-capacitor": "0.6.2",
|
|
49
49
|
"@mpgd/bridge": "0.10.0",
|
|
50
|
-
"@mpgd/
|
|
51
|
-
"@mpgd/
|
|
50
|
+
"@mpgd/platform": "0.14.0",
|
|
51
|
+
"@mpgd/game-services": "0.17.2"
|
|
52
52
|
},
|
|
53
53
|
"main": "./dist/index.js",
|
|
54
54
|
"types": "./dist/index.d.ts",
|