@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 imjlk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @ait-kit/sdk
|
|
2
|
+
|
|
3
|
+
Frontend SDK adapters for Apps in Toss mini apps: shared contracts plus
|
|
4
|
+
per-runtime entry points.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @ait-kit/sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Entry points
|
|
11
|
+
|
|
12
|
+
| Entry | Use it for |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `@ait-kit/sdk` (root) | Runtime-neutral shared contracts: ad + IAP types, `SdkError`. Importable anywhere — plain Node, web, React Native — with no official SDK installed. |
|
|
15
|
+
| `@ait-kit/sdk/rn` | React Native adapters (full-screen ads, IAP). Requires the official `@apps-in-toss/framework`, declared as an **optional peer** and imported lazily. Never requires the web SDK. |
|
|
16
|
+
| `@ait-kit/sdk/web` | Web adapters (IAP). Requires the official `@apps-in-toss/web-framework`, declared as an **optional peer** and imported lazily. Never requires the React Native SDK. |
|
|
17
|
+
|
|
18
|
+
The two platform entries share one internal engine but keep completely
|
|
19
|
+
separate SDK connections — in JavaScript and in the shipped type
|
|
20
|
+
declarations — so an `/rn` consumer never installs the web package and vice
|
|
21
|
+
versa.
|
|
22
|
+
|
|
23
|
+
## In-app purchases (RN + Web)
|
|
24
|
+
|
|
25
|
+
Both entries expose the same IAP surface; only the loader differs:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createReactNativeIap } from "@ait-kit/sdk/rn"; // or:
|
|
29
|
+
import { createWebIap } from "@ait-kit/sdk/web";
|
|
30
|
+
|
|
31
|
+
const iap = createReactNativeIap({
|
|
32
|
+
// The ONLY place product delivery happens. Resolve only after YOUR
|
|
33
|
+
// server verified the order with the provider and persisted the grant
|
|
34
|
+
// (see @ait-kit/api-core's iapOrderStatus for server-side verification).
|
|
35
|
+
grant: async ({ orderId, sku }) => {
|
|
36
|
+
const response = await fetch("/api/iap/grant", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: JSON.stringify({ orderId, sku })
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
// Resolve-only-on-success is part of the contract: never report a
|
|
42
|
+
// grant your server did not verify and persist.
|
|
43
|
+
throw new Error(`grant request failed: HTTP ${response.status}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 1. Start a purchase.
|
|
49
|
+
const result = await iap.purchaseOneTime("SKU_100_COINS");
|
|
50
|
+
// result.status: "completed" | "canceled" | "failed"
|
|
51
|
+
// | "grant_failed" | "unknown"
|
|
52
|
+
|
|
53
|
+
// 2. Subscriptions work the same way.
|
|
54
|
+
await iap.purchaseSubscription("SKU_PREMIUM", offerId);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`completed` requires **both** the platform's success event and your grant
|
|
58
|
+
callback having resolved for the **same order**: a success event alone never
|
|
59
|
+
completes a purchase, a grant for a different order ends as
|
|
60
|
+
`failed`/`ORDER_MISMATCH`, and a failed server grant ends as `grant_failed`
|
|
61
|
+
— never as purchase success. `unknown` (e.g. timeout) means the grant may
|
|
62
|
+
still be in progress; the client timeout does not cancel it.
|
|
63
|
+
|
|
64
|
+
### Duplicate grant control (client-side, scoped to one adapter)
|
|
65
|
+
|
|
66
|
+
- Concurrent grants for the same order share one in-flight call — only for
|
|
67
|
+
identical targets (same `orderId` and `sku`; callers that do not know the
|
|
68
|
+
`subscriptionId`, like pending-order recovery, may join a cached
|
|
69
|
+
subscription grant). Explicitly conflicting duplicates reject.
|
|
70
|
+
- A successfully granted order is reused within the adapter's scope for the
|
|
71
|
+
same target (a later recovery or duplicate success does not re-run your
|
|
72
|
+
server call).
|
|
73
|
+
- Failed grants are not cached — the next attempt retries for real.
|
|
74
|
+
- This only reduces duplicate client work: **server-side grant idempotency
|
|
75
|
+
is still mandatory** (verify the order, persist exactly once).
|
|
76
|
+
|
|
77
|
+
### Pending-order recovery (never automatic)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const { orders } = await iap.getPendingOrders();
|
|
81
|
+
for (const order of orders) {
|
|
82
|
+
// Runs (or reuses) the server grant FIRST; the platform's
|
|
83
|
+
// completeProductGrant notification is sent only after it confirms.
|
|
84
|
+
const outcome = await iap.recoverPendingOrder(order);
|
|
85
|
+
// outcome.status: "completed" | "grant_failed" | "notify_failed"
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
SDK initialization never grants or completes pending orders by itself.
|
|
90
|
+
|
|
91
|
+
### Feature matrix
|
|
92
|
+
|
|
93
|
+
| Capability | `/rn` | `/web` | Notes |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| Product list | ✅ | ✅ | one-time + subscription together |
|
|
96
|
+
| One-time purchase | ✅ | ✅ | grant callback contract applies |
|
|
97
|
+
| Subscription purchase | ✅ | ✅ | `offerId` optional; `subscriptionId` surfaced on completion |
|
|
98
|
+
| Pending orders | ✅ | ✅ | recovery is consumer-driven |
|
|
99
|
+
| Grant completion notify | ✅ | ✅ | sent only after server grant confirms |
|
|
100
|
+
| Full-screen ads | ✅ | ➖ | ads are RN-only today |
|
|
101
|
+
| Notification agreement | ✅ | ✅ | event-based, one template per request |
|
|
102
|
+
| Share link / share sheet | ✅ | ✅ | `intoss://` paths; `closed` ≠ shared |
|
|
103
|
+
| Unsupported app version | `SdkError("UNSUPPORTED")` | same | per-function `isSupported` gates |
|
|
104
|
+
|
|
105
|
+
Notification and sharing adapters ship in both entries (see below).
|
|
106
|
+
|
|
107
|
+
## Login, anonymous identity, and storage (RN + Web)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createReactNativeIdentity, createReactNativeStorage } from "@ait-kit/sdk/rn";
|
|
111
|
+
// or: import { createWebIdentity, createWebStorage } from "@ait-kit/sdk/web";
|
|
112
|
+
|
|
113
|
+
const identity = createReactNativeIdentity();
|
|
114
|
+
|
|
115
|
+
// 1. Login: the adapter validates and preserves authorizationCode/referrer.
|
|
116
|
+
const login = await identity.login();
|
|
117
|
+
// Forward BOTH values to YOUR server for the token exchange
|
|
118
|
+
// (@ait-kit/api-core exposes the server-side endpoint) and create the
|
|
119
|
+
// application session there. The adapter never performs the exchange.
|
|
120
|
+
|
|
121
|
+
// 2. Anonymous key: { type: "HASH", hash } or a typed error — never a
|
|
122
|
+
// fabricated key.
|
|
123
|
+
const anon = await identity.getAnonymousKey();
|
|
124
|
+
|
|
125
|
+
// 3. Storage: string values, verbatim keys.
|
|
126
|
+
const storage = createReactNativeStorage();
|
|
127
|
+
await storage.set("cart:items", "[]");
|
|
128
|
+
const raw = await storage.get("cart:items"); // string | null
|
|
129
|
+
await storage.remove("cart:items");
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Contracts:
|
|
133
|
+
|
|
134
|
+
- **Login** results are validated (`authorizationCode` non-empty, `referrer`
|
|
135
|
+
one of the documented values) and preserved verbatim. Malformed results
|
|
136
|
+
reject with `SdkError("INVALID_LOGIN_RESULT")`; unsupported environments
|
|
137
|
+
with `UNSUPPORTED`; SDK rejections propagate unchanged.
|
|
138
|
+
- **Anonymous key** results must be the documented `{ type: "HASH", hash }`
|
|
139
|
+
shape. Anything else (including sentinel values) rejects with
|
|
140
|
+
`SdkError("INVALID_ANONYMOUS_KEY")` — the adapter never invents a key.
|
|
141
|
+
- **Storage** keys and string values pass through byte-for-byte: no
|
|
142
|
+
namespace prefixing, no key transformation. Compose namespaced keys
|
|
143
|
+
yourself (e.g. `cart:items`). `get` resolves `null` for missing keys;
|
|
144
|
+
`set`/`remove` rejections propagate so failures stay observable.
|
|
145
|
+
- **No environment guessing**: the defaults never substitute a fake login
|
|
146
|
+
or storage based on the runtime. For development, inject an explicit
|
|
147
|
+
replacement via the `framework` option.
|
|
148
|
+
- Session invalidation when the anonymous identifier changes, migration of
|
|
149
|
+
previously stored keys, and bootstrap sequencing stay with the consumer.
|
|
150
|
+
|
|
151
|
+
## Notification agreement and sharing (RN + Web)
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import {
|
|
155
|
+
createReactNativeNotification,
|
|
156
|
+
createReactNativeShare
|
|
157
|
+
} from "@ait-kit/sdk/rn";
|
|
158
|
+
// or: import { createWebNotification, createWebShare } from "@ait-kit/sdk/web";
|
|
159
|
+
|
|
160
|
+
// 1. Notification agreement: one template, one request.
|
|
161
|
+
const notification = createReactNativeNotification();
|
|
162
|
+
const result = await notification.requestAgreement("TEMPLATE_CODE");
|
|
163
|
+
// result.status: "agreed" (newAgreement | alreadyAgreed) | "rejected"
|
|
164
|
+
// | "failed" | "timeout"
|
|
165
|
+
// result.templateCode and result.sourceEvent are preserved verbatim. The
|
|
166
|
+
// outcome describes ONLY this request — it is not the user's global
|
|
167
|
+
// notification setting, nor any server-persisted consent state. Syncing
|
|
168
|
+
// consent to your server (and any smart-message sending) is your job.
|
|
169
|
+
|
|
170
|
+
// 2. Share links: intoss:// deeplink paths, optional OG image.
|
|
171
|
+
const share = createReactNativeShare();
|
|
172
|
+
const link = await share.createLink("intoss://my-app/about", "https://cdn/og.png");
|
|
173
|
+
|
|
174
|
+
// 3. Share sheet: "closed" means the sheet flow ended — nothing more.
|
|
175
|
+
const uiResult = await share.sendMessage(`check this out ${link}`);
|
|
176
|
+
// uiResult.status: "closed" | "failed" — closed does NOT prove the user
|
|
177
|
+
// shared and never grants share-reward eligibility.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Contracts:
|
|
181
|
+
|
|
182
|
+
- **Agreement** runs on the shared event-flow base: settle-once, duplicate/
|
|
183
|
+
late-event immunity, single error-swallowing cleanup, registration-throw
|
|
184
|
+
recovery, one overall deadline (`timeoutMs`, default 60s). SDK errors keep
|
|
185
|
+
their `code`/`reason`; timeouts report `timeout` with the template.
|
|
186
|
+
- **Share links** validate the documented `intoss://` path contract
|
|
187
|
+
(`INVALID_SHARE_PATH` otherwise) and pass the resolved link through
|
|
188
|
+
verbatim. OG image generation is the consumer's concern.
|
|
189
|
+
- **Share sheet** resolution is `closed`, not "completed" — reward grants
|
|
190
|
+
and completion tracking stay with the consumer.
|
|
191
|
+
- Unsupported surfaces/app versions reject with `SdkError("UNSUPPORTED")`.
|
|
192
|
+
|
|
193
|
+
## React Native full-screen ads
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { createReactNativeAds } from "@ait-kit/sdk/rn";
|
|
197
|
+
|
|
198
|
+
const ads = createReactNativeAds();
|
|
199
|
+
await ads.loadFullScreenAd("AD_GROUP_ID"); // joins an in-flight duplicate load
|
|
200
|
+
const result = await ads.showFullScreenAd("AD_GROUP_ID");
|
|
201
|
+
|
|
202
|
+
if (result.status === "rewarded") {
|
|
203
|
+
// result.reward came from the provider's userEarnedReward event — nothing
|
|
204
|
+
// else (load success, show success, dismissal) ever grants a reward.
|
|
205
|
+
// Verify against YOUR server before crediting anything.
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Behavior:
|
|
210
|
+
|
|
211
|
+
- Load state is tracked per ad type + ad unit. Concurrent duplicate loads
|
|
212
|
+
share one registration; failed loads clear the slot so the next attempt
|
|
213
|
+
retries instead of being blocked.
|
|
214
|
+
- Showing requires a completed load (`AD_NOT_LOADED` otherwise). The same ad
|
|
215
|
+
group cannot be shown concurrently (`AD_ALREADY_SHOWING`). Full-screen ads
|
|
216
|
+
are single-use: after a show flow ends (rewarded, dismissed, failed,
|
|
217
|
+
timeout), a fresh load is required.
|
|
218
|
+
- The whole show flow has one deadline (`showTimeoutMs`, default 60s);
|
|
219
|
+
`loadTimeoutMs` (default 30s) bounds loads.
|
|
220
|
+
- Missing or failed-to-import framework → `SdkError("SDK_UNAVAILABLE")`;
|
|
221
|
+
unsupported app versions → `SdkError("UNSUPPORTED")`. Load failures are
|
|
222
|
+
never permanently cached — a later call retries the import.
|
|
223
|
+
- By default the adapter never fabricates ad success or reward results:
|
|
224
|
+
outcomes come only from the provider's events.
|
|
225
|
+
|
|
226
|
+
## Responsibility split
|
|
227
|
+
|
|
228
|
+
The SDK provides ad behavior and outcomes. Your application owns server-side
|
|
229
|
+
ad reward requests, user session checks, payout limits, and ledger updates.
|
|
230
|
+
(Login/anonymous-key helpers and storage arrive in later entries, as do
|
|
231
|
+
notification and sharing.)
|
|
232
|
+
|
|
233
|
+
## Versioning
|
|
234
|
+
|
|
235
|
+
`@ait-kit/sdk` is versioned independently from the server API packages
|
|
236
|
+
(`@ait-kit/api-*`), which release in lockstep with each other.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal, runtime-neutral event flow used by the ad adapters (and later
|
|
3
|
+
* IAP flows). Not exported from any public subpath.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees:
|
|
6
|
+
* - The promise settles exactly once: success, failure, timeout, or
|
|
7
|
+
* cancellation are terminal and later events are ignored.
|
|
8
|
+
* - Cleanup runs at most once. Cleanup exceptions never block settlement
|
|
9
|
+
* and never change an already-decided result.
|
|
10
|
+
* - SDK registration functions that invoke a callback synchronously and
|
|
11
|
+
* then return the cleanup function are supported: the registration
|
|
12
|
+
* wrapper defers cleanup attachment until register returns.
|
|
13
|
+
* - If the registration function itself throws, timers and any partially
|
|
14
|
+
* registered resources are cleaned up and the flow rejects.
|
|
15
|
+
*/
|
|
16
|
+
export type EventFlowResolution<TEvent, TResult> = {
|
|
17
|
+
done: true;
|
|
18
|
+
result: TResult;
|
|
19
|
+
} | {
|
|
20
|
+
done: false;
|
|
21
|
+
} | undefined;
|
|
22
|
+
export interface EventFlowOptions<TEvent, TResult> {
|
|
23
|
+
/** Registers SDK listeners; receives the flow's emit function. Returns an optional cleanup. */
|
|
24
|
+
register: (emit: (event: TEvent) => void) => (() => void) | void;
|
|
25
|
+
/** Maps an emitted event to a terminal result; anything else keeps the flow running. */
|
|
26
|
+
reduce: (event: TEvent) => EventFlowResolution<TEvent, TResult>;
|
|
27
|
+
/** Produces the terminal result when timeoutMs elapses without a resolution. */
|
|
28
|
+
onTimeout: () => TResult;
|
|
29
|
+
/** Overall deadline; omit or pass 0 to disable. */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Hooks for tests: scheduling and clock injection. */
|
|
32
|
+
schedule?: typeof setTimeout;
|
|
33
|
+
cancelSchedule?: typeof clearTimeout;
|
|
34
|
+
}
|
|
35
|
+
export declare function runEventFlow<TEvent, TResult>(options: EventFlowOptions<TEvent, TResult>): Promise<TResult>;
|
|
36
|
+
//# sourceMappingURL=event-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-flow.d.ts","sourceRoot":"","sources":["../src/event-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,mBAAmB,CAAC,MAAM,EAAE,OAAO,IAC3C;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf,SAAS,CAAC;AAEd,MAAM,WAAW,gBAAgB,CAAC,MAAM,EAAE,OAAO;IAC/C,+FAA+F;IAC/F,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IACjE,wFAAwF;IACxF,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChE,gFAAgF;IAChF,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,UAAU,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,YAAY,CAAC;CACtC;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAqF1G"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal, runtime-neutral event flow used by the ad adapters (and later
|
|
3
|
+
* IAP flows). Not exported from any public subpath.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees:
|
|
6
|
+
* - The promise settles exactly once: success, failure, timeout, or
|
|
7
|
+
* cancellation are terminal and later events are ignored.
|
|
8
|
+
* - Cleanup runs at most once. Cleanup exceptions never block settlement
|
|
9
|
+
* and never change an already-decided result.
|
|
10
|
+
* - SDK registration functions that invoke a callback synchronously and
|
|
11
|
+
* then return the cleanup function are supported: the registration
|
|
12
|
+
* wrapper defers cleanup attachment until register returns.
|
|
13
|
+
* - If the registration function itself throws, timers and any partially
|
|
14
|
+
* registered resources are cleaned up and the flow rejects.
|
|
15
|
+
*/
|
|
16
|
+
export function runEventFlow(options) {
|
|
17
|
+
const { register, reduce, onTimeout, timeoutMs = 0 } = options;
|
|
18
|
+
const schedule = options.schedule ?? setTimeout;
|
|
19
|
+
const cancelSchedule = options.cancelSchedule ?? clearTimeout;
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let settled = false;
|
|
22
|
+
let cleanedUp = false;
|
|
23
|
+
let timer;
|
|
24
|
+
let registeredCleanup;
|
|
25
|
+
const runCleanup = () => {
|
|
26
|
+
if (cleanedUp)
|
|
27
|
+
return;
|
|
28
|
+
cleanedUp = true;
|
|
29
|
+
if (timer !== undefined) {
|
|
30
|
+
cancelSchedule(timer);
|
|
31
|
+
timer = undefined;
|
|
32
|
+
}
|
|
33
|
+
if (registeredCleanup) {
|
|
34
|
+
const cleanup = registeredCleanup;
|
|
35
|
+
registeredCleanup = undefined;
|
|
36
|
+
try {
|
|
37
|
+
cleanup();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Cleanup failures must never block or alter the settled result.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const settle = (result) => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
runCleanup();
|
|
49
|
+
resolve(result);
|
|
50
|
+
};
|
|
51
|
+
const fail = (error) => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return;
|
|
54
|
+
settled = true;
|
|
55
|
+
runCleanup();
|
|
56
|
+
reject(error);
|
|
57
|
+
};
|
|
58
|
+
const emit = (event) => {
|
|
59
|
+
if (settled)
|
|
60
|
+
return;
|
|
61
|
+
let resolution;
|
|
62
|
+
try {
|
|
63
|
+
resolution = reduce(event);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// A throwing reducer must fail the flow instead of leaving it
|
|
67
|
+
// pending until the deadline (which may be disabled).
|
|
68
|
+
fail(error);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (resolution?.done) {
|
|
72
|
+
settle(resolution.result);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
if (timeoutMs > 0) {
|
|
76
|
+
timer = schedule(() => {
|
|
77
|
+
settle(onTimeout());
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
registeredCleanup = register(emit);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
fail(error);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// register() may have emitted synchronously and settled the flow before
|
|
88
|
+
// its cleanup existed; runCleanup already fired without it, so invoke
|
|
89
|
+
// the returned cleanup here with the same error-swallowing guarantee.
|
|
90
|
+
if (settled && registeredCleanup) {
|
|
91
|
+
const cleanup = registeredCleanup;
|
|
92
|
+
registeredCleanup = undefined;
|
|
93
|
+
try {
|
|
94
|
+
cleanup();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Same guarantee as runCleanup.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type IapGrantCallback, type IapProduct } from "../index.js";
|
|
2
|
+
import type { IapPendingOrder, IapPurchaseResult } from "../index.js";
|
|
3
|
+
import { type IapPlatformLoader } from "./platform-contract.js";
|
|
4
|
+
export interface IapAdapterOptions {
|
|
5
|
+
grant: IapGrantCallback;
|
|
6
|
+
/** Loader for the platform SDK module (already normalized per entry point). */
|
|
7
|
+
loader: IapPlatformLoader;
|
|
8
|
+
/** Overall deadline per purchase flow (default 180000ms; 0 disables). */
|
|
9
|
+
purchaseTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface IapRecoveryResult {
|
|
12
|
+
status: "completed" | "grant_failed" | "notify_failed";
|
|
13
|
+
orderId: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface IapAdapter {
|
|
17
|
+
/** Lists purchasable products (one-time and subscription together). */
|
|
18
|
+
getProductItemList(): Promise<{
|
|
19
|
+
products: IapProduct[];
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Starts a one-time purchase. Resolves `completed` only after the
|
|
23
|
+
* platform success event AND your grant callback confirmed the same
|
|
24
|
+
* order.
|
|
25
|
+
*/
|
|
26
|
+
purchaseOneTime(sku: string): Promise<IapPurchaseResult>;
|
|
27
|
+
/** Starts a subscription purchase (optionally with a chosen offer). */
|
|
28
|
+
purchaseSubscription(sku: string, offerId?: string): Promise<IapPurchaseResult>;
|
|
29
|
+
/** Lists orders whose payment completed but whose grant was not finished. */
|
|
30
|
+
getPendingOrders(): Promise<{
|
|
31
|
+
orders: IapPendingOrder[];
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Recovers one pending order: runs (or reuses) the server grant for the
|
|
35
|
+
* order first, and only after it confirms calls the platform's
|
|
36
|
+
* grant-completion notification. Never invoked automatically — the
|
|
37
|
+
* consumer drives recovery.
|
|
38
|
+
*/
|
|
39
|
+
recoverPendingOrder(order: IapPendingOrder): Promise<IapRecoveryResult>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Platform-neutral IAP adapter used by both entry points. The consumer's
|
|
43
|
+
* `grant` callback is the only place product delivery happens; the adapter
|
|
44
|
+
* contains no backend URLs, auth, or persistence.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createIapAdapter(options: IapAdapterOptions): IapAdapter;
|
|
47
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/iap/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EAErB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,KAAK,iBAAiB,EAA8B,MAAM,wBAAwB,CAAC;AAG5F,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,gBAAgB,CAAC;IACxB,+EAA+E;IAC/E,MAAM,EAAE,iBAAiB,CAAC;IAC1B,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,eAAe,CAAC;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,kBAAkB,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC;IAC1D;;;;OAIG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzD,uEAAuE;IACvE,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChF,6EAA6E;IAC7E,gBAAgB,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC,CAAC;IAC3D;;;;;OAKG;IACH,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACzE;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CA0JvE"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { SdkError } from "../index.js";
|
|
2
|
+
import { IapGrantCoordinator } from "./grant-coordinator.js";
|
|
3
|
+
import {} from "./platform-contract.js";
|
|
4
|
+
import { runPurchaseFlow } from "./purchase-flow.js";
|
|
5
|
+
const DEFAULT_PURCHASE_TIMEOUT_MS = 180_000;
|
|
6
|
+
/**
|
|
7
|
+
* Platform-neutral IAP adapter used by both entry points. The consumer's
|
|
8
|
+
* `grant` callback is the only place product delivery happens; the adapter
|
|
9
|
+
* contains no backend URLs, auth, or persistence.
|
|
10
|
+
*/
|
|
11
|
+
export function createIapAdapter(options) {
|
|
12
|
+
const purchaseTimeoutMs = options.purchaseTimeoutMs ?? DEFAULT_PURCHASE_TIMEOUT_MS;
|
|
13
|
+
const coordinator = new IapGrantCoordinator(options.grant);
|
|
14
|
+
const load = async () => {
|
|
15
|
+
const result = await options.loader();
|
|
16
|
+
if (!result.available) {
|
|
17
|
+
throw new SdkError("SDK_UNAVAILABLE", result.reason);
|
|
18
|
+
}
|
|
19
|
+
return result.module;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Per-operation capability gate: an installed framework may expose some
|
|
23
|
+
* IAP functions but not others, so a missing function is UNSUPPORTED only
|
|
24
|
+
* when that specific operation is requested — never a blanket rejection.
|
|
25
|
+
*/
|
|
26
|
+
const ensureOperation = (fn, label) => {
|
|
27
|
+
if (typeof fn !== "function") {
|
|
28
|
+
throw new SdkError("UNSUPPORTED", `the installed IAP SDK does not expose ${label}`);
|
|
29
|
+
}
|
|
30
|
+
const supported = fn;
|
|
31
|
+
if (typeof supported.isSupported === "function" && !supported.isSupported()) {
|
|
32
|
+
throw new SdkError("UNSUPPORTED", `${label} is not supported on this app version`);
|
|
33
|
+
}
|
|
34
|
+
return fn;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
async getProductItemList() {
|
|
38
|
+
const platform = await load();
|
|
39
|
+
ensureOperation(platform.getProductItemList, "getProductItemList");
|
|
40
|
+
// Called as a member so class-instance modules keep their receiver.
|
|
41
|
+
return platform.getProductItemList();
|
|
42
|
+
},
|
|
43
|
+
purchaseOneTime(sku) {
|
|
44
|
+
return purchaseWithDeadline(async (platform, remainingMs) => {
|
|
45
|
+
ensureOperation(platform.createOneTimePurchaseOrder, "one-time purchases");
|
|
46
|
+
return await runPurchaseFlow({
|
|
47
|
+
startOrder: (params) => platform.createOneTimePurchaseOrder(params),
|
|
48
|
+
coordinator,
|
|
49
|
+
sku,
|
|
50
|
+
subscription: false,
|
|
51
|
+
timeoutMs: remainingMs
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
purchaseSubscription(sku, offerId) {
|
|
56
|
+
return purchaseWithDeadline(async (platform, remainingMs) => {
|
|
57
|
+
ensureOperation(platform.createSubscriptionPurchaseOrder, "subscription purchases");
|
|
58
|
+
return await runPurchaseFlow({
|
|
59
|
+
startOrder: (params) => platform.createSubscriptionPurchaseOrder(params),
|
|
60
|
+
coordinator,
|
|
61
|
+
sku,
|
|
62
|
+
...(offerId !== undefined ? { offerId } : {}),
|
|
63
|
+
subscription: true,
|
|
64
|
+
timeoutMs: remainingMs
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
async getPendingOrders() {
|
|
69
|
+
const platform = await load();
|
|
70
|
+
ensureOperation(platform.getPendingOrders, "getPendingOrders");
|
|
71
|
+
return platform.getPendingOrders();
|
|
72
|
+
},
|
|
73
|
+
async recoverPendingOrder(order) {
|
|
74
|
+
const platform = await load();
|
|
75
|
+
ensureOperation(platform.completeProductGrant, "completeProductGrant");
|
|
76
|
+
try {
|
|
77
|
+
// Server grant confirmation first (deduped within this adapter's
|
|
78
|
+
// scope); the completion notification follows only after it.
|
|
79
|
+
await coordinator.run({ orderId: order.orderId, sku: order.sku });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
83
|
+
return { status: "grant_failed", orderId: order.orderId, reason };
|
|
84
|
+
}
|
|
85
|
+
const notified = await notifyGrantComplete(platform, order.orderId);
|
|
86
|
+
if (!notified.ok) {
|
|
87
|
+
return {
|
|
88
|
+
status: "notify_failed",
|
|
89
|
+
orderId: order.orderId,
|
|
90
|
+
reason: notified.reason
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { status: "completed", orderId: order.orderId };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Runs a purchase under one overall deadline that also covers platform
|
|
98
|
+
* loading. Only the loading phase races the outer deadline — the purchase
|
|
99
|
+
* flow owns the remaining budget exclusively, so its order-aware timeout
|
|
100
|
+
* result is never preempted by a generic one. A stalled loader cannot
|
|
101
|
+
* wedge checkout, and a loader resolving after the deadline never
|
|
102
|
+
* registers the purchase.
|
|
103
|
+
*/
|
|
104
|
+
async function purchaseWithDeadline(run) {
|
|
105
|
+
const timedOut = () => ({
|
|
106
|
+
status: "unknown",
|
|
107
|
+
reason: "purchase flow timed out; the server grant may still be in progress — verify the order server-side and recover it via pending orders"
|
|
108
|
+
});
|
|
109
|
+
if (!(purchaseTimeoutMs > 0)) {
|
|
110
|
+
return run(await load(), 0);
|
|
111
|
+
}
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
let cancelled = false;
|
|
114
|
+
let timer;
|
|
115
|
+
const loadDeadline = new Promise((resolve) => {
|
|
116
|
+
timer = setTimeout(() => {
|
|
117
|
+
cancelled = true;
|
|
118
|
+
resolve(null);
|
|
119
|
+
}, purchaseTimeoutMs);
|
|
120
|
+
});
|
|
121
|
+
let platform;
|
|
122
|
+
try {
|
|
123
|
+
platform = await Promise.race([
|
|
124
|
+
load().then((loaded) => (cancelled ? null : loaded)),
|
|
125
|
+
loadDeadline
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (timer !== undefined)
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
if (!platform || cancelled || Date.now() - startedAt >= purchaseTimeoutMs) {
|
|
133
|
+
// The loader may resolve as an overdue microtask before the timer
|
|
134
|
+
// callback runs; a fresh clock comparison is authoritative.
|
|
135
|
+
return timedOut();
|
|
136
|
+
}
|
|
137
|
+
const remainingMs = Math.max(1, purchaseTimeoutMs - (Date.now() - startedAt));
|
|
138
|
+
const result = await run(platform, remainingMs);
|
|
139
|
+
if (Date.now() - startedAt >= purchaseTimeoutMs) {
|
|
140
|
+
// A synchronous startOrder can block past the deadline and settle the
|
|
141
|
+
// flow before the overdue timer callback runs. The deadline is the
|
|
142
|
+
// deadline: preserve any order identity the result already carries
|
|
143
|
+
// for server-side recovery.
|
|
144
|
+
const identified = result;
|
|
145
|
+
return {
|
|
146
|
+
...timedOut(),
|
|
147
|
+
...(identified.orderId !== undefined ? { orderId: identified.orderId } : {}),
|
|
148
|
+
...(identified.subscriptionId !== undefined
|
|
149
|
+
? { subscriptionId: identified.subscriptionId }
|
|
150
|
+
: {})
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function notifyGrantComplete(platform, orderId) {
|
|
157
|
+
let notified;
|
|
158
|
+
try {
|
|
159
|
+
notified = await platform.completeProductGrant({ params: { orderId } });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
reason: `completeProductGrant rejected: ${reason}; retry — the server grant is already confirmed in this scope`
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!notified) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason: "completeProductGrant returned false; retry — the server grant is already confirmed in this scope"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IapGrantTarget } from "./platform-contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* Client-side dedupe for server grant callbacks, scoped to one adapter
|
|
4
|
+
* instance (one "processing scope"):
|
|
5
|
+
*
|
|
6
|
+
* - Concurrent grants for the same order share the in-flight Promise, so a
|
|
7
|
+
* duplicate callback never re-runs the server request.
|
|
8
|
+
* - A successfully granted order is reused within the same scope: later
|
|
9
|
+
* flows (pending-order recovery, duplicate success events) resolve
|
|
10
|
+
* immediately instead of re-granting.
|
|
11
|
+
* - Failed grants are never cached: the entry is removed when the attempt
|
|
12
|
+
* rejects, so the next call retries for real.
|
|
13
|
+
*
|
|
14
|
+
* This only reduces duplicate client-side work. Server-side grant
|
|
15
|
+
* idempotency (verify the order, persist exactly once) is a separate,
|
|
16
|
+
* mandatory server responsibility.
|
|
17
|
+
*/
|
|
18
|
+
export declare class IapGrantCoordinator {
|
|
19
|
+
private readonly grant;
|
|
20
|
+
private readonly inFlight;
|
|
21
|
+
private readonly granted;
|
|
22
|
+
constructor(grant: (target: IapGrantTarget) => Promise<void>);
|
|
23
|
+
/**
|
|
24
|
+
* Runs (or joins) the grant for the target order. Resolves when the
|
|
25
|
+
* consumer's callback resolves; rejects with the callback's error when it
|
|
26
|
+
* fails, leaving the order retryable. A duplicate joins only when it
|
|
27
|
+
* describes the same target: orderId and sku must match, and a caller
|
|
28
|
+
* that does not know the subscriptionId (pending-order recovery) may join
|
|
29
|
+
* a cached subscription grant. Explicitly conflicting duplicates reject
|
|
30
|
+
* instead of silently inheriting another flow's grant.
|
|
31
|
+
*/
|
|
32
|
+
run(target: IapGrantTarget): Promise<void>;
|
|
33
|
+
/** True when this scope already granted the order successfully. */
|
|
34
|
+
isGranted(orderId: string): boolean;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=grant-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-coordinator.d.ts","sourceRoot":"","sources":["../../src/iap/grant-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,mBAAmB;IAIlB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHlC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyE;IAClG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D,YAA6B,KAAK,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,EAAI;IAEjF;;;;;;;;OAQG;IACH,GAAG,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCzC;IAED,mEAAmE;IACnE,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElC;CACF"}
|