@smartico/public-api 0.0.386 → 0.0.388

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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/dist/Avatars/AvatarCustomizeErrorCode.d.ts +7 -1
  3. package/dist/Avatars/AvatarCustomizeResponse.d.ts +16 -0
  4. package/dist/Avatars/index.d.ts +1 -0
  5. package/dist/CustomSections/AchCustomSection.d.ts +2 -1
  6. package/dist/Missions/UserAchievementTask.d.ts +4 -0
  7. package/dist/SmarticoAPI.d.ts +15 -2
  8. package/dist/WSAPI/WSAPIAvatars.d.ts +103 -0
  9. package/dist/WSAPI/WSAPILeaderBoard.d.ts +76 -10
  10. package/dist/WSAPI/WSAPITypes.d.ts +22 -5
  11. package/dist/index.js +441 -149
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.modern.mjs +280 -29
  14. package/dist/index.modern.mjs.map +1 -1
  15. package/docs/SmarticoObject.md +15 -0
  16. package/docs/api/README.md +2 -0
  17. package/docs/api/classes/WSAPIAvatars.md +135 -0
  18. package/docs/api/classes/WSAPIClans.md +3 -5
  19. package/docs/api/classes/WSAPILeaderBoard.md +86 -10
  20. package/docs/api/documents/SmarticoObject.md +15 -0
  21. package/docs/api/enumerations/AvatarCustomizeErrorCode.md +21 -0
  22. package/docs/api/enumerations/JoinClanErrorCode.md +16 -17
  23. package/docs/api/interfaces/AvatarCustomizeResponse.md +31 -0
  24. package/docs/api/interfaces/LeaderBoardDetailsT.md +19 -0
  25. package/docs/api/interfaces/LeaderBoardUserT.md +14 -2
  26. package/docs/api/interfaces/TAvatarPrompt.md +2 -2
  27. package/docs/api/interfaces/TMissionOrBadgeTask.md +16 -0
  28. package/docs/api/interfaces/UserAchievementTask.md +16 -0
  29. package/docs/native/PROTOCOL.md +37 -7
  30. package/docs/ui/avatars/UIGuide_avatarsCustomize.md +69 -0
  31. package/docs/ui/leaderboard/UIGuide_getLeaderBoard.md +15 -0
  32. package/package.json +1 -1
  33. package/src/Avatars/AvatarCustomizeErrorCode.ts +7 -5
  34. package/src/Avatars/AvatarCustomizeResponse.ts +17 -0
  35. package/src/Avatars/index.ts +1 -0
  36. package/src/CustomSections/AchCustomSection.ts +1 -0
  37. package/src/Leaderboard/LeaderBoards.ts +9 -12
  38. package/src/Missions/UserAchievement.ts +2 -0
  39. package/src/Missions/UserAchievementTask.ts +4 -0
  40. package/src/SmarticoAPI.ts +84 -3
  41. package/src/WSAPI/WSAPIAvatars.ts +121 -0
  42. package/src/WSAPI/WSAPILeaderBoard.ts +91 -14
  43. package/src/WSAPI/WSAPITypes.ts +22 -5
  44. package/tsconfig.json +3 -1
@@ -1802,6 +1802,8 @@ Join (or switch to) a clan. On labels with no cooldown, switching clans re-charg
1802
1802
 
1803
1803
  Avatar selection (`setAvatar`) is documented under [User](#setavatar). The methods below read the avatar catalog and the user's AI-customized avatars.
1804
1804
 
1805
+ > **Note:** Generating a new AI-customized variant (`avatarsCustomize` in the JS SDK) is a separate **HTTP POST** to the avatar server, not part of this WebSocket protocol. It returns a CDN URL that you then activate via `setAvatar`. Native clients must call that HTTP endpoint directly; it is out of scope for this spec.
1806
+
1805
1807
  ### getAvatarsList
1806
1808
 
1807
1809
  Get the avatar catalog available to the current user.
@@ -1862,7 +1864,7 @@ No method-specific fields. Send only the common fields (see [Common Message Fiel
1862
1864
  |-------|------|-------------|
1863
1865
  | `prompts` | [`AvatarPrompt[]`](../api/interfaces/AvatarPrompt.md) | Available AI style prompts |
1864
1866
 
1865
- > **Note:** Prompt name/icon are nested at `public_meta.name` / `public_meta.icon_url`. `cost_currency_type_id`: `0` = points, `1` = gems, `2` = diamonds; `cost_value` is the amount.
1867
+ > **Note:** Prompt name/icon are nested at `public_meta.name` / `public_meta.icon_url`. `cost_currency_type_id`: `0` = points, `1` = gems, `2` = diamonds, `3` = free; `cost_value` is the amount (a `cost_value` of `0` is also free).
1866
1868
 
1867
1869
  ---
1868
1870
 
@@ -1870,7 +1872,8 @@ No method-specific fields. Send only the common fields (see [Common Message Fiel
1870
1872
 
1871
1873
  ### getLeaderBoard
1872
1874
 
1873
- Get leaderboard for a specific period type.
1875
+ Get one period's leaderboard with its ranked players, or the list of all
1876
+ configured boards without players.
1874
1877
 
1875
1878
  #### Request
1876
1879
 
@@ -1878,9 +1881,14 @@ Get leaderboard for a specific period type.
1878
1881
 
1879
1882
  | Field | Type | Description |
1880
1883
  |-------|------|-------------|
1881
- | `period_type_id` | `number` | Period type: `1` = Daily, `2` = Weekly, `3` = Monthly (optional, if not set returns all boards) |
1884
+ | `period_type_id` | `number` | Period type: `1` = Daily, `2` = Weekly, `3` = Monthly. Optional — if omitted, the response `map` contains **every** configured board. |
1882
1885
  | `snapshot_offset` | `number` | `0` = current period, `1` = previous period, `2` = period before previous, etc. |
1883
- | `include_users` | `boolean` | Whether to include user details (optional) |
1886
+ | `include_users` | `boolean` | `true` → each board includes its `positions` (and `userPosition`). `false` (or omitted) → board metadata only, no players — the lightweight "list available boards" mode. |
1887
+
1888
+ > **Two modes from one request.** Send `period_type_id` + `include_users: true`
1889
+ > to load a single board's standings. Omit `period_type_id` and send
1890
+ > `include_users: false` to enumerate all configured boards (metadata only) —
1891
+ > useful to build the board/tab selector before loading any standings.
1884
1892
 
1885
1893
  #### Response
1886
1894
 
@@ -1888,9 +1896,31 @@ Get leaderboard for a specific period type.
1888
1896
 
1889
1897
  | Field | Type | Description |
1890
1898
  |-------|------|-------------|
1891
- | `leaders` | [`LeaderBoardUserT[]`](../api/interfaces/LeaderBoardUserT.md) | Array of leaderboard entries |
1892
- | `user_position` | `number` | Current user's position |
1893
- | `user_points` | `number` | Current user's points |
1899
+ | `map` | `object` | Keyed by `period_type_id` (as a string: `"1"` / `"2"` / `"3"`), one entry per board. With `period_type_id` set in the request only that key is present; omitted → every configured board. |
1900
+
1901
+ Each value of `map` is a leaderboard object:
1902
+
1903
+ | Field | Type | Description |
1904
+ |-------|------|-------------|
1905
+ | `board_id` | `number` | Stable board id |
1906
+ | `period_type_id` | `number` | `1` = Daily, `2` = Weekly, `3` = Monthly |
1907
+ | `create_date` | `number` | `0` for the current period; the snapshot timestamp for a previous period |
1908
+ | `versiod_id` | `number` | Snapshot version; `0` for the current period (the field is spelled `versiod_id` on the wire) |
1909
+ | `reward_points` | `number[]` | Per-place prize points; index `0` = place 1 |
1910
+ | `board_public_meta` | `object` | `{ name, description, rules }` display strings (HTML allowed in `description` / `rules`) |
1911
+ | `positions` | `LeaderBoardPosition[]` | Ranked entries, top 20, ordered by rank. **Absent when `include_users` is `false`.** |
1912
+ | `userPosition` | `LeaderBoardPosition` | The requesting user's own entry. Absent when `include_users` is `false`, or when the user is unranked. |
1913
+
1914
+ Each `LeaderBoardPosition`:
1915
+
1916
+ | Field | Type | Description |
1917
+ |-------|------|-------------|
1918
+ | `position_in_board` | `number` | Rank (DENSE_RANK); `-1` on `userPosition` = unranked / outside the window |
1919
+ | `points_accumulated` | `number` | Participant's points for the period |
1920
+ | `public_username` | `string` | Display name (falls back to `user_alt_name`) |
1921
+ | `is_me` | `boolean` | `true` for the requesting user's row inside `positions` |
1922
+ | `avatar_id` | `string` | Raw avatar id — build the image URL client-side |
1923
+ | `level_id` | `number` | The participant's level (factor it into avatar resolution to match the default Smartico UI) |
1894
1924
 
1895
1925
  ---
1896
1926
 
@@ -0,0 +1,69 @@
1
+ # UI Guide — `avatarsCustomize`
2
+
3
+ ## Overview
4
+ - A **paid, generate-only** AI step: it applies an operator-defined style
5
+ prompt to a base avatar and returns a `cdn_url` for the generated image.
6
+ - It does **not** change the user's active avatar. Activation is a second,
7
+ explicit step — call `setAvatar({ avatar_url: cdn_url, avatar_real_id })`.
8
+ - Synchronous and slow (~5–20 s). No SDK cache, no push.
9
+
10
+ ## Flow (the default Smartico UI)
11
+ 1. **Pick a base avatar** from the avatar catalogue (`getAvatarsList()` /
12
+ `getAvatarsCustomized()`), then open the "Customize" panel.
13
+ 2. **Pick a style prompt** from `getAvatarPrompts()`. Each prompt carries its
14
+ cost (`cost_currency_type_id` + `cost_value`); render the price on the
15
+ prompt tile and pre-disable prompts the user can't afford (compare against
16
+ the balances on `getUserProfile()`).
17
+ 3. **Generate.** On tap, call `avatarsCustomize(...)`. Immediately enter a
18
+ loading state (see below) and lock the prompt list + Generate button.
19
+ 4. **Preview.** On success, render `cdn_url` as a preview. The user can keep
20
+ browsing previously generated variants (they live in the customization
21
+ history surfaced by `getAvatarsCustomized()`).
22
+ 5. **Apply (explicit).** Only when the user taps "Apply" do you call
23
+ `setAvatar(...)` with the currently-selected URL (which may be the new
24
+ `cdn_url`, a history item, or the untouched base avatar).
25
+
26
+ ## Loading state
27
+ - The generation blocks for several seconds. Show a spinner/skeleton in the
28
+ preview slot for the full duration; keep the modal open.
29
+ - Disable the Generate trigger and the prompt list while in flight — there is
30
+ no SDK-side debounce, so a second tap fires a second (charged) generation.
31
+
32
+ ## Cost gating
33
+ - Read `cost_currency_type_id` (`0` points · `1` gems · `2` diamonds · `3`
34
+ free) and `cost_value` from the chosen `getAvatarPrompts()` entry.
35
+ - `cost_value === 0` (or type `3`) is free.
36
+ - Gate before calling: if the user can't afford the prompt, disable the tile
37
+ and show the price; don't rely on the server to be the only guard
38
+ (insufficient balance comes back as the generic `-1`).
39
+
40
+ ## Error / limit handling
41
+ | Result | UI |
42
+ |---|---|
43
+ | `cdn_url` set | Success. Render the preview; enable "Apply". |
44
+ | `errCode === 12001` | User hit their **monthly per-user cap**. Show e.g. "You've used all your custom avatars this month." Nothing was charged. |
45
+ | `errCode === 12002` | The **brand's monthly pool** is exhausted. Show "Custom avatars are unavailable right now — try again later." Nothing was charged. |
46
+ | `errCode === -1` | Generic failure (insufficient balance, unavailable prompt, generation error). Show a retryable error; nothing was charged. `errMessage` is fixed text — don't surface it as the reason. |
47
+
48
+ Both monthly caps are operator-configured and reset at the start of each
49
+ calendar month.
50
+
51
+ ## Important behavioral caveats
52
+ - **Generation is irreversible and counts toward the monthly caps on success
53
+ — even if the user never applies it.** Don't auto-generate on hover/scroll;
54
+ require an explicit tap.
55
+ - **Apply uses the currently-selected image**, which may be the new variant, a
56
+ history item, or the base avatar. Track selection explicitly so "Apply"
57
+ sends the right URL.
58
+ - After a successful generation the SDK refreshes the
59
+ `getAvatarsCustomized()` cache, so the new variant appears in the history
60
+ list on the next call.
61
+
62
+ ## Empty / unavailable states
63
+ - No prompts returned by `getAvatarPrompts()` → hide the Customize entry point.
64
+ - `12002` for the whole brand → consider hiding/greying the Customize entry
65
+ point until next month rather than letting every tap fail.
66
+
67
+ ## Visitor mode
68
+ - Not supported — `avatarsCustomize` throws in visitor mode. Hide the
69
+ customization surface for unauthenticated sessions.
@@ -9,6 +9,15 @@
9
9
  - No subscription, no push refresh. Poll manually if the consumer
10
10
  needs live updates during an in-progress period.
11
11
 
12
+ ## Discovering boards (`getLeaderBoards`)
13
+ Before loading any standings, call `getLeaderBoards()` to discover which
14
+ boards the operator configured (DAILY / WEEKLY / MONTHLY, or none). It
15
+ returns the board list as **metadata only** — `name`, `period_type_id`,
16
+ `rewards`, etc., with `users` empty and `me` undefined — in one lightweight
17
+ round-trip. Build the board/tab selector from that list, then call
18
+ `getLeaderBoard(period_type_id)` to load a tab's standings on demand. If it
19
+ returns an empty array, hide the leaderboard surface entirely.
20
+
12
21
  ## List view organization
13
22
 
14
23
  The default Smartico UI renders the leaderboard with three top-level
@@ -113,6 +122,12 @@ a fresh fetch within the TTL (e.g. a manual refresh button), call
113
122
  `clearCaches()` first — it clears the whole SDK cache, not just this
114
123
  board.
115
124
 
125
+ To page back through **older** snapshots (not just the immediately
126
+ previous one), pass a number instead of a boolean: `getLeaderBoard(periodType, 2)`
127
+ fetches the period before last, `3` the one before that, etc. Label each
128
+ historical board with its `create_date` / `version_id` so the user knows
129
+ which run they're viewing.
130
+
116
131
  ## Status-specific visual treatments
117
132
 
118
133
  - **Current period**: full-color rendering; live point counts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartico/public-api",
3
- "version": "0.0.386",
3
+ "version": "0.0.388",
4
4
  "description": "Smartico public API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,9 +1,11 @@
1
+ /**
2
+ * Limit error codes returned in `errCode` by the `avatarsCustomize` method.
3
+ * See the `avatarsCustomize` TSDoc for the full table (including the generic
4
+ * `-1` failure, which has no named member here).
5
+ */
1
6
  export enum AvatarCustomizeErrorCode {
7
+ /** The user has reached the operator-configured per-user monthly cap on custom-avatar generations. */
2
8
  AVATAR_USER_LIMIT = 12001,
9
+ /** The brand's operator-configured shared monthly pool of custom-avatar generations is exhausted. */
3
10
  AVATAR_LABEL_LIMIT = 12002,
4
11
  }
5
-
6
- export const AvatarCustomizeErrorMessage: Record<AvatarCustomizeErrorCode, string> = {
7
- [AvatarCustomizeErrorCode.AVATAR_USER_LIMIT]: 'You have exceeded number of custom avatars creation, please try later',
8
- [AvatarCustomizeErrorCode.AVATAR_LABEL_LIMIT]: 'The custom avatars are not available, please try again later',
9
- };
@@ -0,0 +1,17 @@
1
+ import { AvatarCustomizeErrorCode } from './AvatarCustomizeErrorCode';
2
+
3
+ /**
4
+ * Result of `_smartico.api.avatarsCustomize()`. On success only `cdn_url` is
5
+ * set; on failure only `errCode` / `errMessage`.
6
+ */
7
+ export interface AvatarCustomizeResponse {
8
+ /** CDN URL of the generated avatar variant. Present on success. */
9
+ cdn_url?: string;
10
+ /** Error code. Present on failure. Typed values are members of
11
+ * {@link AvatarCustomizeErrorCode}; `-1` is a generic failure. See the
12
+ * `avatarsCustomize` TSDoc for the full table. */
13
+ errCode?: AvatarCustomizeErrorCode | number;
14
+ /** Optional error message. Present on failure; the generic (`-1`) message
15
+ * is fixed text, so branch on `errCode`, not this string. */
16
+ errMessage?: string;
17
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './AvatarCustomizeErrorCode';
2
+ export * from './AvatarCustomizeResponse';
2
3
  export * from './AvatarDefinition';
3
4
  export * from './AvatarCustomized';
4
5
  export * from './AvatarPrompt';
@@ -48,4 +48,5 @@ export enum LiquidEntityData {
48
48
  Tournament = 10,
49
49
  Raffles = 11,
50
50
  SingleRaffle = 12,
51
+ Clans = 13,
51
52
  }
@@ -13,19 +13,16 @@ const pointsRewardTransform = (reward_points: number[]): LeaderBoardsRewardsT[]
13
13
 
14
14
  const getLeaderBoardPlayerTransformed = (user: LeaderBoardPosition, isMe?: boolean): LeaderBoardUserT => {
15
15
  if (user) {
16
- const x: LeaderBoardUserT = {
16
+ return {
17
17
  public_username: user?.public_username || user.user_alt_name,
18
18
  avatar_url: user.avatar_url,
19
+ level_id: user.level_id,
19
20
  position: user.position_in_board,
20
21
  points: user.points_accumulated,
21
- is_me: user.is_me,
22
+ // The `me` entry is the current user by definition, so force `true`
23
+ // there; list rows carry the server-provided flag.
24
+ is_me: isMe ? true : user.is_me,
22
25
  };
23
-
24
- if (isMe) {
25
- delete x.is_me;
26
- }
27
-
28
- return x;
29
26
  }
30
27
 
31
28
  return null;
@@ -33,18 +30,18 @@ const getLeaderBoardPlayerTransformed = (user: LeaderBoardPosition, isMe?: boole
33
30
 
34
31
  export const getLeaderBoardTransform = (board: LeaderBoardDetails): LeaderBoardDetailsT => {
35
32
  if (board) {
36
- const x: LeaderBoardDetailsT = {
33
+ return {
37
34
  board_id: board.board_id,
38
35
  name: board.board_public_meta.name,
39
36
  description: board.board_public_meta.description,
40
37
  rules: board.board_public_meta.rules,
41
38
  period_type_id: board.period_type_id,
39
+ version_id: board.versiod_id,
40
+ create_date: board.create_date,
42
41
  rewards: pointsRewardTransform(board.reward_points),
43
- users: board.positions.map((p) => getLeaderBoardPlayerTransformed(p)),
42
+ users: (board.positions || []).map((p) => getLeaderBoardPlayerTransformed(p)),
44
43
  me: getLeaderBoardPlayerTransformed(board.userPosition, true),
45
44
  };
46
-
47
- return x;
48
45
  }
49
46
 
50
47
  return null;
@@ -123,6 +123,8 @@ export const UserAchievementTransform = (items: UserAchievement[]): TMissionOrBa
123
123
  id: t.task_id,
124
124
  name: t.task_public_meta?.name,
125
125
  points_reward: t.points_reward,
126
+ gems_reward: t.gems_reward,
127
+ diamonds_reward: t.diamonds_reward,
126
128
  is_completed: t.isCompleted,
127
129
  progress: t.userProgress,
128
130
  execution_count_expected: t.executionCount,
@@ -6,6 +6,10 @@ export interface UserAchievementTask {
6
6
  task_id?: number;
7
7
  task_public_meta?: AchievementTaskPublicMeta;
8
8
  points_reward?: number;
9
+ /** Gems awarded when this task is completed. */
10
+ gems_reward?: number;
11
+ /** Diamonds awarded when this task is completed. */
12
+ diamonds_reward?: number;
9
13
  task_type_id: AchievementTaskType;
10
14
  isCompleted?: boolean;
11
15
  userExecutedCount?: number; // This is the number of times the user has executed 'activity' of the task. e.g. he bet 5 times out of 100. here will be 5
@@ -144,6 +144,7 @@ import { GetAvatarsListResponse, avatarDefinitionTransform } from './Avatars/Get
144
144
  import { GetAvatarsCustomizedResponse, avatarCustomizedTransform } from './Avatars/GetAvatarsCustomizedResponse';
145
145
  import { GetAvatarPromptsResponse, avatarPromptTransform } from './Avatars/GetAvatarPromptsResponse';
146
146
  import { SetAvatarRequest, SetAvatarResponse } from './Avatars';
147
+ import { AvatarCustomizeResponse } from './Avatars/AvatarCustomizeResponse';
147
148
  import { GetJackpotWinnersResponse, GetJackpotWinnersResponseTransform, JackpotWinnerHistory } from './Jackpots/GetJackpotWinnersResponse';
148
149
  import { GetJackpotWinnersRequest } from './Jackpots/GetJackpotWinnersRequest';
149
150
  import { GetJackpotEligibleGamesRequest } from './Jackpots/GetJackpotEligibleGamesRequest';
@@ -183,6 +184,7 @@ class SmarticoAPI {
183
184
  private gamesApiUrl: string;
184
185
  private baseRgApiParams: Record<string, string>;
185
186
  public avatarDomain: string;
187
+ private avatarOriginDomain: string;
186
188
  private envId: number;
187
189
 
188
190
  private logger: ILogger;
@@ -220,6 +222,7 @@ class SmarticoAPI {
220
222
  }
221
223
 
222
224
  this.avatarDomain = SmarticoAPI.getAvatarUrl(label_api_key || options.tracker?.label_api_key);
225
+ this.avatarOriginDomain = SmarticoAPI.getAvatarOriginUrl(label_api_key || options.tracker?.label_api_key);
223
226
 
224
227
  this.label_api_key = SmarticoAPI.getCleanLabelApiKey(label_api_key);
225
228
  }
@@ -292,6 +295,10 @@ class SmarticoAPI {
292
295
  return SmarticoAPI.replaceSmrDomainsWithCloudfront(avatarUrl);
293
296
  }
294
297
 
298
+ public static getAvatarOriginUrl(label_api_key: string): string {
299
+ return AVATAR_DOMAIN.replace('{ENV_ID}', SmarticoAPI.getEnvDnsSuffix(label_api_key));
300
+ }
301
+
295
302
  private async send<T>(message: any, expectCID?: ClassId, force_language?: string): Promise<T> {
296
303
  if (this.logCIDs.includes(message.cid)) {
297
304
  this.logger.info('REQ', message);
@@ -1137,15 +1144,18 @@ class SmarticoAPI {
1137
1144
  public async leaderboardGet(
1138
1145
  user_ext_id: string,
1139
1146
  period_type_id?: LeaderBoardPeriodType,
1140
- prevPeriod: boolean = false,
1147
+ prevPeriod: boolean | number = false,
1141
1148
  force_language?: string,
1142
1149
  ): Promise<LeaderBoardDetails> {
1150
+ // boolean keeps back-compat (false → current, true → previous);
1151
+ // a number selects an arbitrary snapshot offset (1 = previous, 2 = the one before, …).
1152
+ const snapshot_offset = typeof prevPeriod === 'number' ? prevPeriod : prevPeriod ? 1 : 0;
1143
1153
  const message = this.buildMessage<GetLeaderBoardsRequest, GetLeaderBoardsResponse>(
1144
1154
  user_ext_id,
1145
1155
  ClassId.GET_LEADERS_BOARD_REQUEST,
1146
1156
  {
1147
1157
  period_type_id,
1148
- snapshot_offset: prevPeriod ? 1 : 0,
1158
+ snapshot_offset,
1149
1159
  include_users: true,
1150
1160
  },
1151
1161
  );
@@ -1177,11 +1187,36 @@ class SmarticoAPI {
1177
1187
  public async leaderboardsGetT(
1178
1188
  user_ext_id: string,
1179
1189
  period_type_id: LeaderBoardPeriodType = LeaderBoardPeriodType.DAILY,
1180
- prevPeriod: boolean = false,
1190
+ prevPeriod: boolean | number = false,
1181
1191
  ): Promise<LeaderBoardDetailsT> {
1182
1192
  return getLeaderBoardTransform(await this.leaderboardGet(user_ext_id, period_type_id, prevPeriod));
1183
1193
  }
1184
1194
 
1195
+ public async leaderboardsGetList(
1196
+ user_ext_id: string,
1197
+ force_language?: string,
1198
+ ): Promise<LeaderBoardDetails[]> {
1199
+ const message = this.buildMessage<GetLeaderBoardsRequest, GetLeaderBoardsResponse>(
1200
+ user_ext_id,
1201
+ ClassId.GET_LEADERS_BOARD_REQUEST,
1202
+ {
1203
+ // No period_type_id → the server returns every configured board.
1204
+ // include_users: false → board metadata only (no positions / userPosition).
1205
+ snapshot_offset: 0,
1206
+ include_users: false,
1207
+ },
1208
+ );
1209
+
1210
+ const response = await this.send<GetLeaderBoardsResponse>(message, ClassId.GET_LEADERS_BOARD_RESPONSE, force_language);
1211
+
1212
+ // map is keyed by period_type_id; integer-like keys iterate in ascending order.
1213
+ return Object.values(response.map || {});
1214
+ }
1215
+
1216
+ public async leaderboardsGetListT(user_ext_id: string): Promise<LeaderBoardDetailsT[]> {
1217
+ return (await this.leaderboardsGetList(user_ext_id)).map(getLeaderBoardTransform);
1218
+ }
1219
+
1185
1220
  public async levelsGet(user_ext_id: string, force_language?: string): Promise<GetLevelMapResponse> {
1186
1221
  const message = this.buildMessage<any, GetLevelMapResponse>(user_ext_id, ClassId.GET_LEVEL_MAP_REQUEST);
1187
1222
  return await this.send<GetLevelMapResponse>(message, ClassId.GET_LEVEL_MAP_RESPONSE, force_language);
@@ -1438,6 +1473,31 @@ class SmarticoAPI {
1438
1473
  }
1439
1474
  }
1440
1475
 
1476
+ private async sendAvatarApi<T>({ method, body }: { method: string, body: Record<string, any> }): Promise<T> {
1477
+ if (!this.tracker) {
1478
+ throw new Error(`Avatar APIs are not available in the visitor mode`);
1479
+ }
1480
+
1481
+ const url = `${this.avatarOriginDomain}/${method}`;
1482
+
1483
+ try {
1484
+ const response = await fetch(url, {
1485
+ method: 'POST',
1486
+ headers: {
1487
+ 'Accept': 'application/json',
1488
+ 'Content-Type': 'application/json',
1489
+ },
1490
+ body: JSON.stringify(body),
1491
+ });
1492
+
1493
+ const data = await response.json();
1494
+ return SmarticoAPI.replaceSmrDomainsWithCloudfront<T>(data);
1495
+ } catch (e) {
1496
+ this.logger.error(`Failed to call avatar API: ${method}`, { url, error: e.message });
1497
+ throw new Error(`Failed to call avatar API: ${method}. ${e.message}`);
1498
+ }
1499
+ }
1500
+
1441
1501
  public async gpGetActiveRounds(saw_template_id: number): Promise<GamesApiResponse<GamePickRound[]>> {
1442
1502
  const params = {
1443
1503
  ext_game_id: saw_template_id,
@@ -1539,6 +1599,27 @@ class SmarticoAPI {
1539
1599
  return await this.send<SetAvatarResponse>(message, ClassId.CLIENT_SET_AVATAR_RESPONSE);
1540
1600
  }
1541
1601
 
1602
+ public async avatarsCustomize(params: {
1603
+ label_id: number;
1604
+ user_id: number;
1605
+ prompt_id: number;
1606
+ avatar_url: string;
1607
+ avatar_real_id: number;
1608
+ }): Promise<AvatarCustomizeResponse> {
1609
+ return this.sendAvatarApi<AvatarCustomizeResponse>({
1610
+ method: 'avatar-customize',
1611
+ body: {
1612
+ label_id: params.label_id,
1613
+ user_id: params.user_id,
1614
+ user_ext_id: this.tracker?.getExtUserId(),
1615
+ brand_key: this.brand_api_key || this.tracker?.params?.brand_key,
1616
+ prompt_id: params.prompt_id,
1617
+ avatar_url: params.avatar_url,
1618
+ avatar_real_id: params.avatar_real_id,
1619
+ },
1620
+ });
1621
+ }
1622
+
1542
1623
  public async avatarsGetListT(user_ext_id: string): Promise<TAvatarDefinition[]> {
1543
1624
  return avatarDefinitionTransform((await this.avatarsGetList(user_ext_id)).avatars ?? [], this.avatarDomain);
1544
1625
  }
@@ -10,6 +10,7 @@ import {
10
10
  onUpdateContextKey,
11
11
  } from './WSAPIBase';
12
12
  import { WSAPIMiniGames } from './WSAPIMiniGames';
13
+ import { AvatarCustomizeResponse } from '../Avatars/AvatarCustomizeResponse';
13
14
 
14
15
  /** @group Avatars */
15
16
  export class WSAPIAvatars extends WSAPIMiniGames {
@@ -342,4 +343,124 @@ export class WSAPIAvatars extends WSAPIMiniGames {
342
343
  err_message: r.errMsg,
343
344
  };
344
345
  }
346
+
347
+ /**
348
+ * Generates an AI-customized variant of a base avatar by applying an
349
+ * operator-defined style prompt, returning the CDN URL of the result.
350
+ * This is a paid, generate-only step: it does NOT change the user's
351
+ * active avatar — pass the returned `cdn_url` to {@link setAvatar} to
352
+ * activate it.
353
+ *
354
+ * @remarks
355
+ * **Preconditions**
356
+ * - Authenticated session required; not available in visitor mode (throws).
357
+ * - All five fields are mandatory — the SDK throws synchronously
358
+ * (`Error("<field> is required")`) if any is missing or zero.
359
+ * - `prompt_id` must be one of the prompts from {@link getAvatarPrompts};
360
+ * `avatar_url` / `avatar_real_id` identify the base avatar to style.
361
+ *
362
+ * **Cost**
363
+ * Each generation costs the currency the operator configured on the
364
+ * chosen prompt (points, gems, or diamonds — some prompts are free; read
365
+ * `cost_currency_type_id` / `cost_value` from the {@link getAvatarPrompts}
366
+ * entry to gate the UI before calling). The cost is charged only on
367
+ * success; if the user can't afford it the call fails with `errCode -1`
368
+ * and nothing is generated or charged.
369
+ *
370
+ * **Latency**
371
+ * Generation runs server-side AI image synthesis and is synchronous from
372
+ * the caller's side — expect several seconds (roughly 5–20 s) before the
373
+ * promise resolves. There is no SDK cache and no push/subscription; show
374
+ * a loading state for the full duration and disable the trigger to
375
+ * prevent duplicate submissions.
376
+ *
377
+ * **Result & error codes**
378
+ * On success the result has `cdn_url` set; on failure it has `errCode` /
379
+ * `errMessage` set and no `cdn_url`. Branch on `errCode` — the generic
380
+ * message is fixed text, not cause-specific:
381
+ * - `12001` (`AvatarCustomizeErrorCode.AVATAR_USER_LIMIT`) — the user hit
382
+ * their monthly per-user generation cap. Nothing is generated or charged.
383
+ * - `12002` (`AvatarCustomizeErrorCode.AVATAR_LABEL_LIMIT`) — the brand's
384
+ * shared monthly pool is exhausted. Nothing is generated or charged;
385
+ * surface a "try again later" message.
386
+ * - `-1` — generic failure: insufficient balance, an unavailable prompt,
387
+ * or a generation error. Nothing is charged. Allow the user to retry.
388
+ *
389
+ * Both monthly caps are operator-configured and reset at the start of
390
+ * each calendar month.
391
+ *
392
+ * **Side effects (on success)**
393
+ * The generated variant is persisted to the user's customization history
394
+ * and counts toward the monthly caps **whether or not** it is later
395
+ * activated — generation is not reversible. The SDK clears the
396
+ * {@link getAvatarsCustomized} cache so the next call surfaces the new
397
+ * variant. The user's active avatar is unchanged until you call
398
+ * {@link setAvatar} with the returned `cdn_url`.
399
+ *
400
+ * **Idempotency**: not idempotent — each successful call generates (and
401
+ * charges for) a new variant. Guard the call site against double-clicks.
402
+ *
403
+ * **UI guidance**: see [UI Guide — `avatarsCustomize`](../../docs/ui/avatars/UIGuide_avatarsCustomize.md).
404
+ *
405
+ * **Visitor mode**: not supported (throws).
406
+ *
407
+ * @param props.label_id Numeric Smartico label (brand) ID the user belongs to.
408
+ * @param props.user_id Numeric Smartico user ID.
409
+ * @param props.prompt_id Style-prompt ID (`prompt_id`) from {@link getAvatarPrompts}.
410
+ * @param props.avatar_url CDN URL of the base avatar to customize.
411
+ * @param props.avatar_real_id `avatar_real_id` of the base avatar.
412
+ * @returns {@link AvatarCustomizeResponse} — `cdn_url` set on success;
413
+ * `errCode` / `errMessage` set on failure.
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * // label_id / user_id identify the Smartico user being customized.
418
+ * const prompts = await window._smartico.api.getAvatarPrompts();
419
+ * const prompt = prompts[0];
420
+ *
421
+ * console.log('[smartico] generating avatar — show a spinner for ~5-20s and disable the Generate button');
422
+ * const result = await window._smartico.api.avatarsCustomize({
423
+ * label_id,
424
+ * user_id,
425
+ * prompt_id: prompt.prompt_id,
426
+ * avatar_url: baseAvatar.avatar_url,
427
+ * avatar_real_id: baseAvatar.avatar_real_id,
428
+ * });
429
+ *
430
+ * if (result.cdn_url) {
431
+ * console.log('[smartico] generation succeeded — preview this URL, then call setAvatar to apply it:', result.cdn_url);
432
+ * // await window._smartico.api.setAvatar({ avatar_url: result.cdn_url, avatar_real_id: baseAvatar.avatar_real_id });
433
+ * } else if (result.errCode === 12001) {
434
+ * console.error('[smartico] user monthly limit reached — show "you have used all your custom avatars this month"');
435
+ * } else if (result.errCode === 12002) {
436
+ * console.error('[smartico] brand monthly pool exhausted — show "custom avatars are unavailable right now, try later"');
437
+ * } else {
438
+ * console.error('[smartico] generation failed (e.g. not enough balance) — let the user retry:', result.errMessage);
439
+ * }
440
+ * ```
441
+ */
442
+ public async avatarsCustomize(props: {
443
+ label_id: number;
444
+ user_id: number;
445
+ prompt_id: number;
446
+ avatar_url: string;
447
+ avatar_real_id: number;
448
+ }): Promise<AvatarCustomizeResponse> {
449
+ const required: (keyof typeof props)[] = ['label_id', 'user_id', 'prompt_id', 'avatar_url', 'avatar_real_id'];
450
+ for (const field of required) {
451
+ if (!props[field]) {
452
+ throw new Error(`${field} is required`);
453
+ }
454
+ }
455
+
456
+ const r = await this.api.avatarsCustomize(props);
457
+
458
+ // On success a new variant is persisted server-side — drop the cached
459
+ // list so the next getAvatarsCustomized() surfaces it.
460
+ if (r.cdn_url) {
461
+ OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.AvatarsCustomized);
462
+ }
463
+
464
+ return r;
465
+ }
345
466
  }