@gamecore-api/sdk 0.12.0 → 0.14.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 CHANGED
@@ -1,17 +1,29 @@
1
- # @gamecore/sdk
1
+ # @gamecore-api/sdk
2
2
 
3
3
  TypeScript SDK for GameCore API — zero external dependencies, browser-safe.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @gamecore/sdk
8
+ npm install @gamecore-api/sdk
9
9
  ```
10
10
 
11
+ ## What's new in 0.14.0
12
+
13
+ - **BREAKING** `giftCards.purchase()` signature changed: first arg is now `amountRub` (was `amountUsd`). GiftCard payload fields renamed — `amount_usd` → `amount_rub`, added `currency`, `remainingBalance`, `expiresAt`. `denomination` is now optional (legacy)
14
+ - `cart.merge(items)` — guest → authed cart handoff; new response fields `quantity`, `addedAt`, `gameIcon`
15
+ - `auth.linkEmail(email, password)` — add email identity to an existing Telegram/VK account
16
+ - `profile.getConversations / getConversationMessages / submitCode / submitScreenshot` — in-profile support chat
17
+ - `profile.getPushPublicKey / subscribePush / unsubscribePush` — web push subscriptions
18
+ - `referrals.getPopularProducts(limit)` + `referrals.getPerformance({ from, to })` — affiliate analytics
19
+ - `site.requestGame({ gameName, contact })` — public "request a game" lead capture
20
+ - `site.getSitemapData()` — data source for `sitemap.xml`
21
+ - `checkout.completeWithBalance` now returns `{ newBalance }`
22
+
11
23
  ## Quick Start
12
24
 
13
25
  ```typescript
14
- import { GameCoreClient } from "@gamecore/sdk";
26
+ import { GameCoreClient } from "@gamecore-api/sdk";
15
27
 
16
28
  const gc = new GameCoreClient({
17
29
  apiKey: "gc_live_YOUR_KEY",
@@ -50,6 +62,23 @@ console.log("Logged in:", user.firstName);
50
62
  const { user } = await gc.auth.verifyVk(vkAccessToken);
51
63
  ```
52
64
 
65
+ ### Email + Password
66
+
67
+ ```typescript
68
+ await gc.auth.register(email, password, firstName, ref);
69
+ await gc.auth.login(email, password);
70
+ await gc.auth.changePassword(currentPassword, newPassword);
71
+ ```
72
+
73
+ ### Link additional identity
74
+
75
+ ```typescript
76
+ // Add email to an existing Telegram/VK account
77
+ await gc.auth.linkEmail(email, password);
78
+ // Or link a VK access token
79
+ await gc.auth.linkVk(vkAccessToken);
80
+ ```
81
+
53
82
  ### Session
54
83
 
55
84
  ```typescript
@@ -88,10 +117,14 @@ const { suggestions } = await gc.catalog.searchSuggestions("rob");
88
117
  ```typescript
89
118
  // Cart
90
119
  const items = await gc.cart.get();
91
- await gc.cart.add({ productId: 10, gameId: "roblox", gameName: "Roblox", productName: "800 Robux", price: 799, deliveryData: { username: "player123" } });
120
+ // items: [{ id, productId, quantity, addedAt, gameIcon, ... }]
121
+ await gc.cart.add({ productId: 10, gameId: "roblox", gameName: "Roblox", productName: "800 Robux", price: 799, deliveryData: { username: "player123" }, quantity: 2 });
92
122
  await gc.cart.remove(itemId);
93
123
  await gc.cart.clear();
94
124
 
125
+ // Merge guest cart into authed session (on login)
126
+ await gc.cart.merge(guestItems);
127
+
95
128
  // Checkout (auto-generates idempotency key)
96
129
  const checkout = await gc.checkout.create({
97
130
  items: [{ productId: 10, deliveryData: { username: "player123" } }],
@@ -144,6 +177,17 @@ const notifications = await gc.profile.getNotifications();
144
177
  const { count } = await gc.profile.getUnreadCount();
145
178
  await gc.profile.markRead(notificationId);
146
179
  await gc.profile.markAllRead();
180
+
181
+ // Support conversations (in-profile chat)
182
+ const conversations = await gc.profile.getConversations();
183
+ const messages = await gc.profile.getConversationMessages(conversationId);
184
+ await gc.profile.submitCode(conversationId, requestId, "ABC-123");
185
+ await gc.profile.submitScreenshot(conversationId, requestId, file);
186
+
187
+ // Web push subscriptions
188
+ const { publicKey } = await gc.profile.getPushPublicKey();
189
+ await gc.profile.subscribePush({ endpoint, keys: { p256dh, auth } });
190
+ await gc.profile.unsubscribePush(endpoint);
147
191
  ```
148
192
 
149
193
  ## Favorites
@@ -185,8 +229,10 @@ await gc.coupons.remove();
185
229
  const active = await gc.coupons.getActive();
186
230
 
187
231
  // Gift cards
188
- const card = await gc.giftCards.purchase(5); // amountUsd
232
+ const card = await gc.giftCards.purchase(500, "Happy birthday!"); // amountRub + optional message
189
233
  await gc.giftCards.redeem("GC-XXXX-XXXX-XXXX");
234
+ const mine = await gc.giftCards.getMine();
235
+ // { code, amount_rub, currency, remainingBalance, expiresAt, ... }
190
236
  ```
191
237
 
192
238
  ## Referrals
@@ -198,6 +244,15 @@ const link = await gc.referrals.createLink({ label: "YouTube", slug: "my-channel
198
244
  await gc.referrals.updateLink(link.id, { label: "Updated" });
199
245
  const linkStats = await gc.referrals.getLinkStats(link.id);
200
246
  const commissions = await gc.referrals.getCommissions();
247
+
248
+ // Popular products referred by this user
249
+ const popular = await gc.referrals.getPopularProducts(10);
250
+
251
+ // Performance over a date range
252
+ const perf = await gc.referrals.getPerformance({
253
+ from: "2026-04-01",
254
+ to: "2026-04-30",
255
+ });
201
256
  ```
202
257
 
203
258
  ## Balance Top-up
@@ -236,7 +291,7 @@ const schema = await gc.seo.getSchema("product", 42);
236
291
 
237
292
  ```typescript
238
293
  // Import from server entrypoint (uses node:crypto)
239
- import { verifyWebhookSignature, parseWebhookPayload } from "@gamecore/sdk/server";
294
+ import { verifyWebhookSignature, parseWebhookPayload } from "@gamecore-api/sdk/server";
240
295
 
241
296
  const isValid = verifyWebhookSignature(
242
297
  requestBody,
@@ -253,7 +308,7 @@ if (isValid) {
253
308
  ## Utilities
254
309
 
255
310
  ```typescript
256
- import { convertPrice, formatPrice, generateIdempotencyKey } from "@gamecore/sdk";
311
+ import { convertPrice, formatPrice, generateIdempotencyKey } from "@gamecore-api/sdk";
257
312
 
258
313
  const rub = convertPrice(1.99, 92.5); // 184.08
259
314
  const formatted = formatPrice(rub, "RUB"); // "184 ₽"
@@ -266,7 +321,7 @@ const key = generateIdempotencyKey(); // UUID v4
266
321
 
267
322
  ```typescript
268
323
  // app/catalog/page.tsx
269
- import { GameCoreClient } from "@gamecore/sdk";
324
+ import { GameCoreClient } from "@gamecore-api/sdk";
270
325
 
271
326
  const gc = new GameCoreClient({
272
327
  apiKey: process.env.GAMECORE_API_KEY!,
@@ -297,7 +352,7 @@ export function CartWidget() {
297
352
 
298
353
  ```typescript
299
354
  // app/api/webhooks/gamecore/route.ts
300
- import { verifyWebhookSignature, parseWebhookPayload } from "@gamecore/sdk/server";
355
+ import { verifyWebhookSignature, parseWebhookPayload } from "@gamecore-api/sdk/server";
301
356
 
302
357
  export async function POST(req: Request) {
303
358
  const body = await req.text();
@@ -317,16 +372,16 @@ export async function POST(req: Request) {
317
372
 
318
373
  | Namespace | Methods |
319
374
  |-----------|---------|
320
- | `gc.site` | getConfig, getRates, getLegal |
321
- | `gc.auth` | initTelegram, pollTelegramStatus, verifyVk, getMe, logout, getIdentities, linkVk, unlinkProvider |
322
- | `gc.catalog` | getGames, getHomepageGames, getGame, getProducts, getProductsGrouped, search, searchSuggestions, getProduct |
323
- | `gc.cart` | get, add, sync, remove, clear |
375
+ | `gc.site` | getConfig, getRates, getLegal, getStats, getSocialProof, getThemeConfig, getTranslations, getUIConfig, getCookieConsent, getCatalogSections, getBanners, getAnnouncementBar, getSitemapData, requestGame |
376
+ | `gc.auth` | initTelegram, pollTelegramStatus, verifyMiniApp, verifyTelegramWidget, getVkAuthUrl, vkCallback, verifyVk, register, login, changePassword, getMe, logout, getIdentities, linkVk, linkEmail, unlinkProvider, mergePreview, mergeConfirm |
377
+ | `gc.catalog` | getGames, getHomepageGames, getGame, getRecommendations, getCategories, getProducts, getProductsGrouped, search, searchSuggestions, getProduct |
378
+ | `gc.cart` | get, add, merge, sync, remove, clear |
324
379
  | `gc.checkout` | create, completeWithBalance, getPaymentMethods |
325
380
  | `gc.orders` | list, get, getByPayment, cancel |
326
- | `gc.profile` | getBalance, getLevelStatus, getTransactions, getOrders, getNotifications, getUnreadCount, markRead, markAllRead |
381
+ | `gc.profile` | getBalance, getLevelStatus, getTransactions, getOrders, getNotifications, getUnreadCount, markRead, markAllRead, getConversations, getConversationMessages, submitCode, submitScreenshot, getPushPublicKey, subscribePush, unsubscribePush |
327
382
  | `gc.favorites` | list, add, remove |
328
383
  | `gc.coupons` | apply, remove, validate, getActive |
329
- | `gc.referrals` | getStats, getLinks, createLink, updateLink, deleteLink, getLinkStats, getCommissions |
384
+ | `gc.referrals` | getStats, getLinks, createLink, updateLink, deleteLink, getLinkStats, getCommissions, getPopularProducts, getPerformance |
330
385
  | `gc.reviews` | listPublic, getStats, getRandom, getMine, getPending, create |
331
386
  | `gc.topup` | getPaymentMethods, create, getStatus |
332
387
  | `gc.giftCards` | purchase, redeem, check, getMine |
@@ -339,5 +394,5 @@ export async function POST(req: Request) {
339
394
 
340
395
  | Import | Environment | Includes |
341
396
  |--------|-------------|----------|
342
- | `@gamecore/sdk` | Browser + Node | Client, types, utilities |
343
- | `@gamecore/sdk/server` | Node only | Webhook verification (uses node:crypto) |
397
+ | `@gamecore-api/sdk` | Browser + Node | Client, types, utilities |
398
+ | `@gamecore-api/sdk/server` | Node only | Webhook verification (uses node:crypto) |
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TelegramInitResponse, TelegramWidgetUser, TelegramAuthResponse, User, SiteConfig, ExchangeRates, LegalDocument, Game, GameDetail, Product, SearchResult, Category, CartItem, CheckoutRequest, CheckoutResponse, Order, UserBalance, LevelStatus, Transaction, Notification, Favorite, Review, ReviewStats, CouponResult, ReferralStats, ReferralLink, ReferralCommission, TopupMethod, TopupResponse, TopupStatus, GiftCard, Announcement, AnnouncementBar, SiteUIConfig, PaymentMethod, CatalogSection, SiteStats, ProductFilters, PaginatedResponse, PagedGamesResponse } from "./types";
1
+ import type { TelegramInitResponse, TelegramWidgetUser, TelegramAuthResponse, User, SiteConfig, ExchangeRates, LegalDocument, Game, GameDetail, Product, SearchResult, Category, CartItem, CheckoutRequest, CheckoutResponse, Conversation, ConversationDetail, Order, PaymentInfo, CompleteWithBalanceResult, UserBalance, WebPushSubscriptionInput, LevelStatus, Transaction, Notification, Favorite, Review, ReviewCreateResult, ReviewStats, CouponResult, ReferralStats, ReferralLink, ReferralCommission, ReferralPerformance, TopupMethod, TopupResponse, TopupStatus, GiftCard, Announcement, AnnouncementBar, SiteUIConfig, PaymentMethod, CatalogSection, SiteStats, ProductFilters, PaginatedResponse, PagedGamesResponse } from "./types";
2
2
  export interface GameCoreOptions {
3
3
  /** Site API key (gc_live_xxx or gc_test_xxx) */
4
4
  apiKey: string;
@@ -47,18 +47,59 @@ export declare class GameCoreClient {
47
47
  }>;
48
48
  /** Get per-site catalog sections (category tabs config) */
49
49
  getCatalogSections: () => Promise<CatalogSection[]>;
50
- /** Get active hero banners for carousel */
50
+ /**
51
+ * Get active hero banners for the storefront carousel.
52
+ * Each banner carries optional visual fields plus a `url`
53
+ * destination — `null` means the banner is display-only and
54
+ * should not be rendered as a link.
55
+ */
51
56
  getBanners: () => Promise<{
52
57
  id: number;
53
58
  title: string | null;
54
59
  description: string | null;
55
60
  imageUrl: string | null;
56
61
  color: string | null;
62
+ url: string | null;
57
63
  scope: string;
58
64
  priority: number;
59
65
  }[]>;
60
66
  /** Get announcement bar settings (text, link, enabled) */
61
67
  getAnnouncementBar: () => Promise<AnnouncementBar>;
68
+ /**
69
+ * Get sitemap data for self-generated storefront sitemaps.
70
+ * Returns `{ slug, updatedAt }` for every game visible on
71
+ * this site. Use `updatedAt` as `lastModified` in Next.js
72
+ * `sitemap.ts` so Google's crawler respects unchanged pages
73
+ * instead of stamping the whole catalog with `new Date()`.
74
+ */
75
+ getSitemapData: () => Promise<{
76
+ slug: string;
77
+ updatedAt: string;
78
+ }[]>;
79
+ /**
80
+ * Submit a "please add this game" request. Works for both
81
+ * authenticated users (the server attaches the user id so
82
+ * support can reach back) and anonymous visitors.
83
+ *
84
+ * Rate-limited server-side. The optional `website` field is
85
+ * a honeypot — leave it `undefined` in real code; the
86
+ * storefront can render an invisible input under that name
87
+ * so scrapers self-identify.
88
+ *
89
+ * @returns The new request id and a success flag. A filled
90
+ * honeypot still returns `success: true` with
91
+ * `requestId: -1` so bots cannot distinguish a real save
92
+ * from a silent drop.
93
+ */
94
+ requestGame: (data: {
95
+ gameName: string;
96
+ comment?: string;
97
+ gameUrl?: string;
98
+ website?: string;
99
+ }) => Promise<{
100
+ success: boolean;
101
+ requestId: number;
102
+ }>;
62
103
  };
63
104
  auth: {
64
105
  /** Start Telegram auth flow → returns bot link for user to click */
@@ -155,6 +196,22 @@ export declare class GameCoreClient {
155
196
  }>;
156
197
  /** Link VK account to current user */
157
198
  linkVk: (accessToken: string) => Promise<void>;
199
+ /**
200
+ * Attach an email + password identity to the currently
201
+ * authenticated user. Use this for Telegram-only / VK-only
202
+ * accounts that want a backup login method. The user must
203
+ * not already have an email set — update flows go through
204
+ * `changePassword()` or a future `changeEmail()` method.
205
+ *
206
+ * Rate-limited to the same bucket as `/auth/register`.
207
+ * Returns the normalised email on success; throws 409 if the
208
+ * user already has an email or if another account on this
209
+ * site owns the address.
210
+ */
211
+ linkEmail: (email: string, password: string) => Promise<{
212
+ success: boolean;
213
+ email: string;
214
+ }>;
158
215
  /** Unlink auth provider */
159
216
  unlinkProvider: (provider: string) => Promise<void>;
160
217
  /** Preview VK account merge (before confirming) */
@@ -196,7 +253,22 @@ export declare class GameCoreClient {
196
253
  getHomepageGames: () => Promise<Game[]>;
197
254
  /** Get single game with categories */
198
255
  getGame: (slug: string, locale?: string) => Promise<GameDetail>;
199
- /** Get recommended games (same type, random) */
256
+ /**
257
+ * Get recommended games for a game detail page
258
+ * ("you might also like" / "related games").
259
+ *
260
+ * Returns games of the same `type` as the source game, randomly
261
+ * selected and filtered by per-site visibility overrides.
262
+ *
263
+ * @param gameSlug - Source game slug. Must be the **canonical**
264
+ * slug — this endpoint does NOT resolve aliases and returns an
265
+ * empty array for alias inputs. If you only have an alias,
266
+ * call `getGame()` first and use
267
+ * `result.canonicalSlug ?? result.slug`.
268
+ * @param limit - Max recommendations (default 8). The backend
269
+ * caps the effective limit; very large values are trimmed
270
+ * server-side.
271
+ */
200
272
  getRecommendations: (gameSlug: string, limit?: number) => Promise<Game[]>;
201
273
  /** Get categories for a game with product counts and delivery metadata */
202
274
  getCategories: (gameSlug: string) => Promise<Category[]>;
@@ -229,17 +301,22 @@ export declare class GameCoreClient {
229
301
  minItems: number;
230
302
  discountPercent: number;
231
303
  }[]>;
232
- /** Get active promo campaigns */
304
+ /**
305
+ * Get active promo campaigns. `targetUrl` is the clickable
306
+ * destination for the banner artwork (nullable — null means
307
+ * display-only).
308
+ */
233
309
  getPromos: () => Promise<{
234
310
  id: number;
235
311
  name: string;
236
312
  type: string;
237
313
  value: number;
238
314
  scope: string;
239
- bannerTitle?: string;
240
- bannerDescription?: string;
241
- bannerImageUrl?: string;
242
- bannerColor?: string;
315
+ bannerTitle?: string | null;
316
+ bannerDescription?: string | null;
317
+ bannerImageUrl?: string | null;
318
+ bannerColor?: string | null;
319
+ targetUrl?: string | null;
243
320
  endsAt?: string;
244
321
  }[]>;
245
322
  /** Get all games with full metadata (categories, delivery types) */
@@ -254,12 +331,35 @@ export declare class GameCoreClient {
254
331
  }>;
255
332
  };
256
333
  cart: {
257
- /** Get cart items for authenticated user */
334
+ /**
335
+ * Get cart items for the authenticated user. Each row comes
336
+ * back with `quantity`, `addedAt`, and `gameIcon` /
337
+ * `productIcon` populated by the server (2 extra lookups for
338
+ * the whole cart, not per row).
339
+ */
258
340
  get: () => Promise<CartItem[]>;
259
- /** Add single item to cart */
341
+ /**
342
+ * Add a single item to the cart. `quantity` is optional and
343
+ * defaults to `1`. The server caps per-row quantity at 999.
344
+ */
260
345
  add: (item: Omit<CartItem, "id">) => Promise<CartItem>;
261
- /** Sync localStorage cart to server (replaces server cart) */
346
+ /**
347
+ * Replace the server cart with the given list. Kept for
348
+ * backwards compatibility with SDK <= 0.12 and for the
349
+ * "clear everything and insert fresh" use case. Prefer
350
+ * `cart.merge()` when handling the guest → authed handoff so
351
+ * an existing server cart is not wiped.
352
+ */
262
353
  sync: (items: Omit<CartItem, "id">[]) => Promise<CartItem[]>;
354
+ /**
355
+ * Merge the given items into the server cart without wiping
356
+ * it. Items colliding on `(productId, deliveryData)` add
357
+ * their quantities (capped at 999 per row); new rows are
358
+ * inserted. Use this when a guest with a localStorage cart
359
+ * logs in and we want to keep both the old server rows and
360
+ * the newly collected ones.
361
+ */
362
+ merge: (items: Omit<CartItem, "id">[]) => Promise<CartItem[]>;
263
363
  /** Remove item from cart by ID */
264
364
  remove: (id: number) => Promise<void>;
265
365
  /** Clear all cart items */
@@ -275,10 +375,20 @@ export declare class GameCoreClient {
275
375
  * No Telegram/VK login required. Balance payments still require auth.
276
376
  */
277
377
  create: (data: CheckoutRequest, idempotencyKey?: string) => Promise<CheckoutResponse>;
278
- /** Complete payment with balance (no external gateway) */
279
- completeWithBalance: (paymentCode: string) => Promise<{
280
- success: boolean;
281
- }>;
378
+ /**
379
+ * Complete a pending payment by deducting the amount from the
380
+ * user's balance (bonus first, then permanent). Returns the
381
+ * deduction breakdown and the post-deduction balance so the
382
+ * success page can render the receipt without a second
383
+ * `profile.getBalance()` fetch (which is race-prone right
384
+ * after a completion).
385
+ *
386
+ * Idempotent replay: a second call for an already-completed
387
+ * payment still succeeds and returns the current `newBalance`,
388
+ * but the deduction breakdown is absent because the original
389
+ * move is not re-derivable from persisted state.
390
+ */
391
+ completeWithBalance: (paymentCode: string) => Promise<CompleteWithBalanceResult>;
282
392
  /** Get available payment methods for checkout */
283
393
  getPaymentMethods: () => Promise<PaymentMethod[]>;
284
394
  };
@@ -298,7 +408,7 @@ export declare class GameCoreClient {
298
408
  * the endpoint does not leak existence of valid codes).
299
409
  */
300
410
  getByPayment: (paymentCode: string, guestEmail?: string) => Promise<{
301
- payment: unknown;
411
+ payment: PaymentInfo;
302
412
  orders: Order[];
303
413
  }>;
304
414
  /** Preview cancel — check if cancellable and refund amount */
@@ -360,6 +470,57 @@ export declare class GameCoreClient {
360
470
  getBroadcastStatus: () => Promise<{
361
471
  subscribed: boolean;
362
472
  }>;
473
+ /** List the current user's conversations (most-recent first). */
474
+ getConversations: () => Promise<Conversation[]>;
475
+ /**
476
+ * Fetch a conversation header plus its messages. Marks any
477
+ * unread admin/system messages as read as a side effect.
478
+ */
479
+ getConversationMessages: (conversationId: number, params?: {
480
+ limit?: number;
481
+ offset?: number;
482
+ }) => Promise<ConversationDetail>;
483
+ /**
484
+ * Submit a code to fulfill an open "code requested" request.
485
+ * Rate-limited to 5 submissions/minute per IP.
486
+ */
487
+ submitCode: (conversationId: number, requestId: number, code: string) => Promise<{
488
+ success: boolean;
489
+ data?: {
490
+ requestId: number;
491
+ };
492
+ }>;
493
+ /**
494
+ * Submit a screenshot file to fulfill an open "screenshot
495
+ * requested" request. Accepts a browser `File` object and
496
+ * delivers it via multipart upload. Rate-limited to 3
497
+ * submissions/minute per IP.
498
+ */
499
+ submitScreenshot: (conversationId: number, requestId: number, file: File) => Promise<{
500
+ success: boolean;
501
+ data?: {
502
+ requestId: number;
503
+ };
504
+ }>;
505
+ /** Get the VAPID public key needed to create a PushSubscription. */
506
+ getPushPublicKey: () => Promise<{
507
+ publicKey: string;
508
+ }>;
509
+ /**
510
+ * Register a push subscription for the current user + site.
511
+ * Call `PushSubscription.toJSON()` in the browser and pass the
512
+ * result directly.
513
+ */
514
+ subscribePush: (subscription: WebPushSubscriptionInput) => Promise<{
515
+ success: boolean;
516
+ }>;
517
+ /**
518
+ * Remove a push subscription by its endpoint. Call this when
519
+ * the user opts out or changes devices.
520
+ */
521
+ unsubscribePush: (endpoint: string) => Promise<{
522
+ success: boolean;
523
+ }>;
363
524
  };
364
525
  favorites: {
365
526
  /** List user's favorite products */
@@ -408,6 +569,38 @@ export declare class GameCoreClient {
408
569
  }>;
409
570
  /** Get commission history */
410
571
  getCommissions: () => Promise<ReferralCommission[]>;
572
+ /**
573
+ * Get a small grid of popular products so referrers can
574
+ * create a one-click reflink without hunting through the
575
+ * catalog. Sorted primarily by referral transaction count on
576
+ * the current site — i.e. "games that other referrers are
577
+ * actually earning commission on". Falls back to the homepage
578
+ * ranking on brand-new sites that have no referral history
579
+ * yet, so the grid is never empty for a new referrer.
580
+ *
581
+ * @param limit - Max number of entries to return (default 10,
582
+ * capped at 50 server-side).
583
+ */
584
+ getPopularProducts: (limit?: number) => Promise<{
585
+ slug: string;
586
+ name: string;
587
+ icon: string | null;
588
+ }[]>;
589
+ /**
590
+ * Get performance stats aggregated over an optional date range.
591
+ *
592
+ * Pass `from` / `to` as Date or ISO-8601 string to filter
593
+ * commissions, transactions, and new-member counts. Clicks
594
+ * remain lifetime — see `ReferralPerformance` docstring for
595
+ * the rationale. Omit both to get all-time totals.
596
+ *
597
+ * Typical usage: wire to 7d / 30d / 90d / all buttons on the
598
+ * profile referrals dashboard.
599
+ */
600
+ getPerformance: (params?: {
601
+ from?: Date | string;
602
+ to?: Date | string;
603
+ }) => Promise<ReferralPerformance>;
411
604
  };
412
605
  reviews: {
413
606
  /** Get public reviews (paginated) */
@@ -425,8 +618,14 @@ export declare class GameCoreClient {
425
618
  getMine: () => Promise<Review[]>;
426
619
  /** Get orders eligible for review */
427
620
  getPending: () => Promise<Order[]>;
428
- /** Submit review for a completed order */
429
- create: (orderId: number, rating: number, text?: string) => Promise<Review>;
621
+ /**
622
+ * Submit a review for a completed order. Returns a narrow
623
+ * {@link ReviewCreateResult} with the new id, rating, text echo,
624
+ * and optional bonus grant. To fetch the full review record
625
+ * (including `createdAt`, `authorName`, `adminReply`, etc.),
626
+ * call `reviews.getMine()` after the create completes.
627
+ */
628
+ create: (orderId: number, rating: number, text?: string) => Promise<ReviewCreateResult>;
430
629
  };
431
630
  topup: {
432
631
  /** Get available topup payment methods */
@@ -437,8 +636,19 @@ export declare class GameCoreClient {
437
636
  getStatus: (code: string) => Promise<TopupStatus>;
438
637
  };
439
638
  giftCards: {
440
- /** Purchase a gift card (amount in USD) */
441
- purchase: (amountUsd: number) => Promise<GiftCard>;
639
+ /**
640
+ * Purchase a gift card. The amount is in RUB (despite the
641
+ * legacy SDK parameter name — the column was misnamed and has
642
+ * always held RUB values, see Wave 3 audit tasks/112a). The
643
+ * method accepts either a plain number for backwards
644
+ * compatibility or an options object with an explicit message.
645
+ *
646
+ * @param amountRub - RUB amount to deduct from the user's
647
+ * balance and encode into the gift card.
648
+ * @param message - Optional personal note shown to the
649
+ * redeeming user.
650
+ */
651
+ purchase: (amountRub: number, message?: string) => Promise<GiftCard>;
442
652
  /** Redeem a gift card code */
443
653
  redeem: (code: string) => Promise<{
444
654
  amount: number;
package/dist/index.js CHANGED
@@ -84,7 +84,9 @@ class GameCoreClient {
84
84
  getCookieConsent: () => this.request("GET", "/site/cookie-consent"),
85
85
  getCatalogSections: () => this.request("GET", "/site/catalog-sections"),
86
86
  getBanners: () => this.request("GET", "/site/banners"),
87
- getAnnouncementBar: () => this.request("GET", "/site/announcement-bar")
87
+ getAnnouncementBar: () => this.request("GET", "/site/announcement-bar"),
88
+ getSitemapData: () => this.request("GET", "/seo/sitemap-data"),
89
+ requestGame: (data) => this.request("POST", "/site/request-game", data)
88
90
  };
89
91
  auth = {
90
92
  initTelegram: () => this.request("POST", "/auth/telegram/init"),
@@ -123,6 +125,7 @@ class GameCoreClient {
123
125
  logout: () => this.request("POST", "/auth/logout"),
124
126
  getIdentities: () => this.request("GET", "/auth/identities", undefined, { rawResponse: true }),
125
127
  linkVk: (accessToken) => this.request("POST", "/auth/link/vk", { accessToken }),
128
+ linkEmail: (email, password) => this.request("POST", "/auth/link-email", { email, password }),
126
129
  unlinkProvider: (provider) => this.request("POST", `/auth/unlink/${provider}`),
127
130
  mergePreview: (accessToken) => this.request("POST", "/auth/merge/preview", { accessToken }),
128
131
  mergeConfirm: (accessToken) => this.request("POST", "/auth/merge/confirm", { accessToken })
@@ -191,6 +194,7 @@ class GameCoreClient {
191
194
  get: () => this.request("GET", "/cart"),
192
195
  add: (item) => this.request("POST", "/cart", item),
193
196
  sync: (items) => this.request("POST", "/cart/sync", { items }),
197
+ merge: (items) => this.request("POST", "/cart/merge", { items }),
194
198
  remove: (id) => this.request("DELETE", `/cart/${id}`),
195
199
  clear: () => this.request("DELETE", "/cart")
196
200
  };
@@ -237,7 +241,27 @@ class GameCoreClient {
237
241
  updateSettings: (data) => this.request("PUT", "/profile/settings", data),
238
242
  subscribeBroadcast: () => this.request("POST", "/profile/broadcast/subscribe"),
239
243
  unsubscribeBroadcast: () => this.request("POST", "/profile/broadcast/unsubscribe"),
240
- getBroadcastStatus: () => this.request("GET", "/profile/broadcast/status")
244
+ getBroadcastStatus: () => this.request("GET", "/profile/broadcast/status"),
245
+ getConversations: () => this.request("GET", "/profile/conversations"),
246
+ getConversationMessages: (conversationId, params) => {
247
+ const qs = new URLSearchParams;
248
+ if (params?.limit)
249
+ qs.set("limit", String(params.limit));
250
+ if (params?.offset)
251
+ qs.set("offset", String(params.offset));
252
+ const q = qs.toString();
253
+ return this.request("GET", `/profile/conversations/${conversationId}/messages${q ? `?${q}` : ""}`);
254
+ },
255
+ submitCode: (conversationId, requestId, code) => this.request("POST", `/profile/conversations/${conversationId}/submit-code`, { requestId, code }),
256
+ submitScreenshot: (conversationId, requestId, file) => {
257
+ const form = new FormData;
258
+ form.append("requestId", String(requestId));
259
+ form.append("file", file);
260
+ return this.request("POST", `/profile/conversations/${conversationId}/submit-screenshot`, form);
261
+ },
262
+ getPushPublicKey: () => this.request("GET", "/profile/push/public-key"),
263
+ subscribePush: (subscription) => this.request("POST", "/profile/push/subscribe", subscription),
264
+ unsubscribePush: (endpoint) => this.request("POST", "/profile/push/unsubscribe", { endpoint })
241
265
  };
242
266
  favorites = {
243
267
  list: () => this.request("GET", "/favorites"),
@@ -257,7 +281,22 @@ class GameCoreClient {
257
281
  updateLink: (id, data) => this.request("PUT", `/referral/links/${id}`, data),
258
282
  deleteLink: (id) => this.request("DELETE", `/referral/links/${id}`),
259
283
  getLinkStats: (id) => this.request("GET", `/referral/links/${id}/stats`),
260
- getCommissions: () => this.request("GET", "/referral/commissions")
284
+ getCommissions: () => this.request("GET", "/referral/commissions"),
285
+ getPopularProducts: (limit) => {
286
+ const qs = limit ? `?limit=${limit}` : "";
287
+ return this.request("GET", `/referral/popular-products${qs}`);
288
+ },
289
+ getPerformance: (params) => {
290
+ const qs = new URLSearchParams;
291
+ if (params?.from) {
292
+ qs.set("from", params.from instanceof Date ? params.from.toISOString() : params.from);
293
+ }
294
+ if (params?.to) {
295
+ qs.set("to", params.to instanceof Date ? params.to.toISOString() : params.to);
296
+ }
297
+ const q = qs.toString();
298
+ return this.request("GET", `/referral/performance${q ? `?${q}` : ""}`);
299
+ }
261
300
  };
262
301
  reviews = {
263
302
  listPublic: (params) => {
@@ -280,7 +319,11 @@ class GameCoreClient {
280
319
  getRandom: (limit = 5) => this.request("GET", `/reviews/public/random?limit=${limit}`),
281
320
  getMine: () => this.request("GET", "/reviews/mine"),
282
321
  getPending: () => this.request("GET", "/reviews/pending"),
283
- create: (orderId, rating, text) => this.request("POST", "/reviews", { orderId, rating, text })
322
+ create: (orderId, rating, text) => this.request("POST", "/reviews", {
323
+ orderId,
324
+ rating,
325
+ text
326
+ })
284
327
  };
285
328
  topup = {
286
329
  getPaymentMethods: () => this.request("GET", "/topup/payment-methods"),
@@ -288,7 +331,10 @@ class GameCoreClient {
288
331
  getStatus: (code) => this.request("GET", `/topup/${code}`)
289
332
  };
290
333
  giftCards = {
291
- purchase: (amountUsd) => this.request("POST", "/gift-cards/purchase", { amountUsd }),
334
+ purchase: (amountRub, message) => this.request("POST", "/gift-cards/purchase", {
335
+ amountRub,
336
+ message
337
+ }),
292
338
  redeem: (code) => this.request("POST", "/gift-cards/redeem", { code }),
293
339
  check: (code) => this.request("GET", `/gift-cards/check/${code}`),
294
340
  getMine: () => this.request("GET", "/profile/gift-cards")
package/dist/types.d.ts CHANGED
@@ -140,6 +140,21 @@ export interface GameDetail {
140
140
  productCount?: number;
141
141
  categories: Category[];
142
142
  seo?: SeoContent | null;
143
+ /**
144
+ * Present only when the request used an alias slug and the backend
145
+ * resolved it to a different canonical slug. When set, the storefront
146
+ * should issue a 301 redirect to `/games/${canonicalSlug}` instead
147
+ * of rendering the page. Absent when the requested slug is already
148
+ * canonical.
149
+ */
150
+ canonicalSlug?: string;
151
+ /**
152
+ * ISO-8601 timestamp of the last meaningful edit on this canonical
153
+ * game — bumped on catalog changes (products, pricing, metadata).
154
+ * Storefronts should use this as `lastModified` in sitemap entries
155
+ * so Google doesn't refetch pages that haven't changed.
156
+ */
157
+ updatedAt?: string;
143
158
  }
144
159
  /**
145
160
  * Per-site SEO content for a game page.
@@ -237,6 +252,29 @@ export interface CartItem {
237
252
  productName: string;
238
253
  price: number;
239
254
  deliveryData: Record<string, string>;
255
+ /**
256
+ * Quantity for multi-amount products (e.g. N × 500 Robux).
257
+ * Optional when writing (server defaults to `1`); always
258
+ * present on server responses.
259
+ */
260
+ quantity?: number;
261
+ /**
262
+ * ISO-8601 timestamp of when the row was inserted. Absent when
263
+ * the item was freshly constructed client-side and hasn't been
264
+ * sent to the server yet.
265
+ */
266
+ addedAt?: string;
267
+ /**
268
+ * Canonical game icon (localIcon → icon fallback). May be `null`
269
+ * if the underlying game has no artwork. Never present on
270
+ * client-authored items — only populated on server responses.
271
+ */
272
+ gameIcon?: string | null;
273
+ /**
274
+ * Supplier product icon (localIcon → icon fallback). Same
275
+ * semantics as `gameIcon`.
276
+ */
277
+ productIcon?: string | null;
240
278
  }
241
279
  export interface CheckoutRequest {
242
280
  email?: string;
@@ -264,6 +302,46 @@ export interface CheckoutResponse {
264
302
  }>;
265
303
  }
266
304
  export type OrderStatus = "pending" | "paid" | "processing" | "completed" | "cancelled" | "refunded" | "failed";
305
+ /**
306
+ * Payment record returned by `checkout.getByPayment()`.
307
+ *
308
+ * A payment groups one or more orders created in the same checkout
309
+ * call. The `code` is the external identifier exposed to the
310
+ * payment gateway and returned via the success URL query string.
311
+ *
312
+ * `paymentMethod` and `gatewayType` are `null` when the payment is
313
+ * fully settled from the user's balance (no external gateway is
314
+ * involved). `completedAt` is set when the payment transitions to
315
+ * `completed` via webhook confirmation or balance settlement; it
316
+ * remains `null` for `pending`, `failed`, and `cancelled` payments
317
+ * (the failure/cancellation timestamp is tracked separately by
318
+ * the stale-payment scanner and not surfaced on this shape).
319
+ */
320
+ export interface PaymentInfo {
321
+ code: string;
322
+ totalAmount: number;
323
+ status: "pending" | "completed" | "failed" | "cancelled";
324
+ paymentMethod: string | null;
325
+ gatewayType: string | null;
326
+ createdAt: string;
327
+ completedAt: string | null;
328
+ }
329
+ /**
330
+ * Normalised delivery hint attached to an order. Used by the
331
+ * storefront "My Orders" page to decide which CTA to show
332
+ * (robux-via-pass → "Check Roblox transactions"; gift card →
333
+ * "Copy code"; login delivery → "Waiting for credentials"; etc).
334
+ *
335
+ * `helpTextKey` is an i18n key — the storefront owns the
336
+ * translation layer, so copy changes do not require a backend
337
+ * deploy. Unknown `kind` values should render a generic
338
+ * "processing" message as a fallback.
339
+ */
340
+ export interface OrderDeliveryMeta {
341
+ kind: "cdkey" | "login" | "pass_based" | "service" | "manual" | (string & {});
342
+ helpTextKey: string;
343
+ checkTransactionsUrl?: string;
344
+ }
267
345
  export interface Order {
268
346
  id: number;
269
347
  code: string;
@@ -280,6 +358,13 @@ export interface Order {
280
358
  };
281
359
  createdAt: string;
282
360
  completedAt?: string;
361
+ /**
362
+ * Delivery hint for the storefront UI — present on responses
363
+ * from `/profile/orders` and `/orders/:code` starting with
364
+ * SDK 0.13. Absent on legacy responses and on endpoints that
365
+ * do not currently surface it.
366
+ */
367
+ deliveryMeta?: OrderDeliveryMeta;
283
368
  }
284
369
  export interface OrderItem {
285
370
  id: number;
@@ -306,6 +391,20 @@ export interface UserBalance {
306
391
  expiresAt: string;
307
392
  }>;
308
393
  }
394
+ /**
395
+ * One rung of the loyalty level ladder — used by the "how levels
396
+ * work" explainer on the profile page. Shape matches what the
397
+ * server computes from `loyalty_levels` (per-site custom) or the
398
+ * hardcoded default ladder.
399
+ */
400
+ export interface LevelSystemLevel {
401
+ level: number;
402
+ name: string;
403
+ discountPercent: number;
404
+ minSpendingUsd: number;
405
+ minReviews: number;
406
+ minReferrals: number;
407
+ }
309
408
  export interface LevelStatus {
310
409
  currentLevel: number;
311
410
  currentDiscount: number;
@@ -329,6 +428,19 @@ export interface LevelStatus {
329
428
  met: boolean;
330
429
  };
331
430
  };
431
+ /**
432
+ * Highest level in the ladder. Storefronts should use this
433
+ * instead of hardcoding "15" so per-site custom ladders render
434
+ * correctly. Always present on responses from SDK 0.13+.
435
+ */
436
+ maxLevel?: number;
437
+ /** Highest discount percent in the ladder. */
438
+ maxDiscount?: number;
439
+ /**
440
+ * Full ordered ladder. Use this to render the explainer table
441
+ * or a progress timeline. Omitted on very old responses.
442
+ */
443
+ allLevels?: LevelSystemLevel[];
332
444
  }
333
445
  export interface Transaction {
334
446
  id: number;
@@ -360,27 +472,212 @@ export interface Favorite {
360
472
  categorySlug?: string;
361
473
  createdAt: string;
362
474
  }
475
+ /**
476
+ * Review record. Fields are endpoint-specific — this interface is a
477
+ * union of everything any review endpoint may return, and real-world
478
+ * shape depends on which route you call:
479
+ *
480
+ * - `reviews.listPublic()` → `id`, `rating`, `text`, `authorName`,
481
+ * `gameName`, `adminReply`, `adminReplyAt`,
482
+ * `createdAt`
483
+ * - `reviews.mine()` → `id`, `rating`, `text`, `adminReply`,
484
+ * `adminReplyAt`, `createdAt`, `order`
485
+ * - `reviews.create()` → the freshly-created review + `bonus`
486
+ *
487
+ * Fields that are not relevant to a given endpoint are `undefined`. Do
488
+ * not assume any single field is always present.
489
+ */
363
490
  export interface Review {
364
491
  id: number;
365
- userId: number;
366
- userName?: string;
367
- orderId: number;
368
- orderCode?: string;
369
492
  rating: number;
370
493
  text?: string;
494
+ createdAt: string;
495
+ /** Public display name (author). Returned by `listPublic()`. */
496
+ authorName?: string;
497
+ /** Legacy alias for `authorName`. Prefer `authorName` on new code. */
498
+ userName?: string;
499
+ /** Owner of the review. Returned only to authenticated admins. */
500
+ userId?: number;
501
+ /** Link back to the order the review was left for. */
502
+ orderId?: number;
503
+ orderCode?: string;
371
504
  gameName?: string;
505
+ /**
506
+ * Public reply from site support. Rendered under the review body
507
+ * as "Ответ поддержки". `null` if support has not replied or the
508
+ * reply was removed. Included in both the public listing and the
509
+ * authenticated profile reviews list.
510
+ */
511
+ adminReply?: string | null;
512
+ /**
513
+ * ISO-8601 timestamp of when the support reply was created or last
514
+ * updated. `null` when no reply is present.
515
+ */
516
+ adminReplyAt?: string | null;
517
+ /**
518
+ * Nested order summary, returned only by `reviews.mine()` so the
519
+ * profile "My reviews" page can show game + order code without a
520
+ * second fetch. `null` when the underlying order was deleted.
521
+ */
522
+ order?: {
523
+ code: string;
524
+ gameName: string;
525
+ createdAt: string;
526
+ } | null;
527
+ /** Bonus granted for leaving the review, if any. */
372
528
  bonus?: {
373
529
  amount: number;
374
530
  percent: number;
375
531
  expiresAt: string;
376
532
  };
377
- createdAt: string;
378
533
  }
379
534
  export interface ReviewStats {
380
535
  averageRating: number;
381
536
  totalCount: number;
382
537
  distribution?: Record<string, number>;
383
538
  }
539
+ /**
540
+ * Response shape returned by `checkout.completeWithBalance()`.
541
+ *
542
+ * The post-deduction balance (`newBalance`) is always present on a
543
+ * successful response — the storefront success page can render
544
+ * "new balance: X" without a second `profile.getBalance()` fetch
545
+ * (which is race-prone right after completion).
546
+ *
547
+ * The deduction breakdown (`balanceUsed`, `balanceDeduction`) is
548
+ * present **only on the first successful call** — it reflects what
549
+ * was actually moved by this request. If the client retries the
550
+ * endpoint for an already-completed payment (idempotent replay),
551
+ * those fields are `undefined` because the original breakdown is
552
+ * not re-derivable from persisted state. The `newBalance` is still
553
+ * included in that case so the receipt UI has something to render.
554
+ *
555
+ * When present, `balanceUsed` equals
556
+ * `balanceDeduction.fromBonus + balanceDeduction.fromPermanent`.
557
+ */
558
+ export interface CompleteWithBalanceResult {
559
+ paymentCode: string;
560
+ status: "completed";
561
+ balanceUsed?: number;
562
+ balanceDeduction?: {
563
+ fromBonus: number;
564
+ fromPermanent: number;
565
+ };
566
+ newBalance: {
567
+ total: number;
568
+ permanent: number;
569
+ bonus: number;
570
+ };
571
+ }
572
+ /**
573
+ * Short summary of a conversation as returned by
574
+ * `profile.getConversations()` — one row per user-owned order
575
+ * thread. Use the `id` to fetch full messages via
576
+ * `profile.getConversationMessages(id)`.
577
+ */
578
+ export interface Conversation {
579
+ id: number;
580
+ orderId: number;
581
+ orderCode: string | null;
582
+ gameName: string | null;
583
+ orderStatus: string | null;
584
+ /** Number of unread system / admin messages. User messages don't count. */
585
+ unreadCount: number;
586
+ /**
587
+ * Preview of the most recent message. `body` is truncated to ~100
588
+ * chars. `null` if the conversation has no messages yet.
589
+ */
590
+ lastMessage: {
591
+ body: string;
592
+ authorType: "user" | "admin" | "system";
593
+ messageType: "text" | "status_change" | "code_requested" | "screenshot_requested" | (string & {});
594
+ createdAt: string;
595
+ } | null;
596
+ /** Last time either party posted a message, ISO-8601. */
597
+ lastMessageAt: string;
598
+ }
599
+ /**
600
+ * Single message inside a conversation. `metadata` is parsed JSON —
601
+ * system messages may carry status-change payloads, request ids, etc.
602
+ * Free chat (user-authored `text` messages) is currently disabled —
603
+ * expect only `admin`, `system`, and request-response messages.
604
+ */
605
+ export interface ConversationMessage {
606
+ id: number;
607
+ authorType: "user" | "admin" | "system";
608
+ authorId: number | null;
609
+ messageType: "text" | "status_change" | "code_requested" | "screenshot_requested" | (string & {});
610
+ body: string;
611
+ metadata: Record<string, unknown> | null;
612
+ /** Set when the current viewer marked the message as read. */
613
+ readAt: string | null;
614
+ createdAt: string;
615
+ }
616
+ /**
617
+ * Open supplier-initiated request inside a conversation. The UI
618
+ * decides which input to show the user based on `requestType`.
619
+ */
620
+ export interface ConversationRequest {
621
+ id: number;
622
+ conversationId: number;
623
+ orderId: number;
624
+ orderItemId: number;
625
+ requestType: "code" | "screenshot";
626
+ status: "open" | "fulfilled" | "cancelled" | "expired";
627
+ requestedAt: string;
628
+ fulfilledAt: string | null;
629
+ }
630
+ /**
631
+ * Full detail returned by `profile.getConversationMessages(id)`.
632
+ * The conversation header is included so the UI does not need a
633
+ * separate fetch to render the page title / order summary.
634
+ */
635
+ export interface ConversationDetail {
636
+ conversation: {
637
+ id: number;
638
+ orderId: number;
639
+ orderCode: string | null;
640
+ gameName: string | null;
641
+ orderStatus: string | null;
642
+ };
643
+ messages: ConversationMessage[];
644
+ /** Open supplier requests the user can respond to right now. */
645
+ openRequests: ConversationRequest[];
646
+ }
647
+ /**
648
+ * Web Push subscription payload returned by the browser's
649
+ * `PushSubscription.toJSON()`. Pass this to
650
+ * `profile.subscribePush()` to start receiving push notifications
651
+ * for order status updates and conversation replies.
652
+ */
653
+ export interface WebPushSubscriptionInput {
654
+ endpoint: string;
655
+ keys: {
656
+ p256dh: string;
657
+ auth: string;
658
+ };
659
+ }
660
+ /**
661
+ * Narrow response returned by `reviews.create()`.
662
+ *
663
+ * The create endpoint deliberately returns less than a full `Review`
664
+ * — only the fields the storefront needs to acknowledge the
665
+ * submission: the new review id, the numeric rating, the text echo,
666
+ * and the optional bonus grant metadata.
667
+ *
668
+ * Callers who need the full review (with `createdAt`, `authorName`,
669
+ * etc.) should re-fetch via `reviews.getMine()` after the create.
670
+ */
671
+ export interface ReviewCreateResult {
672
+ id: number;
673
+ rating: number;
674
+ text: string | null;
675
+ bonus: {
676
+ amount: number;
677
+ percent: number;
678
+ expiresAt: string;
679
+ } | null;
680
+ }
384
681
  export interface CouponResult {
385
682
  type: string;
386
683
  value: number;
@@ -415,13 +712,50 @@ export interface ReferralCommission {
415
712
  commissionPercent: number;
416
713
  createdAt: string;
417
714
  }
715
+ /**
716
+ * Date-range aggregated performance metrics for a referrer.
717
+ *
718
+ * Commission, transaction and member counts honour the `from` /
719
+ * `to` range passed to `referrals.getPerformance()`. `totalClicks`
720
+ * is **lifetime across all of the referrer's links** and is NOT
721
+ * date-filtered — click events are not individually timestamped in
722
+ * the current schema. Storefronts should label the clicks metric
723
+ * accordingly ("all-time clicks" vs. "period earnings").
724
+ */
725
+ export interface ReferralPerformance {
726
+ totalCommission: number;
727
+ totalClicks: number;
728
+ totalTransactions: number;
729
+ newMembers: number;
730
+ }
418
731
  export interface TopupMethod {
419
732
  type: string;
420
733
  label: string;
421
734
  description?: string;
422
735
  feePercent?: number;
423
736
  gatewayType?: string;
424
- invoiceCurrency?: string;
737
+ invoiceCurrency?: string | null;
738
+ /**
739
+ * Minimum topup amount in RUB. `null` means the storefront
740
+ * should use its global minimum (typically 50 RUB). The topup
741
+ * route enforces this server-side too.
742
+ */
743
+ minAmount?: number | null;
744
+ /**
745
+ * Maximum topup amount in RUB. `null` means "use global cap".
746
+ */
747
+ maxAmount?: number | null;
748
+ /**
749
+ * Grouping bucket for collapsible UIs. Known values: `"direct"`,
750
+ * `"p2p"`, `"intl"`. Unknown values should render as a
751
+ * generic "Other" group.
752
+ */
753
+ group?: "direct" | "p2p" | "intl" | (string & {}) | null;
754
+ /**
755
+ * Optional warning text rendered underneath the method
756
+ * selector (e.g. "Временно нестабильно"). `null` means no note.
757
+ */
758
+ stabilityNote?: string | null;
425
759
  }
426
760
  export interface TopupResponse {
427
761
  code: string;
@@ -434,13 +768,48 @@ export interface TopupStatus {
434
768
  amount: number;
435
769
  createdAt?: string;
436
770
  }
771
+ /**
772
+ * Gift card voucher. Stored in RUB; the field was historically
773
+ * called `denomination` in the SDK and `amount_usd` in the DB
774
+ * column, but both were misnomers — the value has always been RUB
775
+ * (see Wave 3 #34 audit `tasks/112a-giftcard-currency-audit.md`).
776
+ * Use `amountRub` on new code; `amountUsd` and `denomination` are
777
+ * deprecated aliases that echo the same value.
778
+ */
437
779
  export interface GiftCard {
438
780
  id: number;
439
781
  code: string;
440
- denomination: number;
441
- status: string;
782
+ /** Canonical RUB value. Use this on new code. */
783
+ amountRub: number;
784
+ /**
785
+ * @deprecated Legacy alias echoing `amountRub`. The name was
786
+ * wrong historically — the value was always RUB despite the
787
+ * `Usd` suffix. Remove once storefronts upgrade past SDK 0.13.x.
788
+ */
789
+ amountUsd?: number;
790
+ /**
791
+ * @deprecated Legacy alias echoing `amountRub`. Kept for
792
+ * storefronts that were reading this field from typed code; at
793
+ * runtime the backend never emitted `denomination`, so most
794
+ * consumers were already getting `undefined`.
795
+ */
796
+ denomination?: number;
797
+ /** Currency of `amountRub`. Always `"RUB"` for now. */
798
+ currency: "RUB";
799
+ /**
800
+ * Remaining balance if the card supports partial redemption.
801
+ * For active and expired cards this equals `amountRub`. For
802
+ * redeemed or cancelled cards it is `0`. Partial redemption is
803
+ * not implemented yet — the field is reserved so storefronts
804
+ * can start rendering it now without a future breaking change.
805
+ */
806
+ remainingBalance: number;
807
+ status: "active" | "redeemed" | "expired" | "cancelled" | (string & {});
808
+ message?: string | null;
809
+ /** ISO-8601 expiry timestamp; `null` if the card never expires. */
810
+ expiresAt?: string | null;
442
811
  createdAt: string;
443
- redeemedAt?: string;
812
+ redeemedAt?: string | null;
444
813
  }
445
814
  export interface Announcement {
446
815
  id: number;
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@gamecore-api/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "TypeScript SDK for GameCore API — browser-safe, zero dependencies",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
- "module": "dist/index.mjs",
8
7
  "exports": {
9
8
  ".": {
10
9
  "import": "./dist/index.js",