Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fff971ad15 | |||
| e6128e54bd | |||
| 68287d212e | |||
| 1013f2395f |
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
- `client.auth.loginWithCustom({ customData, region?, language?, nickname? })`
|
||||||
|
— custom webhook auth, same token + runtime start as `loginWithDevice`.
|
||||||
|
- `QuestMetrics.purchaseOffer` / `purchaseItem` helpers for the shop purchase
|
||||||
|
metric format.
|
||||||
|
- Typecheck against regenerated models: `listQuests` no longer takes a body,
|
||||||
|
battle-pass `track` is `'free' | 'premium'`, scenario run status has no
|
||||||
|
`unknown_run`.
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
- Fixed `BattlePassLevelSession.level` reading node data key `level` — the
|
- Fixed `BattlePassLevelSession.level` reading node data key `level` — the
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ client.dispose();
|
|||||||
|
|
||||||
| Area | Access |
|
| Area | Access |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Auth | `client.auth.loginWithDevice()`, `client.auth.logout()`, `client.auth.isAuthenticated`, `client.auth.onAuthStateChange(cb)` |
|
| Auth | `client.auth.loginWithDevice()`, `client.auth.loginWithCustom({ customData })`, `client.auth.logout()`, `client.auth.isAuthenticated`, `client.auth.onAuthStateChange(cb)` |
|
||||||
| Player / wallets | `client.player` (observable) |
|
| Player / wallets | `client.player` (observable) |
|
||||||
| Inventory | `client.inventory` (observable, catalog-merged) |
|
| Inventory | `client.inventory` (observable, catalog-merged) |
|
||||||
| Catalog | `client.catalog` (observable) |
|
| Catalog | `client.catalog` (observable) |
|
||||||
@@ -61,7 +61,7 @@ client.dispose();
|
|||||||
| Storage | `client.storage` (observable) + `.save(items)` / `.delete(type)` |
|
| Storage | `client.storage` (observable) + `.save(items)` / `.delete(type)` |
|
||||||
| Leaderboards | `client.leaderboards.findBySlug(slug)` → `handle.submit(score)` / `handle.list(limit?)` |
|
| Leaderboards | `client.leaderboards.findBySlug(slug)` → `handle.submit(score)` / `handle.list(limit?)` |
|
||||||
| Battle pass | `client.battlePass` (getProgress / addXp / claimReward / purchasePremium) |
|
| Battle pass | `client.battlePass` (getProgress / addXp / claimReward / purchasePremium) |
|
||||||
| Quests | `client.quests.list()` / `client.quests.claim(id)` |
|
| Quests | `client.quests.list()` / `client.quests.claim(id)` / `client.quests.reportProgress(metric, amount)` |
|
||||||
| Scenario effects | `client.effects.on*` |
|
| Scenario effects | `client.effects.on*` |
|
||||||
|
|
||||||
Type the remote config for `client.remoteConfig`:
|
Type the remote config for `client.remoteConfig`:
|
||||||
@@ -75,6 +75,37 @@ const client = new RudderClient<GameConfig>({ /* … */ });
|
|||||||
client.remoteConfig.get('player_speed', 200); // number
|
client.remoteConfig.get('player_speed', 200); // number
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Quests
|
||||||
|
|
||||||
|
`client.quests` covers the player's global quests — list with per-objective
|
||||||
|
progress, claim, and metric reports. These are distinct from scenario quest
|
||||||
|
nodes, which advance through the `onQuest` effect's `QuestSession`. Global
|
||||||
|
quests have no live sync: re-list after a claim or report.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const quests = await client.quests.list();
|
||||||
|
for (const quest of quests) {
|
||||||
|
if (quest.status === 'completed' && quest.id) {
|
||||||
|
await client.quests.claim(quest.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom metrics advance matching objectives server-side; the call returns
|
||||||
|
// the ids of quests completed by this report.
|
||||||
|
const completedIds = await client.quests.reportProgress('kills', 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Purchase metrics are reported automatically when a purchase goes through
|
||||||
|
`client.stores`; the `QuestMetrics` helpers name the format so quest configs
|
||||||
|
and client code agree on it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { QuestMetrics } from '@rudder/js-sdk';
|
||||||
|
|
||||||
|
QuestMetrics.purchaseOffer('starter-pack'); // "purchase.offer:starter-pack"
|
||||||
|
QuestMetrics.purchaseItem('moonberry'); // "purchase.item:moonberry"
|
||||||
|
```
|
||||||
|
|
||||||
## Scenario effects
|
## Scenario effects
|
||||||
|
|
||||||
The scenario runtime is not exposed directly; scenario nodes surface through
|
The scenario runtime is not exposed directly; scenario nodes surface through
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@rudder/js-sdk",
|
"name": "@rudder/js-sdk",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@rudder/js-sdk",
|
"name": "@rudder/js-sdk",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rudder/js-sdk",
|
"name": "@rudder/js-sdk",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://hub.rudder.build/api/packages/rudder/npm/"
|
"registry": "https://hub.rudder.build/api/packages/rudder/npm/"
|
||||||
},
|
},
|
||||||
|
|||||||
+41
-4
@@ -1,15 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* AuthService — device-based player authentication.
|
* AuthService — player authentication.
|
||||||
*
|
*
|
||||||
* Handles login via device ID (the primary auth flow for game clients),
|
* Handles login via device ID (the primary auth flow for game clients) or a
|
||||||
* logout (token clearing), and auth state observation.
|
* custom backend webhook, logout (token clearing), and auth state observation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RudderContext } from '../core/context.js';
|
import type { RudderContext } from '../core/context.js';
|
||||||
import type { RemoteConfigShape } from '../state/RemoteConfigState.js';
|
import type { RemoteConfigShape } from '../state/RemoteConfigState.js';
|
||||||
import { getOrCreateDeviceId } from '../device/DeviceId.js';
|
import { getOrCreateDeviceId } from '../device/DeviceId.js';
|
||||||
import { api } from '../generated/api.js';
|
import { api } from '../generated/api.js';
|
||||||
import type { LoginViaDeviceResponse } from '../generated/auth.js';
|
import type { LoginViaCustomResponse, LoginViaDeviceResponse } from '../generated/auth.js';
|
||||||
|
|
||||||
export type AuthState = 'signed-in' | 'signed-out';
|
export type AuthState = 'signed-in' | 'signed-out';
|
||||||
|
|
||||||
@@ -24,6 +24,17 @@ export interface LoginWithDeviceOptions {
|
|||||||
nickname?: string;
|
nickname?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoginWithCustomOptions {
|
||||||
|
/** Arbitrary payload forwarded to the developer's custom auth webhook. */
|
||||||
|
customData: Record<string, unknown>;
|
||||||
|
/** Player's region code (default: "global"). */
|
||||||
|
region?: string;
|
||||||
|
/** Player's language code (default: "en"). */
|
||||||
|
language?: string;
|
||||||
|
/** Optional player nickname; omitted from the request when not set. */
|
||||||
|
nickname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape> {
|
export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape> {
|
||||||
private readonly listeners = new Set<AuthStateListener>();
|
private readonly listeners = new Set<AuthStateListener>();
|
||||||
private state: AuthState;
|
private state: AuthState;
|
||||||
@@ -81,6 +92,32 @@ export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape>
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates via the project's custom authorization webhook and returns
|
||||||
|
* access + refresh tokens.
|
||||||
|
*
|
||||||
|
* The payload is forwarded to the developer's backend; on success, tokens are
|
||||||
|
* saved to the client's TokenStore.
|
||||||
|
*/
|
||||||
|
async loginWithCustom(options: LoginWithCustomOptions): Promise<LoginViaCustomResponse> {
|
||||||
|
const { customData, region = 'global', language = 'en', nickname } = options;
|
||||||
|
const response = await api.loginViaCustom(this.ctx, {
|
||||||
|
key: this.ctx.options.projectKey,
|
||||||
|
customData,
|
||||||
|
region,
|
||||||
|
language,
|
||||||
|
nickname,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.options.tokenStore.saveTokens(
|
||||||
|
response.accessToken ?? '',
|
||||||
|
response.refreshToken ?? '',
|
||||||
|
);
|
||||||
|
await this.startRuntime();
|
||||||
|
this.setState('signed-in');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
/** Clears all stored tokens (logout). */
|
/** Clears all stored tokens (logout). */
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.ctx.options.tokenStore.clear();
|
this.ctx.options.tokenStore.clear();
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export interface QuestEffect {
|
|||||||
export interface BattlePassEffect {
|
export interface BattlePassEffect {
|
||||||
getProgress(): Promise<GetBattlePassProgressResponse>;
|
getProgress(): Promise<GetBattlePassProgressResponse>;
|
||||||
addXp(source: string, amount: number): Promise<AddBattlePassXpResponse>;
|
addXp(source: string, amount: number): Promise<AddBattlePassXpResponse>;
|
||||||
claimReward(level: number, track?: string): Promise<ClaimBattlePassRewardResponse>;
|
claimReward(level: number, track?: 'free' | 'premium'): Promise<ClaimBattlePassRewardResponse>;
|
||||||
purchasePremium(): Promise<PurchaseBattlePassPremiumResponse>;
|
purchasePremium(): Promise<PurchaseBattlePassPremiumResponse>;
|
||||||
levelUp(): Promise<void>;
|
levelUp(): Promise<void>;
|
||||||
end(): Promise<void>;
|
end(): Promise<void>;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
// Code generated by apigen. DO NOT EDIT.
|
||||||
|
|
||||||
import type { LoginViaDeviceRequest, LoginViaDeviceResponse, RefreshAccessTokenRequest, RefreshAccessTokenResponse } from './auth.js';
|
import type { LoginViaCustomRequest, LoginViaCustomResponse, LoginViaDeviceRequest, LoginViaDeviceResponse, RefreshAccessTokenRequest, RefreshAccessTokenResponse } from './auth.js';
|
||||||
import type { AddBattlePassXpRequest, AddBattlePassXpResponse, ClaimBattlePassRewardRequest, ClaimBattlePassRewardResponse, GetBattlePassProgressRequest, GetBattlePassProgressResponse, PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse } from './battlepass.js';
|
import type { AddBattlePassXpRequest, AddBattlePassXpResponse, ClaimBattlePassRewardRequest, ClaimBattlePassRewardResponse, GetBattlePassProgressRequest, GetBattlePassProgressResponse, PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse } from './battlepass.js';
|
||||||
import type { ListCatalogItemsResponse } from './catalog.js';
|
import type { ListCatalogItemsResponse } from './catalog.js';
|
||||||
import type { GetInventoryResponse } from './inventory.js';
|
import type { GetInventoryResponse } from './inventory.js';
|
||||||
import type { GetRankingResponse, SubmitScoreRequest } from './leaderboards.js';
|
import type { GetRankingResponse, SubmitScoreRequest } from './leaderboards.js';
|
||||||
import type { PlayerProfile } from './player.js';
|
import type { PlayerProfile } from './player.js';
|
||||||
import type { GetProjectStorageResponse, UpdateProjectStorageRequest } from './project-storage.js';
|
import type { GetProjectStorageResponse, UpdateProjectStorageRequest } from './project-storage.js';
|
||||||
import type { ClaimQuestRequest, ClaimQuestResponse, ListQuestsRequest, ListQuestsResponse, ReportQuestProgressRequest, ReportQuestProgressResponse } from './quests.js';
|
import type { ClaimQuestRequest, ClaimQuestResponse, ListQuestsResponse, ReportQuestProgressRequest, ReportQuestProgressResponse } from './quests.js';
|
||||||
import type { ListRemoteConfigsResponse, RemoteConfig } from './remote-config.js';
|
import type { ListRemoteConfigsResponse, RemoteConfig } from './remote-config.js';
|
||||||
import type { GetScenarioRunRequest, GetScenarioRunResponse, HandleScenarioCallbackRequest, HandleScenarioCallbackResponse, TriggerScenarioRequest, TriggerScenarioResponse, UpdateScenarioCounterRequest, UpdateScenarioCounterResponse } from './scenarios.js';
|
import type { GetScenarioRunRequest, GetScenarioRunResponse, HandleScenarioCallbackRequest, HandleScenarioCallbackResponse, TriggerScenarioRequest, TriggerScenarioResponse, UpdateScenarioCounterRequest, UpdateScenarioCounterResponse } from './scenarios.js';
|
||||||
import type { GetStorageResponse, UpdateStorageRequest } from './storage.js';
|
import type { GetStorageResponse, UpdateStorageRequest } from './storage.js';
|
||||||
@@ -131,9 +131,8 @@ export const api = {
|
|||||||
|
|
||||||
listQuests: (
|
listQuests: (
|
||||||
t: Transport,
|
t: Transport,
|
||||||
body: ListQuestsRequest,
|
|
||||||
): Promise<ListQuestsResponse> =>
|
): Promise<ListQuestsResponse> =>
|
||||||
t.request<ListQuestsResponse>('POST', '/sdk/v1/quests/list', body),
|
t.request<ListQuestsResponse>('POST', '/sdk/v1/quests/list'),
|
||||||
|
|
||||||
listSdkRemoteConfigs: (
|
listSdkRemoteConfigs: (
|
||||||
t: Transport,
|
t: Transport,
|
||||||
@@ -145,6 +144,12 @@ export const api = {
|
|||||||
): Promise<ListStoresResponse> =>
|
): Promise<ListStoresResponse> =>
|
||||||
t.request<ListStoresResponse>('GET', '/sdk/v1/stores'),
|
t.request<ListStoresResponse>('GET', '/sdk/v1/stores'),
|
||||||
|
|
||||||
|
loginViaCustom: (
|
||||||
|
t: Transport,
|
||||||
|
body: LoginViaCustomRequest,
|
||||||
|
): Promise<LoginViaCustomResponse> =>
|
||||||
|
t.request<LoginViaCustomResponse>('POST', '/sdk/v1/authorization/custom', body),
|
||||||
|
|
||||||
loginViaDevice: (
|
loginViaDevice: (
|
||||||
t: Transport,
|
t: Transport,
|
||||||
body: LoginViaDeviceRequest,
|
body: LoginViaDeviceRequest,
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
// Code generated by apigen. DO NOT EDIT.
|
||||||
|
|
||||||
|
export interface LoginViaCustomRequest {
|
||||||
|
"customData"?: { [key: string]: unknown };
|
||||||
|
"key"?: string;
|
||||||
|
"language"?: string;
|
||||||
|
"nickname"?: string;
|
||||||
|
"region"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginViaCustomResponse {
|
||||||
|
"accessToken"?: string;
|
||||||
|
"refreshToken"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginViaDeviceRequest {
|
export interface LoginViaDeviceRequest {
|
||||||
"deviceId"?: string;
|
"deviceId"?: string;
|
||||||
"key"?: string;
|
"key"?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
// Code generated by apigen. DO NOT EDIT.
|
||||||
|
|
||||||
import type { ExecutionPlan } from './common.js';
|
import type { ExecutionPlan, Reward } from './common.js';
|
||||||
|
|
||||||
export interface AddBattlePassXpRequest {
|
export interface AddBattlePassXpRequest {
|
||||||
"amount"?: number;
|
"amount"?: number;
|
||||||
@@ -18,24 +18,18 @@ export interface AddBattlePassXpResponse {
|
|||||||
"xp"?: number;
|
"xp"?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BattlePassReward {
|
|
||||||
"amount"?: number;
|
|
||||||
"currency"?: string;
|
|
||||||
"itemId"?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClaimBattlePassRewardRequest {
|
export interface ClaimBattlePassRewardRequest {
|
||||||
"level"?: number;
|
"level"?: number;
|
||||||
"nodeId"?: string;
|
"nodeId"?: string;
|
||||||
"runId"?: string;
|
"runId"?: string;
|
||||||
"scenarioId"?: string;
|
"scenarioId"?: string;
|
||||||
"track"?: string;
|
"track"?: "free" | "premium";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimBattlePassRewardResponse {
|
export interface ClaimBattlePassRewardResponse {
|
||||||
"alreadyClaimed"?: boolean;
|
"alreadyClaimed"?: boolean;
|
||||||
"error"?: string;
|
"error"?: string;
|
||||||
"granted"?: BattlePassReward[];
|
"granted"?: Reward[];
|
||||||
"success"?: boolean;
|
"success"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface BoundaryNode {
|
|||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
"code"?: "early_completion" | "forbidden" | "level_not_reached" | "node_not_active" | "objectives_incomplete" | "run_expired" | "run_not_active" | "scenario_not_active" | "unknown_run";
|
"code"?: "early_completion" | "forbidden" | "level_not_reached" | "node_not_active" | "objectives_incomplete" | "run_expired" | "run_not_active" | "scenario_not_active" | "unknown_run";
|
||||||
"error"?: string;
|
"error"?: string;
|
||||||
|
"index"?: number;
|
||||||
|
"playerId"?: string;
|
||||||
"requestId"?: string;
|
"requestId"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,3 +44,9 @@ export interface PlanEdge {
|
|||||||
"targetHandle"?: string;
|
"targetHandle"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Reward {
|
||||||
|
"amount"?: number;
|
||||||
|
"currency"?: string;
|
||||||
|
"itemId"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-12
@@ -1,5 +1,7 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
// Code generated by apigen. DO NOT EDIT.
|
||||||
|
|
||||||
|
import type { Reward } from './common.js';
|
||||||
|
|
||||||
export interface ClaimQuestRequest {
|
export interface ClaimQuestRequest {
|
||||||
"questId"?: string;
|
"questId"?: string;
|
||||||
}
|
}
|
||||||
@@ -7,13 +9,10 @@ export interface ClaimQuestRequest {
|
|||||||
export interface ClaimQuestResponse {
|
export interface ClaimQuestResponse {
|
||||||
"alreadyClaimed"?: boolean;
|
"alreadyClaimed"?: boolean;
|
||||||
"error"?: string;
|
"error"?: string;
|
||||||
"granted"?: QuestReward[];
|
"granted"?: Reward[];
|
||||||
"success"?: boolean;
|
"success"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListQuestsRequest {
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListQuestsResponse {
|
export interface ListQuestsResponse {
|
||||||
"quests"?: Quest[];
|
"quests"?: Quest[];
|
||||||
}
|
}
|
||||||
@@ -22,8 +21,8 @@ export interface Quest {
|
|||||||
"id"?: string;
|
"id"?: string;
|
||||||
"name"?: string;
|
"name"?: string;
|
||||||
"objectives"?: QuestObjectiveProgress[];
|
"objectives"?: QuestObjectiveProgress[];
|
||||||
"rewards"?: QuestReward[];
|
"rewards"?: Reward[];
|
||||||
"status"?: string;
|
"status"?: "active" | "claimed" | "completed";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestObjectiveProgress {
|
export interface QuestObjectiveProgress {
|
||||||
@@ -34,12 +33,6 @@ export interface QuestObjectiveProgress {
|
|||||||
"target"?: number;
|
"target"?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestReward {
|
|
||||||
"amount"?: number;
|
|
||||||
"currency"?: string;
|
|
||||||
"itemId"?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReportQuestProgressRequest {
|
export interface ReportQuestProgressRequest {
|
||||||
"amount"?: number;
|
"amount"?: number;
|
||||||
"metric"?: string;
|
"metric"?: string;
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ export interface RemoteConfig {
|
|||||||
"projectId"?: string;
|
"projectId"?: string;
|
||||||
"updatedAt"?: string;
|
"updatedAt"?: string;
|
||||||
"value"?: string;
|
"value"?: string;
|
||||||
"valueType"?: string;
|
"valueType"?: "bool" | "float" | "int" | "json" | "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface GetScenarioRunRequest {
|
|||||||
export interface GetScenarioRunResponse {
|
export interface GetScenarioRunResponse {
|
||||||
"plan"?: ExecutionPlan;
|
"plan"?: ExecutionPlan;
|
||||||
"runId"?: string;
|
"runId"?: string;
|
||||||
"status"?: string;
|
"status"?: "active" | "completed" | "expired";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HandleScenarioCallbackRequest {
|
export interface HandleScenarioCallbackRequest {
|
||||||
|
|||||||
+3
-2
@@ -22,11 +22,13 @@ export type {
|
|||||||
AuthService,
|
AuthService,
|
||||||
AuthState,
|
AuthState,
|
||||||
AuthStateListener,
|
AuthStateListener,
|
||||||
|
LoginWithCustomOptions,
|
||||||
LoginWithDeviceOptions,
|
LoginWithDeviceOptions,
|
||||||
} from './auth/AuthService.js';
|
} from './auth/AuthService.js';
|
||||||
export type { LeaderboardsService, LeaderboardHandle } from './leaderboards/LeaderboardsService.js';
|
export type { LeaderboardsService, LeaderboardHandle } from './leaderboards/LeaderboardsService.js';
|
||||||
export type { BattlePassService } from './battlepass/BattlePassService.js';
|
export type { BattlePassService } from './battlepass/BattlePassService.js';
|
||||||
export type { QuestsService } from './quests/QuestsService.js';
|
export type { QuestsService } from './quests/QuestsService.js';
|
||||||
|
export * as QuestMetrics from './quests/QuestMetrics.js';
|
||||||
|
|
||||||
// Observable state primitives
|
// Observable state primitives
|
||||||
export { SyncedState } from './state/SyncedState.js';
|
export { SyncedState } from './state/SyncedState.js';
|
||||||
@@ -87,14 +89,13 @@ export type {
|
|||||||
ProjectStorageUpdateItem,
|
ProjectStorageUpdateItem,
|
||||||
} from './generated/project-storage.js';
|
} from './generated/project-storage.js';
|
||||||
export type { RemoteConfig } from './generated/remote-config.js';
|
export type { RemoteConfig } from './generated/remote-config.js';
|
||||||
|
export type { Reward } from './generated/common.js';
|
||||||
export type {
|
export type {
|
||||||
Quest,
|
Quest,
|
||||||
QuestObjectiveProgress,
|
QuestObjectiveProgress,
|
||||||
QuestReward,
|
|
||||||
ClaimQuestResponse,
|
ClaimQuestResponse,
|
||||||
} from './generated/quests.js';
|
} from './generated/quests.js';
|
||||||
export type {
|
export type {
|
||||||
BattlePassReward,
|
|
||||||
ClaimedTier,
|
ClaimedTier,
|
||||||
AddBattlePassXpRequest,
|
AddBattlePassXpRequest,
|
||||||
AddBattlePassXpResponse,
|
AddBattlePassXpResponse,
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* QuestMetrics — builders for the quest metric strings known to the platform.
|
||||||
|
*
|
||||||
|
* Purchase metrics are reported automatically server-side by the shop
|
||||||
|
* purchase fan-out, so these helpers exist mainly to name the format
|
||||||
|
* instead of hardcoding it. Any other metric is a custom string reported via
|
||||||
|
* {@link QuestsService.reportProgress}.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Metric for purchasing a store offer; auto-reported on purchase. */
|
||||||
|
export function purchaseOffer(offerId: string): string {
|
||||||
|
return `purchase.offer:${offerId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Metric for purchasing a catalog item; auto-reported on purchase. */
|
||||||
|
export function purchaseItem(itemId: string): string {
|
||||||
|
return `purchase.item:${itemId}`;
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export class QuestsService {
|
|||||||
|
|
||||||
/** Lists the player's quests with per-objective progress and rewards. */
|
/** Lists the player's quests with per-objective progress and rewards. */
|
||||||
async list(): Promise<Quest[]> {
|
async list(): Promise<Quest[]> {
|
||||||
const response = await api.listQuests(this.ctx, {});
|
const response = await api.listQuests(this.ctx);
|
||||||
return response.quests ?? [];
|
return response.quests ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,8 +136,9 @@ export class ScenarioService<TConfig extends RemoteConfigShape = RemoteConfigSha
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.getScenarioRun(this.ctx, { runId: saved.runId });
|
const response = await api.getScenarioRun(this.ctx, { runId: saved.runId });
|
||||||
|
const status: string | undefined = response?.status;
|
||||||
|
|
||||||
if (response?.status === 'unknown_run' || response?.status === 'expired') {
|
if (status === 'unknown_run' || status === 'expired') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +467,9 @@ export class ScenarioService<TConfig extends RemoteConfigShape = RemoteConfigSha
|
|||||||
let reconciled = false;
|
let reconciled = false;
|
||||||
try {
|
try {
|
||||||
const reconcile = await api.getScenarioRun(this.ctx, { runId: run.runId });
|
const reconcile = await api.getScenarioRun(this.ctx, { runId: run.runId });
|
||||||
|
const reconcileStatus: string | undefined = reconcile?.status;
|
||||||
|
|
||||||
if (reconcile?.status === 'unknown_run' || reconcile?.status === 'expired') {
|
if (reconcileStatus === 'unknown_run' || reconcileStatus === 'expired') {
|
||||||
this.runs.delete(run.runId);
|
this.runs.delete(run.runId);
|
||||||
await this.persist();
|
await this.persist();
|
||||||
reconciled = true;
|
reconciled = true;
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export class BattlePassSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Claims a tier reward at a reached level. */
|
/** Claims a tier reward at a reached level. */
|
||||||
claimReward(level: number, track?: string): Promise<ClaimBattlePassRewardResponse> {
|
claimReward(level: number, track?: 'free' | 'premium'): Promise<ClaimBattlePassRewardResponse> {
|
||||||
const { scenarioId, nodeId, runId } = this.ids();
|
const { scenarioId, nodeId, runId } = this.ids();
|
||||||
return this.battlePass.claimReward({ scenarioId, nodeId, level, track, runId });
|
return this.battlePass.claimReward({ scenarioId, nodeId, level, track, runId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ export async function runSeed(
|
|||||||
tags: ['currency'],
|
tags: ['currency'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Remote configs — one per value type.
|
// 5. Remote configs — one per value type. RCs are live per-env flags served
|
||||||
|
// straight from the outbox/cache, so they go directly into the runtime env.
|
||||||
const remoteConfigs: Array<{ key: string; value: string; valueType: string }> = [
|
const remoteConfigs: Array<{ key: string; value: string; valueType: string }> = [
|
||||||
{ key: 'max_energy', value: '100', valueType: 'int' },
|
{ key: 'max_energy', value: '100', valueType: 'int' },
|
||||||
{ key: 'spawn_rate', value: '1.5', valueType: 'float' },
|
{ key: 'spawn_rate', value: '1.5', valueType: 'float' },
|
||||||
@@ -142,9 +143,9 @@ export async function runSeed(
|
|||||||
{ key: 'shop_layout', value: '{"cols":3}', valueType: 'json' },
|
{ key: 'shop_layout', value: '{"cols":3}', valueType: 'json' },
|
||||||
];
|
];
|
||||||
for (const rc of remoteConfigs) {
|
for (const rc of remoteConfigs) {
|
||||||
await api.post(`${projPath}/remote-configs${q}`, {
|
await api.post(`${projPath}/remote-configs${prodQ}`, {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
environment: authoringEnv,
|
environment: runtimeEnv,
|
||||||
...rc,
|
...rc,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,7 +228,11 @@ export async function runSeed(
|
|||||||
log(`scenario ${scenario.id} flow set`);
|
log(`scenario ${scenario.id} flow set`);
|
||||||
|
|
||||||
// 11. Promote staging content to prod (the runtime serves prod/latest.json).
|
// 11. Promote staging content to prod (the runtime serves prod/latest.json).
|
||||||
const release = await api.post<ReleaseResponse>(`${projPath}/releases/promote`);
|
const release = await api.post<ReleaseResponse>(`${projPath}/releases/promote`, {
|
||||||
|
projectId: project.id,
|
||||||
|
fromEnvironment: authoringEnv,
|
||||||
|
toEnvironment: runtimeEnv,
|
||||||
|
});
|
||||||
await pollRelease(api, projPath, runtimeEnv, release.id, log);
|
await pollRelease(api, projPath, runtimeEnv, release.id, log);
|
||||||
|
|
||||||
const prodStores = await api.get<StoreResponse[]>(`${projPath}/stores${prodQ}`);
|
const prodStores = await api.get<StoreResponse[]>(`${projPath}/stores${prodQ}`);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('e2e-prod: purchase (wallet/inventory/limits)', () => {
|
|||||||
const offer = await resolvePaidOffer(client);
|
const offer = await resolvePaidOffer(client);
|
||||||
|
|
||||||
// 1. Grant currency via the new admin endpoint.
|
// 1. Grant currency via the new admin endpoint.
|
||||||
await api.post(`/game/v1/projects/${artifact.projectId}/players/${playerId}/wallet/adjust`, {
|
await api.post(`/platform/v1/projects/${artifact.projectId}/players/${playerId}/wallet/adjust`, {
|
||||||
currencyCode: currency,
|
currencyCode: currency,
|
||||||
amount: 1000,
|
amount: 1000,
|
||||||
reason: 'e2e grant',
|
reason: 'e2e grant',
|
||||||
@@ -68,7 +68,7 @@ describe('e2e-prod: purchase (wallet/inventory/limits)', () => {
|
|||||||
|
|
||||||
// 3. Inventory credited (verified via admin player details).
|
// 3. Inventory credited (verified via admin player details).
|
||||||
const details = await api.get<AdminPlayerDetails>(
|
const details = await api.get<AdminPlayerDetails>(
|
||||||
`/game/v1/projects/${artifact.projectId}/players/${playerId}`,
|
`/platform/v1/projects/${artifact.projectId}/players/${playerId}`,
|
||||||
);
|
);
|
||||||
const inv = details.inventory?.find((i) => i.slug === artifact.item.id);
|
const inv = details.inventory?.find((i) => i.slug === artifact.item.id);
|
||||||
expect(Number(inv?.amount)).toBe(artifact.store.paidGrantAmount);
|
expect(Number(inv?.amount)).toBe(artifact.store.paidGrantAmount);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
||||||
import { loadArtifact, makeProdClient } from './_setup/harness.js';
|
import { adminApi, loadArtifact, makeProdClient } from './_setup/harness.js';
|
||||||
import type { SeedArtifact } from './_setup/types.js';
|
import type { SeedArtifact } from './_setup/types.js';
|
||||||
import type { NotificationEffect, StoreOfferEffect } from '../../src/index.js';
|
import type { NotificationEffect, StoreOfferEffect } from '../../src/index.js';
|
||||||
import { getScenarioRuntime } from '../../src/client/RudderClient.js';
|
import { getScenarioRuntime } from '../../src/client/RudderClient.js';
|
||||||
@@ -30,6 +30,17 @@ describe('e2e-prod: scenarios', () => {
|
|||||||
};
|
};
|
||||||
await client.auth.loginWithDevice({ region: 'en', language: 'en' });
|
await client.auth.loginWithDevice({ region: 'en', language: 'en' });
|
||||||
|
|
||||||
|
// The boundary buy uses the paid offer — fund the wallet so the purchase succeeds.
|
||||||
|
const playerId = (await client.player.reload()).player!.id!;
|
||||||
|
await adminApi(artifact).post(
|
||||||
|
`/platform/v1/projects/${artifact.projectId}/players/${playerId}/wallet/adjust`,
|
||||||
|
{
|
||||||
|
currencyCode: artifact.store.paidPrice.currency,
|
||||||
|
amount: artifact.store.paidPrice.amount,
|
||||||
|
reason: 'e2e scenario funding',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// First notification dispatched.
|
// First notification dispatched.
|
||||||
await vi.waitFor(() => expect(notifications.length).toBe(1), { timeout: 10_000 });
|
await vi.waitFor(() => expect(notifications.length).toBe(1), { timeout: 10_000 });
|
||||||
await notifications[0].done();
|
await notifications[0].done();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface GameRemoteConfig extends Record<string, unknown> {
|
|||||||
missing_declared: string;
|
missing_declared: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cfg(key: string, value: string, valueType: string, active = true): RemoteConfig {
|
function cfg(key: string, value: string, valueType: NonNullable<RemoteConfig['valueType']>, active = true): RemoteConfig {
|
||||||
return { key, value, valueType, active };
|
return { key, value, valueType, active };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user