@mpgd/capacitor-storekit 0.4.2 → 0.4.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 +17 -3
- package/dist/store-sdk.js +38 -13
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -63,7 +63,8 @@ listed. A `revokedAt` maps to `state: 'revoked'`, and one malformed
|
|
|
63
63
|
transaction is reported as a `purchaseError` without blocking its siblings);
|
|
64
64
|
`finishTransaction({ transactionId, ledgerEntryId })` resolves the session
|
|
65
65
|
app account token and finishes only a listed, non-revoked transaction bound
|
|
66
|
-
to that token
|
|
66
|
+
to that token, or a durable of another token a restore shared to this
|
|
67
|
+
player (see Recovery);
|
|
67
68
|
`restorePurchases` over `sync`; `transactionUpdated` as `purchaseUpdated`;
|
|
68
69
|
and an availability probe as `getAvailability`, probing the configured
|
|
69
70
|
SKUs batch by batch until one is sold. Every purchase carries the
|
|
@@ -182,12 +183,25 @@ each one on the backend under its original idempotency key or returns the
|
|
|
182
183
|
existing ledger grant, and calls `finishTransaction` only after a verified
|
|
183
184
|
response with a ledger entry id. The StoreKit SDK and the native plugin both
|
|
184
185
|
refuse to call `transaction.finish()` on a transaction whose app account
|
|
185
|
-
token is missing or does not match
|
|
186
|
+
token is missing or does not match the token they are given; the SDK gives the
|
|
187
|
+
session's, except for a durable of another token (below). A caller who can read the token from the plugin can
|
|
186
188
|
still present it with a forged ledger entry id; that remaining hole needs a
|
|
187
189
|
signed server finish capability. A listed, non-revoked durable StoreKit
|
|
188
190
|
already finished (`unfinished: false`) answers `{ finished: true }` without a
|
|
189
191
|
native call whatever its token, so a purchase shared to this guest from an
|
|
190
|
-
earlier guest's restore ends there.
|
|
192
|
+
earlier guest's restore ends there. A durable of the configured catalog still
|
|
193
|
+
unfinished under another token (the earlier guest crashed between its grant
|
|
194
|
+
and its finish, then the app was reinstalled; or a native build omits
|
|
195
|
+
`unfinished`) is finished with that transaction's own token, which the native
|
|
196
|
+
token match accepts, so the shared grant's record does not retry forever. The
|
|
197
|
+
reconciler calls it only for this player's ledger grant, and finishing a
|
|
198
|
+
durable takes nothing from the earlier guest: StoreKit keeps it in
|
|
199
|
+
`Transaction.currentEntitlements`, where that guest's own read still lists it
|
|
200
|
+
(`unfinished: false`). The native plugin is unchanged and still filters
|
|
201
|
+
nothing by token on reads. A webview caller can therefore finish another
|
|
202
|
+
token's unfinished durable, which, for the same reason, grants or removes
|
|
203
|
+
nothing. A consumable of another token and a transaction bound to no token
|
|
204
|
+
are never finished. Permanently rejected and account-mismatched
|
|
191
205
|
transactions are not finished: the client cannot prove content delivery for
|
|
192
206
|
them, so persist that terminal result in the game-owned operation journal and
|
|
193
207
|
do not run an automatic retry loop for it. An unknown backend result stays
|
package/dist/store-sdk.js
CHANGED
|
@@ -259,11 +259,25 @@ export function createCapacitorStoreKitStoreSdk(input) {
|
|
|
259
259
|
/**
|
|
260
260
|
* Mirrors the native finish gate: the transaction must be listed, not
|
|
261
261
|
* revoked, and bound to the session app account token. A missing list is
|
|
262
|
-
* an invalid response, not a quiet "not finished".
|
|
263
|
-
* durable
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
262
|
+
* an invalid response, not a quiet "not finished". Two exceptions for a
|
|
263
|
+
* listed, non-revoked durable, the only kind the restore read admits from
|
|
264
|
+
* another token:
|
|
265
|
+
*
|
|
266
|
+
* - StoreKit already finished it (`unfinished: false`): nothing is left to
|
|
267
|
+
* finish whatever its token, so a restore shared to this player ends
|
|
268
|
+
* there without a native call.
|
|
269
|
+
* - It is unfinished (or the build omits the flag), the catalog declares
|
|
270
|
+
* its SKU durable, and it is bound to another token: an earlier guest
|
|
271
|
+
* crashed between its grant and its finish, then the app was
|
|
272
|
+
* reinstalled. The caller holds this player's ledger grant
|
|
273
|
+
* (the reconciler finishes only `granted` or `finishing` records), and a
|
|
274
|
+
* durable stays in `Transaction.currentEntitlements` after its finish, so
|
|
275
|
+
* finishing it takes nothing from any player: the earlier guest's next
|
|
276
|
+
* read still lists it (`unfinished: false`) for its own verification. It
|
|
277
|
+
* is finished with its own token, which the native gate matches.
|
|
278
|
+
*
|
|
279
|
+
* A consumable of another token, or a transaction bound to no token, is
|
|
280
|
+
* never finished here.
|
|
267
281
|
*/
|
|
268
282
|
const finishPermitted = async (transactionId, appAccountToken) => {
|
|
269
283
|
const response = await sdk.getTransactions();
|
|
@@ -272,12 +286,21 @@ export function createCapacitorStoreKitStoreSdk(input) {
|
|
|
272
286
|
}
|
|
273
287
|
const transaction = response.transactions.find((candidate) => isValidStoreKitTransaction(candidate) && candidate.transactionId === transactionId);
|
|
274
288
|
if (transaction === undefined || transaction.revokedAt !== undefined) {
|
|
275
|
-
return 'refused';
|
|
289
|
+
return { verdict: 'refused' };
|
|
276
290
|
}
|
|
277
291
|
if (transaction.type === 'non_consumable' && transaction.unfinished === false) {
|
|
278
|
-
return 'finished';
|
|
292
|
+
return { verdict: 'finished' };
|
|
279
293
|
}
|
|
280
|
-
|
|
294
|
+
const bound = transaction.appAccountToken?.toLowerCase();
|
|
295
|
+
if (bound === appAccountToken) {
|
|
296
|
+
return { verdict: 'finish', appAccountToken };
|
|
297
|
+
}
|
|
298
|
+
if (bound !== undefined
|
|
299
|
+
&& transaction.type === 'non_consumable'
|
|
300
|
+
&& kinds.get(transaction.productId) === 'durable') {
|
|
301
|
+
return { verdict: 'finish', appAccountToken: bound };
|
|
302
|
+
}
|
|
303
|
+
return { verdict: 'refused' };
|
|
281
304
|
};
|
|
282
305
|
return {
|
|
283
306
|
store: appStoreStoreId,
|
|
@@ -371,7 +394,8 @@ export function createCapacitorStoreKitStoreSdk(input) {
|
|
|
371
394
|
async finishTransaction(request) {
|
|
372
395
|
requireIos();
|
|
373
396
|
// The session token is the binding. A bridge or plugin caller cannot
|
|
374
|
-
// substitute another account's token on this path
|
|
397
|
+
// substitute another account's token on this path: the only other
|
|
398
|
+
// token used is a durable's own, read from the native list.
|
|
375
399
|
const appAccountToken = await Promise.resolve(resolveAppAccountToken());
|
|
376
400
|
const { transactionId, ledgerEntryId } = request;
|
|
377
401
|
if (typeof transactionId !== 'string' || !transactionPattern.test(transactionId)
|
|
@@ -384,15 +408,16 @@ export function createCapacitorStoreKitStoreSdk(input) {
|
|
|
384
408
|
}
|
|
385
409
|
const promise = (async () => {
|
|
386
410
|
// Same match the native plugin applies: missing, revoked, unbound, or
|
|
387
|
-
// another account never reaches `transaction.finish()
|
|
411
|
+
// another account never reaches `transaction.finish()`, except a
|
|
412
|
+
// durable of another token (see `finishPermitted`).
|
|
388
413
|
const permitted = await finishPermitted(transactionId, appAccountToken);
|
|
389
|
-
if (permitted !== 'finish') {
|
|
390
|
-
return { finished: permitted === 'finished' };
|
|
414
|
+
if (permitted.verdict !== 'finish') {
|
|
415
|
+
return { finished: permitted.verdict === 'finished' };
|
|
391
416
|
}
|
|
392
417
|
const reported = await sdk.finishTransaction({
|
|
393
418
|
transactionId,
|
|
394
419
|
ledgerEntryId,
|
|
395
|
-
appAccountToken,
|
|
420
|
+
appAccountToken: permitted.appAccountToken,
|
|
396
421
|
});
|
|
397
422
|
if (!isRecord(reported) || typeof reported.finished !== 'boolean') {
|
|
398
423
|
throw evidenceInvalid('StoreKit returned an invalid finish result.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/capacitor-storekit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Opt-in StoreKit 2 one-time purchase collector and commerce v2 provider for mpgd Capacitor games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"@capacitor/core": "8.5.2",
|
|
47
47
|
"ttsc": "0.32.1",
|
|
48
48
|
"typescript": "7.0.2",
|
|
49
|
-
"@mpgd/adapter-capacitor": "0.16.
|
|
49
|
+
"@mpgd/adapter-capacitor": "0.16.1",
|
|
50
|
+
"@mpgd/game-services": "0.30.3",
|
|
50
51
|
"@mpgd/bridge": "0.16.4",
|
|
51
|
-
"@mpgd/platform": "0.32.0"
|
|
52
|
-
"@mpgd/game-services": "0.30.2"
|
|
52
|
+
"@mpgd/platform": "0.32.0"
|
|
53
53
|
},
|
|
54
54
|
"main": "./dist/index.js",
|
|
55
55
|
"types": "./dist/index.d.ts",
|