diff --git a/CHANGELOG.md b/CHANGELOG.md index 527d526..b18120d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 2.0.0 + +Breaking change — major bump, required by the backend environments release. +Every project now has exactly two environments, `staging` and `prod`, and the +SDK key passed as `projectKey` decides which one the player belongs to. The +environment never appears in the client API: it is resolved at login and +carried inside the access and refresh tokens. Tokens issued before this +release have no environment claim and are rejected with 401, so the first call +after the backend upgrade refreshes, fails, clears the token store and emits +`'signed-out'` to every `onAuthStateChange` listener. Log the player in again +with `client.auth.loginWithDevice()` / `loginWithCustom()`. + +Quests, scenarios and offers are addressed by their slug instead of their id, +because ids differ between staging and prod while slugs are stable. Renamed +accordingly: `Quest.id` is now `Quest.slug`, `client.quests.claim()` takes a +quest slug, `reportProgress()` resolves to the slugs of the completed quests +(`ReportQuestProgressResponse.completedQuestSlugs`), +`client.stores.purchase(storeSlug, offerSlug, options?)` takes an offer slug +and `OfferHandle` carries a `slug` alongside its `id`, +`QuestMetrics.purchaseOffer(offerSlug)` builds its metric from the offer slug, +and every scenario-scoped type exposes `scenarioSlug` instead of `scenarioId` — +`PendingEffect`, `ScenarioCompletedEffect`, `ScenarioFailedEffect`, the battle +pass requests and `client.battlePass.getProgress(scenarioSlug, nodeId)`. +`offer.buy()` is unchanged; it now binds the slug for you. Leaderboards, items +and stores already used slugs and are untouched. + +Also shipped here, previously committed but never published: the effects client +reconciles against every `GET /sdk/v1/scenarios/pending` response, so a run +that disappears server-side (finished elsewhere, expired after a promote) now +emits `onScenarioCompleted` instead of lingering; and `run_not_active` joins +`unknown_run` and `run_expired` as a terminal rejection that drops the run and +emits `onScenarioFailed`. + ## 1.0.0 - Scenario execution moved server-side. The SDK no longer walks a local DAG diff --git a/package-lock.json b/package-lock.json index 1f84745..911eb24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rudder/js-sdk", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rudder/js-sdk", - "version": "1.0.0", + "version": "2.0.0", "devDependencies": { "@types/node": "^26.1.1", "jsdom": "^29.1.1", diff --git a/package.json b/package.json index fe10785..e17ecd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rudder/js-sdk", - "version": "1.0.0", + "version": "2.0.0", "publishConfig": { "registry": "https://hub.rudder.build/api/packages/rudder/npm/" }, diff --git a/skills/rudder-web-sdk/SKILL.md b/skills/rudder-web-sdk/SKILL.md index b4ebf09..e13b3fe 100644 --- a/skills/rudder-web-sdk/SKILL.md +++ b/skills/rudder-web-sdk/SKILL.md @@ -74,7 +74,7 @@ service, or standalone economy service in this SDK — wallets live on | Player profile + wallets | `client.player` | observable `SyncedState` | | Inventory (catalog-merged) | `client.inventory` | observable `SyncedState` | | Catalog | `client.catalog` | observable `SyncedState>` | -| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerId, opts?)` | observable `SyncedState` | +| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerSlug, opts?)` | observable `SyncedState` | | Remote config | `client.remoteConfig` + `.get(key, default?)` | observable, typed | | Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations | | Project storage | `client.projectStorage` + `.save(items)` | observable + mutation | diff --git a/skills/rudder-web-sdk/reference/auth.md b/skills/rudder-web-sdk/reference/auth.md index b887a2b..ee3213b 100644 --- a/skills/rudder-web-sdk/reference/auth.md +++ b/skills/rudder-web-sdk/reference/auth.md @@ -50,6 +50,19 @@ 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'`. +## Environments + +A project has two environments, `staging` and `prod`. The SDK key you pass as +`RudderClientOptions.projectKey` belongs to one of them, so the environment is +resolved at login and carried inside the access and refresh tokens; nothing in +the client API takes an environment argument, and a player created in one +environment is invisible in the other. Content released only to `staging` is +empty for a `prod` key and vice versa. + +Tokens issued before SDK 2.0.0 carry no environment claim and are rejected with +401. The transport's refresh then fails, clears the token store and emits +`'signed-out'` — log the player in again. + ## Auth state ```ts diff --git a/skills/rudder-web-sdk/reference/battlepass.md b/skills/rudder-web-sdk/reference/battlepass.md index b9d5224..04ed11d 100644 --- a/skills/rudder-web-sdk/reference/battlepass.md +++ b/skills/rudder-web-sdk/reference/battlepass.md @@ -2,7 +2,7 @@ `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 `scenarioId` + `nodeId` (plus +a **scenario battle pass node**, so calls carry `scenarioSlug` + `nodeId` (plus `runId` for mutating calls). NOT observable — re-fetch progress explicitly after a mutation. @@ -14,7 +14,7 @@ you already know the scenario/node/run identifiers. ## Methods ```ts -getProgress(scenarioId: string, nodeId: string): Promise +getProgress(scenarioSlug: string, nodeId: string): Promise addXp(request: AddBattlePassXpRequest): Promise claimReward(request: ClaimBattlePassRewardRequest): Promise purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise @@ -27,7 +27,7 @@ interface AddBattlePassXpRequest { amount?: number; nodeId?: string; runId?: string; - scenarioId?: string; + scenarioSlug?: string; source?: string; // configured XP source } interface AddBattlePassXpResponse { @@ -41,7 +41,7 @@ interface ClaimBattlePassRewardRequest { level?: number; nodeId?: string; runId?: string; - scenarioId?: string; + scenarioSlug?: string; track?: 'free' | 'premium'; } interface ClaimBattlePassRewardResponse { @@ -62,7 +62,7 @@ interface PurchaseBattlePassPremiumRequest { idempotencyKey?: string; nodeId?: string; runId?: string; - scenarioId?: string; + scenarioSlug?: string; } interface PurchaseBattlePassPremiumResponse { error?: string; @@ -80,5 +80,5 @@ interface PurchaseBattlePassPremiumResponse { 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 `scenarioId` / +- Prefer the `onBattlePass` effect session, which binds `scenarioSlug` / `nodeId` / `runId` and posts scenario callbacks for you. diff --git a/skills/rudder-web-sdk/reference/quests.md b/skills/rudder-web-sdk/reference/quests.md index 0c309fd..a3af37b 100644 --- a/skills/rudder-web-sdk/reference/quests.md +++ b/skills/rudder-web-sdk/reference/quests.md @@ -11,15 +11,15 @@ Distinct from scenario quest nodes, which advance through ```ts list(): Promise -claim(questId: string): Promise -reportProgress(metric: string, amount: number): Promise // ids of quests completed by this report +claim(questSlug: string): Promise +reportProgress(metric: string, amount: number): Promise // slugs of quests completed by this report ``` ## Types ```ts interface Quest { - id?: string; + slug?: string; // stable across environments — what claim() takes name?: string; objectives?: QuestObjectiveProgress[]; rewards?: Reward[]; @@ -47,12 +47,12 @@ interface ClaimQuestResponse { ```ts const quests = await client.quests.list(); for (const quest of quests) { - if (quest.status === 'completed' && quest.id) { - const res = await client.quests.claim(quest.id); + if (quest.status === 'completed' && quest.slug) { + const res = await client.quests.claim(quest.slug); // res.success / res.alreadyClaimed / res.granted } } -const completedIds = await client.quests.reportProgress('kills', 1); +const completedSlugs = await client.quests.reportProgress('kills', 1); ``` - `claim` is idempotent server-side; check `success` / `alreadyClaimed` / diff --git a/skills/rudder-web-sdk/reference/scenarios.md b/skills/rudder-web-sdk/reference/scenarios.md index a41d9fb..fc6c24c 100644 --- a/skills/rudder-web-sdk/reference/scenarios.md +++ b/skills/rudder-web-sdk/reference/scenarios.md @@ -122,7 +122,7 @@ counter response may carry the next `PendingEffect`. } ``` -These wrap `client.battlePass` with the run's `scenarioId`/`nodeId`/`runId` +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) @@ -134,9 +134,9 @@ already bound — prefer them over calling the service manually. ### Run lifecycle effects ```ts -interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioId: string } +interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string } interface ScenarioFailedEffect { - readonly runId: string; readonly scenarioId: string; readonly nodeId: string; + readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string; readonly error: Error; } ``` @@ -154,7 +154,7 @@ not delivered to the client. ## Reliability notes - Completion methods POST `/sdk/v1/scenarios/callback` with - `{scenarioId, runId, nodeId, handle}` using the handles `output`, + `{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 diff --git a/skills/rudder-web-sdk/reference/stores.md b/skills/rudder-web-sdk/reference/stores.md index c427153..cf47d0b 100644 --- a/skills/rudder-web-sdk/reference/stores.md +++ b/skills/rudder-web-sdk/reference/stores.md @@ -13,7 +13,7 @@ client.stores.onChange(cb); await client.stores.load() / .reload(); // Direct purchase (bypassing handles): -await client.stores.purchase(storeSlug, offerId, options?); +await client.stores.purchase(storeSlug, offerSlug, options?); ``` ## Handles @@ -28,7 +28,8 @@ class ShopHandle { } class OfferHandle { - readonly id: string; + 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 }[] @@ -48,15 +49,15 @@ interface PurchaseOfferResponse { ## Purchase semantics -- `stores.purchase(storeSlug, offerId, options?)` and `offer.buy(options?)` are - the same executor; handles just bind the slug/id. +- `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:`, `purchase.item:`) are +- Purchase metrics (`purchase.offer:`, `purchase.item:`) are reported to quests automatically server-side — do not report them manually (see `reference/quests.md`). @@ -71,10 +72,11 @@ interface Store { } interface Offer { contents?: OfferContent[]; createdAt?: string; id?: string; - maxPurchases?: number; name?: string; price?: OfferPrice; updatedAt?: 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` — in practice the domain only builds -handles from server data that has both. +without `slug` / an offer without `id` or `slug` — in practice the domain only +builds handles from server data that has all of them.