feat: modules and resources domains, drop removed domains, regenerated models
This commit is contained in:
@@ -70,9 +70,9 @@ interface LoginViaAppleResponse { accessToken?: string; refreshToken?: string }
|
||||
interface LoginViaCustomResponse { accessToken?: string; refreshToken?: string }
|
||||
```
|
||||
|
||||
On success both tokens are saved to the client's `TokenStore`, the runtime
|
||||
starts (domains warmed, scenario engine restored, `player_login` event fired),
|
||||
and the state flips to `'signed-in'`.
|
||||
On success both tokens are saved to the client's `TokenStore`, player and
|
||||
catalog are warmed, the revision poll starts, and the state flips to
|
||||
`'signed-in'`.
|
||||
|
||||
## Identity linking
|
||||
|
||||
@@ -127,8 +127,8 @@ type AuthStateListener = (state: AuthState) => void;
|
||||
- `isAuthenticated` is `true` while an access token is present in the token store.
|
||||
- `onAuthStateChange` fires the listener **immediately** with the current state
|
||||
and returns an unsubscribe function.
|
||||
- `logout()` clears tokens, stops the runtime (sync poll, scenario runs,
|
||||
cached domain data), and emits `'signed-out'`.
|
||||
- `logout()` clears tokens, stops the revision poll, drops cached domain
|
||||
data, and emits `'signed-out'`.
|
||||
|
||||
## Token refresh (automatic, transport level)
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# Battle pass — `client.battlePass`
|
||||
|
||||
`BattlePassService` (source: `src/battlepass/BattlePassService.ts`).
|
||||
Call-and-response access to battle pass endpoints. Battle pass state is tied to
|
||||
a **scenario battle pass node**, so calls carry `scenarioSlug` + `nodeId` (plus
|
||||
`runId` for mutating calls). NOT observable — re-fetch progress explicitly
|
||||
after a mutation.
|
||||
|
||||
In most games you do not call this service directly: a scenario battle pass
|
||||
node surfaces through `client.effects.onBattlePass` with a session object that
|
||||
wraps these calls (see `reference/scenarios.md`). Use the service directly when
|
||||
you already know the scenario/node/run identifiers.
|
||||
|
||||
## Methods
|
||||
|
||||
```ts
|
||||
getProgress(scenarioSlug: string, nodeId: string): Promise<GetBattlePassProgressResponse>
|
||||
addXp(request: AddBattlePassXpRequest): Promise<AddBattlePassXpResponse>
|
||||
claimReward(request: ClaimBattlePassRewardRequest): Promise<ClaimBattlePassRewardResponse>
|
||||
purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise<PurchaseBattlePassPremiumResponse>
|
||||
```
|
||||
|
||||
## Request / response types
|
||||
|
||||
```ts
|
||||
interface AddBattlePassXpRequest {
|
||||
amount?: number;
|
||||
nodeId?: string;
|
||||
runId?: string;
|
||||
scenarioSlug?: string;
|
||||
source?: string; // configured XP source
|
||||
}
|
||||
interface AddBattlePassXpResponse {
|
||||
level?: number;
|
||||
leveledUp?: boolean;
|
||||
maxLevel?: boolean;
|
||||
xp?: number;
|
||||
}
|
||||
|
||||
interface ClaimBattlePassRewardRequest {
|
||||
level?: number;
|
||||
nodeId?: string;
|
||||
runId?: string;
|
||||
scenarioSlug?: string;
|
||||
track?: 'free' | 'premium';
|
||||
}
|
||||
interface ClaimBattlePassRewardResponse {
|
||||
alreadyClaimed?: boolean;
|
||||
error?: string;
|
||||
granted?: Reward[]; // { amount?, currency?, itemId? }
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
interface GetBattlePassProgressResponse {
|
||||
claimedTiers?: ClaimedTier[]; // { level?, track? }
|
||||
level?: number;
|
||||
premiumOwned?: boolean;
|
||||
xp?: number;
|
||||
}
|
||||
|
||||
interface PurchaseBattlePassPremiumRequest {
|
||||
idempotencyKey?: string;
|
||||
nodeId?: string;
|
||||
runId?: string;
|
||||
scenarioSlug?: string;
|
||||
}
|
||||
interface PurchaseBattlePassPremiumResponse {
|
||||
error?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Semantics
|
||||
|
||||
- `addXp` credits XP from a configured source; returns the new `xp`/`level`
|
||||
plus `leveledUp` / `maxLevel` flags.
|
||||
- `claimReward` claims a tier reward at a reached level; idempotent
|
||||
server-side (`alreadyClaimed`). Claiming a tier above the current level (or
|
||||
a premium tier without premium) fails server-side — check `success`/`error`
|
||||
in the response and the typed error `code` (`RudderErrorCodes`).
|
||||
- `purchasePremium` charges the player's wallet; idempotent; pass your own
|
||||
`idempotencyKey` for safe retries.
|
||||
- Prefer the `onBattlePass` effect session, which binds `scenarioSlug` /
|
||||
`nodeId` / `runId` and posts scenario callbacks for you.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Inventory + catalog — `client.inventory`, `client.catalog`
|
||||
|
||||
## Inventory
|
||||
|
||||
`InventoryDomain` (source: `src/domains/InventoryDomain.ts`) — owned items
|
||||
merged with their catalog entries. Synced under revision key `inventory`;
|
||||
also refreshed whenever the catalog changes.
|
||||
|
||||
```ts
|
||||
client.inventory.data // InventoryItem[] | undefined
|
||||
client.inventory.onChange(cb);
|
||||
await client.inventory.load() / .reload();
|
||||
```
|
||||
|
||||
### `InventoryItem` (merged view, source: `src/state/inventory.ts`)
|
||||
|
||||
```ts
|
||||
interface InventoryItem {
|
||||
slug: string; // '' when the wire item has no slug
|
||||
amount: number; // 0 when absent on the wire
|
||||
name: string; // nameOverride || catalog name || slug
|
||||
properties: Record<string, unknown>; // catalog properties + propertiesOverride (override wins)
|
||||
tags: string[]; // from the catalog entry
|
||||
}
|
||||
```
|
||||
|
||||
The raw wire shape is `PlayerInventoryItem`
|
||||
(`slug?, amount?, nameOverride?, propertiesOverride?, updatedAt?`) — you rarely
|
||||
need it; the domain hands out the merged `InventoryItem`.
|
||||
|
||||
## Catalog
|
||||
|
||||
`CatalogDomain` (source: `src/domains/CatalogDomain.ts`) — the item catalog
|
||||
keyed by slug. Synced under revision key `catalog`.
|
||||
|
||||
```ts
|
||||
client.catalog.data // Map<string, CatalogItem> | undefined
|
||||
|
||||
interface CatalogItem {
|
||||
name?: string;
|
||||
properties?: { [key: string]: unknown };
|
||||
slug?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
Items without a `slug` are skipped when the map is built.
|
||||
|
||||
## Behavior notes
|
||||
|
||||
- The catalog is warmed at login; inventory loads on first use, waits on the
|
||||
catalog load, and merges, so `client.inventory.data` always has catalog
|
||||
fields filled in.
|
||||
- Invalidated after store purchases and scenario callbacks.
|
||||
- Inventory has no client-side mutations — items change via purchases, quest /
|
||||
battle pass rewards, and scenario nodes (all server-side).
|
||||
@@ -1,50 +0,0 @@
|
||||
# Leaderboards — `client.leaderboards`
|
||||
|
||||
`LeaderboardsService` (source: `src/leaderboards/LeaderboardsService.ts`).
|
||||
Plain call-and-response service, NOT observable — refetch explicitly.
|
||||
|
||||
## Surface
|
||||
|
||||
```ts
|
||||
const board = client.leaderboards.findBySlug('weekly-kills'); // cached handle
|
||||
await board.submit(score);
|
||||
const entries = await board.list(limit?);
|
||||
board.getEntries();
|
||||
```
|
||||
|
||||
## `LeaderboardHandle`
|
||||
|
||||
```ts
|
||||
class LeaderboardHandle {
|
||||
readonly slug: string;
|
||||
|
||||
getEntries(): readonly RankEntry[]; // last fetched list, [] initially
|
||||
submit(score: number): Promise<void>;
|
||||
list(limit = 100): Promise<readonly RankEntry[]>; // fetches and caches
|
||||
}
|
||||
```
|
||||
|
||||
- `findBySlug(slug)` caches handles per slug — repeated calls return the same
|
||||
instance (and its cached entries).
|
||||
- `list(limit)`: `limit <= 0` sends no limit to the server; default is 100.
|
||||
- `submit` does not update the cached entries; call `list()` afterwards to see
|
||||
the effect.
|
||||
|
||||
## Types
|
||||
|
||||
```ts
|
||||
interface RankEntry {
|
||||
playerId?: string;
|
||||
playerName?: string;
|
||||
rank?: number;
|
||||
score?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- No player-around-me or metadata endpoints are exposed by this SDK — submit
|
||||
and top-N list only.
|
||||
- Scenario `leaderboard` nodes surface through
|
||||
`client.effects.onLeaderboard` (`end()`, `rewardClaimed()`) — see
|
||||
`reference/scenarios.md`.
|
||||
@@ -4,8 +4,8 @@
|
||||
profile: identity plus currency wallets. Synced under revision key `profile`.
|
||||
|
||||
There is no separate wallet service in this SDK. Wallet balances are read from
|
||||
the profile and are credited/debited server-side (purchases, quest/battle pass
|
||||
rewards, scenario nodes).
|
||||
the profile and are credited/debited server-side (admin wallet adjust, module
|
||||
functions).
|
||||
|
||||
## Surface
|
||||
|
||||
@@ -51,10 +51,9 @@ interface Wallet {
|
||||
|
||||
## Behavior notes
|
||||
|
||||
- Warmed automatically at login (one of the four parallel warm loads).
|
||||
- Invalidated (refetched) after every successful store purchase and after
|
||||
scenario server callbacks — subscribers see fresh balances without waiting
|
||||
for the revision poll.
|
||||
- Warmed automatically at login (with catalog).
|
||||
- Kept fresh by the revision poll; call `reload()` after a module function
|
||||
that mutates wallets.
|
||||
- All fields are optional on the wire (`?`); code defensively.
|
||||
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
|
||||
README "React recipe").
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# Quests — `client.quests`
|
||||
|
||||
`QuestsService` (source: `src/quests/QuestsService.ts`). The player's **global**
|
||||
quests — list, claim, report metric progress. Plain call-and-response service,
|
||||
NOT observable (no sync revision key): re-list after a claim or report.
|
||||
|
||||
Distinct from scenario quest nodes, which advance through
|
||||
`client.effects.onQuest` (see `reference/scenarios.md`).
|
||||
|
||||
## Methods
|
||||
|
||||
```ts
|
||||
list(): Promise<Quest[]>
|
||||
claim(questSlug: string): Promise<ClaimQuestResponse>
|
||||
reportProgress(metric: string, amount: number): Promise<string[]> // slugs of quests completed by this report
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
```ts
|
||||
interface Quest {
|
||||
slug?: string; // stable across environments — what claim() takes
|
||||
name?: string;
|
||||
objectives?: QuestObjectiveProgress[];
|
||||
rewards?: Reward[];
|
||||
status?: 'active' | 'claimed' | 'completed';
|
||||
}
|
||||
|
||||
interface QuestObjectiveProgress {
|
||||
completed?: boolean;
|
||||
current?: number;
|
||||
metric?: string;
|
||||
objectiveId?: string;
|
||||
target?: number;
|
||||
}
|
||||
|
||||
interface ClaimQuestResponse {
|
||||
alreadyClaimed?: boolean;
|
||||
error?: string;
|
||||
granted?: Reward[]; // { amount?, currency?, itemId? }
|
||||
success?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage notes
|
||||
|
||||
```ts
|
||||
const quests = await client.quests.list();
|
||||
for (const quest of quests) {
|
||||
if (quest.status === 'completed' && quest.slug) {
|
||||
const res = await client.quests.claim(quest.slug);
|
||||
// res.success / res.alreadyClaimed / res.granted
|
||||
}
|
||||
}
|
||||
const completedSlugs = await client.quests.reportProgress('kills', 1);
|
||||
```
|
||||
|
||||
- `claim` is idempotent server-side; check `success` / `alreadyClaimed` /
|
||||
`error` in the response rather than relying on exceptions.
|
||||
- Objective completion is judged server-side from metric reports.
|
||||
|
||||
## `QuestMetrics` helpers
|
||||
|
||||
Exported as a namespace: `import { QuestMetrics } from '@rudder/js-sdk'`
|
||||
(source: `src/quests/QuestMetrics.ts`).
|
||||
|
||||
```ts
|
||||
QuestMetrics.purchaseOffer('starter-pack'); // "purchase.offer:starter-pack"
|
||||
QuestMetrics.purchaseItem('moonberry'); // "purchase.item:moonberry"
|
||||
```
|
||||
|
||||
Purchase metrics are reported automatically server-side by the store purchase
|
||||
fan-out — these helpers exist so quest configs and client code name the format
|
||||
consistently. Catalog counter slugs are reported via `reportProgress`. Custom
|
||||
free-text metrics no longer progress quests — they no-op at runtime.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Remote config — `client.remoteConfig`
|
||||
|
||||
`ConfigDomain<TConfig>` (source: `src/domains/ConfigDomain.ts`, base class
|
||||
`src/state/RemoteConfigState.ts`) — typed remote configuration as an
|
||||
observable entity. Synced under revision key `config`.
|
||||
|
||||
## Typed access
|
||||
|
||||
```ts
|
||||
interface GameConfig extends Record<string, unknown> {
|
||||
player_speed: number;
|
||||
feature_x: boolean;
|
||||
}
|
||||
|
||||
const client = new RudderClient<GameConfig>({ baseUrl, projectKey });
|
||||
|
||||
client.remoteConfig.get('player_speed', 200); // number
|
||||
client.remoteConfig.get('feature_x'); // boolean | undefined
|
||||
```
|
||||
|
||||
`get()` overloads:
|
||||
|
||||
```ts
|
||||
get(key): TConfig[key] | undefined;
|
||||
get(key, defaultValue: TConfig[key]): TConfig[key];
|
||||
```
|
||||
|
||||
- Values are parsed synchronously from the loaded snapshot according to the
|
||||
config's server-declared `valueType`.
|
||||
- Returns the default value (or `undefined`) while not loaded, for unknown
|
||||
keys, and when parsing fails.
|
||||
- Parsing rules: `int`/`integer` → `parseInt`; `float`/`double`/`number` →
|
||||
`parseFloat`; `bool`/`boolean` → `value === 'true'`; `json`/`object` →
|
||||
`JSON.parse`; anything else → raw string.
|
||||
|
||||
## Observable
|
||||
|
||||
```ts
|
||||
client.remoteConfig.data // Map<string, RemoteConfig> | undefined
|
||||
client.remoteConfig.onChange(cb); // fires immediately
|
||||
await client.remoteConfig.reload();
|
||||
```
|
||||
|
||||
```ts
|
||||
interface RemoteConfig {
|
||||
active?: boolean; createdAt?: string; description?: string;
|
||||
environment?: string; id?: string; key?: string; projectId?: string;
|
||||
updatedAt?: string; value?: string;
|
||||
valueType?: 'bool' | 'float' | 'int' | 'json' | 'string';
|
||||
}
|
||||
```
|
||||
|
||||
Only configs that are not explicitly `active: false` enter the map (the server
|
||||
already filters inactive configs out and may omit the flag).
|
||||
|
||||
## Behavior notes
|
||||
|
||||
- Warmed at login.
|
||||
- Scenario `remote_config_override` nodes are applied server-side and do not
|
||||
reach the client. Reload or wait for the config sync poll to observe the
|
||||
patched value via `get()`.
|
||||
- Without a `TConfig` type argument, `RemoteConfigShape` defaults to
|
||||
`Record<string, unknown>` and `get()` returns `unknown`.
|
||||
@@ -1,166 +0,0 @@
|
||||
# Scenarios + effects — `client.effects`
|
||||
|
||||
Scenarios are server-authored node graphs (configured in the dashboard) that
|
||||
run per player. The server executes the graph. The SDK is a thin effects
|
||||
client: it sends trigger events, turns `PendingEffect` payloads into typed
|
||||
`client.effects` handlers, posts callbacks when the game completes an
|
||||
effect, and polls for pending effects. The runtime is deliberately **not**
|
||||
part of the public client surface (source: `src/effects/EffectsCenter.ts`)
|
||||
and loads lazily on first login.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- After login the SDK fetches `GET /sdk/v1/scenarios/pending` (replacing any
|
||||
local restore), then fires the login event (`'player_login'` by default).
|
||||
- A heartbeat polls pending every ~30s (±20% jitter), paused while
|
||||
`document.hidden`. Each effect with a `waitDeadline` also schedules a
|
||||
local timer so waits fire on time.
|
||||
- An effect with an already-active `(runId, nodeId)` is not re-emitted.
|
||||
- `logout()` / `dispose()` stop the poll, cancel wait timers, and drop
|
||||
active effects. There is no local plan persistence.
|
||||
|
||||
### Runtime options (`RudderClientOptions.runtime`)
|
||||
|
||||
```ts
|
||||
runtime?: {
|
||||
// Scenario event fired automatically after login.
|
||||
// undefined = 'player_login'; null = fire nothing.
|
||||
loginEvent?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
## Subscribing
|
||||
|
||||
Every `on*` method takes a handler `(effect) => void | Promise<void>` and
|
||||
returns an unsubscribe function. Errors thrown inside handlers go to the
|
||||
`onEffectError` client option (default `console.error`).
|
||||
|
||||
```ts
|
||||
const off = client.effects.onNotification(async (n) => {
|
||||
showToast(n.title, n.message);
|
||||
await n.done(); // always resolve the session or the run stalls
|
||||
});
|
||||
```
|
||||
|
||||
## Effect types
|
||||
|
||||
```ts
|
||||
interface Effects {
|
||||
onNotification(handler): EffectUnsubscribe;
|
||||
onStoreOffer(handler): EffectUnsubscribe;
|
||||
onLeaderboard(handler): EffectUnsubscribe;
|
||||
onConfigChanged(handler): EffectUnsubscribe;
|
||||
onWait(handler): EffectUnsubscribe;
|
||||
onScenarioCompleted(handler): EffectUnsubscribe;
|
||||
onScenarioFailed(handler): EffectUnsubscribe;
|
||||
onQuest(handler): EffectUnsubscribe;
|
||||
onBattlePass(handler): EffectUnsubscribe;
|
||||
onBattlePassLevel(handler): EffectUnsubscribe;
|
||||
}
|
||||
```
|
||||
|
||||
`onConfigChanged` remains on the public surface but scenario
|
||||
`remote_config_override` nodes no longer reach the client — the server
|
||||
applies them. Patched values show up through `client.remoteConfig` after
|
||||
sync/reload.
|
||||
|
||||
### `NotificationEffect` — a notification node became active
|
||||
|
||||
```ts
|
||||
{ readonly title: string; readonly message: string; done(): Promise<void> }
|
||||
```
|
||||
|
||||
### `StoreOfferEffect` — a store node became active
|
||||
|
||||
```ts
|
||||
{
|
||||
readonly store: ShopHandle;
|
||||
readonly offers: readonly OfferHandle[];
|
||||
readonly message?: string;
|
||||
buy(offer: OfferHandle, options?: BuyOptions): Promise<PurchaseOfferResponse>;
|
||||
dismiss(): Promise<void>; // declines the offer and advances the run
|
||||
}
|
||||
```
|
||||
|
||||
### `LeaderboardEffect` — a leaderboard node became active
|
||||
|
||||
```ts
|
||||
{ end(): Promise<void>; claim(): Promise<void>; rewardClaimed(): Promise<void> }
|
||||
```
|
||||
|
||||
`rewardClaimed()` is a deprecated alias for `claim()`.
|
||||
|
||||
### `WaitEffect` — a wait node became active
|
||||
|
||||
```ts
|
||||
{ readonly deadlineUtc: Date } // SDK polls pending at the deadline; the server advances the run
|
||||
```
|
||||
|
||||
### `QuestEffect` — a scenario quest node became active
|
||||
|
||||
```ts
|
||||
{
|
||||
readonly name: string;
|
||||
readonly objectives: ReadonlyArray<Record<string, unknown>>;
|
||||
reportProgress(objectiveId: string, amount?: number): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
The node auto-completes server-side once every objective is satisfied; the
|
||||
counter response may carry the next `PendingEffect`.
|
||||
|
||||
### `BattlePassEffect` — a battle pass node became active
|
||||
|
||||
```ts
|
||||
{
|
||||
getProgress(): Promise<GetBattlePassProgressResponse>;
|
||||
addXp(source: string, amount: number): Promise<AddBattlePassXpResponse>;
|
||||
claimReward(level: number, track?: 'free' | 'premium'): Promise<ClaimBattlePassRewardResponse>;
|
||||
purchasePremium(): Promise<PurchaseBattlePassPremiumResponse>;
|
||||
levelUp(): Promise<void>;
|
||||
end(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
These wrap `client.battlePass` with the run's `scenarioSlug`/`nodeId`/`runId`
|
||||
already bound — prefer them over calling the service manually.
|
||||
|
||||
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
|
||||
|
||||
```ts
|
||||
{ readonly level: number; claim(): Promise<void> }
|
||||
```
|
||||
|
||||
### Run lifecycle effects
|
||||
|
||||
```ts
|
||||
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string }
|
||||
interface ScenarioFailedEffect {
|
||||
readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string;
|
||||
readonly error: Error;
|
||||
}
|
||||
```
|
||||
|
||||
Always subscribe to `onScenarioFailed` — otherwise run failures surface only
|
||||
as console warnings.
|
||||
|
||||
## Supported node types
|
||||
|
||||
`wait`, `notification`, `store`, `leaderboard`, `quest`, `battlepass`,
|
||||
`battlepass_level`. Any other node type fails the run (surfaced via
|
||||
`onScenarioFailed`). `remote_config_override` is applied server-side and is
|
||||
not delivered to the client.
|
||||
|
||||
## Reliability notes
|
||||
|
||||
- Completion methods POST `/sdk/v1/scenarios/callback` with
|
||||
`{scenarioSlug, runId, nodeId, handle}` using the handles `output`,
|
||||
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
|
||||
`onPremiumPurchase`.
|
||||
- Transient callback failures (network error, 5xx) leave the effect active
|
||||
for retry; `unknown_run` / `run_expired` drop that run's effects and fire
|
||||
`onScenarioFailed`.
|
||||
- Server scenario errors use the typed codes `run_expired`, `run_not_active`,
|
||||
`node_not_active`, `scenario_not_active`, `unknown_run`,
|
||||
`early_completion`, `objectives_incomplete`, `level_not_reached`,
|
||||
`forbidden` (`RudderErrorCodes`).
|
||||
@@ -1,82 +0,0 @@
|
||||
# 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, offerSlug, 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; // authoring id, differs between staging and prod
|
||||
readonly slug: string; // stable across environments — what purchases use
|
||||
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, offerSlug, options?)` and `offer.buy(options?)` are
|
||||
the same executor; handles just bind the store slug and the offer slug.
|
||||
- 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:<offerSlug>`, `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; slug?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
|
||||
without `slug` / an offer without `id` or `slug` — in practice the domain only
|
||||
builds handles from server data that has all of them.
|
||||
Reference in New Issue
Block a user