@smartico/public-api 0.0.379 → 0.0.381

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 (219) hide show
  1. package/.nvmrc +1 -1
  2. package/CLAUDE.md +103 -0
  3. package/README.md +53 -2
  4. package/dist/Base/ClassId.d.ts +2 -0
  5. package/dist/Jackpots/JackpotDetails.d.ts +14 -15
  6. package/dist/Jackpots/JackpotPot.d.ts +10 -6
  7. package/dist/Raffle/GetRaffleWonPrizesRequest.d.ts +9 -0
  8. package/dist/Raffle/GetRaffleWonPrizesResponse.d.ts +52 -0
  9. package/dist/Raffle/index.d.ts +2 -0
  10. package/dist/SmarticoAPI.d.ts +6 -1
  11. package/dist/SmarticoLib/index.d.ts +5 -1
  12. package/dist/Store/BuyStoreItemErrorCode.d.ts +9 -1
  13. package/dist/Tournaments/TournamentRegistrationError.d.ts +3 -1
  14. package/dist/WSAPI/WSAPI.d.ts +13 -1235
  15. package/dist/WSAPI/WSAPIAvatars.d.ts +296 -0
  16. package/dist/WSAPI/WSAPIBase.d.ts +71 -0
  17. package/dist/WSAPI/WSAPIBonuses.d.ts +240 -0
  18. package/dist/WSAPI/WSAPIClans.d.ts +388 -0
  19. package/dist/WSAPI/WSAPIGamePick.d.ts +593 -0
  20. package/dist/WSAPI/WSAPIGeneral.d.ts +172 -0
  21. package/dist/WSAPI/WSAPIInbox.d.ts +615 -0
  22. package/dist/WSAPI/WSAPIJackpots.d.ts +344 -0
  23. package/dist/WSAPI/WSAPILeaderBoard.d.ts +139 -0
  24. package/dist/WSAPI/WSAPIMiniGames.d.ts +510 -0
  25. package/dist/WSAPI/WSAPIMissions.d.ts +517 -0
  26. package/dist/WSAPI/WSAPIRaffles.d.ts +643 -0
  27. package/dist/WSAPI/WSAPIStore.d.ts +656 -0
  28. package/dist/WSAPI/WSAPITournaments.d.ts +501 -0
  29. package/dist/WSAPI/WSAPITypes.d.ts +376 -205
  30. package/dist/WSAPI/WSAPIUser.d.ts +533 -0
  31. package/dist/index.js +6745 -1661
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.modern.mjs +6324 -1342
  34. package/dist/index.modern.mjs.map +1 -1
  35. package/docs/Callbacks.md +162 -0
  36. package/docs/SmarticoObject.md +445 -0
  37. package/docs/Transforms.md +99 -0
  38. package/docs/api/README.md +82 -2
  39. package/docs/api/classes/WSAPIAvatars.md +344 -0
  40. package/docs/api/classes/WSAPIBonuses.md +271 -0
  41. package/docs/api/classes/WSAPIClans.md +436 -0
  42. package/docs/api/classes/WSAPIGamePick.md +710 -0
  43. package/docs/api/classes/WSAPIGeneral.md +212 -0
  44. package/docs/api/classes/WSAPIInbox.md +801 -0
  45. package/docs/api/classes/WSAPIJackpots.md +439 -0
  46. package/docs/api/classes/WSAPILeaderBoard.md +157 -0
  47. package/docs/api/classes/WSAPIMiniGames.md +612 -0
  48. package/docs/api/classes/WSAPIMissions.md +590 -0
  49. package/docs/api/classes/WSAPIRaffles.md +773 -0
  50. package/docs/api/classes/WSAPIStore.md +719 -0
  51. package/docs/api/classes/WSAPITournaments.md +571 -0
  52. package/docs/api/classes/WSAPIUser.md +637 -0
  53. package/docs/api/documents/Callbacks.md +162 -0
  54. package/docs/api/documents/SmarticoObject.md +445 -0
  55. package/docs/api/documents/Transforms.md +99 -0
  56. package/docs/api/enumerations/AchievementClaimPeriodTypeId.md +13 -0
  57. package/docs/api/enumerations/BonusStatus-1.md +11 -6
  58. package/docs/api/enumerations/BuyStoreItemErrorCode.md +32 -0
  59. package/docs/api/enumerations/JoinClanErrorCode.md +81 -0
  60. package/docs/api/enumerations/TournamentRegistrationError.md +12 -0
  61. package/docs/api/functions/clanTournamentPlayersTransform.md +13 -0
  62. package/docs/api/interfaces/AvatarCustomized.md +25 -0
  63. package/docs/api/interfaces/AvatarDefinition.md +94 -0
  64. package/docs/api/interfaces/AvatarPrompt.md +45 -0
  65. package/docs/api/interfaces/BonusMetaMap-1.md +4 -1
  66. package/docs/api/interfaces/BonusTemplateMetaMap-1.md +9 -4
  67. package/docs/api/interfaces/Clan.md +4 -0
  68. package/docs/api/interfaces/ClanInfo.md +109 -0
  69. package/docs/api/interfaces/ClanMember.md +53 -0
  70. package/docs/api/interfaces/ClanPrizePublicMeta.md +41 -0
  71. package/docs/api/interfaces/ClanPrizeStructureEntry.md +7 -1
  72. package/docs/api/interfaces/ClanPublicMeta.md +4 -0
  73. package/docs/api/interfaces/ClanTournamentPlayerRaw.md +67 -0
  74. package/docs/api/interfaces/GemsDiamondsLog.md +43 -0
  75. package/docs/api/interfaces/GetClanInfoResponse.md +61 -0
  76. package/docs/api/interfaces/GetClanTournamentPlayersResponse.md +81 -0
  77. package/docs/api/interfaces/GetRaffleWonPrizesRequest.md +59 -0
  78. package/docs/api/interfaces/GetRaffleWonPrizesResponse.md +95 -0
  79. package/docs/api/interfaces/InboxMarkMessageAction.md +10 -2
  80. package/docs/api/interfaces/JackpotDetails.md +14 -15
  81. package/docs/api/interfaces/JackpotPot.md +9 -6
  82. package/docs/api/interfaces/LeaderBoardDetailsT.md +14 -8
  83. package/docs/api/interfaces/LeaderBoardUserT.md +8 -5
  84. package/docs/api/interfaces/LeaderBoardsRewardsT.md +7 -0
  85. package/docs/api/interfaces/PointsLog.md +43 -0
  86. package/docs/api/interfaces/RaffleWonPrize.md +59 -0
  87. package/docs/api/interfaces/RaffleWonPrizePublicMeta.md +27 -0
  88. package/docs/api/interfaces/RaffleWonPrizeUser.md +43 -0
  89. package/docs/api/interfaces/SAWTemplateUI.md +10 -0
  90. package/docs/api/interfaces/TAchCategory.md +14 -4
  91. package/docs/api/interfaces/TAvatarCustomized.md +4 -4
  92. package/docs/api/interfaces/TAvatarDefinition.md +13 -14
  93. package/docs/api/interfaces/TAvatarPrompt.md +6 -6
  94. package/docs/api/interfaces/TBonus.md +16 -8
  95. package/docs/api/interfaces/TBuyStoreItemResult.md +4 -11
  96. package/docs/api/interfaces/TClaimBonusResult.md +5 -5
  97. package/docs/api/interfaces/TClan.md +8 -4
  98. package/docs/api/interfaces/TClanInfo.md +47 -0
  99. package/docs/api/interfaces/TClanJoinResult.md +11 -1
  100. package/docs/api/interfaces/TClanTournamentPlayer.md +55 -0
  101. package/docs/api/interfaces/TClanTournamentPlayersResult.md +27 -0
  102. package/docs/api/interfaces/TClans.md +3 -1
  103. package/docs/api/interfaces/TGetTranslations.md +4 -0
  104. package/docs/api/interfaces/TInboxMessage.md +14 -6
  105. package/docs/api/interfaces/TInboxMessageBody.md +18 -12
  106. package/docs/api/interfaces/TLevel.md +17 -20
  107. package/docs/api/interfaces/TLevelCurrent.md +19 -18
  108. package/docs/api/interfaces/TMiniGamePlayBatchResult.md +14 -6
  109. package/docs/api/interfaces/TMiniGamePlayResult.md +9 -4
  110. package/docs/api/interfaces/TMissionClaimRewardResult.md +6 -3
  111. package/docs/api/interfaces/TMissionOptInResult.md +6 -3
  112. package/docs/api/interfaces/TMissionOrBadge.md +5 -4
  113. package/docs/api/interfaces/TRaffleOptinResponse.md +4 -2
  114. package/docs/api/interfaces/TSegmentCheckResult.md +10 -0
  115. package/docs/api/interfaces/TSetAvatarResult.md +3 -3
  116. package/docs/api/interfaces/TStoreItem.md +11 -1
  117. package/docs/api/interfaces/TTournament.md +1 -1
  118. package/docs/api/interfaces/TTournamentDetailed.md +9 -1
  119. package/docs/api/interfaces/TTournamentRegistrationResult.md +4 -2
  120. package/docs/api/interfaces/TUICustomSection.md +23 -19
  121. package/docs/api/interfaces/TUserProfile.md +18 -16
  122. package/docs/api/interfaces/TransformedRaffleClaimPrizeResponse.md +6 -2
  123. package/docs/api/interfaces/UserAchievement.md +1 -1
  124. package/docs/api/interfaces/UserLevelExtraCountersT.md +9 -2
  125. package/docs/api/type-aliases/ActivityLogEntry.md +3 -0
  126. package/docs/native/ADDING_METHODS.md +7 -2
  127. package/docs/native/PROTOCOL.md +337 -0
  128. package/docs/ui/avatars/UIGuide_getAvatarPrompts.md +100 -0
  129. package/docs/ui/avatars/UIGuide_getAvatarsCustomized.md +90 -0
  130. package/docs/ui/avatars/UIGuide_getAvatarsList.md +105 -0
  131. package/docs/ui/avatars/UIGuide_setAvatar.md +118 -0
  132. package/docs/ui/bonuses/UIGuide_claimBonus.md +144 -0
  133. package/docs/ui/bonuses/UIGuide_getBonuses.md +176 -0
  134. package/docs/ui/clans/UIGuide_getClanInfo.md +153 -0
  135. package/docs/ui/clans/UIGuide_getClans.md +172 -0
  136. package/docs/ui/clans/UIGuide_joinClan.md +196 -0
  137. package/docs/ui/gamepick/UIGuide_gamePickGetActiveRound.md +99 -0
  138. package/docs/ui/gamepick/UIGuide_gamePickGetActiveRounds.md +76 -0
  139. package/docs/ui/gamepick/UIGuide_gamePickGetBoard.md +81 -0
  140. package/docs/ui/gamepick/UIGuide_gamePickGetGameInfo.md +78 -0
  141. package/docs/ui/gamepick/UIGuide_gamePickGetHistory.md +55 -0
  142. package/docs/ui/gamepick/UIGuide_gamePickGetRoundInfoForUser.md +71 -0
  143. package/docs/ui/gamepick/UIGuide_gamePickGetUserInfo.md +64 -0
  144. package/docs/ui/gamepick/UIGuide_gamePickSubmitSelection.md +103 -0
  145. package/docs/ui/gamepick/UIGuide_gamePickSubmitSelectionQuiz.md +67 -0
  146. package/docs/ui/general/UIGuide_getCustomSections.md +137 -0
  147. package/docs/ui/general/UIGuide_getRelatedItemsForGame.md +140 -0
  148. package/docs/ui/inbox/UIGuide_deleteInboxMessage.md +135 -0
  149. package/docs/ui/inbox/UIGuide_getInboxMessageBody.md +133 -0
  150. package/docs/ui/inbox/UIGuide_getInboxMessages.md +186 -0
  151. package/docs/ui/inbox/UIGuide_markInboxMessageAsRead.md +122 -0
  152. package/docs/ui/inbox/UIGuide_markUnmarkInboxMessageAsFavorite.md +84 -0
  153. package/docs/ui/jackpots/UIGuide_getJackpotEligibleGames.md +73 -0
  154. package/docs/ui/jackpots/UIGuide_getJackpotWinners.md +83 -0
  155. package/docs/ui/jackpots/UIGuide_jackpotGet.md +138 -0
  156. package/docs/ui/jackpots/UIGuide_jackpotOptIn.md +85 -0
  157. package/docs/ui/jackpots/UIGuide_jackpotOptOut.md +65 -0
  158. package/docs/ui/leaderboard/UIGuide_getLeaderBoard.md +166 -0
  159. package/docs/ui/minigames/UIGuide_getMiniGames.md +153 -0
  160. package/docs/ui/minigames/UIGuide_getMiniGamesHistory.md +90 -0
  161. package/docs/ui/minigames/UIGuide_playMiniGame.md +150 -0
  162. package/docs/ui/minigames/UIGuide_playMiniGameBatch.md +128 -0
  163. package/docs/ui/missions/UIGuide_getAchCategories.md +17 -0
  164. package/docs/ui/missions/UIGuide_getBadges.md +237 -0
  165. package/docs/ui/missions/UIGuide_getMissions.md +268 -0
  166. package/docs/ui/missions/UIGuide_requestMissionClaimReward.md +21 -0
  167. package/docs/ui/missions/UIGuide_requestMissionOptIn.md +16 -0
  168. package/docs/ui/raffles/UIGuide_claimRafflePrize.md +99 -0
  169. package/docs/ui/raffles/UIGuide_getRaffleDrawRun.md +117 -0
  170. package/docs/ui/raffles/UIGuide_getRaffleDrawRunsHistory.md +92 -0
  171. package/docs/ui/raffles/UIGuide_getRaffleWonPrizes.md +120 -0
  172. package/docs/ui/raffles/UIGuide_getRaffles.md +175 -0
  173. package/docs/ui/raffles/UIGuide_requestRaffleOptin.md +114 -0
  174. package/docs/ui/store/UIGuide_buyStoreItem.md +133 -0
  175. package/docs/ui/store/UIGuide_getStoreCategories.md +19 -0
  176. package/docs/ui/store/UIGuide_getStoreItems.md +264 -0
  177. package/docs/ui/store/UIGuide_getStorePurchasedItems.md +122 -0
  178. package/docs/ui/tournaments/UIGuide_getClanTournamentPlayers.md +140 -0
  179. package/docs/ui/tournaments/UIGuide_getTournamentInstanceInfo.md +265 -0
  180. package/docs/ui/tournaments/UIGuide_getTournamentsList.md +236 -0
  181. package/docs/ui/tournaments/UIGuide_registerInTournament.md +156 -0
  182. package/docs/ui/user/UIGuide_getActivityLog.md +130 -0
  183. package/docs/ui/user/UIGuide_getLevels.md +167 -0
  184. package/docs/ui/user/UIGuide_getUserProfile.md +213 -0
  185. package/package.json +3 -3
  186. package/scripts/postdoc-cleanup.js +195 -0
  187. package/src/Base/ClassId.ts +2 -0
  188. package/src/Jackpots/JackpotDetails.ts +14 -15
  189. package/src/Jackpots/JackpotPot.ts +10 -6
  190. package/src/Missions/MissionsUtils.ts +5 -2
  191. package/src/Raffle/GetRaffleWonPrizesRequest.ts +12 -0
  192. package/src/Raffle/GetRaffleWonPrizesResponse.ts +56 -0
  193. package/src/Raffle/index.ts +3 -1
  194. package/src/SmarticoAPI.ts +14 -1
  195. package/src/SmarticoLib/index.ts +4 -0
  196. package/src/Store/BuyStoreItemErrorCode.ts +8 -0
  197. package/src/Store/StoreItem.ts +1 -0
  198. package/src/Store/StoreItemPurchased.ts +1 -0
  199. package/src/Store/StoreItemType.ts +1 -1
  200. package/src/Tournaments/TournamentRegistrationError.ts +4 -0
  201. package/src/WSAPI/WSAPI.ts +27 -2007
  202. package/src/WSAPI/WSAPIAvatars.ts +345 -0
  203. package/src/WSAPI/WSAPIBase.ts +93 -0
  204. package/src/WSAPI/WSAPIBonuses.ts +268 -0
  205. package/src/WSAPI/WSAPIClans.ts +417 -0
  206. package/src/WSAPI/WSAPIGamePick.ts +667 -0
  207. package/src/WSAPI/WSAPIGeneral.ts +199 -0
  208. package/src/WSAPI/WSAPIInbox.ts +711 -0
  209. package/src/WSAPI/WSAPIJackpots.ts +448 -0
  210. package/src/WSAPI/WSAPILeaderBoard.ts +154 -0
  211. package/src/WSAPI/WSAPIMiniGames.ts +597 -0
  212. package/src/WSAPI/WSAPIMissions.ts +572 -0
  213. package/src/WSAPI/WSAPIRaffles.ts +703 -0
  214. package/src/WSAPI/WSAPIStore.ts +732 -0
  215. package/src/WSAPI/WSAPITournaments.ts +541 -0
  216. package/src/WSAPI/WSAPITypes.ts +376 -207
  217. package/src/WSAPI/WSAPIUser.ts +606 -0
  218. package/tsconfig.json +26 -2
  219. package/docs/api/classes/WSAPI.md +0 -1927
@@ -0,0 +1,268 @@
1
+ import { ECacheContext, OCache } from '../OCache';
2
+ import {
3
+ TBonus,
4
+ TClaimBonusResult,
5
+ } from './WSAPITypes';
6
+ import {
7
+ CACHE_DATA_SEC,
8
+ onUpdateContextKey,
9
+ } from './WSAPIBase';
10
+ import { WSAPIMissions } from './WSAPIMissions';
11
+
12
+ /** @group Bonuses */
13
+ export class WSAPIBonuses extends WSAPIMissions {
14
+ /**
15
+ * Returns the bonuses awarded to the current user — both pending
16
+ * (player-claim required) and already-redeemed. Each bonus carries
17
+ * a `bonus_status_id` ({@link BonusStatus}) describing its lifecycle
18
+ * stage and an `is_redeemable` flag that signals whether the
19
+ * consumer must trigger a manual {@link claimBonus} call.
20
+ *
21
+ * Bonuses originate from many sources (CRM rules, mission/tournament
22
+ * rewards, store redemptions, mini-game prizes, manual operator
23
+ * awards) and all surface through this single list. The consumer
24
+ * cannot distinguish the source server-side without operator
25
+ * coordination.
26
+ *
27
+ * @remarks
28
+ * **Subscription model (`onUpdate`)**
29
+ * The callback receives the FULL refreshed bonuses list (never a
30
+ * diff/patch). Each subsequent call to `getBonuses({ onUpdate })`
31
+ * REPLACES the prior callback. Pass `onUpdate: undefined` (or omit
32
+ * it) to keep the prior callback in place; the callback is never
33
+ * auto-cleared.
34
+ *
35
+ * **Update triggers** — the callback fires after every
36
+ * {@link claimBonus} call resolves on this connection (success OR
37
+ * failure). This is the ONLY trigger.
38
+ *
39
+ * Does NOT fire when: a new bonus is awarded server-side
40
+ * (CRM rule, mission completion, store purchase, etc.), a bonus
41
+ * expires, or an operator manually issues / cancels a bonus. Those
42
+ * changes surface only on the next cache miss (after the 30 s TTL).
43
+ * Re-call `getBonuses()` manually if your UI needs near-live state
44
+ * for server-driven awards.
45
+ *
46
+ * **Reading state from the returned bonus**
47
+ * Drive UI bucketing from `bonus_status_id` (enum {@link BonusStatus}):
48
+ *
49
+ * - `COUPON_ISSUED` (2) — actionable; player must claim. Pair with
50
+ * `is_redeemable === true` to gate the Claim button.
51
+ * - `REDEEM_FAILED` (4) — also actionable; the previous claim
52
+ * attempt failed but the bonus is still valid. Surface a retry CTA
53
+ * — the default Smartico UI treats this identically to
54
+ * `COUPON_ISSUED` from the player's perspective.
55
+ * - `REDEEMED` (3) — historical; show in a "claimed" tab.
56
+ * - `New` (1), `COUPON_ISSUE_FAILED` (5), `EXPIRED` (6) — these
57
+ * should not normally reach the client and are typically filtered
58
+ * out by operator-side widget configuration; if they do appear,
59
+ * they have no player-actionable state.
60
+ *
61
+ * `is_redeemable` is the authoritative gate for showing the Claim
62
+ * button — it's `true` only when the bonus is in a claimable status
63
+ * AND the operator's integration uses a coupon model (where the
64
+ * player must explicitly trigger delivery). Auto-redeemed bonuses
65
+ * arrive already in `REDEEMED` state with `is_redeemable: false`.
66
+ *
67
+ * **Field hierarchy — display amount**
68
+ * Bonuses carry both template-level (`label_bonus_template_meta_map.description`)
69
+ * and instance-level (`bonus_meta_map.uiAmount`) display strings.
70
+ * Prefer `bonus_meta_map.uiAmount` when present — it's the dynamic
71
+ * amount computed at issuance time (e.g. from a dynamic-bonus
72
+ * formula). Fall back to `label_bonus_template_meta_map.description`
73
+ * (which can include HTML; sanitize before injecting).
74
+ *
75
+ * **Date format**
76
+ * `create_date` and `redeem_date` are ISO 8601 UTC strings WITHOUT
77
+ * a timezone suffix (`"YYYY-MM-DDTHH:MM:SS"`). Parse as UTC and
78
+ * display in the user's local timezone. `redeem_date` is absent
79
+ * until the bonus reaches `REDEEMED` status.
80
+ *
81
+ * **Cache TTL**: the SDK caches the response for 30 seconds. Cache
82
+ * is fully cleared on login / logout.
83
+ *
84
+ * **Idempotency / Side effects**: safe. Read-only.
85
+ *
86
+ * **UI guidance**: see [UI Guide — `getBonuses`](../../docs/ui/bonuses/UIGuide_getBonuses.md).
87
+ *
88
+ * **Visitor mode**: not supported.
89
+ *
90
+ * @param params Optional. Omit to fetch without subscribing.
91
+ * @param params.onUpdate Callback invoked with the full refreshed
92
+ * bonuses list after every
93
+ * {@link claimBonus} resolves. Each call
94
+ * to `getBonuses` overwrites the prior
95
+ * callback.
96
+ * @returns Promise resolving to the bonuses list.
97
+ * Empty array if no bonuses are visible.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const bonuses = await window._smartico.api.getBonuses({
102
+ * onUpdate: (refreshed) => {
103
+ * console.log('[smartico] bonuses refreshed (after claimBonus) — re-render the bonuses UI from this array:', refreshed);
104
+ * },
105
+ * });
106
+ *
107
+ * // Bucket into pending vs redeemed tabs. The default Smartico UI groups
108
+ * // COUPON_ISSUED + REDEEM_FAILED into the same "Pending" tab.
109
+ * const pending = bonuses.filter(b =>
110
+ * b.bonus_status_id === 2 || b.bonus_status_id === 4 // COUPON_ISSUED or REDEEM_FAILED
111
+ * );
112
+ * const redeemed = bonuses.filter(b => b.bonus_status_id === 3); // REDEEMED
113
+ *
114
+ * console.log('[smartico] render bonus tabs: pending=', pending.length, 'redeemed=', redeemed.length);
115
+ *
116
+ * // Render each pending bonus card; show Claim button only when redeemable.
117
+ * for (const b of pending) {
118
+ * const amount = b.bonus_meta_map?.uiAmount || b.label_bonus_template_meta_map?.description;
119
+ * if (b.is_redeemable) {
120
+ * console.log('[smartico] render Claim button for bonus', b.bonus_id, '— amount:', amount);
121
+ * } else {
122
+ * console.log('[smartico] render bonus as informational (no Claim button) — amount:', amount);
123
+ * }
124
+ * }
125
+ *
126
+ * // Server-awarded bonuses (e.g. from a CRM campaign) do NOT push;
127
+ * // poll if you need near-live awareness.
128
+ * setInterval(async () => {
129
+ * const fresh = await window._smartico.api.getBonuses();
130
+ * const newCount = fresh.filter(b => b.bonus_status_id === 2 || b.bonus_status_id === 4).length;
131
+ * console.log('[smartico] poll tick — pending bonuses now:', newCount);
132
+ * }, 30_000);
133
+ * ```
134
+ */
135
+ public async getBonuses({ onUpdate }: { onUpdate?: (data: TBonus[]) => void } = {}): Promise<TBonus[]> {
136
+ if (onUpdate) {
137
+ this.onUpdateCallback.set(onUpdateContextKey.Bonuses, onUpdate);
138
+ }
139
+
140
+ return OCache.use(onUpdateContextKey.Bonuses, ECacheContext.WSAPI, () => this.api.bonusesGetItemsT(this.userExtId), CACHE_DATA_SEC);
141
+ }
142
+
143
+ /**
144
+ * Triggers manual redemption of a coupon-model bonus the user has
145
+ * been awarded. For bonuses with `is_redeemable === true` returned
146
+ * by {@link getBonuses}, this is the call that asks the operator's
147
+ * integration to actually credit the bonus to the player's account.
148
+ * Bonuses on integrations that auto-redeem arrive already in
149
+ * `REDEEMED` state and do not require this call.
150
+ *
151
+ * Use this for `bonus_status_id` of `COUPON_ISSUED` (2) as the
152
+ * first-time claim, and for `REDEEM_FAILED` (4) as a retry after a
153
+ * prior unsuccessful attempt.
154
+ *
155
+ * @remarks
156
+ * **Preconditions**
157
+ * Read the candidate bonus from {@link getBonuses} and gate the
158
+ * call on `is_redeemable === true`. The SDK forwards the request
159
+ * unconditionally — calling on a non-claimable bonus returns
160
+ * `err_code = -1`.
161
+ *
162
+ * **Error codes** (in `err_code`)
163
+ *
164
+ * The SDK currently types `err_code` as {@link SAWSpinErrorCode} for
165
+ * historical reasons, but the values returned by this method come
166
+ * from the server's general error space and are NOT mini-game codes.
167
+ * Branch on the numeric values:
168
+ *
169
+ * - `0` — success; the bonus has been redeemed and the operator's
170
+ * integration has credited the player's account.
171
+ * - `-1` — bonus not claimable: not found, already claimed by
172
+ * another session, or its status is no longer `COUPON_ISSUED` /
173
+ * `REDEEM_FAILED`. Treat as idempotent — refresh
174
+ * {@link getBonuses} and reconcile.
175
+ * - `1` — generic server error. Transient; allow retry after a brief
176
+ * delay.
177
+ * - `9999` — uncaught server exception. Treat as a transient failure;
178
+ * surface a generic error and allow retry.
179
+ * - other non-zero — generic server error. Surface `err_message` if
180
+ * any.
181
+ *
182
+ * **`success` field is unreliable**: the wire response does not
183
+ * carry a `success` boolean — the field on `TClaimBonusResult` will
184
+ * be `undefined` in practice. Always branch on `err_code === 0`
185
+ * for the success check.
186
+ *
187
+ * **Refresh after success (and after failure)**
188
+ * The SDK automatically refreshes the bonuses cache on every
189
+ * response (success OR failure) and fires any `onUpdate` callback
190
+ * registered via {@link getBonuses}'s `onUpdate`. After
191
+ * `err_code === 0`, the redeemed bonus shifts from
192
+ * `COUPON_ISSUED` / `REDEEM_FAILED` to `REDEEMED` on the refreshed
193
+ * array, with `redeem_date` populated. After a failure, the bonus
194
+ * typically lands in `REDEEM_FAILED` and remains claimable for a
195
+ * retry.
196
+ *
197
+ * **Idempotency**: NOT idempotent at the SDK level. A second call
198
+ * returns `err_code = -1` once the first has succeeded — treat that
199
+ * as success in the UI rather than as a real error. Guard the call
200
+ * site against double-clicks (set a local "claiming" flag on click,
201
+ * clear it on response).
202
+ *
203
+ * **Side effects** (on `err_code === 0`)
204
+ * - Bonus status transitions to `REDEEMED`; `redeem_date` is set.
205
+ * - The operator's integration credits the player's account
206
+ * externally. The SDK does not observe that credit directly —
207
+ * balance updates flow over the user-properties channel from the
208
+ * operator's side, not from this response.
209
+ * - Server-side analytics events fire downstream (not directly
210
+ * observable from the SDK).
211
+ *
212
+ * **UI guidance**: see [UI Guide — `claimBonus`](../../docs/ui/bonuses/UIGuide_claimBonus.md).
213
+ *
214
+ * **Visitor mode**: not supported.
215
+ *
216
+ * @param bonus_id The `bonus_id` from a `TBonus` returned by
217
+ * {@link getBonuses}.
218
+ * @returns `{ err_code, err_message, success? }`; success when
219
+ * `err_code === 0` (or `err_code === -1` when treated as
220
+ * idempotent no-op). The `success` field is unreliable —
221
+ * ignore it.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * const bonuses = await window._smartico.api.getBonuses({
226
+ * onUpdate: (refreshed) => console.log('[smartico] bonuses refreshed — re-render from this array', refreshed),
227
+ * });
228
+ * const bonus = bonuses.find(b => b.bonus_id === bonusId);
229
+ *
230
+ * if (!bonus) {
231
+ * console.log('[smartico] bonus no longer visible — refresh list and hide CTA');
232
+ * return;
233
+ * }
234
+ * if (!bonus.is_redeemable) {
235
+ * console.log('[smartico] bonus is not claimable (auto-redeemed or already finalised) — keep CTA hidden');
236
+ * return;
237
+ * }
238
+ *
239
+ * console.log('[smartico] claim starting — set in-flight flag, show loading dots on the Claim button, keep the modal open');
240
+ * const r = await window._smartico.api.claimBonus(bonus.bonus_id);
241
+ * console.log('[smartico] claim response received — clear in-flight flag');
242
+ *
243
+ * if (r.err_code === 0 || r.err_code === -1) {
244
+ * console.log('[smartico] claim succeeded (or was already done) — show a success toast; the getBonuses onUpdate above will fire with the refreshed list');
245
+ * } else if (r.err_code === 9999) {
246
+ * console.error('[smartico] uncaught server exception — show a generic error toast and allow retry');
247
+ * } else {
248
+ * console.error('[smartico] claim failed — surface this error message to the user; the bonus will move to REDEEM_FAILED state and remain claimable for a retry:', r.err_message);
249
+ * }
250
+ * ```
251
+ */
252
+ public async claimBonus(bonus_id: number): Promise<TClaimBonusResult> {
253
+ const r = await this.api.bonusClaimItem(this.userExtId, bonus_id);
254
+
255
+ const o: TClaimBonusResult = {
256
+ err_code: r.errCode,
257
+ err_message: r.errMsg,
258
+ success: r.success,
259
+ };
260
+
261
+ return o;
262
+ }
263
+
264
+ protected async updateBonuses() {
265
+ const payload = await this.api.bonusesGetItemsT(this.userExtId);
266
+ this.updateEntity(onUpdateContextKey.Bonuses, payload);
267
+ }
268
+ }
@@ -0,0 +1,417 @@
1
+ import { ECacheContext, OCache } from '../OCache';
2
+ import {
3
+ TClans,
4
+ TClanInfo,
5
+ TClanJoinResult,
6
+ } from './WSAPITypes';
7
+ import {
8
+ CACHE_DATA_SEC,
9
+ onUpdateContextKey,
10
+ } from './WSAPIBase';
11
+ import { WSAPITournaments } from './WSAPITournaments';
12
+
13
+ /** @group Clans */
14
+ export class WSAPIClans extends WSAPITournaments {
15
+ /**
16
+ * Returns the active clans visible to the current user along with the
17
+ * user's own membership state (`user_clan_id`, `cooldown_until`,
18
+ * `join_date`). Use this to power a clan-picker / clan-browser screen.
19
+ *
20
+ * The returned `clans[]` is server-sorted by `rating_position` ASC
21
+ * (1 = highest-ranked). The server has already filtered out archived
22
+ * clans, clans the user fails segment conditions for, and (when any
23
+ * clan has a non-zero score) clans with a zero rating score.
24
+ *
25
+ * @remarks
26
+ * **Subscription model (`onUpdate`)**
27
+ * The callback receives the FULL refreshed `TClans` payload (never a
28
+ * diff/patch). Each subsequent call to `getClans({ onUpdate })`
29
+ * REPLACES the prior callback. Pass `onUpdate: undefined` (or omit
30
+ * it) to keep the prior callback in place; the callback is never
31
+ * auto-cleared.
32
+ *
33
+ * **Update triggers** — note that, unlike most other subscription
34
+ * methods in the SDK, this callback is **poll-driven only**. There
35
+ * is NO server push that refreshes the clans list. The callback
36
+ * fires when:
37
+ *
38
+ * 1. The 30-second cache TTL expires and a subsequent `getClans()`
39
+ * call (from the consumer) triggers a fresh fetch.
40
+ *
41
+ * Consequence: clan-membership changes (other users joining your
42
+ * clan, an operator-driven kick, a clan being archived) surface
43
+ * only on the next consumer-driven fetch. If your UI needs
44
+ * near-live state, poll `getClans()` on an interval; or re-call
45
+ * after a {@link joinClan} resolves to pick up the new
46
+ * `user_clan_id` immediately.
47
+ *
48
+ * **Reading state from the returned payload**
49
+ * Drive list rendering from `clans[]` (already sorted). Identify
50
+ * the user's own clan via `clan.clan_id === user_clan_id`
51
+ * (`null` for clanless users). Detect an active switch-cooldown
52
+ * with `cooldown_until !== null` — when set, the user cannot join
53
+ * any clan until the cooldown expires, even clans they could
54
+ * normally join. The cooldown is **user-level**, not per-clan.
55
+ *
56
+ * `cooldown_until` is an ISO 8601 UTC datetime string with no
57
+ * timezone suffix (`"YYYY-MM-DDTHH:MM:SS"`). Parse as UTC:
58
+ * `new Date(cooldown_until + 'Z')` or `moment.utc(cooldown_until)`.
59
+ * Display in the user's local time. The default cooldown period
60
+ * is configured per label by the operator (typically 7 days).
61
+ *
62
+ * `rating_position` is a global rank computed server-side across
63
+ * all active clans in the label (1 = highest-rated). Because some
64
+ * clans may be hidden by per-user segment visibility, positions
65
+ * the user sees may skip (e.g. 1, 3, 7) — don't assume the array
66
+ * spans a contiguous range.
67
+ *
68
+ * The `entry_fee_currency_type_id` enum on each clan uses values
69
+ * `0` = points, `1` = gems, `2` = diamonds, `3` = free. Compare
70
+ * `entry_fee_amount` against the user's matching balance (from
71
+ * {@link getUserProfile}) to drive Join button affordability.
72
+ *
73
+ * **Cache TTL**: the SDK caches the response for 30 seconds. Cache
74
+ * is fully cleared on login / logout.
75
+ *
76
+ * **Idempotency**: safe. Read-only. Repeated calls within the
77
+ * cache window return a deep-cloned cached payload without a
78
+ * network round-trip.
79
+ *
80
+ * **Side effects**: none — pure metadata read.
81
+ *
82
+ * **UI guidance**: see [UI Guide — `getClans`](../../docs/ui/clans/UIGuide_getClans.md).
83
+ *
84
+ * **Visitor mode**: not supported.
85
+ *
86
+ * @param params Optional. Omit to fetch without subscribing.
87
+ * @param params.onUpdate Callback invoked with the full refreshed
88
+ * `TClans` payload whenever the 30 s cache
89
+ * expires and a subsequent fetch lands.
90
+ * Each call to `getClans` overwrites the
91
+ * prior callback.
92
+ * @returns Promise resolving to `TClans`. Empty
93
+ * `clans` array if no clans are visible.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const result = await window._smartico.api.getClans({
98
+ * onUpdate: (refreshed) => {
99
+ * console.log('[smartico] clans payload refreshed (poll-driven; ~30 s cadence) — re-render the clan list from this payload:', refreshed);
100
+ * },
101
+ * });
102
+ *
103
+ * console.log('[smartico] user is', result.user_clan_id == null ? 'clanless' : 'in clan ' + result.user_clan_id);
104
+ *
105
+ * // Cooldown handling — affects ALL clans, not a specific one.
106
+ * if (result.cooldown_until) {
107
+ * const cooldownEndsUtc = new Date(result.cooldown_until + 'Z');
108
+ * console.log('[smartico] switch-cooldown active — disable all Join buttons until', cooldownEndsUtc.toLocaleString(),
109
+ * '(local). Render the cooldown end on the disabled button label.');
110
+ * }
111
+ *
112
+ * // Render clans — already sorted by rating_position ASC.
113
+ * for (const clan of result.clans) {
114
+ * const isMine = clan.clan_id === result.user_clan_id;
115
+ * const isFull = clan.member_count >= clan.capacity_limit;
116
+ *
117
+ * if (isMine) {
118
+ * console.log('[smartico] highlight clan', clan.clan_id, 'as "Your clan"; CTA label "Your clan", no-op');
119
+ * } else if (result.cooldown_until) {
120
+ * console.log('[smartico] clan', clan.clan_id, '— Join blocked by user cooldown; show disabled button with cooldown end date');
121
+ * } else if (isFull) {
122
+ * console.log('[smartico] clan', clan.clan_id, '— Clan full; show disabled "Clan is full" button');
123
+ * } else {
124
+ * const label = clan.entry_fee_amount === 0 || clan.entry_fee_currency_type_id === 3
125
+ * ? 'Join free'
126
+ * : `Join (${clan.entry_fee_amount} ${['Points','Gems','Diamonds'][clan.entry_fee_currency_type_id]})`;
127
+ * console.log('[smartico] clan', clan.clan_id, '— render enabled Join button labelled', label);
128
+ * }
129
+ * }
130
+ *
131
+ * // After a successful joinClan(), re-call getClans manually — no push refresh.
132
+ * // const r = await window._smartico.api.joinClan(targetClanId);
133
+ * // if (r.errCode === 0) await window._smartico.api.getClans();
134
+ * ```
135
+ */
136
+ public async getClans({ onUpdate }: { onUpdate?: (data: TClans) => void } = {}): Promise<TClans> {
137
+ if (onUpdate) {
138
+ this.onUpdateCallback.set(onUpdateContextKey.Clans, onUpdate);
139
+ }
140
+
141
+ return OCache.use(
142
+ onUpdateContextKey.Clans,
143
+ ECacheContext.WSAPI,
144
+ () => this.api.clansGetListT(this.userExtId),
145
+ CACHE_DATA_SEC,
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Returns the full detail of a single clan — adds the ranked
151
+ * `members[]` roster and a fresh `cooldown_until` on top of the same
152
+ * fields exposed on a `TClan` list entry. Use this to power a clan
153
+ * detail screen / popup after the user picks an item from
154
+ * {@link getClans}.
155
+ *
156
+ * Each call is a fresh server round-trip — there is no client cache
157
+ * and no push event refreshes detail state. Re-call to refresh.
158
+ *
159
+ * @remarks
160
+ * **Preconditions**
161
+ * Pass a valid `clanId` (typically read from
162
+ * `TClans.clans[i].clan_id` returned by {@link getClans}). The
163
+ * method works standalone — `getClans()` is not required first —
164
+ * but is the only stable source of valid IDs.
165
+ *
166
+ * **Refresh model**
167
+ * - **No subscription.** One-shot promise.
168
+ * - **No client cache.** Every call sends a network request.
169
+ * - **No push event** refreshes the detail. Member-join, kick, or
170
+ * contribution-score changes require a fresh `getClanInfo` call.
171
+ *
172
+ * **Returned shape — beyond `TClan`**
173
+ * `TClanInfo` adds two things on top of the same identity / capacity
174
+ * / fee / rating fields you'd see on a `TClan` list entry:
175
+ * 1. A ranked `members[]` array — each entry carries `user_id`,
176
+ * `public_username`, `avatar_id` / `avatar_real_id`, the resolved
177
+ * `avatar_url`, the member's `position` (rank within this clan),
178
+ * `contribution_score`, the `is_me` flag identifying the current
179
+ * user's row, and the optional `clean_ext_user_id`. The server
180
+ * orders members by score DESC (i.e. `position` ASC); no
181
+ * client-side re-sort is required.
182
+ * 2. A fresh `cooldown_until` on the clan info itself. This is the
183
+ * SAME user-level cooldown as `TClans.cooldown_until` (the
184
+ * cooldown is global to the user, not per clan) — but `getClans`
185
+ * returns a value cached for up to 30 s while `getClanInfo`
186
+ * always returns the current value. If the list-cached cooldown
187
+ * has just expired, this detail call will reflect it first.
188
+ *
189
+ * **Username display**: in the default Smartico UI, member rows use
190
+ * `public_username`. Some surfaces (e.g. the tournament clan
191
+ * drill-down) prefer `clean_ext_user_id` as the primary display
192
+ * with `public_username` as fallback — pick the convention that
193
+ * matches your product's identity model.
194
+ *
195
+ * **Idempotency**: safe. Read-only.
196
+ *
197
+ * **Side effects**: none — pure metadata read.
198
+ *
199
+ * **UI guidance**: see [UI Guide — `getClanInfo`](../../docs/ui/clans/UIGuide_getClanInfo.md).
200
+ *
201
+ * **Visitor mode**: not supported.
202
+ *
203
+ * @param clanId The clan ID from `TClans.clans[i].clan_id`.
204
+ * @returns Promise resolving to `TClanInfo`. Rejects if the
205
+ * clan ID is invalid or the user lacks visibility
206
+ * for it.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * const result = await window._smartico.api.getClans();
211
+ * const clan = result.clans[0];
212
+ *
213
+ * console.log('[smartico] loading detail for clan', clan.clan_id);
214
+ * const detail = await window._smartico.api.getClanInfo(clan.clan_id);
215
+ *
216
+ * console.log('[smartico] render detail with', detail.members.length, 'members');
217
+ *
218
+ * // Find current user's row to power a sticky "me" footer.
219
+ * const meRow = detail.members.find(m => m.is_me);
220
+ * if (meRow) {
221
+ * console.log('[smartico] current user rank in this clan:', meRow.position,
222
+ * 'contribution:', meRow.contribution_score, '— render sticky my-member footer');
223
+ * }
224
+ *
225
+ * // Resolve Join CTA state using the detail's cooldown (always fresh).
226
+ * const isMyClan = detail.clan_id === result.user_clan_id;
227
+ * const inCooldown = detail.cooldown_until != null;
228
+ * const isFull = detail.member_count >= detail.capacity_limit;
229
+ * if (isMyClan) {
230
+ * console.log('[smartico] this is the user\'s clan — render disabled "Your clan" button');
231
+ * } else if (inCooldown) {
232
+ * const ends = new Date(detail.cooldown_until + 'Z');
233
+ * console.log('[smartico] user is in clan-switch cooldown until', ends.toLocaleString(),
234
+ * '— render disabled cooldown button');
235
+ * } else if (isFull) {
236
+ * console.log('[smartico] clan is full — render disabled "Clan is full" button');
237
+ * } else {
238
+ * console.log('[smartico] joinable — render enabled Join button');
239
+ * }
240
+ * ```
241
+ */
242
+ public async getClanInfo(clanId: number): Promise<TClanInfo> {
243
+ return this.api.clansGetInfoT(this.userExtId, clanId);
244
+ }
245
+
246
+ /**
247
+ * Joins (or switches into) a clan on behalf of the current user. For
248
+ * paid clans (`entry_fee_amount > 0`), the user's balance for the
249
+ * matching currency is debited synchronously before the response
250
+ * returns. If the user is already in a clan, this is treated as an
251
+ * atomic clan-switch (deactivate old membership + insert new + debit
252
+ * fee, all in one transaction — the user is never observably
253
+ * clanless).
254
+ *
255
+ * A successful response (`errCode === 0`) means: membership is
256
+ * persisted, the fee (if any) has been debited, the user's
257
+ * switch-cooldown has restarted, and a clan-joined CRM event has
258
+ * fired server-side.
259
+ *
260
+ * @remarks
261
+ * **Result shape note**: `TClanJoinResult` uses `errCode` / `errMsg`
262
+ * (camelCase) — different from most other SDK result types in this
263
+ * library which use `err_code` / `err_message` (snake_case). Branch
264
+ * on the camelCase keys when reading this method's result.
265
+ *
266
+ * **Preconditions**
267
+ * Read the candidate from {@link getClans} and gate the call on
268
+ * `clan.member_count < clan.capacity_limit`, `cooldown_until == null`
269
+ * (on the `TClans` payload — the cooldown is user-level, not
270
+ * per-clan), and the user's balance for `entry_fee_currency_type_id`
271
+ * being at least `entry_fee_amount` (free clans bypass this check).
272
+ * The SDK forwards the call unconditionally — calling without
273
+ * satisfying these will most likely return one of the error codes
274
+ * below.
275
+ *
276
+ * **Error codes** (in `errCode`, typed as {@link JoinClanErrorCode})
277
+ * - `0` (`JOIN_CLAN_OK`) — success; membership persisted, fee
278
+ * debited, cooldown restarted.
279
+ * - `1000` (`JOIN_CLAN_INVALID_PARAMETERS`) — request body missing
280
+ * `clan_id`. Should not occur via the SDK; treat as a programming
281
+ * error.
282
+ * - `1001` (`JOIN_CLAN_NOT_FOUND`) — clan doesn't exist for this
283
+ * label, OR the clan is archived / in draft state. The server
284
+ * merges "missing" and "archived" into this single code. Refresh
285
+ * `getClans()` and remove the entry from the UI.
286
+ * - `1002` (`JOIN_CLAN_FULL`) — capacity reached between the list
287
+ * fetch and the join click (`member_count >= capacity_limit` at
288
+ * the server's atomic check). Refresh the list — the button will
289
+ * become "Clan is full".
290
+ * - `1003` (`JOIN_CLAN_INSUFFICIENT_FUNDS`) — insufficient balance
291
+ * for the clan's entry fee. A single code covers all three
292
+ * currencies (points / gems / diamonds); read
293
+ * `clan.entry_fee_currency_type_id` to identify which currency
294
+ * the user is short on. Show an insufficient-balance UI naming
295
+ * the deficit.
296
+ * - `1004` (`JOIN_CLAN_SEGMENT_MISMATCH`) — user does not meet the
297
+ * clan's entry segment or numeric conditions. There is no
298
+ * client-side pre-guard for this — surface a gating message.
299
+ * - `1005` (`JOIN_CLAN_USER_IS_NOT_IN_CLAN`) — this method NEVER
300
+ * returns `1005`. The code is shared with the tournament
301
+ * registration error space and fires only on
302
+ * {@link registerInTournament} responses when a clan-based
303
+ * tournament requires the user to be in a clan first.
304
+ * - `1006` (`JOIN_CLAN_COOLDOWN_ACTIVE`) — user is inside the
305
+ * switch-cooldown window from a previous clan join (default
306
+ * 7 days, configurable per label). Surface the
307
+ * `TClans.cooldown_until` expiry in the disabled CTA.
308
+ * - `1007` (`JOIN_CLAN_ARCHIVED`) — defined on the public enum but
309
+ * the server does NOT emit this code in practice (archived clans
310
+ * are reported as `1001`). Handle it as a synonym for `1001`
311
+ * defensively, but do not rely on it.
312
+ * - `1011` (`JOIN_CLAN_JOINED_AFTER_TOURNAMENT_START`) — this
313
+ * method does NOT return `1011` directly. The code fires on
314
+ * {@link registerInTournament} responses when the user joined
315
+ * their current clan after the tournament started AND already
316
+ * has tournament scores.
317
+ * - other non-zero — generic server error. Surface `errMsg` if any.
318
+ *
319
+ * **Idempotency**: NOT safe — and notably dangerous. Calling
320
+ * `joinClan` with the user's CURRENT `clanId` does NOT short-circuit
321
+ * server-side; the call is treated as a clan-switch to the same
322
+ * clan. If the label's cooldown is non-zero (the default), the
323
+ * second call returns `1006`. If the cooldown is configured to
324
+ * zero, the second call will deduct the entry fee AGAIN, deactivate
325
+ * the membership, and re-insert it. The SDK does NOT enforce an
326
+ * in-flight lock. Consumers MUST guard the call site against
327
+ * double-clicks (set a local "joining" flag on click, clear it on
328
+ * response) AND short-circuit when the user is already in the
329
+ * target clan.
330
+ *
331
+ * **Refresh after success (and after failure)**
332
+ * The SDK does NOT automatically refresh the {@link getClans} cache
333
+ * after a join. Manually re-call `getClans()` on success to pick up
334
+ * the new `user_clan_id`, refreshed `member_count`, and the new
335
+ * `cooldown_until` value. Until you do, the cached `TClans` payload
336
+ * is stale.
337
+ *
338
+ * **Side effects** (consumer-observable on success)
339
+ * - Entry-fee currency balance decreases by `entry_fee_amount`
340
+ * (visible on the next {@link getUserProfile} refresh / via the
341
+ * user-properties update channel).
342
+ * - The user's clan membership row is created (or replaced, on a
343
+ * switch). `member_count` on the new clan increments by 1; on a
344
+ * switch, the old clan's `member_count` decrements by 1 in the
345
+ * same transaction.
346
+ * - The user's switch-cooldown restarts at the join time (the
347
+ * `TClans.cooldown_until` field on the next list fetch reflects
348
+ * the new expiry).
349
+ * - Server-side CRM analytics fire (clan-joined event, currency-
350
+ * deducted event for paid clans). These surface only as
351
+ * server-side traces; the SDK consumer observes them indirectly
352
+ * via balance / membership refresh.
353
+ *
354
+ * **UI guidance**: see [UI Guide — `joinClan`](../../docs/ui/clans/UIGuide_joinClan.md).
355
+ *
356
+ * **Visitor mode**: not supported. The server rejects the request
357
+ * for unauthenticated sessions.
358
+ *
359
+ * @param clanId The clan ID from `TClans.clans[i].clan_id` (or
360
+ * `TClanInfo.clan_id`).
361
+ * @returns `{ errCode, errMsg }`; success when `errCode === 0`.
362
+ * Note camelCase keys (see "Result shape note" above).
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * const result = await window._smartico.api.getClans();
367
+ * const target = result.clans.find(c => c.clan_id === clanId);
368
+ *
369
+ * if (!target) {
370
+ * console.log('[smartico] clan not in current list — refresh getClans and retry');
371
+ * return;
372
+ * }
373
+ * if (target.clan_id === result.user_clan_id) {
374
+ * console.log('[smartico] user is already in this clan — short-circuit; do NOT call joinClan again, the server will treat it as a switch and may re-deduct the fee');
375
+ * return;
376
+ * }
377
+ * if (result.cooldown_until) {
378
+ * console.log('[smartico] user is in cooldown — disable Join until', new Date(result.cooldown_until + 'Z').toLocaleString());
379
+ * return;
380
+ * }
381
+ * if (target.member_count >= target.capacity_limit) {
382
+ * console.log('[smartico] clan is full — disable Join');
383
+ * return;
384
+ * }
385
+ *
386
+ * console.log('[smartico] join starting — set in-flight flag, show loading dots on Join button, keep modal open');
387
+ * const r = await window._smartico.api.joinClan(target.clan_id);
388
+ * console.log('[smartico] join response received — clear in-flight flag');
389
+ *
390
+ * if (r.errCode === 0) {
391
+ * console.log('[smartico] joined successfully — refresh getClans to pick up the new user_clan_id, member_count, and cooldown_until');
392
+ * await window._smartico.api.getClans();
393
+ * } else if (r.errCode === 1002) {
394
+ * console.error('[smartico] clan is full — refresh list, hide the Join CTA on the now-full clan');
395
+ * } else if (r.errCode === 1003) {
396
+ * const currencyLabel = ['points', 'gems', 'diamonds'][target.entry_fee_currency_type_id] ?? 'currency';
397
+ * console.error('[smartico] insufficient', currencyLabel, '— need', target.entry_fee_amount);
398
+ * } else if (r.errCode === 1004) {
399
+ * console.error('[smartico] user does not meet segment / entry conditions — show a gating message');
400
+ * } else if (r.errCode === 1006) {
401
+ * console.error('[smartico] cooldown active — surface the cooldown_until from getClans:', result.cooldown_until);
402
+ * } else if (r.errCode === 1001) {
403
+ * console.error('[smartico] clan not found or archived — refresh list and remove from UI');
404
+ * } else {
405
+ * console.error('[smartico] join failed — show a generic error toast with this message:', r.errMsg);
406
+ * }
407
+ * ```
408
+ */
409
+ public async joinClan(clanId: number): Promise<TClanJoinResult> {
410
+ return this.api.clanJoin(this.userExtId, clanId);
411
+ }
412
+
413
+ protected async updateClans() {
414
+ const payload = await this.api.clansGetListT(this.userExtId);
415
+ this.updateEntity(onUpdateContextKey.Clans, payload);
416
+ }
417
+ }