@mpgd/capacitor-play-billing 0.3.5 → 0.3.7

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
@@ -69,11 +69,17 @@ when `includePending` is set; each carries `token = purchaseToken` and
69
69
  `transactionId = orderId`, or the `play-token-sha256:<hex>` token key until
70
70
  Play assigns an order id, so the same purchase deduplicates in the ledger. A
71
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
- or a multi-line purchase has no logical id for the reconciler, so it is left
74
- out of the list and of events and handed to the optional `onUnmappedPurchase`
75
- diagnostics hook instead; the backend finalizer still sees it through the
76
- Play Developer API. The plugin's `purchaseUpdated` listener backs
72
+ paid purchase is never dropped. A purchase for a SKU that is not in `products`
73
+ (a retired product) is still listed and emitted, with that product id as
74
+ `sku` and the purchase token intact, so the reconciler can submit it when a
75
+ logical id is still known. A multi-line purchase is listed the same way,
76
+ with every product id joined by a comma (not a product-id character, so it
77
+ cannot match one catalog SKU). The reconciler then fails that purchase's
78
+ catalog lookup visibly instead of granting one line of it.
79
+ `onUnmappedPurchase` still receives both, as diagnostics only: the backend
80
+ finalizer acknowledges or consumes a token only after the client submits it
81
+ and the ledger grants, and it has no path that can acknowledge a token
82
+ without a logical product id. The plugin's `purchaseUpdated` listener backs
77
83
  `addListener`: deliveries leave in the order the plugin reported them (a
78
84
  purchase without an order id waits for the token digest, and later updates
79
85
  queue behind it), and a `cancelled` or `failed` update without a purchase is
@@ -36,7 +36,7 @@ export interface CreateCapacitorPlayBillingCommerceProviderInput {
36
36
  readonly store?: StoreId;
37
37
  /** Extra plugin codes merged over `playBillingStoreSdkErrorCodeMap`. */
38
38
  readonly mapError?: StoreSdkErrorCodeMap;
39
- /** Diagnostics for plugin purchases left out of recovery (retired SKU, multi-line); see the SDK input. */
39
+ /** Diagnostics for a retired SKU or a multi-line purchase. The purchase is still surfaced; see the SDK input. */
40
40
  readonly onUnmappedPurchase?: (purchase: PlayPurchase) => void;
41
41
  }
42
42
  /**
@@ -26,11 +26,13 @@ export interface CreateCapacitorPlayBillingStoreSdkInput {
26
26
  /** Defaults to `Capacitor.getPlatform() === 'android'`. */
27
27
  readonly isAndroid?: () => boolean;
28
28
  /**
29
- * Diagnostics hook for a plugin purchase the SDK leaves out of
30
- * `getAvailablePurchases` and events: a SKU not in `products` (for example
31
- * a retired product) or a multi-line purchase. Such a purchase has no
32
- * logical id for the reconciler; the backend finalizer still sees it
33
- * through the Play Developer API.
29
+ * Diagnostics for a plugin purchase the current catalog does not sell, or
30
+ * that names more than one product id. The purchase is still returned and
31
+ * emitted (token and product ids) so the reconciler can submit it when a
32
+ * logical id is known, or fail that purchase visibly when it is not. This
33
+ * hook is not a substitute for that delivery: the backend finalizer only
34
+ * acknowledges a token the client submits, and it has no path that can
35
+ * acknowledge one without a logical product id.
34
36
  */
35
37
  readonly onUnmappedPurchase?: (purchase: PlayPurchase) => void;
36
38
  }
@@ -64,8 +66,11 @@ export declare class PlayBillingStoreSdkError extends Error implements StoreSdkE
64
66
  * provider normalizes (`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`, and
65
67
  * `PLAY_BILLING_EMPTY_PURCHASE` become `OUTCOME_UNKNOWN` and stay recoverable).
66
68
  * `transactionId` is the order id or, until Play assigns one, the token key
67
- * `play-token-sha256:<hex>` existing ledgers already use. Nothing here grants, and
68
- * nothing here acknowledges or consumes: the backend finalizer owns both.
69
+ * `play-token-sha256:<hex>` existing ledgers already use. An unfinished
70
+ * purchase whose SKU is not in `products`, and a multi-line purchase, are
71
+ * still listed and emitted so the reconciler can see the token. Nothing here
72
+ * grants, and nothing here acknowledges or consumes: the backend finalizer
73
+ * owns both.
69
74
  */
70
75
  export declare function createCapacitorPlayBillingStoreSdk(input: CreateCapacitorPlayBillingStoreSdkInput): CapacitorPlayBillingStoreSdk;
71
76
  /** Validates a Play purchase token: non-empty, at most 4096 characters, no padding or control characters. */
package/dist/store-sdk.js CHANGED
@@ -68,8 +68,11 @@ export class PlayBillingStoreSdkError extends Error {
68
68
  * provider normalizes (`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`, and
69
69
  * `PLAY_BILLING_EMPTY_PURCHASE` become `OUTCOME_UNKNOWN` and stay recoverable).
70
70
  * `transactionId` is the order id or, until Play assigns one, the token key
71
- * `play-token-sha256:<hex>` existing ledgers already use. Nothing here grants, and
72
- * nothing here acknowledges or consumes: the backend finalizer owns both.
71
+ * `play-token-sha256:<hex>` existing ledgers already use. An unfinished
72
+ * purchase whose SKU is not in `products`, and a multi-line purchase, are
73
+ * still listed and emitted so the reconciler can see the token. Nothing here
74
+ * grants, and nothing here acknowledges or consumes: the backend finalizer
75
+ * owns both.
73
76
  */
74
77
  export function createCapacitorPlayBillingStoreSdk(input) {
75
78
  const plugin = input.plugin ?? CapacitorPlayBilling;
@@ -117,7 +120,10 @@ export function createCapacitorPlayBillingStoreSdk(input) {
117
120
  // Diagnostics never break delivery.
118
121
  }
119
122
  };
120
- /** Whether a plugin purchase maps to one configured SKU; anything else is diagnostics only. */
123
+ /**
124
+ * Whether a plugin purchase maps to one configured SKU. Anything else is
125
+ * still surfaced; this only feeds the diagnostics hook.
126
+ */
121
127
  const isMapped = (purchase) => {
122
128
  const sku = skuOf(purchase);
123
129
  if (sku !== undefined && products.has(sku)) {
@@ -188,7 +194,14 @@ export function createCapacitorPlayBillingStoreSdk(input) {
188
194
  emitLateTerminalResult(outcome);
189
195
  return;
190
196
  }
191
- if (!isRecord(purchase) || !isMapped(purchase)) {
197
+ if (!isRecord(purchase)) {
198
+ return;
199
+ }
200
+ // A retired SKU and a multi-line purchase stay in the event. The
201
+ // reconciler submits a token only when it can resolve a logical id, and
202
+ // otherwise fails the catalog lookup instead of dropping the token.
203
+ isMapped(purchase);
204
+ if (surfaceSku(purchase) === undefined) {
192
205
  return;
193
206
  }
194
207
  enqueue(() => emitConverted(purchase));
@@ -352,9 +365,10 @@ export function createCapacitorPlayBillingStoreSdk(input) {
352
365
  if (!isUnfinished(purchase, includePending)) {
353
366
  continue;
354
367
  }
355
- if (!isMapped(purchase)) {
356
- // A retired SKU or a multi-line purchase has no logical id for the
357
- // reconciler; onUnmappedPurchase sees it.
368
+ isMapped(purchase);
369
+ if (surfaceSku(purchase) === undefined) {
370
+ // Not a purchase the reconciler can name: no product id, or an id
371
+ // the evidence rules reject. The diagnostics hook already saw it.
358
372
  continue;
359
373
  }
360
374
  available.push(await toSdkPurchase(purchase));
@@ -434,7 +448,7 @@ function convertPurchase(purchase) {
434
448
  if (state !== 'pending' && state !== 'purchased') {
435
449
  throw evidenceInvalid('Play purchase state is invalid.');
436
450
  }
437
- const sku = skuOf(purchase);
451
+ const sku = surfaceSku(purchase);
438
452
  if (sku === undefined) {
439
453
  throw evidenceInvalid('Play purchase must name exactly one product.');
440
454
  }
@@ -478,13 +492,40 @@ function buildSdkPurchase(purchase, parsed) {
478
492
  : {}),
479
493
  };
480
494
  }
481
- function skuOf(purchase) {
495
+ function productIdsOf(purchase) {
482
496
  if (!isRecord(purchase)) {
483
497
  return undefined;
484
498
  }
485
499
  const products = purchase.products ?? purchase.productIds;
486
- const sku = Array.isArray(products) && products.length === 1 ? products[0] : undefined;
487
- return typeof sku === 'string' && productIdPattern.test(sku) ? sku : undefined;
500
+ if (!Array.isArray(products) || products.length === 0) {
501
+ return undefined;
502
+ }
503
+ const ids = [];
504
+ for (const id of products) {
505
+ if (typeof id !== 'string' || !productIdPattern.test(id) || ids.includes(id)) {
506
+ return undefined;
507
+ }
508
+ ids.push(id);
509
+ }
510
+ return ids;
511
+ }
512
+ function skuOf(purchase) {
513
+ const ids = productIdsOf(purchase);
514
+ return ids?.length === 1 ? ids[0] : undefined;
515
+ }
516
+ /**
517
+ * The SKU a surfaced purchase carries. One product id is that id, including
518
+ * a retired SKU that is no longer in `products`. Several ids are joined with
519
+ * a comma, which is not a product-id character, so the joined value cannot
520
+ * equal one catalog SKU and the reconciler fails the lookup instead of
521
+ * granting one line of a multi-line purchase.
522
+ */
523
+ function surfaceSku(purchase) {
524
+ const ids = productIdsOf(purchase);
525
+ if (ids === undefined) {
526
+ return undefined;
527
+ }
528
+ return ids.length === 1 ? ids[0] : ids.join(',');
488
529
  }
489
530
  /**
490
531
  * The purchase state under its Billing 9.1.0 name, or under the 0.1 name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/capacitor-play-billing",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Opt-in Android Google Play Billing purchase collector for mpgd Capacitor games.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "peerDependencies": {
40
40
  "@capacitor/core": "^8.5.1",
41
41
  "@mpgd/adapter-capacitor": "^0.12.0",
42
- "@mpgd/bridge": "^0.14.0",
42
+ "@mpgd/bridge": "^0.15.0",
43
43
  "@mpgd/platform": "^0.27.0"
44
44
  },
45
45
  "devDependencies": {
@@ -48,10 +48,10 @@
48
48
  "ttsc": "0.32.1",
49
49
  "typescript": "7.0.2",
50
50
  "vitest": "^4.0.0",
51
- "@mpgd/adapter-capacitor": "0.12.1",
52
- "@mpgd/bridge": "0.14.2",
53
- "@mpgd/game-services": "0.27.0",
54
- "@mpgd/platform": "0.27.0"
51
+ "@mpgd/adapter-capacitor": "0.12.3",
52
+ "@mpgd/bridge": "0.15.0",
53
+ "@mpgd/platform": "0.27.2",
54
+ "@mpgd/game-services": "0.27.2"
55
55
  },
56
56
  "main": "./dist/index.js",
57
57
  "types": "./dist/index.d.ts",