@smartico/public-api 0.0.379 → 0.0.380

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 (99) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +31 -2
  3. package/dist/Store/BuyStoreItemErrorCode.d.ts +9 -1
  4. package/dist/Tournaments/TournamentRegistrationError.d.ts +3 -1
  5. package/dist/WSAPI/WSAPI.d.ts +13 -1235
  6. package/dist/WSAPI/WSAPIAvatars.d.ts +78 -0
  7. package/dist/WSAPI/WSAPIBase.d.ts +71 -0
  8. package/dist/WSAPI/WSAPIBonuses.d.ts +26 -0
  9. package/dist/WSAPI/WSAPIClans.d.ts +388 -0
  10. package/dist/WSAPI/WSAPIGamePick.d.ts +308 -0
  11. package/dist/WSAPI/WSAPIGeneral.d.ts +30 -0
  12. package/dist/WSAPI/WSAPIInbox.d.ts +126 -0
  13. package/dist/WSAPI/WSAPIJackpots.d.ts +109 -0
  14. package/dist/WSAPI/WSAPILeaderBoard.d.ts +25 -0
  15. package/dist/WSAPI/WSAPIMiniGames.d.ts +94 -0
  16. package/dist/WSAPI/WSAPIMissions.d.ts +517 -0
  17. package/dist/WSAPI/WSAPIRaffles.d.ts +119 -0
  18. package/dist/WSAPI/WSAPIStore.d.ts +656 -0
  19. package/dist/WSAPI/WSAPITournaments.d.ts +501 -0
  20. package/dist/WSAPI/WSAPITypes.d.ts +145 -85
  21. package/dist/WSAPI/WSAPIUser.d.ts +383 -0
  22. package/dist/index.js +3818 -1528
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.modern.mjs +3180 -981
  25. package/dist/index.modern.mjs.map +1 -1
  26. package/docs/api/README.md +56 -2
  27. package/docs/api/classes/WSAPIAvatars.md +116 -0
  28. package/docs/api/classes/WSAPIBonuses.md +50 -0
  29. package/docs/api/classes/WSAPIClans.md +66 -0
  30. package/docs/api/classes/WSAPIGamePick.md +181 -0
  31. package/docs/api/classes/WSAPIGeneral.md +63 -0
  32. package/docs/api/classes/WSAPIInbox.md +277 -0
  33. package/docs/api/classes/WSAPIJackpots.md +185 -0
  34. package/docs/api/classes/WSAPILeaderBoard.md +39 -0
  35. package/docs/api/classes/WSAPIMiniGames.md +175 -0
  36. package/docs/api/classes/WSAPIMissions.md +611 -0
  37. package/docs/api/classes/WSAPIRaffles.md +195 -0
  38. package/docs/api/classes/WSAPIStore.md +744 -0
  39. package/docs/api/classes/WSAPITournaments.md +104 -0
  40. package/docs/api/classes/WSAPIUser.md +200 -0
  41. package/docs/api/enumerations/AchievementClaimPeriodTypeId.md +13 -0
  42. package/docs/api/enumerations/BuyStoreItemErrorCode.md +32 -0
  43. package/docs/api/interfaces/ClanPrizePublicMeta.md +41 -0
  44. package/docs/api/interfaces/ClanPrizeStructureEntry.md +7 -1
  45. package/docs/api/interfaces/ClanPublicMeta.md +4 -0
  46. package/docs/api/interfaces/SAWTemplateUI.md +10 -0
  47. package/docs/api/interfaces/TAchCategory.md +19 -4
  48. package/docs/api/interfaces/TBuyStoreItemResult.md +26 -8
  49. package/docs/api/interfaces/TMissionClaimRewardResult.md +14 -3
  50. package/docs/api/interfaces/TMissionOptInResult.md +10 -3
  51. package/docs/api/interfaces/TMissionOrBadge.md +6 -2
  52. package/docs/api/interfaces/TStoreItem.md +21 -1
  53. package/docs/api/interfaces/TTournamentDetailed.md +8 -0
  54. package/docs/api/interfaces/UserAchievement.md +1 -1
  55. package/docs/ui/clans/UIGuide_getClanInfo.md +153 -0
  56. package/docs/ui/clans/UIGuide_getClans.md +172 -0
  57. package/docs/ui/clans/UIGuide_joinClan.md +196 -0
  58. package/docs/ui/missions/UIGuide_getAchCategories.md +17 -0
  59. package/docs/ui/missions/UIGuide_getBadges.md +237 -0
  60. package/docs/ui/missions/UIGuide_getMissions.md +268 -0
  61. package/docs/ui/missions/UIGuide_requestMissionClaimReward.md +21 -0
  62. package/docs/ui/missions/UIGuide_requestMissionOptIn.md +16 -0
  63. package/docs/ui/store/UIGuide_buyStoreItem.md +133 -0
  64. package/docs/ui/store/UIGuide_getStoreCategories.md +19 -0
  65. package/docs/ui/store/UIGuide_getStoreItems.md +264 -0
  66. package/docs/ui/store/UIGuide_getStorePurchasedItems.md +122 -0
  67. package/docs/ui/tournaments/UIGuide_getClanTournamentPlayers.md +140 -0
  68. package/docs/ui/tournaments/UIGuide_getTournamentInstanceInfo.md +265 -0
  69. package/docs/ui/tournaments/UIGuide_getTournamentsList.md +236 -0
  70. package/docs/ui/tournaments/UIGuide_registerInTournament.md +156 -0
  71. package/docs/ui/user/UIGuide_getLevels.md +162 -0
  72. package/docs/ui/user/UIGuide_getUserProfile.md +213 -0
  73. package/package.json +3 -3
  74. package/scripts/postdoc-cleanup.js +195 -0
  75. package/src/Missions/MissionsUtils.ts +5 -2
  76. package/src/Store/BuyStoreItemErrorCode.ts +8 -0
  77. package/src/Store/StoreItem.ts +1 -0
  78. package/src/Store/StoreItemPurchased.ts +1 -0
  79. package/src/Store/StoreItemType.ts +1 -1
  80. package/src/Tournaments/TournamentRegistrationError.ts +4 -0
  81. package/src/WSAPI/WSAPI.ts +27 -2007
  82. package/src/WSAPI/WSAPIAvatars.ts +127 -0
  83. package/src/WSAPI/WSAPIBase.ts +93 -0
  84. package/src/WSAPI/WSAPIBonuses.ts +54 -0
  85. package/src/WSAPI/WSAPIClans.ts +417 -0
  86. package/src/WSAPI/WSAPIGamePick.ts +382 -0
  87. package/src/WSAPI/WSAPIGeneral.ts +57 -0
  88. package/src/WSAPI/WSAPIInbox.ts +222 -0
  89. package/src/WSAPI/WSAPIJackpots.ts +212 -0
  90. package/src/WSAPI/WSAPILeaderBoard.ts +40 -0
  91. package/src/WSAPI/WSAPIMiniGames.ts +181 -0
  92. package/src/WSAPI/WSAPIMissions.ts +572 -0
  93. package/src/WSAPI/WSAPIRaffles.ts +175 -0
  94. package/src/WSAPI/WSAPIStore.ts +732 -0
  95. package/src/WSAPI/WSAPITournaments.ts +541 -0
  96. package/src/WSAPI/WSAPITypes.ts +145 -87
  97. package/src/WSAPI/WSAPIUser.ts +456 -0
  98. package/tsconfig.json +17 -1
  99. package/docs/api/classes/WSAPI.md +0 -1927
@@ -0,0 +1,572 @@
1
+ import { ECacheContext, OCache } from '../OCache';
2
+ import {
3
+ TAchCategory,
4
+ TMissionClaimRewardResult,
5
+ TMissionOptInResult,
6
+ TMissionOrBadge,
7
+ } from './WSAPITypes';
8
+ import {
9
+ CACHE_DATA_SEC,
10
+ onUpdateContextKey,
11
+ } from './WSAPIBase';
12
+ import { WSAPIGeneral } from './WSAPIGeneral';
13
+
14
+ /** @group Missions */
15
+ export class WSAPIMissions extends WSAPIGeneral {
16
+ /**
17
+ * Returns all missions visible to the current user, scoped server-side
18
+ * to what the user is qualified to see. Optionally subscribes to live
19
+ * updates via `onUpdate`: the callback is invoked with the full, refreshed
20
+ * mission array whenever the server pushes a mission-state-changing event.
21
+ *
22
+ * The returned list is the canonical source of truth for mission state
23
+ * (`is_opted_in`, `progress`, `is_completed`, `completion_count`, etc.).
24
+ * Consumers should NEVER mutate it optimistically — always wait for the
25
+ * `onUpdate` callback (or a fresh `getMissions()` call after the 30s cache
26
+ * window expires) to observe the new state.
27
+ *
28
+ * @remarks
29
+ * **Subscription model (`onUpdate`)**
30
+ * The callback receives the FULL refreshed mission array (never a
31
+ * diff/patch). Each subsequent call to `getMissions({ onUpdate })`
32
+ * REPLACES the prior callback — only one active subscriber at a time.
33
+ * Pass `onUpdate: undefined` (or omit it) to keep the prior callback in
34
+ * place; the callback is never auto-cleared.
35
+ *
36
+ * **Update triggers** — the callback fires when:
37
+ * 1. **Mutation responses**: `requestMissionOptIn(...)` or
38
+ * `requestMissionClaimReward(...)` resolves (any `err_code`).
39
+ * 2. **Asynchronous server pushes**: a user activity (deposit, bet,
40
+ * login, etc.) advances task progress on a task configured to push
41
+ * updates, or completes a mission.
42
+ * 3. **Visibility changes**: the user's level changes, or other
43
+ * gamification visibility tags change.
44
+ *
45
+ * Operator-side BO mission-config edits do NOT push to connected
46
+ * clients — those changes surface only on the next cache miss (after
47
+ * the 30 s TTL) or alongside the next trigger from (1)–(3).
48
+ *
49
+ * **What's in the returned list / what's filtered server-side**
50
+ * The response is pre-filtered by the server. Missions EXCLUDED before
51
+ * reaching the SDK:
52
+ * - Missions in `DRAFT` or `ARCHIVED` status (see {@link AchievementStatus}).
53
+ * - Missions where the user fails visibility conditions. Exception:
54
+ * completed missions may remain visible after the user no longer
55
+ * qualifies (configurable per-brand).
56
+ * - `FEATURED_MANUALLY` missions that are still locked — they appear
57
+ * only after the user unlocks them.
58
+ * - Locked missions configured to hide while locked — UNLESS the
59
+ * mission is configured to expose immediately on unlock (then the
60
+ * locked mission is included as a teaser).
61
+ * - `RECURRING` missions between cycles, until the next cycle starts.
62
+ * - `RECURRING_QUANTITY` missions outside their `active_from_ts` /
63
+ * `active_till_ts` window.
64
+ *
65
+ * Client-side filtering you may still want: missions with
66
+ * `only_in_custom_section: true` are intended for their custom-section
67
+ * view only and should be hidden from the main mission list.
68
+ *
69
+ * **Reading state from the returned mission**
70
+ * Drive availability chips ("missed", "coming soon", "needs opt-in",
71
+ * "in progress", "expired") from `availability_status` (enum
72
+ * {@link AchievementAvailabilityStatus}) — it's the canonical derived
73
+ * state and reflects all server-side timing/visibility rules in one
74
+ * field. For time-limited and recurring missions, `dt_start` doubles
75
+ * as the opt-in timestamp (for opt-in missions) or unlock timestamp
76
+ * (for previously-locked missions); the expiration of a time-limited
77
+ * mission is `dt_start + time_limit_ms`. `next_recurrence_date_ts`
78
+ * is populated for `RECURRING` / `RECURRING_QUANTITY` missions but
79
+ * is no longer meaningful once the mission's `active_till_ts` has
80
+ * passed.
81
+ *
82
+ * **Refresh after a mutation**
83
+ * After `requestMissionOptIn(...)` or `requestMissionClaimReward(...)`
84
+ * resolves, the SDK auto-refreshes the mission cache; the `onUpdate`
85
+ * callback fires with the new array shortly after. `is_opted_in`,
86
+ * `progress`, and `is_completed` flip on the refreshed mission — do
87
+ * NOT mutate them optimistically while the round-trip is pending.
88
+ * Render any pending state from your own loading flag.
89
+ *
90
+ * **UI guidance**: see [UI Guide — `getMissions`](../../docs/ui/missions/UIGuide_getMissions.md).
91
+ *
92
+ * @param params Optional. Omit to fetch without subscribing.
93
+ * @param params.onUpdate Callback invoked with the full refreshed
94
+ * mission array on every server-pushed update
95
+ * (after opt-in / claim-reward calls, or when
96
+ * the server pushes an asynchronous
97
+ * mission-state change). Each call to
98
+ * `getMissions` overwrites the prior callback.
99
+ * Never fires in visitor mode.
100
+ * @returns Promise resolving to the current list of
101
+ * missions visible to the user. Empty array if
102
+ * the user has no missions (or if the visitor
103
+ * proxy user has none configured).
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * // 1. Initial fetch + subscribe to live updates
108
+ * const missions = await window._smartico.api.getMissions({
109
+ * onUpdate: (refreshed) => {
110
+ * console.log('[smartico] mission list refreshed — re-render the entire mission list UI from this array, do not merge with prior state:', refreshed);
111
+ *
112
+ * // 2. onUpdate fires for: opt-in/claim responses, task-progress
113
+ * // pushes that complete a mission or move a "pushToClient" task,
114
+ * // and user-level changes. Treat each call as authoritative.
115
+ * const claimable = refreshed.filter(m =>
116
+ * m.is_completed && m.requires_prize_claim && !m.prize_claimed_date_ts,
117
+ * );
118
+ * if (claimable.length > 0) {
119
+ * console.log('[smartico] there are unclaimed mission prizes — surface a "Claim" CTA on each of these missions:', claimable.map(m => m.id));
120
+ * }
121
+ * },
122
+ * });
123
+ *
124
+ * // 3. Filter/render from the returned list. The server already excludes
125
+ * // DRAFT/ARCHIVED missions and ones the user can't see. You may need
126
+ * // client-side filters for view-specific concerns (custom sections,
127
+ * // completed/missed tabs).
128
+ * const generalView = missions
129
+ * .filter(m => !m.only_in_custom_section)
130
+ * .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
131
+ *
132
+ * console.log('[smartico] initial mission list ready — render', generalView.length, 'missions sorted by position');
133
+ *
134
+ * // 4. Visitor-mode equivalent: onUpdate is accepted but never fires.
135
+ * // Re-poll if you need fresh data.
136
+ * // const visitorMissions = await window._smartico.vapi('EN').getMissions();
137
+ * // console.log('[smartico] visitor missions are the brand proxy user\'s — per-user fields are not meaningful');
138
+ * ```
139
+ */
140
+ public async getMissions({ onUpdate }: { onUpdate?: (data: TMissionOrBadge[]) => void } = {}): Promise<TMissionOrBadge[]> {
141
+ if (onUpdate) {
142
+ this.onUpdateCallback.set(onUpdateContextKey.Missions, onUpdate);
143
+ }
144
+
145
+ return OCache.use(
146
+ onUpdateContextKey.Missions,
147
+ ECacheContext.WSAPI,
148
+ () => this.api.missionsGetItemsT(this.userExtId),
149
+ CACHE_DATA_SEC,
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Returns all the badges available to the current user.
155
+ *
156
+ * Badges are completion-tracked achievements that auto-unlock once the user
157
+ * meets their criteria — there is no opt-in step, no claim step, and no
158
+ * task-description reveal. Contrast with {@link getMissions}, which returns
159
+ * the same `TMissionOrBadge[]` shape but represents explicit opt-in flows.
160
+ *
161
+ * @remarks
162
+ * **Why no `onUpdate` callback?**
163
+ * Unlike `getMissions({ onUpdate })`, this method does NOT accept an
164
+ * `onUpdate` subscription. Internally the SDK never refreshes the `Badges`
165
+ * cache from server pushes: the achievement-related push events
166
+ * (mission opt-in / claim / reload) refresh the missions cache, NOT
167
+ * the badges cache. The badges cache is flushed only by:
168
+ * - its 30-second TTL
169
+ * - a full SDK cache wipe at login / logout
170
+ *
171
+ * **How to refresh the badge list**
172
+ * If you need near-live badge state, poll manually after the TTL expires.
173
+ * The first call inside the 30 s window returns the cached payload; the
174
+ * first call after the TTL triggers a fresh server round-trip. Example:
175
+ * ```ts
176
+ * setInterval(async () => {
177
+ * const badges = await window._smartico.api.getBadges();
178
+ * console.log('[smartico] badge list polled — re-render badge grid from this array', badges);
179
+ * }, 30_000);
180
+ * ```
181
+ * Alternatively, re-fetch on a domain event your app already handles
182
+ * (e.g. after the user finishes a game round or claims a bonus).
183
+ *
184
+ * **Differences from missions** (same `TMissionOrBadge` shape, different
185
+ * feature)
186
+ * The opt-in, claim, recurring-cycle, and unlock-description fields on
187
+ * the returned objects are not populated for badges; treat their values
188
+ * as undefined / default. Time windows are absolute calendar timestamps
189
+ * (`active_from_ts` / `active_till_ts`), not opt-in-relative durations —
190
+ * drive availability chips from the SDK-computed `badgeTimeLimitState`
191
+ * (enum {@link BadgesTimeLimitStates}), not `time_limit_ms`. Locking for
192
+ * badges is purely time-based: `is_locked` is `true` only when
193
+ * `badgeTimeLimitState === BadgesTimeLimitStates.BeforeStartDate` (the
194
+ * time window hasn't started yet). The primary navigation field is
195
+ * `category_ids` — call {@link getAchCategories} to resolve category
196
+ * metadata and group badges by category.
197
+ *
198
+ * **Idempotency / Side effects**: fetch-only; safe to call repeatedly.
199
+ * The cache layer deduplicates concurrent calls within the TTL window.
200
+ *
201
+ * **UI guidance**: see [UI Guide — `getBadges`](../../docs/ui/missions/UIGuide_getBadges.md).
202
+ *
203
+ * @returns Promise resolving to `TMissionOrBadge[]`. Every item has
204
+ * `type === 'badge'`. Cached for 30 s under the `Badges` key.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const [badges, categories] = await Promise.all([
209
+ * window._smartico.api.getBadges(),
210
+ * window._smartico.api.getAchCategories(),
211
+ * ]);
212
+ *
213
+ * console.log('[smartico] badges fetched — render grouped grid with', badges.length, 'badges across', categories.length, 'categories');
214
+ *
215
+ * for (const category of categories) {
216
+ * const inCategory = badges.filter(b => b.category_ids?.includes(category.id));
217
+ * const completed = inCategory.filter(b => b.is_completed).length;
218
+ * console.log(`[smartico] render category section "${category.name}" header showing ${completed}/${inCategory.length}`);
219
+ *
220
+ * for (const badge of inCategory) {
221
+ * // Use badgeTimeLimitState (not time_limit_ms) for availability chip
222
+ * if (badge.badgeTimeLimitState === 0) { // BadgesTimeLimitStates.BeforeStartDate
223
+ * console.log('[smartico] badge not yet started — render grayscaled card with "Starts on" chip for badge', badge.id);
224
+ * } else if (badge.is_completed) {
225
+ * console.log('[smartico] badge completed — render with completed styling and check-mark for badge', badge.id);
226
+ * } else {
227
+ * console.log('[smartico] badge in progress — render with stage counter (completed_tasks / total) for badge', badge.id);
228
+ * }
229
+ * }
230
+ * }
231
+ *
232
+ * // No onUpdate available — poll if live state matters
233
+ * setInterval(async () => {
234
+ * try {
235
+ * const fresh = await window._smartico.api.getBadges();
236
+ * console.log('[smartico] badge poll tick — diff against previous list and re-render any changed badge cards', fresh);
237
+ * } catch (e) {
238
+ * console.error('[smartico] badge poll failed — keep showing last known state, retry on next tick:', e);
239
+ * }
240
+ * }, 30_000);
241
+ * ```
242
+ */
243
+ public async getBadges(): Promise<TMissionOrBadge[]> {
244
+ return OCache.use(onUpdateContextKey.Badges, ECacheContext.WSAPI, () => this.api.badgetsGetItemsT(this.userExtId), CACHE_DATA_SEC);
245
+ }
246
+
247
+ /**
248
+ * Returns the list of active mission/badge categories configured for the current label.
249
+ * Categories are operator-defined groupings used to organize missions and badges into
250
+ * sections, tabs, or filter chips in the gamification UI. The same category list serves
251
+ * BOTH missions and badges — there is no separate "mission categories" vs "badge
252
+ * categories" split on the server.
253
+ *
254
+ * @remarks
255
+ * **What categories are**
256
+ * - Operator-defined groupings, configured per-label by the brand operator.
257
+ * Only active categories are returned; inactive/deleted ones are filtered out.
258
+ * - Each `TAchCategory` exposes `{ id, name, order }`. `name` is the display label;
259
+ * `order` is the relative position (lower = appears first).
260
+ * - The same array covers BOTH missions and badges — categories are not filtered by
261
+ * entity type on the server.
262
+ *
263
+ * **Localization / translation**
264
+ * - `name` is **pre-translated server-side** to the authenticated user's stored language.
265
+ * In visitor mode, the language passed to `_smartico.vapi(lang)` drives the
266
+ * translation. Consumers never need to translate `name` themselves.
267
+ * - Fallback if no translation exists for the user's language: the brand's EN value
268
+ * is returned. The field is never null.
269
+ *
270
+ * **Sort order**
271
+ * The server does NOT pre-sort. Sort client-side before rendering:
272
+ * `categories.sort((a, b) => a.order - b.order)`.
273
+ *
274
+ * **How missions/badges join to categories**
275
+ * Each `TMissionOrBadge` exposes `category_ids: number[]` (zero-or-more,
276
+ * many-to-many). A single mission/badge can belong to multiple categories
277
+ * simultaneously, or to none. To render a "category → items" view:
278
+ * `missions.filter(m => m.category_ids.includes(cat.id))`.
279
+ *
280
+ * **Uncategorized items**
281
+ * Items with `category_ids: []` are NOT represented by a synthetic "Other" entry.
282
+ * If your UI needs one, generate it client-side by checking
283
+ * `item.category_ids.length === 0`.
284
+ *
285
+ * **Empty categories**
286
+ * A category may be returned even if zero missions/badges reference it. Filter
287
+ * out empty categories on the consumer side if you don't want to render section
288
+ * headers for them.
289
+ *
290
+ * **Refresh cadence / cache**
291
+ * - The SDK caches the response for 30 seconds. Subsequent calls within that
292
+ * window return the cached array without a network round-trip.
293
+ * - The cache is NOT invalidated by mission/badge push events — operator-driven
294
+ * category changes propagate at the next 30 s cache miss.
295
+ * - Cache is fully cleared on login / logout.
296
+ * - In **visitor mode** additional server-side caching applies on top of the
297
+ * SDK cache; worst-case staleness for an edited/added category is on the order
298
+ * of a few minutes for anonymous viewers.
299
+ *
300
+ * **Idempotency**: safe. Read-only metadata fetch. Cache de-duplicates concurrent calls.
301
+ *
302
+ * **Side effects**: none — pure metadata read.
303
+ *
304
+ * **UI guidance**: see [UI Guide — `getAchCategories`](../../docs/ui/missions/UIGuide_getAchCategories.md).
305
+ *
306
+ * @returns Array of `{ id, name, order }`. Empty array if no active categories are
307
+ * configured for the label. `name` is pre-translated. Order is NOT applied — caller
308
+ * must sort.
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * const categories = await window._smartico.api.getAchCategories();
313
+ *
314
+ * if (categories.length === 0) {
315
+ * console.log('[smartico] no categories configured — render missions/badges as a flat list without section headers');
316
+ * } else {
317
+ * const sorted = [...categories].sort((a, b) => a.order - b.order);
318
+ * console.log('[smartico] categories loaded — render one tab/section per entry in this order:', sorted.map(c => c.name));
319
+ *
320
+ * // Join to missions: render each category as a section
321
+ * const missions = await window._smartico.api.getMissions();
322
+ * for (const cat of sorted) {
323
+ * const items = missions.filter(m => m.category_ids.includes(cat.id));
324
+ * if (items.length === 0) {
325
+ * console.log('[smartico] category has no missions — skip rendering its section header:', cat.name);
326
+ * continue;
327
+ * }
328
+ * console.log(`[smartico] render section "${cat.name}" with ${items.length} mission(s)`);
329
+ * }
330
+ *
331
+ * // Uncategorized items: surface in a caller-provided "Other" bucket if needed
332
+ * const uncategorized = missions.filter(m => m.category_ids.length === 0);
333
+ * if (uncategorized.length > 0) {
334
+ * console.log('[smartico] uncategorized missions exist — render a caller-provided "Other" section; server does NOT return id:-1:', uncategorized.length);
335
+ * }
336
+ * }
337
+ *
338
+ * // Visitor mode — same shape, language driven by vapi(lang) argument
339
+ * const visitorCats = await window._smartico.vapi('DE').getAchCategories();
340
+ * console.log('[smartico] visitor-mode categories (names translated to DE):', visitorCats);
341
+ * ```
342
+ */
343
+ public async getAchCategories(): Promise<TAchCategory[]> {
344
+ return OCache.use(
345
+ onUpdateContextKey.AchCategories,
346
+ ECacheContext.WSAPI,
347
+ () => this.api.achGetCategoriesT(this.userExtId),
348
+ CACHE_DATA_SEC,
349
+ );
350
+ }
351
+
352
+ /**
353
+ * Opt the current user in to a mission. The mission must have
354
+ * `is_requires_optin: true` (from `getMissions()`) and must not already
355
+ * be opted-in or locked. After a successful opt-in, the user's task
356
+ * progress starts counting toward this mission — events fired BEFORE
357
+ * opt-in do not count retroactively (except `UNLOCK_ACHIEVEMENT` tasks,
358
+ * which are exempt and can fire pre-opt-in).
359
+ *
360
+ * @remarks
361
+ * **Preconditions**
362
+ * Only call when the mission has `is_requires_optin === true`,
363
+ * `is_opted_in === false`, and `is_locked === false`. Calling on a
364
+ * mission that does not require opt-in is wasted; calling twice returns
365
+ * `40010`.
366
+ *
367
+ * **Error codes** (in `err_code`)
368
+ * - `0` — success; task progress counting begins
369
+ * - `105` — wrong mission id (different label) OR visibility conditions
370
+ * not met. Same code covers both; use `err_message` to disambiguate.
371
+ * - `40010` — already opted-in. Safe to treat as "no-op success"; usually
372
+ * means local mission state was stale (e.g. user opted in from another
373
+ * session/tab).
374
+ * - `40013` — mission not opt-in-able: missing `requires_optin`, in
375
+ * DRAFT/ARCHIVED state, or outside its `active_from_ts` /
376
+ * `active_till_ts` window.
377
+ * - `40014` — mission is locked; user has unlock-tasks remaining.
378
+ * Surface the mission's `unlock_mission_description` to guide them.
379
+ * - other non-zero — generic server error. Surface `err_message` if any.
380
+ *
381
+ * **Time-limited missions**
382
+ * For missions with `time_limit_ms > 0`, the countdown begins at opt-in
383
+ * time, not at mission creation. Server records expiration as
384
+ * `optin_date + time_limit_ms`.
385
+ *
386
+ * **Refresh after success**
387
+ * The SDK refreshes its mission cache automatically on opt-in response;
388
+ * any `onUpdate` callback passed to a prior `getMissions({ onUpdate })`
389
+ * fires with the new state shortly after. No manual re-fetch needed.
390
+ *
391
+ * **Idempotency**: not idempotent. A second call returns `40010`. Guard
392
+ * the call site against double-clicks.
393
+ *
394
+ * **Side effects**: does NOT award points / badges / levels directly.
395
+ * Rewards come from task completion, which is itself gated on opt-in.
396
+ * May affect visibility of other missions whose visibility conditions
397
+ * depend on this opt-in.
398
+ *
399
+ * **UI guidance**: see [UI Guide — `requestMissionOptIn`](../../docs/ui/missions/UIGuide_requestMissionOptIn.md).
400
+ *
401
+ * **Visitor mode: not supported**
402
+ *
403
+ * @param mission_id The mission `id` (from `getMissions()`)
404
+ * @returns `{ err_code, err_message }`; success when `err_code === 0`
405
+ *
406
+ * @example
407
+ * ```ts
408
+ * const missions = await window._smartico.api.getMissions({
409
+ * onUpdate: (m) => console.log('[smartico] mission list updated — re-render mission UI from this new array', m),
410
+ * });
411
+ * const mission = missions.find(m => m.id === missionId);
412
+ *
413
+ * if (!mission || !mission.is_requires_optin || mission.is_opted_in || mission.is_locked) {
414
+ * console.log('[smartico] opt-in not applicable — keep UI as-is');
415
+ * return;
416
+ * }
417
+ *
418
+ * console.log('[smartico] opt-in starting — show loading spinner on opt-in button and disable it to prevent double-clicks');
419
+ * const r = await window._smartico.api.requestMissionOptIn(mission.id);
420
+ * console.log('[smartico] opt-in response received — clear loading spinner');
421
+ *
422
+ * if (r.err_code === 0 || r.err_code === 40010) {
423
+ * console.log('[smartico] opt-in succeeded (or was already done) — onUpdate above will fire with refreshed mission state; no manual re-fetch needed');
424
+ * } else if (r.err_code === 40014) {
425
+ * console.error('[smartico] mission is locked — surface the unlock description as guidance:', mission.unlock_mission_description);
426
+ * } else {
427
+ * console.error('[smartico] opt-in failed — show an error toast with this message to the user:', r.err_message);
428
+ * }
429
+ * ```
430
+ */
431
+ public async requestMissionOptIn(mission_id: number): Promise<TMissionOptInResult> {
432
+ const r = await this.api.missionOptIn(this.userExtId, mission_id);
433
+
434
+ const o: TMissionOptInResult = {
435
+ err_code: r.errCode,
436
+ err_message: r.errMsg,
437
+ };
438
+
439
+ return o;
440
+ }
441
+
442
+ /**
443
+ * Claim the prize for a completed mission. This is the step that runs
444
+ * AFTER `requestMissionOptIn()` (when applicable) and AFTER the user has
445
+ * completed all of the mission's tasks. Only call when the mission has
446
+ * `requires_prize_claim === true`, `is_completed === true`, and the prize
447
+ * has not already been claimed.
448
+ *
449
+ * The points / bonuses / badge grants associated with the mission are
450
+ * awarded server-side synchronously inside this call (before the response
451
+ * is returned), so a successful response (`err_code === 0`) means the
452
+ * reward has already landed in the user's account.
453
+ *
454
+ * @remarks
455
+ * **Preconditions**
456
+ * Only call when, on the mission object from `getMissions()`:
457
+ * - `is_completed === true`
458
+ * - `requires_prize_claim === true`
459
+ * - `prize_claimed_date_ts == null` (i.e. not already claimed)
460
+ * - `prize_claim_expiration_date` is `null` OR still in the future
461
+ *
462
+ * The `ach_completed_id` argument must be passed straight from the same
463
+ * mission object (`mission.ach_completed_id`). It is a server-side primary
464
+ * key identifying the specific completion row; the SDK consumer should
465
+ * never synthesise or cache it across sessions — always re-read it from a
466
+ * fresh `getMissions()` result.
467
+ *
468
+ * **Error codes** (in `err_code`)
469
+ * - `0` — success; the prize was claimed and rewards have been credited.
470
+ * - `1` — generic server error. Covers several distinct underlying
471
+ * conditions, all collapsed into the same code: stale or wrong
472
+ * `ach_completed_id` (does not match user / mission), the mission no
473
+ * longer requiring a prize claim, the mission being archived/draft,
474
+ * label mismatch, or the completion being too old (server enforces a
475
+ * freshness window of several months). Recovery: re-fetch missions
476
+ * with `getMissions()`; if the mission still appears claimable,
477
+ * surface a generic error.
478
+ * - `40015` — claim window expired (`prize_claim_expiration_date` has
479
+ * passed). Show a "claim period ended" message; do not retry.
480
+ * - `40016` — mission is not completed yet. Indicates a race or a stale
481
+ * local cache. Re-fetch via `getMissions()`; if `is_completed` is still
482
+ * false, the UI should hide the claim button.
483
+ * - `40017` — prize already claimed. Treat as idempotent success: refresh
484
+ * the mission list (auto-refresh will fire) and hide the claim button.
485
+ * Usually means another tab/session already claimed it.
486
+ * - other non-zero — generic server error. Surface `err_message` if any.
487
+ *
488
+ * **Recurring missions**
489
+ * The server transparently handles both one-shot and recurring mission
490
+ * completions. The SDK consumer passes a single `ach_completed_id`
491
+ * regardless of mission type; the server resolves the correct completion.
492
+ *
493
+ * **Refresh after success**
494
+ * On any claim response (success OR failure), the SDK automatically
495
+ * re-fetches the mission list and fires any `onUpdate` callback
496
+ * registered via `getMissions({ onUpdate })`. No manual re-fetch needed.
497
+ * Note: points balance, badges, and level state arrive through their
498
+ * own push channels — subscribe to them separately if your UI shows
499
+ * those values.
500
+ *
501
+ * **Idempotency**: not idempotent at the row level. A second call on the
502
+ * same `ach_completed_id` returns `40017` (already claimed). For a stale
503
+ * `ach_completed_id` the second call returns `1`. Guard the call site
504
+ * against double-clicks; the SDK does not enforce its own in-flight lock.
505
+ *
506
+ * **Side effects** (consumer-observable effects of a successful claim):
507
+ * - Points credited to the user's balance.
508
+ * - CRM-rule activities executed (may grant bonuses, badges; may unlock
509
+ * follow-up missions, which arrive as a later mission auto-refresh).
510
+ * - Analytics / automation events fired downstream; not directly observable
511
+ * in the response, but visible via the mission auto-refresh and any
512
+ * subsequent bonus / badge updates.
513
+ *
514
+ * **UI guidance**: see [UI Guide — `requestMissionClaimReward`](../../docs/ui/missions/UIGuide_requestMissionClaimReward.md).
515
+ *
516
+ * @param mission_id The mission `ach_id` (from `getMissions()`).
517
+ * @param ach_completed_id The completion-row identifier, read from
518
+ * `mission.ach_completed_id` on the same mission
519
+ * object fetched via `getMissions()`. Never
520
+ * fabricate or cache across sessions.
521
+ * @returns `{ err_code, err_message }`; success when `err_code === 0`
522
+ * (or `40017` when treated as idempotent no-op).
523
+ *
524
+ * @example
525
+ * ```ts
526
+ * const missions = await window._smartico.api.getMissions({
527
+ * onUpdate: (m) => console.log('[smartico] mission list updated — re-render mission UI from this new array', m),
528
+ * });
529
+ * const mission = missions.find(m => m.ach_id === missionId);
530
+ *
531
+ * if (
532
+ * !mission ||
533
+ * !mission.is_completed ||
534
+ * !mission.requires_prize_claim ||
535
+ * mission.prize_claimed_date_ts ||
536
+ * (mission.prize_claim_expiration_date && Date.now() >= mission.prize_claim_expiration_date)
537
+ * ) {
538
+ * console.log('[smartico] claim not applicable — keep claim button hidden');
539
+ * return;
540
+ * }
541
+ *
542
+ * console.log('[smartico] claim starting — show loading spinner on claim button and disable it to prevent double-clicks');
543
+ * const r = await window._smartico.api.requestMissionClaimReward(mission.ach_id, mission.ach_completed_id);
544
+ * console.log('[smartico] claim response received — clear loading spinner');
545
+ *
546
+ * if (r.err_code === 0 || r.err_code === 40017) {
547
+ * console.log('[smartico] claim succeeded (or was already claimed) — points/bonuses are credited; mission list auto-refresh will fire via the onUpdate above; hide the claim button');
548
+ * } else if (r.err_code === 40015) {
549
+ * console.error('[smartico] claim window expired — show a "claim period ended" message and remove the claim button:', mission.prize_claim_expiration_date);
550
+ * } else if (r.err_code === 40016) {
551
+ * console.error('[smartico] mission reported as not completed by server — local state was stale; auto-refresh will reconcile, do not retry');
552
+ * } else {
553
+ * console.error('[smartico] claim failed — show an error toast with this message to the user (and re-fetch missions to recover from a stale ach_completed_id):', r.err_message);
554
+ * }
555
+ * ```
556
+ */
557
+ public async requestMissionClaimReward(mission_id: number, ach_completed_id: number): Promise<TMissionClaimRewardResult> {
558
+ const r = await this.api.missionClaimPrize(this.userExtId, mission_id, ach_completed_id);
559
+
560
+ const o: TMissionClaimRewardResult = {
561
+ err_code: r.errCode,
562
+ err_message: r.errMsg,
563
+ };
564
+
565
+ return o;
566
+ }
567
+
568
+ protected async updateMissions() {
569
+ const payload = await this.api.missionsGetItemsT(this.userExtId);
570
+ this.updateEntity(onUpdateContextKey.Missions, payload);
571
+ }
572
+ }