81 lines
2.6 KiB
Markdown
81 lines
2.6 KiB
Markdown
|
|
# Stores / purchases — `client.stores`
|
||
|
|
|
||
|
|
`StoresDomain` (source: `src/domains/StoresDomain.ts`, handles in
|
||
|
|
`src/state/shops.ts`). The observable store list plus the purchase executor.
|
||
|
|
Synced under revision key `stores`. This is the SDK's economy surface — there
|
||
|
|
is no separate economy service.
|
||
|
|
|
||
|
|
## Surface
|
||
|
|
|
||
|
|
```ts
|
||
|
|
client.stores.data // ShopHandle[] | undefined
|
||
|
|
client.stores.onChange(cb);
|
||
|
|
await client.stores.load() / .reload();
|
||
|
|
|
||
|
|
// Direct purchase (bypassing handles):
|
||
|
|
await client.stores.purchase(storeSlug, offerId, options?);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Handles
|
||
|
|
|
||
|
|
```ts
|
||
|
|
class ShopHandle {
|
||
|
|
readonly slug: string;
|
||
|
|
readonly name?: string;
|
||
|
|
readonly description?: string;
|
||
|
|
readonly data?: { [key: string]: unknown };
|
||
|
|
readonly offers: OfferHandle[];
|
||
|
|
}
|
||
|
|
|
||
|
|
class OfferHandle {
|
||
|
|
readonly id: string;
|
||
|
|
readonly name?: string;
|
||
|
|
readonly price?: OfferPrice; // { amount?: number; currency?: string }
|
||
|
|
readonly contents: OfferContent[]; // { amount?: number; itemId?: string }[]
|
||
|
|
readonly maxPurchases?: number;
|
||
|
|
|
||
|
|
buy(options?: BuyOptions): Promise<PurchaseOfferResponse>;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface BuyOptions { idempotencyKey?: string }
|
||
|
|
|
||
|
|
interface PurchaseOfferResponse {
|
||
|
|
error?: string;
|
||
|
|
purchaseId?: string;
|
||
|
|
success?: boolean;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Purchase semantics
|
||
|
|
|
||
|
|
- `stores.purchase(storeSlug, offerId, options?)` and `offer.buy(options?)` are
|
||
|
|
the same executor; handles just bind the slug/id.
|
||
|
|
- When `options.idempotencyKey` is omitted the SDK generates
|
||
|
|
`crypto.randomUUID()` per call — safe retries require passing your own key.
|
||
|
|
- On success the executor invalidates `player`, `inventory`, and `stores`, so
|
||
|
|
subscribers observe fresh wallet/inventory/store data immediately.
|
||
|
|
- Check `response.success` / `response.error` — a failed purchase is a resolved
|
||
|
|
response, not necessarily a thrown error.
|
||
|
|
- Purchase metrics (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) are
|
||
|
|
reported to quests automatically server-side — do not report them manually
|
||
|
|
(see `reference/quests.md`).
|
||
|
|
|
||
|
|
## Wire types
|
||
|
|
|
||
|
|
```ts
|
||
|
|
interface Store {
|
||
|
|
createdAt?: string; data?: { [key: string]: unknown }; description?: string;
|
||
|
|
environment?: string; id?: string; name?: string; offers?: Offer[];
|
||
|
|
projectId?: string; scenarioId?: string; slug?: string; status?: string;
|
||
|
|
updatedAt?: string;
|
||
|
|
}
|
||
|
|
interface Offer {
|
||
|
|
contents?: OfferContent[]; createdAt?: string; id?: string;
|
||
|
|
maxPurchases?: number; name?: string; price?: OfferPrice; updatedAt?: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
|
||
|
|
without `slug` / an offer without `id` — in practice the domain only builds
|
||
|
|
handles from server data that has both.
|