@onborn/billing 0.1.0-beta.2 → 0.1.0-beta.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.3
4
+
5
+ - Fixed StoreKit 2 purchases incorrectly using the signed transaction JWS as
6
+ the transaction identifier.
7
+ - Bounded purchase idempotency keys when only a signed purchase token is
8
+ available.
9
+ - Added structured billing request errors with backend messages and codes.
10
+
3
11
  ## 0.1.0-beta.2
4
12
 
5
13
  - Removed `offeringId` from `useOnbornOffering` and
@@ -1,6 +1,7 @@
1
1
  import type { NativeStoreRestoredPurchase } from "@onborn/sdk-contracts";
2
2
  import type { OnbornBillingAdapter, OnbornLoadProductsInput, OnbornPurchaseInput, OnbornRestoreInput } from "../types";
3
3
  type NativeStoresPurchase = {
4
+ id?: string;
4
5
  productId?: string;
5
6
  storeProductId?: string;
6
7
  transactionId?: string;
@@ -143,6 +143,7 @@ function normalizeNativeRestoredPurchase(value) {
143
143
  store,
144
144
  storeProductId,
145
145
  transactionId: readString(record.transactionId) ??
146
+ readString(record.id) ??
146
147
  readString(record.originalTransactionIdentifierIOS),
147
148
  purchaseToken: readString(record.purchaseToken),
148
149
  raw: record.raw ?? value,
@@ -150,7 +151,7 @@ function normalizeNativeRestoredPurchase(value) {
150
151
  }
151
152
  function readTransactionId(purchase) {
152
153
  return (purchase?.transactionId ??
153
- purchase?.purchaseToken ??
154
+ purchase?.id ??
154
155
  purchase?.originalTransactionIdentifierIOS);
155
156
  }
156
157
  function readString(value) {
package/dist/client.d.ts CHANGED
@@ -2,6 +2,11 @@ import { type CustomerEntitlementsResponse, type GetOfferingResponse, type GetPa
2
2
  export type BillingClientOptions = {
3
3
  sourceId?: string;
4
4
  };
5
+ export declare class OnbornBillingRequestError extends Error {
6
+ readonly status: number;
7
+ readonly code?: string | undefined;
8
+ constructor(message: string, status: number, code?: string | undefined);
9
+ }
5
10
  export declare class BillingClient {
6
11
  private readonly options;
7
12
  private readonly config;
package/dist/client.js CHANGED
@@ -2,6 +2,16 @@ import { Onborn, } from "@onborn/analytics";
2
2
  import { resolveOnbornBillingConfig } from "./runtime";
3
3
  import { CustomerEntitlementsResponseSchema, GetOfferingResponseSchema, GetPaywallResponseSchema, PurchaseValidationResponseSchema, } from "@onborn/sdk-contracts";
4
4
  const ONBORN_API_BASE_URL = "https://api.testing.onborn.app";
5
+ export class OnbornBillingRequestError extends Error {
6
+ status;
7
+ code;
8
+ constructor(message, status, code) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ this.name = "OnbornBillingRequestError";
13
+ }
14
+ }
5
15
  export class BillingClient {
6
16
  options;
7
17
  config;
@@ -113,7 +123,8 @@ export class BillingClient {
113
123
  body: JSON.stringify(payload),
114
124
  });
115
125
  if (!response.ok) {
116
- throw new Error(`Purchase request failed (${response.status})`);
126
+ const failure = await readBillingFailure(response);
127
+ throw new OnbornBillingRequestError(failure.message ?? `Purchase request failed (${response.status})`, response.status, failure.code);
117
128
  }
118
129
  const parsed = PurchaseValidationResponseSchema.safeParse(await response.json());
119
130
  if (!parsed.success) {
@@ -135,6 +146,22 @@ export class BillingClient {
135
146
  });
136
147
  }
137
148
  }
149
+ async function readBillingFailure(response) {
150
+ try {
151
+ const payload = (await response.json());
152
+ return {
153
+ message: typeof payload.error === "string"
154
+ ? payload.error
155
+ : typeof payload.message === "string"
156
+ ? payload.message
157
+ : undefined,
158
+ code: typeof payload.code === "string" ? payload.code : undefined,
159
+ };
160
+ }
161
+ catch {
162
+ return {};
163
+ }
164
+ }
138
165
  export function createBillingClient(options = {}) {
139
166
  return new BillingClient(options);
140
167
  }
@@ -44,7 +44,7 @@ export async function validateBillingRestore(input) {
44
44
  }
45
45
  function createPurchaseIdempotencyKey(input) {
46
46
  const stablePart = input.result.transactionId ??
47
- input.result.purchaseToken ??
47
+ fingerprintIdempotencyValue(input.result.purchaseToken) ??
48
48
  input.result.productId ??
49
49
  input.item.product?.storeProductId ??
50
50
  input.item.package.id;
@@ -55,6 +55,18 @@ function createPurchaseIdempotencyKey(input) {
55
55
  stablePart,
56
56
  ].join(":");
57
57
  }
58
+ function fingerprintIdempotencyValue(value) {
59
+ if (!value)
60
+ return undefined;
61
+ let first = 0x811c9dc5;
62
+ let second = 0x9e3779b9;
63
+ for (let index = 0; index < value.length; index += 1) {
64
+ const code = value.charCodeAt(index);
65
+ first = Math.imul(first ^ code, 0x01000193);
66
+ second = Math.imul(second ^ code, 0x85ebca6b);
67
+ }
68
+ return `${(first >>> 0).toString(36)}${(second >>> 0).toString(36)}`;
69
+ }
58
70
  function createRestoreIdempotencyKey(input) {
59
71
  return ["restore", input.offering?.id ?? "unknown", String(Date.now())].join(":");
60
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onborn/billing",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Headless billing, offerings, purchases, restores, and entitlements for Onborn.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {