@ait-kit/sdk 0.2.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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/event-flow.d.ts +36 -0
- package/dist/event-flow.d.ts.map +1 -0
- package/dist/event-flow.js +101 -0
- package/dist/iap/adapter.d.ts +47 -0
- package/dist/iap/adapter.d.ts.map +1 -0
- package/dist/iap/adapter.js +175 -0
- package/dist/iap/grant-coordinator.d.ts +36 -0
- package/dist/iap/grant-coordinator.d.ts.map +1 -0
- package/dist/iap/grant-coordinator.js +69 -0
- package/dist/iap/platform-contract.d.ts +55 -0
- package/dist/iap/platform-contract.d.ts.map +1 -0
- package/dist/iap/platform-contract.js +26 -0
- package/dist/iap/purchase-flow.d.ts +34 -0
- package/dist/iap/purchase-flow.d.ts.map +1 -0
- package/dist/iap/purchase-flow.js +167 -0
- package/dist/identity/platform-contract.d.ts +44 -0
- package/dist/identity/platform-contract.d.ts.map +1 -0
- package/dist/identity/platform-contract.js +71 -0
- package/dist/index.d.ts +215 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/notification/platform-contract.d.ts +43 -0
- package/dist/notification/platform-contract.d.ts.map +1 -0
- package/dist/notification/platform-contract.js +89 -0
- package/dist/rn/ads.d.ts +39 -0
- package/dist/rn/ads.d.ts.map +1 -0
- package/dist/rn/ads.js +226 -0
- package/dist/rn/framework-contract.d.ts +45 -0
- package/dist/rn/framework-contract.d.ts.map +1 -0
- package/dist/rn/framework-contract.js +7 -0
- package/dist/rn/framework-loader.d.ts +22 -0
- package/dist/rn/framework-loader.d.ts.map +1 -0
- package/dist/rn/framework-loader.js +26 -0
- package/dist/rn/iap.d.ts +19 -0
- package/dist/rn/iap.d.ts.map +1 -0
- package/dist/rn/iap.js +35 -0
- package/dist/rn/identity.d.ts +20 -0
- package/dist/rn/identity.d.ts.map +1 -0
- package/dist/rn/identity.js +42 -0
- package/dist/rn/index.d.ts +27 -0
- package/dist/rn/index.d.ts.map +1 -0
- package/dist/rn/index.js +24 -0
- package/dist/rn/notify-share.d.ts +42 -0
- package/dist/rn/notify-share.d.ts.map +1 -0
- package/dist/rn/notify-share.js +159 -0
- package/dist/rn/storage.d.ts +18 -0
- package/dist/rn/storage.d.ts.map +1 -0
- package/dist/rn/storage.js +64 -0
- package/dist/share/platform-contract.d.ts +41 -0
- package/dist/share/platform-contract.d.ts.map +1 -0
- package/dist/share/platform-contract.js +34 -0
- package/dist/storage/platform-contract.d.ts +26 -0
- package/dist/storage/platform-contract.d.ts.map +1 -0
- package/dist/storage/platform-contract.js +16 -0
- package/dist/web/framework-loader.d.ts +9 -0
- package/dist/web/framework-loader.d.ts.map +1 -0
- package/dist/web/framework-loader.js +32 -0
- package/dist/web/iap-contract.d.ts +13 -0
- package/dist/web/iap-contract.d.ts.map +1 -0
- package/dist/web/iap-contract.js +1 -0
- package/dist/web/identity.d.ts +30 -0
- package/dist/web/identity.d.ts.map +1 -0
- package/dist/web/identity.js +87 -0
- package/dist/web/index.d.ts +47 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +43 -0
- package/dist/web/notify-share.d.ts +37 -0
- package/dist/web/notify-share.d.ts.map +1 -0
- package/dist/web/notify-share.js +154 -0
- package/package.json +66 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side dedupe for server grant callbacks, scoped to one adapter
|
|
3
|
+
* instance (one "processing scope"):
|
|
4
|
+
*
|
|
5
|
+
* - Concurrent grants for the same order share the in-flight Promise, so a
|
|
6
|
+
* duplicate callback never re-runs the server request.
|
|
7
|
+
* - A successfully granted order is reused within the same scope: later
|
|
8
|
+
* flows (pending-order recovery, duplicate success events) resolve
|
|
9
|
+
* immediately instead of re-granting.
|
|
10
|
+
* - Failed grants are never cached: the entry is removed when the attempt
|
|
11
|
+
* rejects, so the next call retries for real.
|
|
12
|
+
*
|
|
13
|
+
* This only reduces duplicate client-side work. Server-side grant
|
|
14
|
+
* idempotency (verify the order, persist exactly once) is a separate,
|
|
15
|
+
* mandatory server responsibility.
|
|
16
|
+
*/
|
|
17
|
+
export class IapGrantCoordinator {
|
|
18
|
+
grant;
|
|
19
|
+
inFlight = new Map();
|
|
20
|
+
granted = new Map();
|
|
21
|
+
constructor(grant) {
|
|
22
|
+
this.grant = grant;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs (or joins) the grant for the target order. Resolves when the
|
|
26
|
+
* consumer's callback resolves; rejects with the callback's error when it
|
|
27
|
+
* fails, leaving the order retryable. A duplicate joins only when it
|
|
28
|
+
* describes the same target: orderId and sku must match, and a caller
|
|
29
|
+
* that does not know the subscriptionId (pending-order recovery) may join
|
|
30
|
+
* a cached subscription grant. Explicitly conflicting duplicates reject
|
|
31
|
+
* instead of silently inheriting another flow's grant.
|
|
32
|
+
*/
|
|
33
|
+
run(target) {
|
|
34
|
+
const conflict = (existing) => new Error(`conflicting grant target for order ${target.orderId}: already ${existing.sku !== target.sku ? `sku ${existing.sku}` : `subscriptionId ${existing.subscriptionId}`}, requested ${target.sku}`);
|
|
35
|
+
// Join rules: sku must match. A caller without a subscriptionId
|
|
36
|
+
// (pending-order recovery) may join any cached grant for the order; a
|
|
37
|
+
// caller WITH one must match the cached subscriptionId exactly — a
|
|
38
|
+
// cached grant without a subscriptionId never satisfies an explicit one.
|
|
39
|
+
const sameTarget = (existing) => existing.sku === target.sku &&
|
|
40
|
+
(target.subscriptionId === undefined || existing.subscriptionId === target.subscriptionId);
|
|
41
|
+
const inFlight = this.inFlight.get(target.orderId);
|
|
42
|
+
if (inFlight) {
|
|
43
|
+
return sameTarget(inFlight.target) ? inFlight.promise : Promise.reject(conflict(inFlight.target));
|
|
44
|
+
}
|
|
45
|
+
const granted = this.granted.get(target.orderId);
|
|
46
|
+
if (granted) {
|
|
47
|
+
return sameTarget(granted) ? Promise.resolve() : Promise.reject(conflict(granted));
|
|
48
|
+
}
|
|
49
|
+
const attempt = (async () => {
|
|
50
|
+
await this.grant(target);
|
|
51
|
+
// Only successful grants persist: the consumer's contract is to
|
|
52
|
+
// resolve after the server verified the order and persisted the grant.
|
|
53
|
+
this.granted.set(target.orderId, target);
|
|
54
|
+
})();
|
|
55
|
+
this.inFlight.set(target.orderId, { target, promise: attempt });
|
|
56
|
+
attempt
|
|
57
|
+
.catch(() => {
|
|
58
|
+
// Failures are not cached; a later call retries the callback.
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
this.inFlight.delete(target.orderId);
|
|
62
|
+
});
|
|
63
|
+
return attempt;
|
|
64
|
+
}
|
|
65
|
+
/** True when this scope already granted the order successfully. */
|
|
66
|
+
isGranted(orderId) {
|
|
67
|
+
return this.granted.has(orderId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal platform contract shared by the /rn and /web IAP adapters.
|
|
3
|
+
* Each entry point declares its own structural copy of the official SDK
|
|
4
|
+
* surface (they are different npm packages with identical Domains API
|
|
5
|
+
* shapes); this module is the common, platform-neutral engine boundary and
|
|
6
|
+
* is never exported from a public subpath.
|
|
7
|
+
*/
|
|
8
|
+
import type { IapOneTimePurchaseParams, IapPendingOrder, IapProduct, IapSubscriptionPurchaseParams } from "../index.js";
|
|
9
|
+
export interface IapSdkError {
|
|
10
|
+
code?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
export type IapGrantTarget = {
|
|
14
|
+
orderId: string;
|
|
15
|
+
sku: string;
|
|
16
|
+
subscriptionId?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Official SDK functions optionally expose an isSupported() pre-check. */
|
|
19
|
+
type WithSupport<F> = F & {
|
|
20
|
+
isSupported?: () => boolean;
|
|
21
|
+
};
|
|
22
|
+
export interface IapPlatformSdk {
|
|
23
|
+
getProductItemList: WithSupport<() => Promise<{
|
|
24
|
+
products: IapProduct[];
|
|
25
|
+
}>>;
|
|
26
|
+
createOneTimePurchaseOrder: WithSupport<(params: IapOneTimePurchaseParams) => () => void>;
|
|
27
|
+
createSubscriptionPurchaseOrder: WithSupport<(params: IapSubscriptionPurchaseParams) => () => void>;
|
|
28
|
+
getPendingOrders: WithSupport<() => Promise<{
|
|
29
|
+
orders: IapPendingOrder[];
|
|
30
|
+
}>>;
|
|
31
|
+
completeProductGrant: WithSupport<(args: {
|
|
32
|
+
params: {
|
|
33
|
+
orderId: string;
|
|
34
|
+
};
|
|
35
|
+
}) => Promise<boolean>>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Injection surface: installed framework versions may expose only some IAP
|
|
39
|
+
* functions. The loaders accept any partial surface; the adapter gates each
|
|
40
|
+
* operation individually at call time.
|
|
41
|
+
*/
|
|
42
|
+
export type PartialIapPlatformSdk = {
|
|
43
|
+
[K in keyof IapPlatformSdk]?: IapPlatformSdk[K];
|
|
44
|
+
};
|
|
45
|
+
export type IapPlatformLoader = () => Promise<{
|
|
46
|
+
available: true;
|
|
47
|
+
module: PartialIapPlatformSdk;
|
|
48
|
+
} | {
|
|
49
|
+
available: false;
|
|
50
|
+
reason: string;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function toIapErrorMessage(error: unknown): string;
|
|
53
|
+
export declare function toIapErrorCode(error: unknown): string | undefined;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=platform-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-contract.d.ts","sourceRoot":"","sources":["../../src/iap/platform-contract.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,eAAe,EACf,UAAU,EACV,6BAA6B,EAC9B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,2EAA2E;AAC3E,KAAK,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,kBAAkB,EAAE,WAAW,CAAC,MAAM,OAAO,CAAC;QAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC,CAAC;IAC3E,0BAA0B,EAAE,WAAW,CAAC,CAAC,MAAM,EAAE,wBAAwB,KAAK,MAAM,IAAI,CAAC,CAAC;IAC1F,+BAA+B,EAAE,WAAW,CAC1C,CAAC,MAAM,EAAE,6BAA6B,KAAK,MAAM,IAAI,CACtD,CAAC;IACF,gBAAgB,EAAE,WAAW,CAAC,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC,CAAC,CAAC;IAC5E,oBAAoB,EAAE,WAAW,CAC/B,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAC5D,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;KACjC,CAAC,IAAI,MAAM,cAAc,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,CAC3C;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAC1F,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CASxD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMjE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal platform contract shared by the /rn and /web IAP adapters.
|
|
3
|
+
* Each entry point declares its own structural copy of the official SDK
|
|
4
|
+
* surface (they are different npm packages with identical Domains API
|
|
5
|
+
* shapes); this module is the common, platform-neutral engine boundary and
|
|
6
|
+
* is never exported from a public subpath.
|
|
7
|
+
*/
|
|
8
|
+
export function toIapErrorMessage(error) {
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
return error.message;
|
|
11
|
+
}
|
|
12
|
+
if (typeof error === "object" && error !== null) {
|
|
13
|
+
const coded = error;
|
|
14
|
+
if (typeof coded.message === "string" && coded.message)
|
|
15
|
+
return coded.message;
|
|
16
|
+
}
|
|
17
|
+
return String(error);
|
|
18
|
+
}
|
|
19
|
+
export function toIapErrorCode(error) {
|
|
20
|
+
if (typeof error === "object" && error !== null) {
|
|
21
|
+
const code = error.code;
|
|
22
|
+
if (typeof code === "string" && code)
|
|
23
|
+
return code;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IapPurchaseResult } from "../index.js";
|
|
2
|
+
import { type IapGrantCoordinator } from "./grant-coordinator.js";
|
|
3
|
+
export interface PurchaseFlowOptions {
|
|
4
|
+
/** The platform's order-creation function (already capability-checked). */
|
|
5
|
+
startOrder: ((params: import("../index.js").IapOneTimePurchaseParams) => () => void) | ((params: import("../index.js").IapSubscriptionPurchaseParams) => () => void);
|
|
6
|
+
coordinator: IapGrantCoordinator;
|
|
7
|
+
sku: string;
|
|
8
|
+
offerId?: string;
|
|
9
|
+
subscription: boolean;
|
|
10
|
+
/** Overall deadline for the purchase flow; 0 disables. */
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Shared purchase flow for the /rn and /web IAP adapters, built on the
|
|
15
|
+
* common event-flow base:
|
|
16
|
+
*
|
|
17
|
+
* - The platform's `processProductGrant` slot wraps the consumer-injected
|
|
18
|
+
* server grant callback (through the coordinator's dedupe). The platform
|
|
19
|
+
* receives `true` only after the callback resolves.
|
|
20
|
+
* - Completion requires the success event's orderId to match the order the
|
|
21
|
+
* grant confirmed for THIS flow. A grant for a different order ends the
|
|
22
|
+
* flow as `failed` with `ORDER_MISMATCH`, and a success event arriving
|
|
23
|
+
* before the grant settles is parked until the grant for the same order
|
|
24
|
+
* confirms — a success event alone never completes a purchase.
|
|
25
|
+
* - A failed or throwing grant ends the flow as `grant_failed`; a purchase
|
|
26
|
+
* is never reported successful over a failed server grant.
|
|
27
|
+
* - `USER_CANCELED` maps to `canceled`; other SDK errors to `failed`.
|
|
28
|
+
* - Timeout yields `unknown` (with the known orderId when available): the
|
|
29
|
+
* server grant may already be in progress — resolve it through server
|
|
30
|
+
* verification and the pending-order recovery flow. Late events after
|
|
31
|
+
* any terminal state are ignored.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runPurchaseFlow(options: PurchaseFlowOptions): Promise<IapPurchaseResult>;
|
|
34
|
+
//# sourceMappingURL=purchase-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purchase-flow.d.ts","sourceRoot":"","sources":["../../src/iap/purchase-flow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,iBAAiB,EAGlB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAQlE,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,UAAU,EACN,CAAC,CAAC,MAAM,EAAE,OAAO,aAAa,EAAE,wBAAwB,KAAK,MAAM,IAAI,CAAC,GACxE,CAAC,CAAC,MAAM,EAAE,OAAO,aAAa,EAAE,6BAA6B,KAAK,MAAM,IAAI,CAAC,CAAC;IAClF,WAAW,EAAE,mBAAmB,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsJxF"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { runEventFlow } from "../event-flow.js";
|
|
2
|
+
import {} from "./grant-coordinator.js";
|
|
3
|
+
import { toIapErrorCode, toIapErrorMessage } from "./platform-contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared purchase flow for the /rn and /web IAP adapters, built on the
|
|
6
|
+
* common event-flow base:
|
|
7
|
+
*
|
|
8
|
+
* - The platform's `processProductGrant` slot wraps the consumer-injected
|
|
9
|
+
* server grant callback (through the coordinator's dedupe). The platform
|
|
10
|
+
* receives `true` only after the callback resolves.
|
|
11
|
+
* - Completion requires the success event's orderId to match the order the
|
|
12
|
+
* grant confirmed for THIS flow. A grant for a different order ends the
|
|
13
|
+
* flow as `failed` with `ORDER_MISMATCH`, and a success event arriving
|
|
14
|
+
* before the grant settles is parked until the grant for the same order
|
|
15
|
+
* confirms — a success event alone never completes a purchase.
|
|
16
|
+
* - A failed or throwing grant ends the flow as `grant_failed`; a purchase
|
|
17
|
+
* is never reported successful over a failed server grant.
|
|
18
|
+
* - `USER_CANCELED` maps to `canceled`; other SDK errors to `failed`.
|
|
19
|
+
* - Timeout yields `unknown` (with the known orderId when available): the
|
|
20
|
+
* server grant may already be in progress — resolve it through server
|
|
21
|
+
* verification and the pending-order recovery flow. Late events after
|
|
22
|
+
* any terminal state are ignored.
|
|
23
|
+
*/
|
|
24
|
+
export function runPurchaseFlow(options) {
|
|
25
|
+
const { startOrder, coordinator, sku, offerId, subscription, timeoutMs } = options;
|
|
26
|
+
// Per-flow state, shared by the register callbacks and the reducer.
|
|
27
|
+
let observedOrderId;
|
|
28
|
+
let observedSubscriptionId;
|
|
29
|
+
let confirmedOrderId;
|
|
30
|
+
let confirmedSubscriptionId;
|
|
31
|
+
let stashedSuccess;
|
|
32
|
+
let grantFailure;
|
|
33
|
+
const evaluate = () => {
|
|
34
|
+
if (grantFailure) {
|
|
35
|
+
return { done: true, result: { status: "grant_failed", ...grantFailure } };
|
|
36
|
+
}
|
|
37
|
+
if (stashedSuccess && confirmedOrderId !== undefined) {
|
|
38
|
+
if (confirmedOrderId !== stashedSuccess.orderId) {
|
|
39
|
+
return {
|
|
40
|
+
done: true,
|
|
41
|
+
result: {
|
|
42
|
+
status: "failed",
|
|
43
|
+
code: "ORDER_MISMATCH",
|
|
44
|
+
reason: `grant confirmed order ${confirmedOrderId} but the success event reported ${stashedSuccess.orderId}`
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
done: true,
|
|
50
|
+
result: {
|
|
51
|
+
status: "completed",
|
|
52
|
+
orderId: confirmedOrderId,
|
|
53
|
+
...(confirmedSubscriptionId !== undefined
|
|
54
|
+
? { subscriptionId: confirmedSubscriptionId }
|
|
55
|
+
: {}),
|
|
56
|
+
success: stashedSuccess
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { done: false };
|
|
61
|
+
};
|
|
62
|
+
return runEventFlow({
|
|
63
|
+
timeoutMs,
|
|
64
|
+
onTimeout: () => ({
|
|
65
|
+
status: "unknown",
|
|
66
|
+
// Include the order the platform already handed to the grant slot,
|
|
67
|
+
// even when the grant itself has not settled — consumers need it to
|
|
68
|
+
// verify and recover the uncertain purchase.
|
|
69
|
+
orderId: confirmedOrderId ?? observedOrderId ?? stashedSuccess?.orderId,
|
|
70
|
+
...(observedSubscriptionId !== undefined || confirmedSubscriptionId !== undefined
|
|
71
|
+
? { subscriptionId: confirmedSubscriptionId ?? observedSubscriptionId }
|
|
72
|
+
: {}),
|
|
73
|
+
reason: "purchase flow timed out; the server grant may still be in progress — verify the order server-side and recover it via pending orders"
|
|
74
|
+
}),
|
|
75
|
+
register: (emit) => {
|
|
76
|
+
const processProductGrant = async (params) => {
|
|
77
|
+
observedOrderId = params.orderId;
|
|
78
|
+
observedSubscriptionId = params.subscriptionId;
|
|
79
|
+
try {
|
|
80
|
+
await coordinator.run({
|
|
81
|
+
orderId: params.orderId,
|
|
82
|
+
sku,
|
|
83
|
+
...(params.subscriptionId !== undefined
|
|
84
|
+
? { subscriptionId: params.subscriptionId }
|
|
85
|
+
: {})
|
|
86
|
+
});
|
|
87
|
+
confirmedOrderId = params.orderId;
|
|
88
|
+
confirmedSubscriptionId = params.subscriptionId;
|
|
89
|
+
emit({ kind: "grantSettled", orderId: params.orderId, ok: true });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
grantFailure = {
|
|
94
|
+
orderId: params.orderId,
|
|
95
|
+
reason: `server grant callback failed: ${toIapErrorMessage(error)}`
|
|
96
|
+
};
|
|
97
|
+
emit({ kind: "grantSettled", orderId: params.orderId, ok: false });
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const params = {
|
|
102
|
+
options: {
|
|
103
|
+
sku,
|
|
104
|
+
...(offerId !== undefined && offerId !== null ? { offerId } : {}),
|
|
105
|
+
processProductGrant
|
|
106
|
+
},
|
|
107
|
+
onEvent: (event) => {
|
|
108
|
+
if (event.type === "success") {
|
|
109
|
+
emit({ kind: "sdkSuccess", data: event.data });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
onError: (error) => {
|
|
113
|
+
emit({ kind: "sdkError", error });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return startOrder(params);
|
|
117
|
+
},
|
|
118
|
+
reduce: (event) => {
|
|
119
|
+
switch (event.kind) {
|
|
120
|
+
case "sdkSuccess":
|
|
121
|
+
stashedSuccess = event.data;
|
|
122
|
+
return evaluate();
|
|
123
|
+
case "grantSettled":
|
|
124
|
+
return evaluate();
|
|
125
|
+
case "sdkError": {
|
|
126
|
+
if (grantFailure) {
|
|
127
|
+
// The server grant failed first; that outcome wins over the
|
|
128
|
+
// platform's error report.
|
|
129
|
+
return evaluate();
|
|
130
|
+
}
|
|
131
|
+
const code = toIapErrorCode(event.error);
|
|
132
|
+
if (confirmedOrderId !== undefined ||
|
|
133
|
+
observedOrderId !== undefined ||
|
|
134
|
+
stashedSuccess !== undefined) {
|
|
135
|
+
// The order is already identified (grant ran, is running, or the
|
|
136
|
+
// success event named it) — an SDK error here is not a
|
|
137
|
+
// definitive failure. Report unknown so the caller verifies
|
|
138
|
+
// server-side instead of retrying into a second paid order.
|
|
139
|
+
return {
|
|
140
|
+
done: true,
|
|
141
|
+
result: {
|
|
142
|
+
status: "unknown",
|
|
143
|
+
orderId: confirmedOrderId ?? observedOrderId ?? stashedSuccess?.orderId,
|
|
144
|
+
...(confirmedSubscriptionId !== undefined ||
|
|
145
|
+
observedSubscriptionId !== undefined
|
|
146
|
+
? { subscriptionId: confirmedSubscriptionId ?? observedSubscriptionId }
|
|
147
|
+
: {}),
|
|
148
|
+
reason: `SDK error after the order was identified (${code ?? toIapErrorMessage(event.error)}); the order may already be granted — verify server-side and recover it via pending orders`
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (code === "USER_CANCELED") {
|
|
153
|
+
return { done: true, result: { status: "canceled" } };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
done: true,
|
|
157
|
+
result: {
|
|
158
|
+
status: "failed",
|
|
159
|
+
...(code !== undefined ? { code } : {}),
|
|
160
|
+
reason: toIapErrorMessage(event.error)
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type SdkAnonymousKey, type SdkLoginResult } from "../index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Internal platform contract for the login/anonymous-identity adapters,
|
|
4
|
+
* shared by /rn and /web. Each entry declares its own structural copy of
|
|
5
|
+
* the official SDK surface; this module is the common engine boundary and
|
|
6
|
+
* is never exported from a public subpath.
|
|
7
|
+
*/
|
|
8
|
+
export interface TossAuthLike {
|
|
9
|
+
login: FunctionWithSupport<() => Promise<unknown>> | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface UserLike {
|
|
12
|
+
getAnonymousKey: FunctionWithSupport<() => Promise<unknown>> | undefined;
|
|
13
|
+
}
|
|
14
|
+
type FunctionWithSupport<F> = F & {
|
|
15
|
+
isSupported?: () => boolean;
|
|
16
|
+
};
|
|
17
|
+
export interface IdentityPlatformSdk {
|
|
18
|
+
TossAuth?: TossAuthLike;
|
|
19
|
+
User?: UserLike;
|
|
20
|
+
}
|
|
21
|
+
export type IdentityPlatformLoader = () => Promise<{
|
|
22
|
+
available: true;
|
|
23
|
+
module: IdentityPlatformSdk;
|
|
24
|
+
} | {
|
|
25
|
+
available: false;
|
|
26
|
+
reason: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Runs the platform login call and validates the result: the
|
|
30
|
+
* authorizationCode must be a non-empty string and the referrer one of the
|
|
31
|
+
* documented values, both preserved verbatim. SDK rejections propagate to
|
|
32
|
+
* the caller unchanged; unsupported environments reject with
|
|
33
|
+
* SdkError("UNSUPPORTED") when the platform exposes isSupported.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runSdkLogin(platform: IdentityPlatformSdk): Promise<SdkLoginResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Runs the anonymous key lookup and validates the documented result shape
|
|
38
|
+
* ({ type: "HASH", hash }): sentinel values, missing or malformed results
|
|
39
|
+
* reject with SdkError("INVALID_ANONYMOUS_KEY") — the adapter never
|
|
40
|
+
* fabricates a key. SDK rejections propagate unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export declare function runSdkGetAnonymousKey(platform: IdentityPlatformSdk): Promise<SdkAnonymousKey>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=platform-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-contract.d.ts","sourceRoot":"","sources":["../../src/identity/platform-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAElF;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,mBAAmB,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;CAChE;AAED,MAAM,WAAW,QAAQ;IACvB,eAAe,EAAE,mBAAmB,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAC;AAGlE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,OAAO,CAChD;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CACxF,CAAC;AAIF;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA+BzB;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,eAAe,CAAC,CA2B1B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SdkError } from "../index.js";
|
|
2
|
+
const REFERRERS = new Set(["DEFAULT", "SANDBOX"]);
|
|
3
|
+
/**
|
|
4
|
+
* Runs the platform login call and validates the result: the
|
|
5
|
+
* authorizationCode must be a non-empty string and the referrer one of the
|
|
6
|
+
* documented values, both preserved verbatim. SDK rejections propagate to
|
|
7
|
+
* the caller unchanged; unsupported environments reject with
|
|
8
|
+
* SdkError("UNSUPPORTED") when the platform exposes isSupported.
|
|
9
|
+
*/
|
|
10
|
+
export async function runSdkLogin(platform) {
|
|
11
|
+
const login = platform.TossAuth?.login;
|
|
12
|
+
if (typeof login !== "function") {
|
|
13
|
+
throw new SdkError("UNSUPPORTED", "the installed SDK does not expose TossAuth.login");
|
|
14
|
+
}
|
|
15
|
+
if (typeof login.isSupported === "function" && !login.isSupported()) {
|
|
16
|
+
throw new SdkError("UNSUPPORTED", "login is not supported on this app version");
|
|
17
|
+
}
|
|
18
|
+
// Receiver-preserving member call for class-instance modules.
|
|
19
|
+
const result = await platform.TossAuth.login();
|
|
20
|
+
const authorizationCode = typeof result === "object" && result !== null
|
|
21
|
+
? result.authorizationCode
|
|
22
|
+
: undefined;
|
|
23
|
+
const referrer = typeof result === "object" && result !== null
|
|
24
|
+
? result.referrer
|
|
25
|
+
: undefined;
|
|
26
|
+
if (typeof authorizationCode !== "string" || !authorizationCode.trim()) {
|
|
27
|
+
throw new SdkError("INVALID_LOGIN_RESULT", "login result did not include a non-blank authorizationCode string");
|
|
28
|
+
}
|
|
29
|
+
if (typeof referrer !== "string" || !REFERRERS.has(referrer)) {
|
|
30
|
+
throw new SdkError("INVALID_LOGIN_RESULT", `login result carried an unrecognized referrer: ${String(referrer)}`);
|
|
31
|
+
}
|
|
32
|
+
return { authorizationCode, referrer: referrer };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runs the anonymous key lookup and validates the documented result shape
|
|
36
|
+
* ({ type: "HASH", hash }): sentinel values, missing or malformed results
|
|
37
|
+
* reject with SdkError("INVALID_ANONYMOUS_KEY") — the adapter never
|
|
38
|
+
* fabricates a key. SDK rejections propagate unchanged.
|
|
39
|
+
*/
|
|
40
|
+
export async function runSdkGetAnonymousKey(platform) {
|
|
41
|
+
const getAnonymousKey = platform.User?.getAnonymousKey;
|
|
42
|
+
if (typeof getAnonymousKey !== "function") {
|
|
43
|
+
throw new SdkError("UNSUPPORTED", "the installed SDK does not expose User.getAnonymousKey");
|
|
44
|
+
}
|
|
45
|
+
if (typeof getAnonymousKey.isSupported === "function" &&
|
|
46
|
+
!getAnonymousKey.isSupported()) {
|
|
47
|
+
throw new SdkError("UNSUPPORTED", "anonymous keys are not supported in this environment");
|
|
48
|
+
}
|
|
49
|
+
const result = await platform.User.getAnonymousKey();
|
|
50
|
+
const hash = typeof result === "object" && result !== null
|
|
51
|
+
? result.hash
|
|
52
|
+
: undefined;
|
|
53
|
+
const type = typeof result === "object" && result !== null
|
|
54
|
+
? result.type
|
|
55
|
+
: undefined;
|
|
56
|
+
if (type !== "HASH" || typeof hash !== "string" || !hash.trim()) {
|
|
57
|
+
throw new SdkError("INVALID_ANONYMOUS_KEY", `anonymous key result was not { type: "HASH", hash } with a non-blank hash: received ${describe(result)}`);
|
|
58
|
+
}
|
|
59
|
+
return { type: "HASH", hash };
|
|
60
|
+
}
|
|
61
|
+
function describe(value) {
|
|
62
|
+
if (value === undefined)
|
|
63
|
+
return "undefined";
|
|
64
|
+
if (value === null)
|
|
65
|
+
return "null";
|
|
66
|
+
if (typeof value === "object") {
|
|
67
|
+
const type = value.type;
|
|
68
|
+
return typeof type === "string" ? `{ type: ${type} }` : "an unrecognized object";
|
|
69
|
+
}
|
|
70
|
+
return typeof value;
|
|
71
|
+
}
|