@enigma-lake/tower-play-controller-sdk 2.2.2 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { createContext, useContext, useRef, useEffect, useCallback, useState, useMemo, Fragment as Fragment$1 } from 'react';
2
+ import React, { createContext, useContext, useMemo, useRef, useEffect, useCallback, useState, Fragment as Fragment$1 } from 'react';
3
3
  import cx$1 from 'classnames';
4
- import { Currency, sendSetUserCurrencyEvent, sendSetUserCurrencyEventV2, setToggleGameWidgetsVisibility, setToggleGameWidgetsExpansion, getUserInformationEvent, EVENTS, sendOpenHighPlayModalEvent } from '@enigma-lake/zoot-platform-sdk';
4
+ import { createTranslator as createTranslator$1, DEFAULT_LANGUAGE, resolveLanguage, Currency, sendSetUserCurrencyEvent, sendSetUserCurrencyEventV2, setToggleGameWidgetsVisibility, setToggleGameWidgetsExpansion, getUserInformationEvent, EVENTS, sendOpenHighPlayModalEvent } from '@enigma-lake/zoot-platform-sdk';
5
+ export { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGE_CODES, resolveLanguage } from '@enigma-lake/zoot-platform-sdk';
5
6
  import { Menu, MenuButton, MenuItems, MenuItem, Switch as Switch$1, Dialog, DialogPanel, DialogTitle } from '@headlessui/react';
6
7
 
7
8
  const hexToRgb = (hex) => {
@@ -102,6 +103,23 @@ const createAutoplayButtonAction = ({ state, cashoutDisabled, onStopPlay, onPlay
102
103
  };
103
104
  };
104
105
 
106
+ var SELECTORS;
107
+ (function (SELECTORS) {
108
+ SELECTORS["RISK"] = "Risk";
109
+ })(SELECTORS || (SELECTORS = {}));
110
+ var RiskTypes;
111
+ (function (RiskTypes) {
112
+ RiskTypes["LOW"] = "LOW";
113
+ RiskTypes["MEDIUM"] = "MEDIUM";
114
+ RiskTypes["HIGH"] = "HIGH";
115
+ })(RiskTypes || (RiskTypes = {}));
116
+ // standard controller palette (matches multi/plinko play-controller SDKs)
117
+ const DEFAULT_RISK_COLORS = {
118
+ [RiskTypes.LOW]: "#1AE380",
119
+ [RiskTypes.MEDIUM]: "#FFC058",
120
+ [RiskTypes.HIGH]: "#FF5646",
121
+ };
122
+
105
123
  const AutoManualPlayStateContext = createContext(undefined);
106
124
  const useAutoManualPlayState = () => {
107
125
  const context = useContext(AutoManualPlayStateContext);
@@ -111,6 +129,104 @@ const useAutoManualPlayState = () => {
111
129
  return context;
112
130
  };
113
131
 
132
+ const en = {
133
+ playNow: "PLAY NOW",
134
+ playFreeRound: "PLAY FREE ROUND",
135
+ selectPlayAmount: "SELECT PLAY AMOUNT",
136
+ cashout: "CASHOUT",
137
+ collect: "COLLECT",
138
+ doubleTitle: "DOUBLE",
139
+ doubleSubtitle: "OR NOTHING",
140
+ startAutoplay: "START AUTOPLAY",
141
+ stopAutoplay: "STOP AUTOPLAY",
142
+ numberOfPlays: "Number of Plays",
143
+ selectPlayAmountAria: "Select play amount",
144
+ playAmountLabel: "Play amount",
145
+ autoLabel: "Auto",
146
+ selectTileToast: "Please select at least one tile to start autoplay.",
147
+ riskLabel: "Risk",
148
+ riskLow: "Low",
149
+ riskMedium: "Medium",
150
+ riskHigh: "High",
151
+ freeRoundLeftOne: "FREE ROUND LEFT",
152
+ freeRoundLeftOther: "FREE ROUNDS LEFT",
153
+ freeRoundsModalTitle: "Free Game Rounds",
154
+ freeRoundsHave: "You have",
155
+ freeRoundsUnitOne: "FREE GAME ROUND",
156
+ freeRoundsUnitOther: "FREE GAME ROUNDS",
157
+ freeRoundsPerRound: "{amount} per round",
158
+ freeRoundsExpiresIn: "Expires in {days} {unit}",
159
+ dayUnitOne: "day",
160
+ dayUnitOther: "days",
161
+ freeRoundsModalPlayNow: "Play now",
162
+ freeRoundsModalPlayLater: "Maybe later",
163
+ freeRoundsSummaryWon: "You won",
164
+ freeRoundsSummaryIn: "in",
165
+ freeRoundsSummaryUnitOne: "free game round",
166
+ freeRoundsSummaryUnitOther: "free game rounds",
167
+ freeRoundsSummaryCredited: "Winnings have been credited to your bonus balance.",
168
+ freeRoundsSummaryClose: "Close",
169
+ };
170
+
171
+ const zhCN = {
172
+ playNow: "立即游戏",
173
+ playFreeRound: "免费游戏",
174
+ selectPlayAmount: "选择下注金额",
175
+ cashout: "提现",
176
+ collect: "收取",
177
+ doubleTitle: "双倍",
178
+ doubleSubtitle: "或全无",
179
+ startAutoplay: "开始自动游戏",
180
+ stopAutoplay: "停止自动游戏",
181
+ numberOfPlays: "游戏次数",
182
+ selectPlayAmountAria: "选择下注金额",
183
+ playAmountLabel: "下注金额",
184
+ autoLabel: "自动",
185
+ selectTileToast: "请至少选择一个方块以开始自动游戏。",
186
+ riskLabel: "风险",
187
+ riskLow: "低",
188
+ riskMedium: "中",
189
+ riskHigh: "高",
190
+ freeRoundLeftOne: "剩余免费回合",
191
+ freeRoundLeftOther: "剩余免费回合",
192
+ freeRoundsModalTitle: "免费游戏回合",
193
+ freeRoundsHave: "您有",
194
+ freeRoundsUnitOne: "个免费游戏回合",
195
+ freeRoundsUnitOther: "个免费游戏回合",
196
+ freeRoundsPerRound: "每回合 {amount}",
197
+ freeRoundsExpiresIn: "{days} {unit}后到期",
198
+ dayUnitOne: "天",
199
+ dayUnitOther: "天",
200
+ freeRoundsModalPlayNow: "立即游戏",
201
+ freeRoundsModalPlayLater: "稍后再玩",
202
+ freeRoundsSummaryWon: "您赢得了",
203
+ freeRoundsSummaryIn: ",共",
204
+ freeRoundsSummaryUnitOne: "个免费游戏回合",
205
+ freeRoundsSummaryUnitOther: "个免费游戏回合",
206
+ freeRoundsSummaryCredited: "奖金已存入您的奖金余额。",
207
+ freeRoundsSummaryClose: "关闭",
208
+ };
209
+
210
+ const dictionaries = {
211
+ en,
212
+ "zh-CN": zhCN,
213
+ };
214
+ const createTranslator = (language) => createTranslator$1(dictionaries, language);
215
+
216
+ const I18nContext = createContext(undefined);
217
+ const I18nProvider = ({ language, children, }) => {
218
+ const value = useMemo(() => {
219
+ const resolved = resolveLanguage(language);
220
+ return { language: resolved, t: createTranslator(resolved) };
221
+ }, [language]);
222
+ return jsx(I18nContext.Provider, { value: value, children: children });
223
+ };
224
+ const DEFAULT_VALUE = {
225
+ language: DEFAULT_LANGUAGE,
226
+ t: createTranslator(DEFAULT_LANGUAGE),
227
+ };
228
+ const useTranslation = () => useContext(I18nContext) ?? DEFAULT_VALUE;
229
+
114
230
  const getNextPlayAmount = ({ currentAmount, defaultBetOptions, direction, }) => {
115
231
  if (!defaultBetOptions.length) {
116
232
  return currentAmount;
@@ -141,6 +257,7 @@ function isFullMode(config) {
141
257
  return config.withoutRiskAndAutoplay !== true;
142
258
  }
143
259
  const usePlayController = () => {
260
+ const { t } = useTranslation();
144
261
  const { config, mode, autoPlay: { setNumberOfPlays, numberOfPlays, setPlayedRounds, playedRounds, selection, setState, state, }, playValues: { displayPlayAmountView, setDisplayPlayAmountView, togglePlayAmountView, }, } = useAutoManualPlayState();
145
262
  const { current } = config.currencyOptions;
146
263
  const { isPlaying, canCashout, disabledCashout = false, disabledController, playHook, totalBalance, } = config.playOptions;
@@ -155,17 +272,19 @@ const usePlayController = () => {
155
272
  const { playAmount: rawPlayAmount = 0, playLimits, setPlayAmount: rawSetPlayAmount = () => { }, } = playHook?.() ?? {};
156
273
  const minPlayAmount = playLimits?.[current.code]?.limits.min ?? 0;
157
274
  const maxPlayAmount = playLimits?.[current.code]?.limits.max ?? 0;
158
- // Free rounds (opt-in, MANUAL-mode only): only when the player has ENGAGED
159
- // free-round mode are the stake/coin locked to the grant. `available` (pending
160
- // grants exist) does not lock anything on its own it surfaces a "Use Free
161
- // Rounds" affordance. Free rounds are never available/engaged in auto-play
162
- // mode, and engagement coexists with the cashout flow.
275
+ // Free rounds (opt-in): only when the player has ENGAGED free-rounds mode
276
+ // (via the intro modal's PLAY NOW) are the per-round value/coin locked to
277
+ // the grant. When not engaged, the controller renders exactly as if the
278
+ // user had no free rounds. Engagement coexists with the cashout flow.
279
+ //
280
+ // By default engagement is MANUAL-mode only (free rounds are never engaged
281
+ // in auto-play mode). Games whose autoplay submission path funds rounds
282
+ // from engaged grants opt into engaged autoplay via
283
+ // `supportsGrantFundedAutoplay`, which drops the manual-mode requirement.
163
284
  const isManualMode = mode === GAME_MODE.MANUAL;
164
285
  const freeRoundsOptions = config.freeRoundsOptions;
165
- const isFreeRoundsAvailable = Boolean(isManualMode &&
166
- freeRoundsOptions?.available &&
167
- freeRoundsOptions.roundsRemaining > 0);
168
- const isFreeRoundsEngaged = Boolean(isManualMode &&
286
+ const supportsGrantFundedAutoplay = Boolean(freeRoundsOptions?.supportsGrantFundedAutoplay);
287
+ const isFreeRoundsEngaged = Boolean((isManualMode || supportsGrantFundedAutoplay) &&
169
288
  freeRoundsOptions?.engaged &&
170
289
  freeRoundsOptions.roundsRemaining > 0);
171
290
  // playAmount is in display units everywhere in this SDK (e.g. 1.00), while the
@@ -182,6 +301,33 @@ const usePlayController = () => {
182
301
  : rawSetPlayAmount;
183
302
  const playIntervalRef = useRef(null);
184
303
  const isAutoplayActiveRef = useRef(false);
304
+ // Each setTimeout tick invokes the `loopRounds` closure from the render that
305
+ // scheduled it, so reading `config.freeRoundsOptions` there can see a stale,
306
+ // pre-engagement value. Keep the latest free-rounds state in a ref
307
+ // (refreshed every render) so the loop's hard-stop checks always see live
308
+ // values — a grant engaged mid-loop still hard-stops paid autoplay, and an
309
+ // engaged grant-funded loop sees the live remaining count.
310
+ const freeRoundsLiveRef = useRef({
311
+ engaged: false,
312
+ remaining: 0,
313
+ supportsGrantFundedAutoplay: false,
314
+ });
315
+ freeRoundsLiveRef.current = {
316
+ engaged: Boolean(config.freeRoundsOptions?.engaged),
317
+ remaining: config.freeRoundsOptions?.roundsRemaining ?? 0,
318
+ supportsGrantFundedAutoplay,
319
+ };
320
+ // Per-session accounting for engaged (grant-funded) autoplay.
321
+ // roundsRemaining is game-driven and only decrements via an async refetch
322
+ // AFTER each settlement, so the live ref can still read stale-high when the
323
+ // loop schedules the next round. We therefore also count submissions
324
+ // locally: a session that started engaged may never submit more rounds than
325
+ // the grants available when it started, regardless of refetch timing.
326
+ const autoplaySessionRef = useRef({
327
+ startedEngaged: false,
328
+ grantsAtStart: 0,
329
+ roundsSubmitted: 0,
330
+ });
185
331
  const stopAutoplay = () => {
186
332
  isAutoplayActiveRef.current = false;
187
333
  if (playIntervalRef.current) {
@@ -201,6 +347,35 @@ const usePlayController = () => {
201
347
  if (!isAutoplayActiveRef.current || !autoplayEnabled) {
202
348
  return;
203
349
  }
350
+ // Free-rounds gates — all live values come from refs (closures here are
351
+ // stale across rounds).
352
+ const live = freeRoundsLiveRef.current;
353
+ const session = autoplaySessionRef.current;
354
+ if (!live.supportsGrantFundedAutoplay) {
355
+ // Legacy (flag absent/false): tower free rounds are MANUAL-only — there
356
+ // is no free autoplay. If a grant is engaged (or becomes engaged) while
357
+ // an autoplay loop is live, hard-stop instead of letting PAID autoplay
358
+ // run alongside the grant.
359
+ if (live.engaged) {
360
+ stopAutoplay();
361
+ return;
362
+ }
363
+ }
364
+ else if (session.startedEngaged) {
365
+ // Grant-funded autoplay: an autoplay session that started on free
366
+ // rounds ends the moment the grants are exhausted or engagement ends
367
+ // mid-session — it must never submit a paid round. Checked BEFORE each
368
+ // submission so the round that consumes the last grant completes
369
+ // normally, then the loop halts via the normal stop path. Uses the
370
+ // local per-session submission count as well because the game's
371
+ // roundsRemaining refetch can lag behind the next scheduled round.
372
+ const grantsExhausted = session.roundsSubmitted >= session.grantsAtStart || live.remaining <= 0;
373
+ if (grantsExhausted || !live.engaged) {
374
+ setNumberOfPlays(AUTOPLAY_DEFAULT_PLAY_ROUNDS_COUNT);
375
+ stopAutoplay();
376
+ return;
377
+ }
378
+ }
204
379
  if (remainingPlays < 1 || numberOfPlays === 0) {
205
380
  setNumberOfPlays(AUTOPLAY_DEFAULT_PLAY_ROUNDS_COUNT);
206
381
  stopAutoplay();
@@ -208,6 +383,7 @@ const usePlayController = () => {
208
383
  }
209
384
  setPlayedRounds(currentPlayedRounds + 1);
210
385
  setNumberOfPlays((prev) => Math.max(prev - 1, 0));
386
+ session.roundsSubmitted += 1;
211
387
  // onAutoPlay exists only in full mode (guarded by autoplayEnabled)
212
388
  config.onAutoPlay(selection, () => {
213
389
  if (!isAutoplayActiveRef.current) {
@@ -225,13 +401,32 @@ const usePlayController = () => {
225
401
  if (selection.length === 0) {
226
402
  showAutoPlayToast?.({
227
403
  type: "info",
228
- message: "Please select at least one tile to start autoplay.",
404
+ message: t("selectTileToast"),
229
405
  });
230
406
  return;
231
407
  }
408
+ // Grant-funded autoplay: the effective play count is clamped to the
409
+ // remaining grants so autoplay can never spill into paid rounds (and an
410
+ // infinite/over-cap count stops at roundsRemaining). The game client
411
+ // enforces actual per-round grant consumption; this is the UI cap.
412
+ const effectiveNumberOfPlays = supportsGrantFundedAutoplay && isFreeRoundsEngaged
413
+ ? Math.min(numberOfPlays, freeRoundsOptions?.roundsRemaining ?? 0)
414
+ : numberOfPlays;
415
+ if (effectiveNumberOfPlays !== numberOfPlays) {
416
+ setNumberOfPlays(effectiveNumberOfPlays);
417
+ }
418
+ // Snapshot the free-rounds state for this session. Uses the RAW engaged
419
+ // flag (not isFreeRoundsEngaged, which also requires remaining > 0) so a
420
+ // session started while engaged with 0 grants fails closed on the first
421
+ // loop iteration instead of silently becoming a paid session.
422
+ autoplaySessionRef.current = {
423
+ startedEngaged: Boolean(freeRoundsOptions?.engaged),
424
+ grantsAtStart: freeRoundsOptions?.roundsRemaining ?? 0,
425
+ roundsSubmitted: 0,
426
+ };
232
427
  isAutoplayActiveRef.current = true;
233
428
  setState(AUTO_PLAY_STATE.PLAYING);
234
- loopRounds(playedRounds, numberOfPlays);
429
+ loopRounds(playedRounds, effectiveNumberOfPlays);
235
430
  };
236
431
  const adjustPlayAmount = ({ direction }) => {
237
432
  if (isDisabled()) {
@@ -289,16 +484,15 @@ const usePlayController = () => {
289
484
  setDisplayPlayAmountView,
290
485
  togglePlayAmountView,
291
486
  },
292
- // free rounds (manual-mode only)
487
+ // free rounds (binary engagement via the intro modal; manual-mode only
488
+ // unless the game opts into grant-funded autoplay)
293
489
  freeRounds: {
294
- isAvailable: isFreeRoundsAvailable,
295
490
  isEngaged: isFreeRoundsEngaged,
296
491
  roundsRemaining: freeRoundsOptions?.roundsRemaining ?? 0,
297
492
  totalRounds: freeRoundsOptions?.totalRounds ?? 0,
298
493
  stakeCents: freeRoundsOptions?.stakeCents ?? 0,
299
494
  coinType: freeRoundsOptions?.coinType,
300
495
  currency: freeRoundsOptions?.currency,
301
- onUseFreeRounds: freeRoundsOptions?.onUseFreeRounds,
302
496
  },
303
497
  };
304
498
  };
@@ -392,42 +586,117 @@ const SelectMenu = ({ disabled = false }) => {
392
586
  }) })] })) }));
393
587
  };
394
588
 
395
- var styles$5 = {"base":"PlayValueInput-module__base___sTNW2","icon":"PlayValueInput-module__icon___jZj7O","playValueInput":"PlayValueInput-module__playValueInput___7pJtn","playValueInputWithLabel":"PlayValueInput-module__playValueInputWithLabel___S55Cr","label":"PlayValueInput-module__label___1ZSAV","inputContainer":"PlayValueInput-module__inputContainer___RKr1l","playValueWithLabel":"PlayValueInput-module__playValueWithLabel___K4-pr","isClassicGame":"PlayValueInput-module__isClassicGame___lQzVE"};
589
+ var styles$6 = {"base":"PlayValueInput-module__base___sTNW2","icon":"PlayValueInput-module__icon___jZj7O","playValueInput":"PlayValueInput-module__playValueInput___7pJtn","playValueInputWithLabel":"PlayValueInput-module__playValueInputWithLabel___S55Cr","label":"PlayValueInput-module__label___1ZSAV","inputContainer":"PlayValueInput-module__inputContainer___RKr1l","playValueWithLabel":"PlayValueInput-module__playValueWithLabel___K4-pr","isClassicGame":"PlayValueInput-module__isClassicGame___lQzVE"};
396
590
 
397
591
  const PlayValueInput = ({ disabled, children, className, value, onClick, isClassicGame = false, }) => {
592
+ const { t } = useTranslation();
398
593
  const handleOnClick = () => {
399
594
  if (disabled) {
400
595
  return;
401
596
  }
402
597
  onClick();
403
598
  };
404
- return (jsxs("div", { className: cx$1(styles$5.base, className, {
405
- [styles$5.disabled]: disabled,
406
- [styles$5.isClassicGame]: isClassicGame,
407
- }), children: [children && jsx("div", { className: cx$1(styles$5.icon), children: children }), jsxs("div", { className: cx$1(styles$5.inputContainer), onClick: (e) => {
599
+ return (jsxs("div", { className: cx$1(styles$6.base, className, {
600
+ [styles$6.disabled]: disabled,
601
+ [styles$6.isClassicGame]: isClassicGame,
602
+ }), children: [children && jsx("div", { className: cx$1(styles$6.icon), children: children }), jsxs("div", { className: cx$1(styles$6.inputContainer), onClick: (e) => {
408
603
  e.preventDefault();
409
604
  handleOnClick();
410
- }, children: [jsx("span", { className: styles$5.label, children: "Play amount" }), jsx("div", { className: cx$1(styles$5.playValueInput, {
411
- [styles$5.playValueInputWithLabel]: true,
605
+ }, children: [jsx("span", { className: styles$6.label, children: t("playAmountLabel") }), jsx("div", { className: cx$1(styles$6.playValueInput, {
606
+ [styles$6.playValueInputWithLabel]: true,
412
607
  }), children: value })] })] }));
413
608
  };
414
609
 
610
+ const COIN_TYPE_SWEEPS = 0;
611
+ const COIN_TYPE_GOLD = 1;
612
+ // B2C coin decimals (platform convention): Sweeps shows 2 dp, Gold 0 dp.
613
+ const COIN_DECIMALS = {
614
+ [COIN_TYPE_SWEEPS]: 2,
615
+ [COIN_TYPE_GOLD]: 0,
616
+ };
617
+ const resolveFreeRoundsCoinLabel = ({ coinType, currency, }) => {
618
+ if (currency) {
619
+ return currency;
620
+ }
621
+ if (coinType === COIN_TYPE_SWEEPS) {
622
+ return "SC";
623
+ }
624
+ if (coinType === COIN_TYPE_GOLD) {
625
+ return "GC";
626
+ }
627
+ return "";
628
+ };
629
+ // The per-round value belongs to the GRANT's coin, which can differ from the
630
+ // player's active currency (e.g. a Sweeps grant surfaced while the player is
631
+ // on Gold). Use the grant coin's decimals so a 0.20 SC value doesn't render as
632
+ // "0" under Gold's 0-dp formatting. B2B grants (operator `currency`) fall back
633
+ // to the operator currency decimals passed in via `fallback`.
634
+ const resolveFreeRoundsDecimals = ({ coinType, currency, fallback, }) => {
635
+ if (!currency && coinType !== undefined && coinType in COIN_DECIMALS) {
636
+ return COIN_DECIMALS[coinType];
637
+ }
638
+ return fallback;
639
+ };
640
+ // Resolves display decimals from a coin/currency LABEL (cross-SDK standard):
641
+ // SC/SWEEPS = 2, GC/GOLD = 0, B2B operator currency strings = 2.
642
+ const resolveCoinLabelDecimals = (coin) => {
643
+ const normalized = coin.trim().toUpperCase();
644
+ if (normalized === "GC" || normalized === "GOLD") {
645
+ return 0;
646
+ }
647
+ return 2;
648
+ };
649
+ const MS_PER_DAY = 86_400_000;
650
+ // Computes the whole-day countdown for "*Expires in {N} days". Rounds UP so a
651
+ // grant expiring in 36 hours reads "2 days" (it is still claimable on day 2).
652
+ // Returns null when the input is missing/invalid or already expired.
653
+ const computeExpiresInDays = (expiresAt, now = Date.now()) => {
654
+ if (expiresAt === null || expiresAt === undefined) {
655
+ return null;
656
+ }
657
+ const expiryMs = new Date(expiresAt).getTime();
658
+ if (Number.isNaN(expiryMs)) {
659
+ return null;
660
+ }
661
+ const remainingMs = expiryMs - now;
662
+ if (remainingMs <= 0) {
663
+ return null;
664
+ }
665
+ return Math.ceil(remainingMs / MS_PER_DAY);
666
+ };
667
+
415
668
  const PlayAmountControl = ({ isDisabled, forceRefresh, lockCurrency = false, }) => {
416
669
  const { config } = useAutoManualPlayState();
417
- const { playAmount, adjustPlayAmount, playValues: { togglePlayAmountView }, autoPlay: { state }, } = usePlayController();
670
+ const { playAmount, adjustPlayAmount, playValues: { togglePlayAmountView }, autoPlay: { state }, freeRounds, } = usePlayController();
418
671
  const isClassicGame = Boolean(config.isClassicGame || config.classicGame?.forceLeftStyle);
672
+ // While a free-rounds grant is engaged, the input is locked to the GRANT's
673
+ // per-round value and coin (which can differ from the active currency), so
674
+ // the locked amount is always truthful.
675
+ const { current } = config.currencyOptions;
676
+ const grantCoinLabel = resolveFreeRoundsCoinLabel({
677
+ coinType: freeRounds.coinType,
678
+ currency: freeRounds.currency,
679
+ });
680
+ const valueDecimals = freeRounds.isEngaged
681
+ ? resolveFreeRoundsDecimals({
682
+ coinType: freeRounds.coinType,
683
+ currency: freeRounds.currency,
684
+ fallback: current.decimals,
685
+ })
686
+ : current.decimals;
687
+ const coinLabel = freeRounds.isEngaged && grantCoinLabel ? grantCoinLabel : current.abbr;
419
688
  const handleTogglePlayAmount = () => {
420
689
  setToggleGameWidgetsVisibility();
421
690
  togglePlayAmountView();
422
691
  setToggleGameWidgetsExpansion();
423
692
  };
424
- return (jsxs(GroupRow, { isClassicGame: isClassicGame, children: [jsx(PlayValueInput, { className: styles_group.groupItem, value: `${format(playAmount, config.currencyOptions.current.decimals)} ${config.currencyOptions.current.abbr}`, disabled: isDisabled() ||
693
+ return (jsxs(GroupRow, { isClassicGame: isClassicGame, children: [jsx(PlayValueInput, { className: styles_group.groupItem, value: `${format(playAmount, valueDecimals)} ${coinLabel}`, disabled: isDisabled() ||
425
694
  config.playOptions.disableInput ||
426
695
  config.playOptions.disabledController ||
427
696
  state === AUTO_PLAY_STATE.PLAYING ||
428
- lockCurrency, onClick: handleTogglePlayAmount, isClassicGame: isClassicGame, children: jsx(SelectMenu, { disabled: isDisabled() || lockCurrency }) }), jsx(Button, { className: styles_group.groupItem, onClick: () => adjustPlayAmount({ direction: -1 }), theme: "ghost", disabled: isDisabled(), children: jsx("span", { className: styles_group.x2, children: "-" }) }), jsx(Button, { className: styles_group.refreshButton, onClick: () => {
697
+ lockCurrency, onClick: handleTogglePlayAmount, isClassicGame: isClassicGame, children: jsx(SelectMenu, { disabled: isDisabled() || lockCurrency }) }), jsx(Button, { className: styles_group.groupItem, onClick: () => adjustPlayAmount({ direction: -1 }), theme: "ghost", disabled: isDisabled() || lockCurrency, children: jsx("span", { className: styles_group.x2, children: "-" }) }), jsx(Button, { className: styles_group.refreshButton, onClick: () => {
429
698
  forceRefresh();
430
- }, "data-button-refresh": true, children: jsx("span", { className: cx$1(styles_group.x2, styles_group.first) }) }), jsx(Button, { className: styles_group.groupItem, onClick: () => adjustPlayAmount({ direction: 1 }), theme: "ghost", disabled: isDisabled(), children: jsx("span", { className: cx$1(styles_group.x2, styles_group.last), children: "+" }) })] }));
699
+ }, "data-button-refresh": true, children: jsx("span", { className: cx$1(styles_group.x2, styles_group.first) }) }), jsx(Button, { className: styles_group.groupItem, onClick: () => adjustPlayAmount({ direction: 1 }), theme: "ghost", disabled: isDisabled() || lockCurrency, children: jsx("span", { className: cx$1(styles_group.x2, styles_group.last), children: "+" }) })] }));
431
700
  };
432
701
 
433
702
  const getDefaultButtonClassName = (currencyCode, styles) => {
@@ -449,17 +718,11 @@ const getActiveButtonClassName = (currencyCode, styles) => {
449
718
  return styles.buttonDefault__active;
450
719
  };
451
720
 
452
- var AUTOPLAY_LABEL;
453
- (function (AUTOPLAY_LABEL) {
454
- AUTOPLAY_LABEL["START"] = "START AUTOPLAY";
455
- AUTOPLAY_LABEL["STOP"] = "STOP AUTOPLAY";
456
- })(AUTOPLAY_LABEL || (AUTOPLAY_LABEL = {}));
457
-
458
721
  // Space activates buttons/links natively. If the user has keyboard focus on a
459
- // different interactive control (the "Use Free Rounds" affordance, the intro
460
- // modal's PLAY NOW / PLAY LATER buttons, etc.), let Space activate THAT control
461
- // instead of hijacking it to click the play button — otherwise a paid play can
462
- // fire while the user is trying to engage free rounds.
722
+ // different interactive control (the intro modal's PLAY NOW / PLAY LATER
723
+ // buttons, etc.), let Space activate THAT control instead of hijacking it to
724
+ // click the play button — otherwise a paid play can fire while the user is
725
+ // trying to engage free rounds.
463
726
  const isOtherInteractiveControlFocused = (button) => {
464
727
  const active = document.activeElement;
465
728
  return Boolean(active &&
@@ -517,20 +780,29 @@ const useSpacebarButtonTrigger = ({ roleButton, activeClassName, canTrigger, sup
517
780
 
518
781
  const AutoPlayController = () => {
519
782
  const { config } = useAutoManualPlayState();
783
+ const { t } = useTranslation();
520
784
  const [forceRefresh, setForceRefresh] = useState(false);
521
- const { isValidPlayAmount, playValues: { displayPlayAmountView, togglePlayAmountView }, autoPlay: { isDisabled, state, onPlay, onStopPlay, cashoutDisabled }, } = usePlayController();
785
+ const { isValidPlayAmount, freeRounds, playValues: { displayPlayAmountView, togglePlayAmountView }, autoPlay: { isDisabled, state, onPlay, onStopPlay, cashoutDisabled }, } = usePlayController();
522
786
  const { current } = config.currencyOptions;
523
787
  const roleButton = GAME_MODE.AUTOPLAY;
524
- const buttonLabel = state === AUTO_PLAY_STATE.PLAYING
525
- ? AUTOPLAY_LABEL.STOP
526
- : AUTOPLAY_LABEL.START;
788
+ const isStopState = state === AUTO_PLAY_STATE.PLAYING;
789
+ // Free-rounds engaged (grant-funded autoplay): the STOP control shows the
790
+ // live remaining grant count so the player sees the rounds tick down.
791
+ let buttonLabel = isStopState
792
+ ? t("stopAutoplay")
793
+ : t("startAutoplay");
794
+ if (isStopState && freeRounds.isEngaged) {
795
+ buttonLabel = `${t("stopAutoplay")} (${freeRounds.roundsRemaining})`;
796
+ }
527
797
  const activeClassName = getActiveButtonClassName(current.code, styles_button);
798
+ // Cashout styling keys off the PLAYING state (not the label text) so the
799
+ // engaged "(N)" label suffix can never break the style comparison.
528
800
  const defaultClassName = useMemo(() => {
529
- if (buttonLabel === AUTOPLAY_LABEL.STOP) {
801
+ if (isStopState) {
530
802
  return styles_button.buttonCashout;
531
803
  }
532
804
  return getDefaultButtonClassName(current.code, styles_button);
533
- }, [buttonLabel, current.code]);
805
+ }, [isStopState, current.code]);
534
806
  useSpacebarButtonTrigger({
535
807
  roleButton,
536
808
  activeClassName,
@@ -539,6 +811,10 @@ const AutoPlayController = () => {
539
811
  cashoutDisabled,
540
812
  isDisabled: isDisabled(),
541
813
  }),
814
+ // Suppress the global Space shortcut while the free-rounds intro modal is
815
+ // open so Space belongs to the modal's PLAY NOW / PLAY LATER buttons
816
+ // instead of starting/stopping the autoplay mounted behind it.
817
+ suppressed: Boolean(config.freeRoundsOptions?.showIntroModal),
542
818
  });
543
819
  const isButtonDisabled = isAutoplayButtonDisabled({
544
820
  state,
@@ -563,7 +839,7 @@ const AutoPlayController = () => {
563
839
  }, [togglePlayAmountView]);
564
840
  const renderButton = useMemo(() => {
565
841
  if (displayPlayAmountView) {
566
- return (jsx(Button, { className: styles_button.buttonPlayAmount, onClick: handleTogglePlayAmount, roleType: roleButton, children: "SELECT PLAY AMOUNT" }));
842
+ return (jsx(Button, { className: styles_button.buttonPlayAmount, onClick: handleTogglePlayAmount, roleType: roleButton, children: t("selectPlayAmount") }));
567
843
  }
568
844
  return (jsx(Button, { disabled: isButtonDisabled, className: defaultClassName, onClick: buttonAction, roleType: roleButton, children: buttonLabel }));
569
845
  }, [
@@ -574,27 +850,11 @@ const AutoPlayController = () => {
574
850
  handleTogglePlayAmount,
575
851
  isButtonDisabled,
576
852
  roleButton,
853
+ t,
577
854
  ]);
578
855
  return (jsxs(Fragment, { children: [jsx(PlayAmountControl, { isDisabled: isDisabled, forceRefresh: () => setForceRefresh(!forceRefresh), "data-force-refresh": `mini-play-amount-control-data-${forceRefresh.toString()}` }, `mini-play-amount-control-key-${forceRefresh.toString()}`), renderButton] }));
579
856
  };
580
857
 
581
- var freeRoundsStyles = {"affordance":"FreeRoundsAffordance-module__affordance___wBGcv","row":"FreeRoundsAffordance-module__row___KwByQ","playSlot":"FreeRoundsAffordance-module__playSlot___hCXTv","freeSlot":"FreeRoundsAffordance-module__freeSlot___-mQ8T","chip":"FreeRoundsAffordance-module__chip___OajJb","chipDisabled":"FreeRoundsAffordance-module__chipDisabled___W9hSU"};
582
-
583
- // The "Use Free Rounds (N)" chip.
584
- // - available && !engaged → renders the chip (engages via onUseFreeRounds).
585
- // It is laid out beside the Play Now button (35/65 row) by
586
- // ManualPlayController; this component only renders the chip itself.
587
- // - engaged → renders nothing. There is intentionally NO "exit free rounds"
588
- // affordance: once engaged the player cannot leave free-round mode from the
589
- // controller. The engaged play button label is owned by ManualPlayController.
590
- // Purely renders + fires the callback; the game owns the engaged state.
591
- const FreeRoundsAffordance = ({ isAvailable, isEngaged, roundsRemaining, onUseFreeRounds, disabled = false, }) => {
592
- if (isEngaged || !isAvailable || !onUseFreeRounds) {
593
- return null;
594
- }
595
- return (jsxs("button", { type: "button", disabled: disabled, className: cx$1(freeRoundsStyles.chip, { [freeRoundsStyles.chipDisabled]: disabled }), onClick: disabled ? undefined : onUseFreeRounds, children: ["USE FREE ROUNDS (", roundsRemaining, ")"] }));
596
- };
597
-
598
858
  const useIsRunningExternal = () => {
599
859
  const [isRunningExternal, setIsRunningExternal] = useState(false);
600
860
  useEffect(() => {
@@ -619,6 +879,7 @@ const useIsRunningExternal = () => {
619
879
 
620
880
  const ManualPlayController = () => {
621
881
  const { config, doubleOrNothing } = useAutoManualPlayState();
882
+ const { t } = useTranslation();
622
883
  const { isRunningExternal } = useIsRunningExternal();
623
884
  const [forceRefresh, setForceRefresh] = useState(false);
624
885
  const { isValidPlayAmount, playValues: { displayPlayAmountView, togglePlayAmountView }, manualPlay: { isDisabled, onPlay, canCashout, cashoutDisabled }, freeRounds, } = usePlayController();
@@ -668,7 +929,7 @@ const ManualPlayController = () => {
668
929
  }), [cashoutDisabled, config.onCashout]);
669
930
  const renderButton = useMemo(() => {
670
931
  if (displayPlayAmountView) {
671
- return (jsx(Button, { className: styles_button.buttonPlayAmount, onClick: handleTogglePlayAmount, roleType: roleButton, children: "SELECT PLAY AMOUNT" }));
932
+ return (jsx(Button, { className: styles_button.buttonPlayAmount, onClick: handleTogglePlayAmount, roleType: roleButton, children: t("selectPlayAmount") }));
672
933
  }
673
934
  if (canCashout) {
674
935
  const isCashoutDisabled = isManualCashoutDisabled({
@@ -682,21 +943,29 @@ const ManualPlayController = () => {
682
943
  doubleOrNothing.state === DOUBLE_OR_NOTHING_STATE.PROGRESS;
683
944
  return (jsxs("div", { className: styles_button.buttonRow, children: [jsxs(Button, { disabled: isCashoutDisabled, className: cx$1(styles_button.buttonCashout, {
684
945
  [styles_button.buttonRow__cashout]: showDoubleOrNothing,
685
- }), onClick: handleCashoutClick, roleType: roleButton, children: [isRunningExternal ? "COLLECT" : "CASHOUT", " ", format(current.possibleWin ?? 0, current.decimals), " ", current.abbr] }), showDoubleOrNothing ? (jsx(Button, { disabled: isDoubleOrNothingDisabled, className: cx$1(styles_button.buttonDON, styles_button.buttonRow__don, {
946
+ }), onClick: handleCashoutClick, roleType: roleButton, children: [isRunningExternal ? t("collect") : t("cashout"), " ", format(current.possibleWin ?? 0, current.decimals), " ", current.abbr] }), showDoubleOrNothing ? (jsx(Button, { disabled: isDoubleOrNothingDisabled, className: cx$1(styles_button.buttonDON, styles_button.buttonRow__don, {
686
947
  [styles_button.buttonDON__disabled]: isDoubleOrNothingDisabled,
687
- }), onClick: handleDoubleOrNothingClick, roleType: roleButton, children: jsxs("div", { className: styles_button.buttonDON__content, children: [jsx("p", { className: styles_button.buttonDON__title, children: "DOUBLE" }), jsx("p", { className: styles_button.buttonDON__subtitle, children: "OR NOTHING" })] }) })) : null] }));
948
+ }), onClick: handleDoubleOrNothingClick, roleType: roleButton, children: jsxs("div", { className: styles_button.buttonDON__content, children: [jsx("p", { className: styles_button.buttonDON__title, children: t("doubleTitle") }), jsx("p", { className: styles_button.buttonDON__subtitle, children: t("doubleSubtitle") })] }) })) : null] }));
949
+ }
950
+ // While a game-driven autoplay loop is running, the game's stop-autoplay
951
+ // control is the only action — don't stack the manual play CTA behind it.
952
+ // The play amount, widgets and free-rounds footer stay as-is.
953
+ if (config.freeRoundsOptions?.autoplayActive) {
954
+ return null;
688
955
  }
689
956
  // Free rounds reuse the play button label while ENGAGED — it becomes
690
- // "PLAY FREE ROUND (N)". Denominator removed to simplify ("7/10" "7").
957
+ // "PLAY FREE ROUND" (no count; the remaining count lives ONLY in the
958
+ // "{N} FREE ROUNDS LEFT" footer).
691
959
  const playNowText = freeRounds.isEngaged
692
- ? `PLAY FREE ROUND (${freeRounds.roundsRemaining})`
693
- : "PLAY NOW";
960
+ ? t("playFreeRound")
961
+ : t("playNow");
694
962
  return (jsx(Button, { disabled: isButtonDisabled, className: defaultClassName, onClick: onPlay, roleType: roleButton, children: playNowText }));
695
963
  }, [
696
964
  canCashout,
697
965
  cashoutDisabled,
698
966
  config.doubleOrNothing?.disabled,
699
967
  config.doubleOrNothing?.display,
968
+ config.freeRoundsOptions?.autoplayActive,
700
969
  config.playOptions.disabledController,
701
970
  config.playOptions.isPlaying,
702
971
  current.abbr,
@@ -706,7 +975,6 @@ const ManualPlayController = () => {
706
975
  displayPlayAmountView,
707
976
  doubleOrNothing.state,
708
977
  freeRounds.isEngaged,
709
- freeRounds.roundsRemaining,
710
978
  handleDoubleOrNothingClick,
711
979
  handleCashoutClick,
712
980
  handleTogglePlayAmount,
@@ -714,33 +982,20 @@ const ManualPlayController = () => {
714
982
  isRunningExternal,
715
983
  onPlay,
716
984
  roleButton,
985
+ t,
717
986
  ]);
718
- // Grants exist but the player hasn't engaged show the side-by-side row
719
- // (Play Now at 35%, "Use Free Rounds" chip at 65%) instead of a full-width
720
- // play button. Only when not cashing out and not in the play-amount view.
721
- const showFreeRoundsRow = !displayPlayAmountView &&
722
- !canCashout &&
723
- freeRounds.isAvailable &&
724
- !freeRounds.isEngaged;
725
- // Engaging free rounds must be impossible while the controller is disabled or
726
- // a (non-cashout) round is in progress — same signals that disable the play
727
- // button. We deliberately exclude `!isValidPlayAmount` (the chip engages a
728
- // grant, not the paid stake) and rely on `isDisabled()` /
729
- // `disabledController` / `isPlaying` already in scope.
730
- const isFreeRoundsAffordanceDisabled = isDisabled() ||
731
- config.playOptions.disabledController ||
732
- config.playOptions.isPlaying;
733
- return (jsxs(Fragment, { children: [jsx(PlayAmountControl, { isDisabled: isDisabled || displayPlayAmountView, lockCurrency: freeRounds.isEngaged, forceRefresh: () => setForceRefresh(!forceRefresh), "data-force-refresh": `mini-play-amount-control-data-${forceRefresh.toString()}` }, `mini-play-amount-control-key-${forceRefresh.toString()}`), showFreeRoundsRow ? (jsxs("div", { className: freeRoundsStyles.row, children: [jsx("div", { className: freeRoundsStyles.playSlot, children: renderButton }), jsx("div", { className: freeRoundsStyles.freeSlot, children: jsx(FreeRoundsAffordance, { isAvailable: freeRounds.isAvailable, isEngaged: freeRounds.isEngaged, roundsRemaining: freeRounds.roundsRemaining, totalRounds: freeRounds.totalRounds, onUseFreeRounds: freeRounds.onUseFreeRounds, disabled: isFreeRoundsAffordanceDisabled }) })] })) : (renderButton)] }));
987
+ return (jsxs(Fragment, { children: [jsx(PlayAmountControl, { isDisabled: isDisabled || displayPlayAmountView, lockCurrency: freeRounds.isEngaged, forceRefresh: () => setForceRefresh(!forceRefresh), "data-force-refresh": `mini-play-amount-control-data-${forceRefresh.toString()}` }, `mini-play-amount-control-key-${forceRefresh.toString()}`), renderButton] }));
734
988
  };
735
989
 
736
- var styles_ui = {"base":"UI-module__base___wThyQ","container":"UI-module__container___gMSiK","betForm":"UI-module__betForm___hQkYd","isClassicGame":"UI-module__isClassicGame___zV1we","baseForceLeft":"UI-module__baseForceLeft___xfc-h","containerSpread":"UI-module__containerSpread___Axb7e","disabled":"UI-module__disabled___dnZJX","auto":"UI-module__auto___5peb8","controllerWidgets":"UI-module__controllerWidgets___x2lBy","sideWidget":"UI-module__sideWidget___WMGRy","centerWidget":"UI-module__centerWidget___sLLUi","left":"UI-module__left___1wtUp","center":"UI-module__center___E0ILH","right":"UI-module__right___yLHvk","controls":"UI-module__controls___Z6L-V","inActiveRound":"UI-module__inActiveRound___HYAx2"};
990
+ var styles_ui = {"base":"UI-module__base___wThyQ","container":"UI-module__container___gMSiK","betForm":"UI-module__betForm___hQkYd","isClassicGame":"UI-module__isClassicGame___zV1we","baseForceLeft":"UI-module__baseForceLeft___xfc-h","containerSpread":"UI-module__containerSpread___Axb7e","disabled":"UI-module__disabled___dnZJX","auto":"UI-module__auto___5peb8","freeRoundsFooter":"UI-module__freeRoundsFooter___AOprS","freeRoundsFooterCount":"UI-module__freeRoundsFooterCount___eaTrs","controllerWidgets":"UI-module__controllerWidgets___x2lBy","sideWidget":"UI-module__sideWidget___WMGRy","centerWidget":"UI-module__centerWidget___sLLUi","left":"UI-module__left___1wtUp","center":"UI-module__center___E0ILH","right":"UI-module__right___yLHvk","controls":"UI-module__controls___Z6L-V","inActiveRound":"UI-module__inActiveRound___HYAx2"};
737
991
 
738
992
  var style_difficulty = {"base":"DifficultySelector-module__base___lDaQ-"};
739
993
 
740
994
  var style_select = {"base":"Selector-module__base___-q0yf","label":"Selector-module__label___JJdbd","button":"Selector-module__button___MeRoC","menu":"Selector-module__menu___P0ROg","menuItems":"Selector-module__menuItems___chFk2","menuItem":"Selector-module__menuItem___isa4c","selected":"Selector-module__selected___nUJtR","disabled":"Selector-module__disabled___f1qGJ","disabledClassic":"Selector-module__disabledClassic___baiGO"};
741
995
 
742
996
  const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
743
- const Selector = ({ currentValue, label, riskColor, values, onSelect, disabled, isClassicGame = false, }) => {
997
+ const Selector = ({ currentValue, label, riskColor, values, onSelect, disabled, isClassicGame = false, formatDisplay, }) => {
998
+ const display = (value) => formatDisplay ? formatDisplay(value) : capitalize(String(value));
744
999
  const handleClick = (e) => {
745
1000
  if (disabled) {
746
1001
  return;
@@ -755,30 +1010,26 @@ const Selector = ({ currentValue, label, riskColor, values, onSelect, disabled,
755
1010
  return (jsxs("div", { className: cx$1(style_select.base), children: [label && jsx("span", { className: cx$1(style_select.label), children: label }), jsx(Menu, { as: "div", className: cx$1(style_select.menu), children: ({ open }) => (jsxs(Fragment, { children: [jsxs(MenuButton, { className: cx$1(style_select.button, {
756
1011
  [style_select.disabled]: disabled && !isClassicGame,
757
1012
  [style_select.disabledClassic]: disabled && isClassicGame,
758
- }), style: { color: riskColor }, disabled: disabled, "data-difficulty-selector-button": true, children: [capitalize(String(currentValue)), jsx(ChevronIcon, { open: open, disabled: disabled })] }), jsx(MenuItems, { anchor: { to: "top start" }, className: cx$1(style_select.menuItems), children: values.map((value) => (jsx(MenuItem, { as: "div", "data-value": value, onClick: handleClick, className: cx$1(style_select.menuItem, {
1013
+ }), style: { color: riskColor }, disabled: disabled, "data-difficulty-selector-button": true, children: [display(currentValue), jsx(ChevronIcon, { open: open, disabled: disabled })] }), jsx(MenuItems, { anchor: { to: "top start" }, className: cx$1(style_select.menuItems), children: values.map((value) => (jsx(MenuItem, { as: "div", "data-value": value, onClick: handleClick, className: cx$1(style_select.menuItem, {
759
1014
  [style_select.selected]: String(currentValue) === String(value),
760
- }), children: capitalize(String(value)) }, `element-${value}`))) })] })) })] }));
1015
+ }), children: display(value) }, `element-${value}`))) })] })) })] }));
761
1016
  };
762
1017
 
763
- var SELECTORS;
764
- (function (SELECTORS) {
765
- SELECTORS["RISK"] = "Risk";
766
- })(SELECTORS || (SELECTORS = {}));
767
- var RiskTypes;
768
- (function (RiskTypes) {
769
- RiskTypes["LOW"] = "LOW";
770
- RiskTypes["MEDIUM"] = "MEDIUM";
771
- RiskTypes["HIGH"] = "HIGH";
772
- })(RiskTypes || (RiskTypes = {}));
773
-
1018
+ const RISK_LABEL_KEY = {
1019
+ [RiskTypes.LOW]: "riskLow",
1020
+ [RiskTypes.MEDIUM]: "riskMedium",
1021
+ [RiskTypes.HIGH]: "riskHigh",
1022
+ };
774
1023
  const DifficultySelector = ({ playOptions, dropdownConfig, isClassicGame = false, }) => {
1024
+ const { t } = useTranslation();
775
1025
  const { risks, disabledMenu, currentRisk, onRiskChange } = playOptions;
776
- const riskColor = useRef(dropdownConfig.riskColorConfig[currentRisk ?? RiskTypes.LOW]);
1026
+ const riskColors = { ...DEFAULT_RISK_COLORS, ...dropdownConfig.riskColorConfig };
1027
+ const riskColor = useRef(riskColors[currentRisk ?? RiskTypes.LOW]);
777
1028
  function onRiskSelected(value) {
778
- riskColor.current = dropdownConfig.riskColorConfig[value];
1029
+ riskColor.current = riskColors[value];
779
1030
  onRiskChange(value);
780
1031
  }
781
- return (jsx("div", { className: style_difficulty.base, children: jsx(Selector, { currentValue: currentRisk, label: SELECTORS.RISK, values: risks, onSelect: onRiskSelected, disabled: disabledMenu, riskColor: riskColor.current, isClassicGame: isClassicGame }) }));
1032
+ return (jsx("div", { className: style_difficulty.base, children: jsx(Selector, { currentValue: currentRisk, label: t("riskLabel"), values: risks, onSelect: onRiskSelected, disabled: disabledMenu, riskColor: riskColor.current, isClassicGame: isClassicGame, formatDisplay: (risk) => t(RISK_LABEL_KEY[risk]) }) }));
782
1033
  };
783
1034
 
784
1035
  const ALLOWED_SEPARATORS = [",", "."];
@@ -801,7 +1052,7 @@ function cleanInputNumber(value) {
801
1052
  return `${integerPart}.${decimalPart}`;
802
1053
  }
803
1054
 
804
- var styles$4 = {"base":"Input-module__base___IifiA","disabled":"Input-module__disabled___WqyKa","disabledClassic":"Input-module__disabledClassic___hzsCt"};
1055
+ var styles$5 = {"base":"Input-module__base___IifiA","disabled":"Input-module__disabled___WqyKa","disabledClassic":"Input-module__disabledClassic___hzsCt"};
805
1056
 
806
1057
  const Input = ({ onChange, disabled, className, max, isClassicGame = false, ...restProps }) => {
807
1058
  const handleChange = React.useCallback((event) => {
@@ -816,31 +1067,32 @@ const Input = ({ onChange, disabled, className, max, isClassicGame = false, ...r
816
1067
  event.target.value = clamped;
817
1068
  onChange?.(event);
818
1069
  }, [disabled, max, onChange]);
819
- return (jsx("input", { ...restProps, type: "text", max: max, inputMode: "numeric", pattern: "\\d*", maxLength: 2, disabled: disabled, onChange: handleChange, className: cx$1(styles$4.base, className, {
820
- [styles$4.disabled]: disabled && !isClassicGame,
821
- [styles$4.disabledClassic]: disabled && isClassicGame,
1070
+ return (jsx("input", { ...restProps, type: "text", max: max, inputMode: "numeric", pattern: "\\d*", maxLength: 2, disabled: disabled, onChange: handleChange, className: cx$1(styles$5.base, className, {
1071
+ [styles$5.disabled]: disabled && !isClassicGame,
1072
+ [styles$5.disabledClassic]: disabled && isClassicGame,
822
1073
  }) }));
823
1074
  };
824
1075
 
825
- var styles$3 = {"base":"Switch-module__base___gj2ey","isClassicGame":"Switch-module__isClassicGame___H2Ffo","switcher":"Switch-module__switcher___gHXIx","gold":"Switch-module__gold___oewnb","sweeps":"Switch-module__sweeps___yS-IY","default":"Switch-module__default___TmHoU","unchecked":"Switch-module__unchecked___ooSS2","disabled":"Switch-module__disabled___lMRv0","thumb":"Switch-module__thumb___1wJ9D","move-right":"Switch-module__move-right___ca-6D","label":"Switch-module__label___pG2uz"};
1076
+ var styles$4 = {"base":"Switch-module__base___gj2ey","isClassicGame":"Switch-module__isClassicGame___H2Ffo","switcher":"Switch-module__switcher___gHXIx","gold":"Switch-module__gold___oewnb","sweeps":"Switch-module__sweeps___yS-IY","default":"Switch-module__default___TmHoU","unchecked":"Switch-module__unchecked___ooSS2","disabled":"Switch-module__disabled___lMRv0","thumb":"Switch-module__thumb___1wJ9D","move-right":"Switch-module__move-right___ca-6D","label":"Switch-module__label___pG2uz"};
826
1077
 
827
1078
  const Switch = ({ enabled, onSwitch, disabled, isPlaying, }) => {
828
1079
  const { config } = useAutoManualPlayState();
1080
+ const { t } = useTranslation();
829
1081
  const { current } = config.currencyOptions;
830
1082
  const isClassicGame = Boolean(config?.isClassicGame || config?.classicGame?.forceLeftStyle);
831
1083
  const defaultClassName = useMemo(() => {
832
1084
  if (current.code === Currency.GOLD) {
833
- return styles$3.gold;
1085
+ return styles$4.gold;
834
1086
  }
835
1087
  if (current.code === Currency.SWEEPS) {
836
- return styles$3.sweeps;
1088
+ return styles$4.sweeps;
837
1089
  }
838
- return styles$3.default;
1090
+ return styles$4.default;
839
1091
  }, [current.code]);
840
- const switcherClassName = useMemo(() => cx$1(styles$3.switcher, defaultClassName, {
841
- [styles$3.checked]: enabled,
842
- [styles$3.unchecked]: !enabled,
843
- [styles$3.disabled]: disabled,
1092
+ const switcherClassName = useMemo(() => cx$1(styles$4.switcher, defaultClassName, {
1093
+ [styles$4.checked]: enabled,
1094
+ [styles$4.unchecked]: !enabled,
1095
+ [styles$4.disabled]: disabled,
844
1096
  }), [defaultClassName, enabled, disabled]);
845
1097
  useEffect(() => {
846
1098
  const handleGlobalKeyDown = (event) => {
@@ -861,18 +1113,18 @@ const Switch = ({ enabled, onSwitch, disabled, isPlaying, }) => {
861
1113
  };
862
1114
  }, []);
863
1115
  return (jsx(Switch$1, { checked: enabled, onChange: onSwitch, as: Fragment$1, disabled: isPlaying, children: jsxs("div", { className: cx$1({
864
- [styles$3.isClassicGame]: isClassicGame,
865
- [styles$3.base]: !isClassicGame,
866
- }), children: [jsx("span", { className: styles$3.label, children: "Auto" }), jsx("div", { className: switcherClassName, children: jsx("span", { className: cx$1(styles$3.thumb, { [styles$3["move-right"]]: enabled }) }) })] }) }));
1116
+ [styles$4.isClassicGame]: isClassicGame,
1117
+ [styles$4.base]: !isClassicGame,
1118
+ }), children: [jsx("span", { className: styles$4.label, children: t("autoLabel") }), jsx("div", { className: switcherClassName, children: jsx("span", { className: cx$1(styles$4.thumb, { [styles$4["move-right"]]: enabled }) }) })] }) }));
867
1119
  };
868
1120
 
869
- var styles$2 = {"base":"InputWithSwitch-module__base___vjLoh","icon":"InputWithSwitch-module__icon___upeAL","input":"InputWithSwitch-module__input___K6RCD","label":"InputWithSwitch-module__label___tEjpj","classic":"InputWithSwitch-module__classic___3AzII"};
1121
+ var styles$3 = {"base":"InputWithSwitch-module__base___vjLoh","icon":"InputWithSwitch-module__icon___upeAL","input":"InputWithSwitch-module__input___K6RCD","label":"InputWithSwitch-module__label___tEjpj","classic":"InputWithSwitch-module__classic___3AzII"};
870
1122
 
871
1123
  const InputWithSwitch = ({ children, switcherConfig, disabled, className, isClassicGame = false, ...restProps }) => {
872
- return (jsxs("div", { className: cx$1(styles$2.base, className, {
873
- [styles$2.disabled]: switcherConfig.disabled,
874
- [styles$2.classic]: isClassicGame,
875
- }), children: [jsx(Switch, { ...switcherConfig }), jsx(Input, { ...restProps, className: cx$1(styles$2.input), disabled: disabled, isClassicGame: isClassicGame }), jsx("div", { className: cx$1(styles$2.icon), children: children })] }));
1124
+ return (jsxs("div", { className: cx$1(styles$3.base, className, {
1125
+ [styles$3.disabled]: switcherConfig.disabled,
1126
+ [styles$3.classic]: isClassicGame,
1127
+ }), children: [jsx(Switch, { ...switcherConfig }), jsx(Input, { ...restProps, className: cx$1(styles$3.input), disabled: disabled, isClassicGame: isClassicGame }), jsx("div", { className: cx$1(styles$3.icon), children: children })] }));
876
1128
  };
877
1129
 
878
1130
  function getDefaultExportFromCjs (x) {
@@ -976,14 +1228,18 @@ const WidgetContainer = ({ state, widgets, displayPlayAmountView, }) => {
976
1228
  return (jsxs("div", { className: cx("controllerWidgets"), children: [renderZone(left, "left"), renderZone(center, "center"), renderZone(right, "right")] }));
977
1229
  };
978
1230
 
979
- var styles$1 = {"base":"PlayValueList-module__base___5duc6","playValuesWrapper":"PlayValueList-module__playValuesWrapper___-zfj2","playValue":"PlayValueList-module__playValue___TJQhI","selectedPlayValue":"PlayValueList-module__selectedPlayValue___3DhDC","disabledPlayValue":"PlayValueList-module__disabledPlayValue___U9Bkj","isClassicGame":"PlayValueList-module__isClassicGame___AjnGE"};
1231
+ var styles$2 = {"base":"PlayValueList-module__base___5duc6","playValuesWrapper":"PlayValueList-module__playValuesWrapper___-zfj2","locked":"PlayValueList-module__locked___JNPHM","playValue":"PlayValueList-module__playValue___TJQhI","selectedPlayValue":"PlayValueList-module__selectedPlayValue___3DhDC","disabledPlayValue":"PlayValueList-module__disabledPlayValue___U9Bkj","isClassicGame":"PlayValueList-module__isClassicGame___AjnGE"};
980
1232
 
981
1233
  const PlayValueList = () => {
982
1234
  const [forceRefresh, setForceRefresh] = useState(false);
983
1235
  const { config } = useAutoManualPlayState();
1236
+ const { t } = useTranslation();
984
1237
  const isClassicGame = Boolean(config?.isClassicGame || config?.classicGame?.forceLeftStyle);
985
- const { onChangeAmount, playAmount, totalBalance, playValues: { displayPlayAmountView, togglePlayAmountView }, } = usePlayController();
1238
+ const { onChangeAmount, playAmount, totalBalance, autoPlay: { state: autoplayState }, playValues: { displayPlayAmountView, togglePlayAmountView }, } = usePlayController();
986
1239
  const { playLimits } = config.playOptions.playHook();
1240
+ // While autoplay is running the stake is locked for the whole session, so
1241
+ // the play-amount table must not accept changes until it stops.
1242
+ const isAutoplayPlaying = autoplayState === AUTO_PLAY_STATE.PLAYING;
987
1243
  if (!displayPlayAmountView) {
988
1244
  return null;
989
1245
  }
@@ -1014,6 +1270,9 @@ const PlayValueList = () => {
1014
1270
  setForceRefresh(!forceRefresh);
1015
1271
  };
1016
1272
  const handleClick = (e) => {
1273
+ if (isAutoplayPlaying) {
1274
+ return;
1275
+ }
1017
1276
  const el = e.target?.closest("[data-value]");
1018
1277
  if (!el) {
1019
1278
  return;
@@ -1027,39 +1286,43 @@ const PlayValueList = () => {
1027
1286
  applyValue(value);
1028
1287
  }
1029
1288
  };
1030
- return (jsx("div", { className: cx$1(styles$1.base, { [styles$1.isClassicGame]: isClassicGame }), "data-force-refresh": forceRefresh, children: jsx("div", { className: cx$1(styles$1.playValuesWrapper), onClick: handleClick, role: "listbox", "aria-label": "Select play amount", "data-testid": "play-value-list", children: playLimits?.[config.currencyOptions.current.code].defaultBetOptions.map((playValue) => (jsx("div", { className: cx$1(styles$1.playValue, {
1031
- [styles$1.selectedPlayValue]: playValue === playAmount,
1032
- [styles$1.disabledPlayValue]: playValue > totalBalance,
1289
+ return (jsx("div", { className: cx$1(styles$2.base, { [styles$2.isClassicGame]: isClassicGame }), "data-force-refresh": forceRefresh, children: jsx("div", { className: cx$1(styles$2.playValuesWrapper, {
1290
+ [styles$2.locked]: isAutoplayPlaying,
1291
+ }), onClick: handleClick, role: "listbox", "aria-label": t("selectPlayAmountAria"), "aria-disabled": isAutoplayPlaying, "data-testid": "play-value-list", children: playLimits?.[config.currencyOptions.current.code].defaultBetOptions.map((playValue) => (jsx("div", { className: cx$1(styles$2.playValue, {
1292
+ [styles$2.selectedPlayValue]: playValue === playAmount,
1293
+ [styles$2.disabledPlayValue]: playValue > totalBalance,
1033
1294
  }), "data-value": playValue, role: "option", tabIndex: 0, "aria-selected": false, children: format(playValue, config.currencyOptions.current.decimals) }, `${config.currencyOptions.current.code}-${playValue}`))) }) }));
1034
1295
  };
1035
1296
 
1036
- var styles = {"backdrop":"FreeRoundsIntroModal-module__backdrop___qIgNx","panel":"FreeRoundsIntroModal-module__panel___-zQpe","title":"FreeRoundsIntroModal-module__title___1csj0","details":"FreeRoundsIntroModal-module__details___qXMR-","count":"FreeRoundsIntroModal-module__count___lVep8","stake":"FreeRoundsIntroModal-module__stake___vCcPI","actions":"FreeRoundsIntroModal-module__actions___toCBq","button":"FreeRoundsIntroModal-module__button___bSTmX","playNow":"FreeRoundsIntroModal-module__playNow___iVhdt","playLater":"FreeRoundsIntroModal-module__playLater___l9031"};
1297
+ var styles$1 = {"backdrop":"FreeRoundsIntroModal-module__backdrop___qIgNx","column":"FreeRoundsIntroModal-module__column___YG71X","panel":"FreeRoundsIntroModal-module__panel___-zQpe","header":"FreeRoundsIntroModal-module__header___K5jmV","summary":"FreeRoundsIntroModal-module__summary___qpAbF","accent":"FreeRoundsIntroModal-module__accent___m-2s6","perRound":"FreeRoundsIntroModal-module__perRound___5Sx-v","expiry":"FreeRoundsIntroModal-module__expiry___n-fve","actions":"FreeRoundsIntroModal-module__actions___toCBq","button":"FreeRoundsIntroModal-module__button___bSTmX","playNow":"FreeRoundsIntroModal-module__playNow___iVhdt","playLater":"FreeRoundsIntroModal-module__playLater___l9031"};
1037
1298
 
1038
- const COIN_TYPE_SWEEPS = 0;
1039
- const COIN_TYPE_GOLD = 1;
1040
- const resolveCurrencyLabel = ({ coinType, currency, }) => {
1041
- if (currency) {
1042
- return currency;
1043
- }
1044
- if (coinType === COIN_TYPE_SWEEPS) {
1045
- return "SC";
1046
- }
1047
- if (coinType === COIN_TYPE_GOLD) {
1048
- return "GC";
1049
- }
1050
- return "";
1051
- };
1052
- // Centered "Play Now / Play Later" intro modal. Renders + fires callbacks only;
1053
- // the SDK does no fetching. `onPlayNow` should engage free-round mode + close,
1054
- // `onPlayLater` should close and stay on paid play.
1055
- const FreeRoundsIntroModal = ({ open, roundsRemaining, stakeCents, coinType, currency, decimals = 2, onPlayNow, onPlayLater, }) => {
1299
+ // Centered START intro pop-up — binary engagement decision. Renders + fires
1300
+ // callbacks only; the SDK does no fetching. `onPlayNow` should engage
1301
+ // free-rounds mode + close, `onPlayLater` should close and render zero
1302
+ // free-rounds UI for the rest of the session.
1303
+ const FreeRoundsIntroModal = ({ open, roundsRemaining, stakeCents, coinType, currency, decimals = 2, expiresAt, onPlayNow, onPlayLater, }) => {
1304
+ const { t } = useTranslation();
1056
1305
  const handleClose = () => onPlayLater?.();
1057
- const currencyLabel = resolveCurrencyLabel({ coinType, currency });
1058
- const stakeDisplay = format(stakeCents / 100, decimals);
1059
- return (jsx(Dialog, { open: open, onClose: handleClose, className: styles.backdrop, children: jsxs(DialogPanel, { className: styles.panel, children: [jsx(DialogTitle, { className: styles.title, children: "You have free rounds!" }), jsx("div", { className: styles.count, children: roundsRemaining }), jsxs("div", { className: styles.details, children: [roundsRemaining === 1 ? "free round" : "free rounds", " ready to play"] }), jsxs("div", { className: styles.stake, children: [stakeDisplay, currencyLabel ? ` ${currencyLabel}` : "", " per round"] }), jsxs("div", { className: styles.actions, children: [jsx("button", { type: "button", className: `${styles.button} ${styles.playNow}`, onClick: onPlayNow, children: "PLAY NOW" }), jsx("button", { type: "button", className: `${styles.button} ${styles.playLater}`, onClick: onPlayLater, children: "PLAY LATER" })] })] }) }));
1306
+ const coinLabel = resolveFreeRoundsCoinLabel({ coinType, currency });
1307
+ const valueDecimals = resolveFreeRoundsDecimals({
1308
+ coinType,
1309
+ currency,
1310
+ fallback: decimals,
1311
+ });
1312
+ const perRoundDisplay = format(stakeCents / 100, valueDecimals);
1313
+ const expiresInDays = computeExpiresInDays(expiresAt);
1314
+ return (jsx(Dialog, { open: open, onClose: handleClose, className: styles$1.backdrop, children: jsx("div", { className: styles$1.column, children: jsxs(DialogPanel, { className: styles$1.panel, children: [jsx(DialogTitle, { className: styles$1.header, children: t("freeRoundsModalTitle") }), jsxs("div", { className: styles$1.summary, children: [t("freeRoundsHave"), " ", jsx("span", { className: styles$1.accent, children: roundsRemaining }), " ", roundsRemaining === 1
1315
+ ? t("freeRoundsUnitOne")
1316
+ : t("freeRoundsUnitOther")] }), jsx("div", { className: styles$1.perRound, children: t("freeRoundsPerRound", {
1317
+ amount: `${perRoundDisplay}${coinLabel ? ` ${coinLabel}` : ""}`,
1318
+ }) }), expiresInDays !== null && (jsx("div", { className: styles$1.expiry, children: t("freeRoundsExpiresIn", {
1319
+ days: expiresInDays,
1320
+ unit: expiresInDays === 1 ? t("dayUnitOne") : t("dayUnitOther"),
1321
+ }) })), jsxs("div", { className: styles$1.actions, children: [jsx("button", { type: "button", className: `${styles$1.button} ${styles$1.playNow}`, onClick: onPlayNow, children: t("freeRoundsModalPlayNow") }), jsx("button", { type: "button", className: `${styles$1.button} ${styles$1.playLater}`, onClick: onPlayLater, children: t("freeRoundsModalPlayLater") })] })] }) }) }));
1060
1322
  };
1061
1323
 
1062
1324
  const AutoManualPlayProvider = ({ children, config, }) => {
1325
+ const t = useMemo(() => createTranslator(resolveLanguage(config.language)), [config.language]);
1063
1326
  const [mode, setMode] = useState(GAME_MODE.MANUAL);
1064
1327
  const [autoplayState, setAutoplayState] = useState(AUTO_PLAY_STATE.IDLE);
1065
1328
  const [doubleOrNothingState, setDoubleOrNothingState] = useState(DOUBLE_OR_NOTHING_STATE.IDLE);
@@ -1071,17 +1334,31 @@ const AutoManualPlayProvider = ({ children, config, }) => {
1071
1334
  const isControllerDisabled = config.playOptions.disabledController;
1072
1335
  const isGamePlaying = config.playOptions.isPlaying;
1073
1336
  const isAutoplayPlaying = autoplayState === AUTO_PLAY_STATE.PLAYING;
1074
- // Free rounds are MANUAL-only. While a grant is engaged the player must not
1075
- // be able to flip into autoplay (which would silently drop the manual-gated
1076
- // engaged state and let PAID autoplay start). This is independent of `mode`
1077
- // so it stays true even if a switch attempt slipped through.
1078
- const isFreeRoundsEngaged = Boolean(config.freeRoundsOptions?.engaged &&
1079
- (config.freeRoundsOptions?.roundsRemaining ?? 0) > 0);
1337
+ // Free rounds are MANUAL-only by default. While a grant is engaged the
1338
+ // player must not be able to flip into autoplay (which would silently drop
1339
+ // the manual-gated engaged state and let PAID autoplay start). This is
1340
+ // independent of `mode` so it stays true even if a switch attempt slipped
1341
+ // through. Games whose autoplay submission path funds rounds from engaged
1342
+ // grants opt out of the lock via `supportsGrantFundedAutoplay`.
1343
+ const freeRoundsRemaining = config.freeRoundsOptions?.roundsRemaining ?? 0;
1344
+ const isFreeRoundsEngaged = Boolean(config.freeRoundsOptions?.engaged && freeRoundsRemaining > 0);
1345
+ const supportsGrantFundedAutoplay = Boolean(config.freeRoundsOptions?.supportsGrantFundedAutoplay);
1346
+ const isModeLockedByFreeRounds = isFreeRoundsEngaged && !supportsGrantFundedAutoplay;
1080
1347
  const isSimplified = config.withoutRiskAndAutoplay === true;
1081
1348
  const isClassicGame = Boolean(config?.isClassicGame || config?.classicGame?.forceLeftStyle);
1082
1349
  const togglePlayAmountView = useCallback(() => {
1083
1350
  setDisplayPlayAmountView((v) => !v);
1084
1351
  }, []);
1352
+ // Grant-funded autoplay: while free rounds are engaged the selectable
1353
+ // autoplay count is hard-capped to the remaining grants (covers the default
1354
+ // count and the ∞ option — Infinity clamps down to roundsRemaining, so ∞ is
1355
+ // effectively disabled while engaged). No paid spillover.
1356
+ useEffect(() => {
1357
+ if (!supportsGrantFundedAutoplay || !isFreeRoundsEngaged) {
1358
+ return;
1359
+ }
1360
+ setNumberOfPlays((prev) => prev > freeRoundsRemaining ? freeRoundsRemaining : prev);
1361
+ }, [supportsGrantFundedAutoplay, isFreeRoundsEngaged, freeRoundsRemaining]);
1085
1362
  const startAutoplay = useCallback((numPlays) => {
1086
1363
  setMode(GAME_MODE.AUTOPLAY);
1087
1364
  setAutoplayState(AUTO_PLAY_STATE.SELECTING);
@@ -1121,12 +1398,15 @@ const AutoManualPlayProvider = ({ children, config, }) => {
1121
1398
  setSelection([]);
1122
1399
  }, []);
1123
1400
  const toggleMode = useCallback(() => {
1124
- // No-op while a free-round grant is engaged: manual-only, no-exit. The
1125
- // player cannot switch to autoplay until the grant is consumed/released.
1401
+ // No-op while a free-round grant is engaged WITHOUT the game's
1402
+ // grant-funded-autoplay opt-in: manual-only, no-exit. The player cannot
1403
+ // switch to autoplay until the grant is consumed/released. When the game
1404
+ // sets `supportsGrantFundedAutoplay`, engaged switching is allowed (still
1405
+ // blocked while a round is in flight via the gates above).
1126
1406
  if (isGamePlaying ||
1127
1407
  isControllerDisabled ||
1128
1408
  isAutoplayPlaying ||
1129
- isFreeRoundsEngaged) {
1409
+ isModeLockedByFreeRounds) {
1130
1410
  return;
1131
1411
  }
1132
1412
  setNumberOfPlays(AUTOPLAY_DEFAULT_PLAY_ROUNDS_COUNT);
@@ -1135,7 +1415,7 @@ const AutoManualPlayProvider = ({ children, config, }) => {
1135
1415
  isAutoplayPlaying,
1136
1416
  isControllerDisabled,
1137
1417
  isGamePlaying,
1138
- isFreeRoundsEngaged,
1418
+ isModeLockedByFreeRounds,
1139
1419
  ]);
1140
1420
  const updateSelection = useCallback((values) => {
1141
1421
  if (!isAutoPlaying) {
@@ -1195,42 +1475,60 @@ const AutoManualPlayProvider = ({ children, config, }) => {
1195
1475
  displayPlayAmountView,
1196
1476
  togglePlayAmountView,
1197
1477
  ]);
1198
- return (jsxs(AutoManualPlayStateContext.Provider, { value: contextValue, children: [typeof children === "function" ? children(contextValue) : children, mode === GAME_MODE.MANUAL &&
1199
- config.freeRoundsOptions?.showIntroModal && (jsx("div", { style: {
1200
- "--play-panel-bg": hexToRgb(config.panel.bgColorHex ?? "#01243A"),
1201
- }, children: jsx(FreeRoundsIntroModal, { open: config.freeRoundsOptions.showIntroModal, roundsRemaining: config.freeRoundsOptions.roundsRemaining, totalRounds: config.freeRoundsOptions.totalRounds, stakeCents: config.freeRoundsOptions.stakeCents, coinType: config.freeRoundsOptions.coinType, currency: config.freeRoundsOptions.currency, decimals: config.currencyOptions.current.decimals, onPlayNow: config.freeRoundsOptions.onPlayNow, onPlayLater: config.freeRoundsOptions.onPlayLater }) })), config.playOptions.displayController && (jsx("div", { className: cx$1(styles_ui.base, styles_ui.betForm, {
1202
- [styles_ui.baseForceLeft]: config.classicGame?.forceLeftStyle,
1203
- [styles_ui.isClassicGame]: config.isClassicGame,
1204
- [styles_ui.inActiveRound]: mode === GAME_MODE.MANUAL && isGamePlaying,
1205
- }), style: {
1206
- "--play-bottom": config.panel.bottom ?? 0,
1207
- "--play-panel-bg": hexToRgb(config.panel.bgColorHex ?? "#01243A"),
1208
- "--play-dropdown-bg": hexToRgb(config.classicGame?.dropdown?.bgColorHex ??
1209
- config.dropdown.bgColorHex ??
1210
- "#01243A"),
1211
- "--play-panel-bg-opacity": config.panel.bgColorOpacity ?? 0.5,
1212
- }, children: jsxs("div", { className: cx$1(styles_ui.container, {
1213
- [styles_ui.containerSpread]: config.classicGame?.forceLeftStyle,
1214
- }), children: [jsxs("div", { className: cx$1(styles_ui.controls), children: [jsx(PlayValueList, {}), !isSimplified ? (jsxs("div", { className: cx$1(styles_ui.auto), children: [jsx(DifficultySelector, { playOptions: {
1215
- ...config.playOptions,
1216
- disabledMenu: isControllerDisabled || config.playOptions.disabledMenu,
1217
- }, dropdownConfig: config.dropdown, isClassicGame: isClassicGame }), jsx(InputWithSwitch, { value: numberOfPlays === Infinity ? 0 : numberOfPlays, type: "number", onChange: (e) => setNumberOfPlays(Number(e.currentTarget.value)), placeholder: "Number of Plays", min: 0, max: 99, disabled: isControllerDisabled || mode === GAME_MODE.MANUAL, isClassicGame: isClassicGame, switcherConfig: {
1218
- onSwitch: toggleMode,
1219
- isPlaying: isAutoPlaying || isGamePlaying,
1220
- enabled: mode !== GAME_MODE.MANUAL,
1221
- disabled: isControllerDisabled ||
1222
- isAutoplayPlaying ||
1223
- isFreeRoundsEngaged,
1224
- }, children: !isClassicGame && (jsx("span", { className: cx$1({
1225
- [styles_ui.disabled]: mode !== GAME_MODE.AUTOPLAY ||
1226
- numberOfPlays !== Infinity ||
1227
- isAutoplayPlaying,
1228
- }), children: "\u221E" })) })] })) : null, mode === GAME_MODE.MANUAL ? (jsx(ManualPlayController, {})) : (jsx(AutoPlayController, {}))] }), jsx(WidgetContainer, { state: autoplayState, displayPlayAmountView: displayPlayAmountView, widgets: {
1229
- left: config.leftWidgets,
1230
- center: config.centerWidgets,
1231
- right: config.rightWidgets,
1232
- } })] }) }))] }));
1478
+ return (jsx(I18nProvider, { language: config.language, children: jsxs(AutoManualPlayStateContext.Provider, { value: contextValue, children: [typeof children === "function" ? children(contextValue) : children, mode === GAME_MODE.MANUAL &&
1479
+ config.freeRoundsOptions?.showIntroModal && (jsx("div", { style: {
1480
+ "--play-panel-bg": hexToRgb(config.panel.bgColorHex ?? "#221062"),
1481
+ }, children: jsx(FreeRoundsIntroModal, { open: config.freeRoundsOptions.showIntroModal, roundsRemaining: config.freeRoundsOptions.roundsRemaining, totalRounds: config.freeRoundsOptions.totalRounds, stakeCents: config.freeRoundsOptions.stakeCents, coinType: config.freeRoundsOptions.coinType, currency: config.freeRoundsOptions.currency, decimals: config.currencyOptions.current.decimals, expiresAt: config.freeRoundsOptions.expiresAt, onPlayNow: config.freeRoundsOptions.onPlayNow, onPlayLater: config.freeRoundsOptions.onPlayLater }) })), config.playOptions.displayController && (jsx("div", { className: cx$1(styles_ui.base, styles_ui.betForm, {
1482
+ [styles_ui.baseForceLeft]: config.classicGame?.forceLeftStyle,
1483
+ [styles_ui.isClassicGame]: config.isClassicGame,
1484
+ [styles_ui.inActiveRound]: mode === GAME_MODE.MANUAL && isGamePlaying,
1485
+ }), style: {
1486
+ "--play-bottom": config.panel.bottom ?? 0,
1487
+ "--play-panel-bg": hexToRgb(config.panel.bgColorHex ?? "#221062"),
1488
+ "--play-dropdown-bg": hexToRgb(config.classicGame?.dropdown?.bgColorHex ??
1489
+ config.dropdown.bgColorHex ??
1490
+ "#150C47"),
1491
+ "--play-panel-bg-opacity": config.panel.bgColorOpacity ?? 0.5,
1492
+ }, children: jsxs("div", { className: cx$1(styles_ui.container, {
1493
+ [styles_ui.containerSpread]: config.classicGame?.forceLeftStyle,
1494
+ }), children: [jsxs("div", { className: cx$1(styles_ui.controls), children: [jsx(PlayValueList, {}), !isSimplified ? (jsxs("div", { className: cx$1(styles_ui.auto), children: [jsx(DifficultySelector, { playOptions: {
1495
+ ...config.playOptions,
1496
+ disabledMenu: isControllerDisabled ||
1497
+ config.playOptions.disabledMenu,
1498
+ }, dropdownConfig: config.dropdown, isClassicGame: isClassicGame }), jsx(InputWithSwitch, { value: numberOfPlays === Infinity ? 0 : numberOfPlays, type: "number", onChange: (e) => setNumberOfPlays(Number(e.currentTarget.value)), placeholder: t("numberOfPlays"), min: 0, max: 99, disabled: isControllerDisabled || mode === GAME_MODE.MANUAL, isClassicGame: isClassicGame, switcherConfig: {
1499
+ onSwitch: toggleMode,
1500
+ isPlaying: isAutoPlaying || isGamePlaying,
1501
+ enabled: mode !== GAME_MODE.MANUAL,
1502
+ disabled: isControllerDisabled ||
1503
+ isAutoplayPlaying ||
1504
+ isModeLockedByFreeRounds,
1505
+ }, children: !isClassicGame && (jsx("span", { className: cx$1({
1506
+ [styles_ui.disabled]: mode !== GAME_MODE.AUTOPLAY ||
1507
+ numberOfPlays !== Infinity ||
1508
+ isAutoplayPlaying,
1509
+ }), children: "\u221E" })) })] })) : null, mode === GAME_MODE.MANUAL ? (jsx(ManualPlayController, {})) : (jsx(AutoPlayController, {}))] }), isFreeRoundsEngaged && (jsxs("div", { className: styles_ui.freeRoundsFooter, children: [jsx("span", { className: styles_ui.freeRoundsFooterCount, children: config.freeRoundsOptions?.roundsRemaining }), " ", config.freeRoundsOptions?.roundsRemaining === 1
1510
+ ? t("freeRoundLeftOne")
1511
+ : t("freeRoundLeftOther")] })), jsx(WidgetContainer, { state: autoplayState, displayPlayAmountView: displayPlayAmountView, widgets: {
1512
+ left: config.leftWidgets,
1513
+ center: config.centerWidgets,
1514
+ right: config.rightWidgets,
1515
+ } })] }) }))] }) }));
1516
+ };
1517
+
1518
+ var styles = {"backdrop":"FreeRoundsSummaryModal-module__backdrop___tNi3I","panel":"FreeRoundsSummaryModal-module__panel___2MIEp","header":"FreeRoundsSummaryModal-module__header___-BkpP","summary":"FreeRoundsSummaryModal-module__summary___YExX9","accent":"FreeRoundsSummaryModal-module__accent___z9aBF","hint":"FreeRoundsSummaryModal-module__hint___CYWqA","close":"FreeRoundsSummaryModal-module__close___4-irR"};
1519
+
1520
+ // END pop-up — shown by the game after the last free round settles. The game
1521
+ // owns visibility: it mounts open when the play-through completes, unmount
1522
+ // onClose.
1523
+ const FreeRoundsSummaryModal = ({ totalWinAmount, coin, roundsPlayed, decimals, onClose, }) => {
1524
+ const { t } = useTranslation();
1525
+ // Shared monetary formatter — truncates (never rounds up) so the summary
1526
+ // can't display more winnings than were actually credited.
1527
+ const winDisplay = format(totalWinAmount, decimals ?? resolveCoinLabelDecimals(coin));
1528
+ return (jsx(Dialog, { open: true, onClose: onClose, className: styles.backdrop, children: jsxs(DialogPanel, { className: styles.panel, children: [jsx(DialogTitle, { className: styles.header, children: t("freeRoundsModalTitle") }), jsxs("div", { className: styles.summary, children: [t("freeRoundsSummaryWon"), " ", jsxs("span", { className: styles.accent, children: [winDisplay, " ", coin] }), " ", t("freeRoundsSummaryIn"), " ", roundsPlayed, " ", roundsPlayed === 1
1529
+ ? t("freeRoundsSummaryUnitOne")
1530
+ : t("freeRoundsSummaryUnitOther")] }), jsx("div", { className: styles.hint, children: t("freeRoundsSummaryCredited") }), jsx("button", { type: "button", className: styles.close, onClick: onClose, children: t("freeRoundsSummaryClose") })] }) }));
1233
1531
  };
1234
1532
 
1235
- export { AUTO_PLAY_STATE, AutoManualPlayProvider, DOUBLE_OR_NOTHING_STATE, FreeRoundsIntroModal, GAME_MODE, WIDGET, canAutoplaySpaceTrigger, createAutoplayButtonAction, createManualCashoutClickHandler, format, isAutoplayButtonDisabled, isManualCashoutDisabled };
1533
+ export { AUTO_PLAY_STATE, AutoManualPlayProvider, DEFAULT_RISK_COLORS, DOUBLE_OR_NOTHING_STATE, FreeRoundsIntroModal, FreeRoundsSummaryModal, GAME_MODE, I18nProvider, WIDGET, canAutoplaySpaceTrigger, createAutoplayButtonAction, createManualCashoutClickHandler, createTranslator, format, isAutoplayButtonDisabled, isManualCashoutDisabled, useTranslation };
1236
1534
  //# sourceMappingURL=index.mjs.map