@gamecore-api/sdk 0.15.0 → 0.17.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/README.md +49 -7
- package/dist/client.d.ts +62 -1
- package/dist/index.js +98 -2
- package/dist/types.d.ts +115 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,20 +42,62 @@ const results = await gc.catalog.search("roblox");
|
|
|
42
42
|
|
|
43
43
|
## Authentication
|
|
44
44
|
|
|
45
|
-
### Telegram Auth
|
|
45
|
+
### Telegram Auth — two paths, pick both
|
|
46
|
+
|
|
47
|
+
Two flavours, designed to coexist as two buttons in the UI:
|
|
48
|
+
|
|
49
|
+
**1. Official Login Widget** (fastest, needs official Telegram Web session)
|
|
46
50
|
|
|
47
51
|
```typescript
|
|
48
|
-
//
|
|
49
|
-
|
|
52
|
+
// Mount the blue "Log in with Telegram" button into your own <div>.
|
|
53
|
+
// Bot username is pulled from /site/config; no hardcoding.
|
|
54
|
+
// BotFather /setdomain must point at your storefront's origin, or
|
|
55
|
+
// telegram.org refuses to render the widget.
|
|
56
|
+
const cleanup = await gc.auth.renderTelegramWidget({
|
|
57
|
+
container: document.querySelector("#tg-login")!,
|
|
58
|
+
size: "large",
|
|
59
|
+
onAuth: (user) => {
|
|
60
|
+
console.log("Logged in:", user.firstName);
|
|
61
|
+
window.location.href = "/profile";
|
|
62
|
+
},
|
|
63
|
+
onError: (err) => console.error(err),
|
|
64
|
+
});
|
|
50
65
|
|
|
51
|
-
//
|
|
52
|
-
|
|
66
|
+
// Later (React unmount, SPA route change):
|
|
67
|
+
cleanup();
|
|
68
|
+
```
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
**2. Bot-link flow** (works in every Telegram client including 3rd-party)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const user = await gc.auth.loginViaTelegramBot({
|
|
74
|
+
onBotLinkReady: (botLink) => {
|
|
75
|
+
// Open in new tab — every TG client handles tg:// deep links.
|
|
76
|
+
// For desktop-only users you could also render botLink as a QR.
|
|
77
|
+
window.open(botLink, "_blank");
|
|
78
|
+
},
|
|
79
|
+
pollMs: 2000,
|
|
80
|
+
timeoutMs: 120_000,
|
|
81
|
+
});
|
|
56
82
|
console.log("Logged in:", user.firstName);
|
|
57
83
|
```
|
|
58
84
|
|
|
85
|
+
**Low-level pieces (for custom flows)**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Manual init+poll — equivalent to loginViaTelegramBot above
|
|
89
|
+
const { token, botLink } = await gc.auth.initTelegram();
|
|
90
|
+
window.open(botLink, "_blank");
|
|
91
|
+
const user = await gc.auth.pollTelegramStatus(token);
|
|
92
|
+
|
|
93
|
+
// Manual widget verification — when you render Telegram's <script> yourself
|
|
94
|
+
// and wire data-onauth to your own JS callback
|
|
95
|
+
const auth = await gc.auth.verifyTelegramWidget(telegramWidgetUser);
|
|
96
|
+
|
|
97
|
+
// Mini App (inside the Telegram bot's built-in WebApp)
|
|
98
|
+
const auth = await gc.auth.verifyMiniApp(window.Telegram.WebApp.initData);
|
|
99
|
+
```
|
|
100
|
+
|
|
59
101
|
### VK Auth
|
|
60
102
|
|
|
61
103
|
```typescript
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Announcement, AnnouncementBar, CartItem, CatalogSection, Category, CategoryInfo, CheckoutRequest, CheckoutResponse, CompleteWithBalanceResult, Conversation, ConversationDetail, CouponResult, ExchangeRates, Favorite, Game, GameDetail, GiftCard, LegalDocument, LevelStatus, Notification, Order, PagedGamesResponse, PlatformInfo, PaginatedResponse, PaymentInfo, PaymentMethod, Product, ProductFilters, ReferralCommission, ReferralLink, ReferralPerformance, ReferralStats, Review, ReviewCreateResult, ReviewStats, SearchResult, SiteConfig, SiteStats, SiteUIConfig, TelegramAuthResponse, TelegramBotLoginOptions, TelegramInitResponse, TelegramWidgetRenderOptions, TelegramWidgetUser, TopupMethod, TopupResponse, TopupStatus, Transaction, User, UserBalance, WebPushSubscriptionInput } from "./types";
|
|
2
2
|
export interface GameCoreOptions {
|
|
3
3
|
/** Site API key (gc_live_xxx or gc_test_xxx) */
|
|
4
4
|
apiKey: string;
|
|
@@ -124,6 +124,43 @@ export declare class GameCoreClient {
|
|
|
124
124
|
* directly to this method.
|
|
125
125
|
*/
|
|
126
126
|
verifyTelegramWidget: (data: TelegramWidgetUser, ref?: string) => Promise<TelegramAuthResponse>;
|
|
127
|
+
/**
|
|
128
|
+
* High-level helper: mount the official Telegram Login Widget into
|
|
129
|
+
* your own DOM element and hand the resulting User to `onAuth`
|
|
130
|
+
* after server-side HMAC verification. Browser-only.
|
|
131
|
+
*
|
|
132
|
+
* Compared to dropping the raw `<script>` on the page:
|
|
133
|
+
* - Bot username is pulled from `gc.site.getConfig().authConfig.
|
|
134
|
+
* telegram.botUsername` (no hardcoding).
|
|
135
|
+
* - The widget's `data-onauth` bridge is plumbed to
|
|
136
|
+
* {@link verifyTelegramWidget} so the caller only sees a
|
|
137
|
+
* verified User.
|
|
138
|
+
* - Errors (hash mismatch, missing bot, expired payload) surface
|
|
139
|
+
* through `onError` instead of becoming silent no-ops.
|
|
140
|
+
*
|
|
141
|
+
* BotFather prerequisite: `/setdomain` must be set for the bot to
|
|
142
|
+
* the storefront's origin; the widget refuses to render otherwise.
|
|
143
|
+
*
|
|
144
|
+
* Returns a cleanup function that removes the widget from the
|
|
145
|
+
* container.
|
|
146
|
+
*/
|
|
147
|
+
renderTelegramWidget: (opts: TelegramWidgetRenderOptions) => Promise<() => void>;
|
|
148
|
+
/**
|
|
149
|
+
* High-level helper: run the bot-link auth flow (initTelegram →
|
|
150
|
+
* poll) and resolve with the authenticated User. Works in every
|
|
151
|
+
* Telegram client (including third-party like Nicegram, Plus
|
|
152
|
+
* Messenger) because it doesn't depend on the Login Widget — the
|
|
153
|
+
* user just opens the deep link and presses Start in the bot.
|
|
154
|
+
*
|
|
155
|
+
* The caller's `onBotLinkReady` callback receives the generated
|
|
156
|
+
* `https://t.me/<bot>?start=<token>` link exactly once; use it to
|
|
157
|
+
* `window.open(url, "_blank")`, render a QR, or redirect the
|
|
158
|
+
* user. Polling for the auth handshake starts immediately after.
|
|
159
|
+
*
|
|
160
|
+
* Rejects with `GameCoreError("Timeout")` if the user doesn't
|
|
161
|
+
* press Start within `timeoutMs`.
|
|
162
|
+
*/
|
|
163
|
+
loginViaTelegramBot: (opts: TelegramBotLoginOptions) => Promise<User>;
|
|
127
164
|
/**
|
|
128
165
|
* Get VK OAuth authorize URL (authorization code flow).
|
|
129
166
|
*
|
|
@@ -248,7 +285,31 @@ export declare class GameCoreClient {
|
|
|
248
285
|
inStockOnly?: boolean;
|
|
249
286
|
sort?: "popular" | "name" | "new" | "soldCount";
|
|
250
287
|
q?: string;
|
|
288
|
+
/**
|
|
289
|
+
* Filter by canonical platform slug — "steam", "xbox", "psn",
|
|
290
|
+
* "mobile_game", etc. Use slugs from `getPlatforms()`.
|
|
291
|
+
*/
|
|
292
|
+
platform?: string;
|
|
293
|
+
/**
|
|
294
|
+
* Filter by canonical category slug — "currency",
|
|
295
|
+
* "battle_pass", "bundle", etc. Use slugs from `getCategories()`.
|
|
296
|
+
*/
|
|
297
|
+
category?: string;
|
|
251
298
|
}) => Promise<PagedGamesResponse>;
|
|
299
|
+
/**
|
|
300
|
+
* List distribution platforms present in the site catalog with
|
|
301
|
+
* per-platform game counts (post site-overrides). Drives
|
|
302
|
+
* storefront filter chips. Introduced in task 118 Phase 3.
|
|
303
|
+
*/
|
|
304
|
+
getPlatforms: () => Promise<PlatformInfo[]>;
|
|
305
|
+
/**
|
|
306
|
+
* List canonical category slugs present in the site catalog
|
|
307
|
+
* with per-slug game counts. Use the returned `slug` as the
|
|
308
|
+
* `category` argument to `getGames()`. Not to be confused with
|
|
309
|
+
* `getCategories(gameSlug)` which returns categories of a
|
|
310
|
+
* single game. Introduced in task 119 Phase 3.
|
|
311
|
+
*/
|
|
312
|
+
getCatalogCategories: () => Promise<CategoryInfo[]>;
|
|
252
313
|
/** Get homepage ranked games */
|
|
253
314
|
getHomepageGames: () => Promise<Game[]>;
|
|
254
315
|
/** Get single game with categories */
|
package/dist/index.js
CHANGED
|
@@ -109,6 +109,91 @@ class GameCoreClient {
|
|
|
109
109
|
}),
|
|
110
110
|
verifyMiniApp: (initData, ref) => this.request("POST", "/auth/telegram/miniapp", { initData, ref }, { rawResponse: true }),
|
|
111
111
|
verifyTelegramWidget: (data, ref) => this.request("POST", "/auth/telegram/widget", { ...data, ref }, { rawResponse: true }),
|
|
112
|
+
renderTelegramWidget: async (opts) => {
|
|
113
|
+
if (typeof document === "undefined") {
|
|
114
|
+
throw new GameCoreError("renderTelegramWidget requires a browser environment", 0, "NO_DOM");
|
|
115
|
+
}
|
|
116
|
+
let botUsername = opts.botUsername;
|
|
117
|
+
if (!botUsername) {
|
|
118
|
+
const config = await this.request("GET", "/site/config");
|
|
119
|
+
botUsername = config.authConfig?.telegram?.botUsername ?? undefined;
|
|
120
|
+
}
|
|
121
|
+
if (!botUsername) {
|
|
122
|
+
throw new GameCoreError("Telegram bot is not configured for this site — contact the operator to set a bot in master-admin", 0, "TG_NOT_CONFIGURED");
|
|
123
|
+
}
|
|
124
|
+
const callbackName = `__gcTgWidget_${Math.random().toString(36).slice(2)}`;
|
|
125
|
+
const onAuth = opts.onAuth;
|
|
126
|
+
const onError = opts.onError;
|
|
127
|
+
const ref = opts.ref;
|
|
128
|
+
const verify = this.auth.verifyTelegramWidget.bind(this.auth);
|
|
129
|
+
window[callbackName] = async (user) => {
|
|
130
|
+
try {
|
|
131
|
+
const res = await verify(user, ref);
|
|
132
|
+
if (!res.user) {
|
|
133
|
+
throw new GameCoreError(res.error || "Telegram verification returned no user", 401, "VERIFY_FAILED");
|
|
134
|
+
}
|
|
135
|
+
await onAuth(res.user);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (onError)
|
|
138
|
+
onError(err);
|
|
139
|
+
else
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const script = document.createElement("script");
|
|
144
|
+
script.async = true;
|
|
145
|
+
script.src = "https://telegram.org/js/telegram-widget.js?22";
|
|
146
|
+
script.setAttribute("data-telegram-login", botUsername);
|
|
147
|
+
script.setAttribute("data-size", opts.size ?? "large");
|
|
148
|
+
if (typeof opts.cornerRadius === "number") {
|
|
149
|
+
script.setAttribute("data-radius", String(opts.cornerRadius));
|
|
150
|
+
}
|
|
151
|
+
if (opts.showUserPic === false) {
|
|
152
|
+
script.setAttribute("data-userpic", "false");
|
|
153
|
+
}
|
|
154
|
+
if (opts.requestWriteAccess !== false) {
|
|
155
|
+
script.setAttribute("data-request-access", "write");
|
|
156
|
+
}
|
|
157
|
+
script.setAttribute("data-onauth", `${callbackName}(user)`);
|
|
158
|
+
opts.container.replaceChildren();
|
|
159
|
+
opts.container.appendChild(script);
|
|
160
|
+
return () => {
|
|
161
|
+
opts.container.replaceChildren();
|
|
162
|
+
delete window[callbackName];
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
loginViaTelegramBot: async (opts) => {
|
|
166
|
+
const init = await this.auth.initTelegram();
|
|
167
|
+
opts.onBotLinkReady(init.botLink, init.token);
|
|
168
|
+
const pollMs = opts.pollMs ?? 2000;
|
|
169
|
+
const timeoutMs = opts.timeoutMs ?? 120000;
|
|
170
|
+
const deadline = Date.now() + timeoutMs;
|
|
171
|
+
const refParam = opts.ref ? `?ref=${encodeURIComponent(opts.ref)}` : "";
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const tick = async () => {
|
|
174
|
+
if (Date.now() > deadline) {
|
|
175
|
+
reject(new GameCoreError("Telegram login timed out", 408, "TIMEOUT"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const res = await this.request("GET", `/auth/telegram/status/${init.token}${refParam}`, undefined, { rawResponse: true });
|
|
180
|
+
if (res.status === "authenticated" && res.user) {
|
|
181
|
+
resolve(res.user);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (res.status === "expired" || res.status === "used") {
|
|
185
|
+
reject(new GameCoreError("Telegram auth token expired", 410, "TOKEN_EXPIRED"));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
reject(err);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setTimeout(tick, pollMs);
|
|
193
|
+
};
|
|
194
|
+
setTimeout(tick, pollMs);
|
|
195
|
+
});
|
|
196
|
+
},
|
|
112
197
|
getVkAuthUrl: (redirectUri, ref) => {
|
|
113
198
|
const params = new URLSearchParams;
|
|
114
199
|
params.set("redirect", redirectUri);
|
|
@@ -120,7 +205,10 @@ class GameCoreClient {
|
|
|
120
205
|
verifyVk: (accessToken, ref) => this.request("POST", "/auth/vk/verify", { accessToken, ref }, { rawResponse: true }),
|
|
121
206
|
register: (email, password, firstName, ref) => this.request("POST", "/auth/register", { email, password, firstName, ref }, { rawResponse: true }),
|
|
122
207
|
login: (email, password) => this.request("POST", "/auth/login", { email, password }, { rawResponse: true }),
|
|
123
|
-
changePassword: (currentPassword, newPassword) => this.request("POST", "/auth/change-password", {
|
|
208
|
+
changePassword: (currentPassword, newPassword) => this.request("POST", "/auth/change-password", {
|
|
209
|
+
currentPassword,
|
|
210
|
+
newPassword
|
|
211
|
+
}),
|
|
124
212
|
getMe: () => this.request("GET", "/auth/me", undefined, { rawResponse: true }),
|
|
125
213
|
logout: () => this.request("POST", "/auth/logout"),
|
|
126
214
|
getIdentities: () => this.request("GET", "/auth/identities", undefined, { rawResponse: true }),
|
|
@@ -151,9 +239,15 @@ class GameCoreClient {
|
|
|
151
239
|
qs.set("sort", params.sort);
|
|
152
240
|
if (params?.q)
|
|
153
241
|
qs.set("q", params.q);
|
|
242
|
+
if (params?.platform)
|
|
243
|
+
qs.set("platform", params.platform);
|
|
244
|
+
if (params?.category)
|
|
245
|
+
qs.set("category", params.category);
|
|
154
246
|
const q = qs.toString();
|
|
155
247
|
return this.request("GET", `/catalog/games${q ? `?${q}` : ""}`);
|
|
156
248
|
},
|
|
249
|
+
getPlatforms: () => this.request("GET", "/catalog/platforms"),
|
|
250
|
+
getCatalogCategories: () => this.request("GET", "/catalog/categories"),
|
|
157
251
|
getHomepageGames: () => this.request("GET", "/catalog/homepage-games"),
|
|
158
252
|
getGame: (slug, locale) => {
|
|
159
253
|
const qs = locale ? `?locale=${locale}` : "";
|
|
@@ -261,7 +355,9 @@ class GameCoreClient {
|
|
|
261
355
|
},
|
|
262
356
|
getPushPublicKey: () => this.request("GET", "/profile/push/public-key"),
|
|
263
357
|
subscribePush: (subscription) => this.request("POST", "/profile/push/subscribe", subscription),
|
|
264
|
-
unsubscribePush: (endpoint) => this.request("POST", "/profile/push/unsubscribe", {
|
|
358
|
+
unsubscribePush: (endpoint) => this.request("POST", "/profile/push/unsubscribe", {
|
|
359
|
+
endpoint
|
|
360
|
+
})
|
|
265
361
|
};
|
|
266
362
|
favorites = {
|
|
267
363
|
list: () => this.request("GET", "/favorites"),
|
package/dist/types.d.ts
CHANGED
|
@@ -69,6 +69,17 @@ export interface SiteConfig {
|
|
|
69
69
|
};
|
|
70
70
|
modules: Record<string, boolean>;
|
|
71
71
|
auth: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Structured per-method auth config. Companion to the flat `auth` array
|
|
74
|
+
* — `auth` tells you WHICH methods are available, `authConfig` gives
|
|
75
|
+
* the details you need to render each method on the client (bot
|
|
76
|
+
* username for the Telegram Login Widget, etc).
|
|
77
|
+
*/
|
|
78
|
+
authConfig?: {
|
|
79
|
+
telegram: {
|
|
80
|
+
botUsername: string;
|
|
81
|
+
} | null;
|
|
82
|
+
};
|
|
72
83
|
payments: string[];
|
|
73
84
|
displayCurrency: string;
|
|
74
85
|
rateMode: "auto" | "manual";
|
|
@@ -87,6 +98,76 @@ export interface SiteConfig {
|
|
|
87
98
|
} | null;
|
|
88
99
|
};
|
|
89
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Options for the high-level {@link GameCoreClient.auth.renderTelegramWidget}
|
|
103
|
+
* helper. Values map 1:1 to Telegram Login Widget `data-*` attributes
|
|
104
|
+
* (see https://core.telegram.org/widgets/login).
|
|
105
|
+
*/
|
|
106
|
+
export interface TelegramWidgetRenderOptions {
|
|
107
|
+
/** DOM element the widget renders into. Previous children are cleared. */
|
|
108
|
+
container: HTMLElement;
|
|
109
|
+
/** Button size. Default: "large". */
|
|
110
|
+
size?: "small" | "medium" | "large";
|
|
111
|
+
/** Corner radius in pixels. Default: Telegram's own (20). */
|
|
112
|
+
cornerRadius?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Show user photo next to the button. Default: true (matches Telegram's
|
|
115
|
+
* own default of `data-userpic="true"`).
|
|
116
|
+
*/
|
|
117
|
+
showUserPic?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Request permission to send messages to the user from the bot. Sets
|
|
120
|
+
* `data-request-access="write"`. Default: true.
|
|
121
|
+
*/
|
|
122
|
+
requestWriteAccess?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Called after the widget reports an authenticated user AND the
|
|
125
|
+
* payload has been verified by GameCore server-side (HMAC-SHA256).
|
|
126
|
+
* The `user` object is GameCore's {@link TelegramAuthUser} —
|
|
127
|
+
* the same shape the underlying `POST /auth/telegram/widget`
|
|
128
|
+
* endpoint returns.
|
|
129
|
+
*/
|
|
130
|
+
onAuth: (user: TelegramAuthUser) => void | Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Called if verification fails (expired hash, wrong signature, or
|
|
133
|
+
* network error). When omitted the error is thrown to the browser's
|
|
134
|
+
* unhandled-promise handler.
|
|
135
|
+
*/
|
|
136
|
+
onError?: (err: Error) => void;
|
|
137
|
+
/** Optional referral code to thread through to the signup flow. */
|
|
138
|
+
ref?: string;
|
|
139
|
+
/**
|
|
140
|
+
* Bot username override. Defaults to the value from `gc.site.getConfig()`
|
|
141
|
+
* — only pass this when you have not called getConfig() yet and want
|
|
142
|
+
* to skip that round trip.
|
|
143
|
+
*/
|
|
144
|
+
botUsername?: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Options for the high-level {@link GameCoreClient.auth.loginViaTelegramBot}
|
|
148
|
+
* helper. Orchestrates the init → poll flow for users whose Telegram
|
|
149
|
+
* client can't render the Login Widget (third-party mobile clients with
|
|
150
|
+
* no active Telegram Web session, desktop browsers without TG Web
|
|
151
|
+
* logged in, etc).
|
|
152
|
+
*/
|
|
153
|
+
export interface TelegramBotLoginOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Callback that receives the generated bot deep-link. Use it to open
|
|
156
|
+
* a popup, render a QR code, or redirect the user. This is called
|
|
157
|
+
* exactly once per login attempt, as soon as the server returns the
|
|
158
|
+
* link.
|
|
159
|
+
*/
|
|
160
|
+
onBotLinkReady: (botLink: string, token: string) => void;
|
|
161
|
+
/** Poll interval in milliseconds. Default: 2000. */
|
|
162
|
+
pollMs?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Give up after this many milliseconds. Default: 120000 (2 minutes,
|
|
165
|
+
* matches the server-side token TTL).
|
|
166
|
+
*/
|
|
167
|
+
timeoutMs?: number;
|
|
168
|
+
/** Optional referral code to thread through to the signup flow. */
|
|
169
|
+
ref?: string;
|
|
170
|
+
}
|
|
90
171
|
export interface ExchangeRates {
|
|
91
172
|
usdToRub: number;
|
|
92
173
|
base: string;
|
|
@@ -216,6 +297,40 @@ export interface Category {
|
|
|
216
297
|
productCount: number;
|
|
217
298
|
deliveryTypes?: string[];
|
|
218
299
|
platforms?: string[];
|
|
300
|
+
/**
|
|
301
|
+
* Canonical category slug (e.g. "currency", "battle_pass"). Present
|
|
302
|
+
* on responses from servers running task 119 Phase 2+. Stable across
|
|
303
|
+
* suppliers — Nexus "Алмазы" and Vendoria "Diamonds" both carry
|
|
304
|
+
* canonicalSlug="currency". Use this (not `name`) for storefront
|
|
305
|
+
* dedup and routing.
|
|
306
|
+
*/
|
|
307
|
+
canonicalSlug?: string | null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Distribution platform descriptor from GET /catalog/platforms.
|
|
311
|
+
* Use `slug` as the key for ?platform= filter and as a stable identifier
|
|
312
|
+
* across locales. `label` is storefront-ready display text.
|
|
313
|
+
* Introduced by task 118 Phase 3.
|
|
314
|
+
*/
|
|
315
|
+
export interface PlatformInfo {
|
|
316
|
+
slug: string;
|
|
317
|
+
label: string;
|
|
318
|
+
group: string;
|
|
319
|
+
gameCount: number;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Canonical category descriptor from GET /catalog/categories. Use
|
|
323
|
+
* `slug` as the key for ?category= filter. `labelRu`/`labelEn` are
|
|
324
|
+
* storefront-ready display text in each supported UI locale; pick
|
|
325
|
+
* whichever matches the current site locale. Introduced by task 119
|
|
326
|
+
* Phase 3.
|
|
327
|
+
*/
|
|
328
|
+
export interface CategoryInfo {
|
|
329
|
+
slug: string;
|
|
330
|
+
labelRu: string;
|
|
331
|
+
labelEn: string;
|
|
332
|
+
group: string;
|
|
333
|
+
gameCount: number;
|
|
219
334
|
}
|
|
220
335
|
export interface FulfillmentMeta {
|
|
221
336
|
needsPlayerId: boolean;
|