@ton/appkit-react 1.0.0-alpha.0 → 1.0.0-alpha.2

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 (128) hide show
  1. package/README.md +7 -6
  2. package/dist/esm/components/shared/amount-preview/amount-preview.js +17 -0
  3. package/dist/esm/components/shared/amount-preview/amount-preview.js.map +1 -0
  4. package/dist/esm/components/shared/amount-preview/amount-preview.module.css +40 -0
  5. package/dist/esm/{features/balances/components/balance-badge → components/shared/amount-preview}/index.js +1 -1
  6. package/dist/esm/components/shared/amount-preview/index.js.map +1 -0
  7. package/dist/esm/components/shared/flow-preview/flow-preview.js +24 -0
  8. package/dist/esm/components/shared/flow-preview/flow-preview.js.map +1 -0
  9. package/dist/esm/components/shared/flow-preview/flow-preview.module.css +37 -0
  10. package/dist/esm/components/shared/flow-preview/index.js +9 -0
  11. package/dist/esm/components/shared/flow-preview/index.js.map +1 -0
  12. package/dist/esm/components/shared/settings-button/settings-button.js +1 -1
  13. package/dist/esm/components/shared/settings-button/settings-button.js.map +1 -1
  14. package/dist/esm/components/ui/button/button.module.css +1 -1
  15. package/dist/esm/components/ui/logo/logo.module.css +1 -3
  16. package/dist/esm/components/ui/modal/modal.module.css +1 -1
  17. package/dist/esm/components/ui/tabs/tabs.module.css +1 -1
  18. package/dist/esm/features/balances/components/send-jetton-button/send-jetton-button.js +3 -3
  19. package/dist/esm/features/balances/components/send-jetton-button/send-jetton-button.js.map +1 -1
  20. package/dist/esm/features/balances/index.js +0 -1
  21. package/dist/esm/features/balances/index.js.map +1 -1
  22. package/dist/esm/features/staking/components/select-unstake-mode/select-unstake-mode.js +2 -1
  23. package/dist/esm/features/staking/components/select-unstake-mode/select-unstake-mode.js.map +1 -1
  24. package/dist/esm/features/staking/components/select-unstake-mode/select-unstake-mode.module.css +2 -6
  25. package/dist/esm/features/staking/components/staking-confirm-modal/index.js +9 -0
  26. package/dist/esm/features/staking/components/staking-confirm-modal/index.js.map +1 -0
  27. package/dist/esm/features/staking/components/staking-confirm-modal/staking-confirm-modal.js +46 -0
  28. package/dist/esm/features/staking/components/staking-confirm-modal/staking-confirm-modal.js.map +1 -0
  29. package/dist/esm/features/staking/components/staking-confirm-modal/staking-confirm-modal.module.css +11 -0
  30. package/dist/esm/features/staking/components/staking-widget-provider/staking-widget-provider.js +27 -4
  31. package/dist/esm/features/staking/components/staking-widget-provider/staking-widget-provider.js.map +1 -1
  32. package/dist/esm/features/staking/components/staking-widget-provider/use-staking-validation.js +12 -3
  33. package/dist/esm/features/staking/components/staking-widget-provider/use-staking-validation.js.map +1 -1
  34. package/dist/esm/features/staking/components/staking-widget-ui/staking-widget-ui.js +16 -4
  35. package/dist/esm/features/staking/components/staking-widget-ui/staking-widget-ui.js.map +1 -1
  36. package/dist/esm/features/staking/components/staking-widget-ui/staking-widget-ui.module.css +4 -0
  37. package/dist/esm/features/staking/hooks/use-build-stake-transaction.js +2 -2
  38. package/dist/esm/features/staking/hooks/use-build-stake-transaction.js.map +1 -1
  39. package/dist/esm/features/staking/utils/map-staking-error.js +6 -4
  40. package/dist/esm/features/staking/utils/map-staking-error.js.map +1 -1
  41. package/dist/esm/features/swap/components/swap-confirm-modal/index.js +9 -0
  42. package/dist/esm/features/swap/components/swap-confirm-modal/index.js.map +1 -0
  43. package/dist/esm/features/swap/components/swap-confirm-modal/swap-confirm-modal.js +12 -0
  44. package/dist/esm/features/swap/components/swap-confirm-modal/swap-confirm-modal.js.map +1 -0
  45. package/dist/esm/features/swap/components/swap-confirm-modal/swap-confirm-modal.module.css +7 -0
  46. package/dist/esm/features/swap/components/swap-widget-provider/swap-widget-provider.js +28 -6
  47. package/dist/esm/features/swap/components/swap-widget-provider/swap-widget-provider.js.map +1 -1
  48. package/dist/esm/features/swap/components/swap-widget-provider/use-swap-validation.js +12 -3
  49. package/dist/esm/features/swap/components/swap-widget-provider/use-swap-validation.js.map +1 -1
  50. package/dist/esm/features/swap/components/swap-widget-ui/swap-widget-ui.js +16 -4
  51. package/dist/esm/features/swap/components/swap-widget-ui/swap-widget-ui.js.map +1 -1
  52. package/dist/esm/features/swap/utils/map-swap-error.js +10 -8
  53. package/dist/esm/features/swap/utils/map-swap-error.js.map +1 -1
  54. package/dist/esm/locales/en.js +9 -0
  55. package/dist/esm/locales/en.js.map +1 -1
  56. package/dist/esm/styles/index.css +3 -3
  57. package/dist/esm/utils/map-defi-error.js +7 -7
  58. package/dist/esm/utils/map-defi-error.js.map +1 -1
  59. package/dist/types/components/shared/amount-preview/amount-preview.d.ts +24 -0
  60. package/dist/types/components/shared/amount-preview/amount-preview.d.ts.map +1 -0
  61. package/dist/types/{features/balances/components/balance-badge → components/shared/amount-preview}/index.d.ts +1 -1
  62. package/dist/types/components/shared/amount-preview/index.d.ts.map +1 -0
  63. package/dist/types/components/shared/flow-preview/flow-preview.d.ts +18 -0
  64. package/dist/types/components/shared/flow-preview/flow-preview.d.ts.map +1 -0
  65. package/dist/types/components/shared/flow-preview/index.d.ts +9 -0
  66. package/dist/types/components/shared/flow-preview/index.d.ts.map +1 -0
  67. package/dist/types/features/balances/index.d.ts +0 -1
  68. package/dist/types/features/balances/index.d.ts.map +1 -1
  69. package/dist/types/features/staking/components/select-unstake-mode/select-unstake-mode.d.ts.map +1 -1
  70. package/dist/types/features/staking/components/staking-confirm-modal/index.d.ts +9 -0
  71. package/dist/types/features/staking/components/staking-confirm-modal/index.d.ts.map +1 -0
  72. package/dist/types/features/staking/components/staking-confirm-modal/staking-confirm-modal.d.ts +23 -0
  73. package/dist/types/features/staking/components/staking-confirm-modal/staking-confirm-modal.d.ts.map +1 -0
  74. package/dist/types/features/staking/components/staking-widget-provider/staking-widget-provider.d.ts.map +1 -1
  75. package/dist/types/features/staking/components/staking-widget-provider/use-staking-validation.d.ts +3 -1
  76. package/dist/types/features/staking/components/staking-widget-provider/use-staking-validation.d.ts.map +1 -1
  77. package/dist/types/features/staking/components/staking-widget-ui/staking-widget-ui.d.ts.map +1 -1
  78. package/dist/types/features/staking/hooks/use-build-stake-transaction.d.ts +3 -2
  79. package/dist/types/features/staking/hooks/use-build-stake-transaction.d.ts.map +1 -1
  80. package/dist/types/features/staking/utils/map-staking-error.d.ts +5 -3
  81. package/dist/types/features/staking/utils/map-staking-error.d.ts.map +1 -1
  82. package/dist/types/features/swap/components/swap-confirm-modal/index.d.ts +9 -0
  83. package/dist/types/features/swap/components/swap-confirm-modal/index.d.ts.map +1 -0
  84. package/dist/types/features/swap/components/swap-confirm-modal/swap-confirm-modal.d.ts +26 -0
  85. package/dist/types/features/swap/components/swap-confirm-modal/swap-confirm-modal.d.ts.map +1 -0
  86. package/dist/types/features/swap/components/swap-widget-provider/swap-widget-provider.d.ts.map +1 -1
  87. package/dist/types/features/swap/components/swap-widget-provider/use-swap-validation.d.ts +4 -1
  88. package/dist/types/features/swap/components/swap-widget-provider/use-swap-validation.d.ts.map +1 -1
  89. package/dist/types/features/swap/components/swap-widget-ui/swap-widget-ui.d.ts.map +1 -1
  90. package/dist/types/features/swap/utils/map-swap-error.d.ts +4 -2
  91. package/dist/types/features/swap/utils/map-swap-error.d.ts.map +1 -1
  92. package/dist/types/libs/i18n.d.ts +9 -0
  93. package/dist/types/libs/i18n.d.ts.map +1 -1
  94. package/dist/types/locales/en.d.ts +9 -0
  95. package/dist/types/locales/en.d.ts.map +1 -1
  96. package/package.json +12 -12
  97. package/src/components/shared/amount-preview/amount-preview.tsx +74 -0
  98. package/src/{features/balances/components/balance-badge → components/shared/amount-preview}/index.ts +1 -1
  99. package/src/components/shared/flow-preview/flow-preview.tsx +64 -0
  100. package/src/components/shared/flow-preview/index.ts +9 -0
  101. package/src/components/shared/settings-button/settings-button.tsx +1 -1
  102. package/src/features/balances/components/send-jetton-button/send-jetton-button.tsx +3 -3
  103. package/src/features/balances/index.ts +0 -1
  104. package/src/features/staking/components/select-unstake-mode/select-unstake-mode.tsx +12 -4
  105. package/src/features/staking/components/staking-confirm-modal/index.ts +9 -0
  106. package/src/features/staking/components/staking-confirm-modal/staking-confirm-modal.tsx +121 -0
  107. package/src/features/staking/components/staking-widget-provider/staking-widget-provider.tsx +39 -4
  108. package/src/features/staking/components/staking-widget-provider/use-staking-validation.ts +14 -2
  109. package/src/features/staking/components/staking-widget-ui/staking-widget-ui.tsx +39 -13
  110. package/src/features/staking/hooks/use-build-stake-transaction.ts +7 -2
  111. package/src/features/staking/utils/map-staking-error.ts +6 -4
  112. package/src/features/swap/components/swap-confirm-modal/index.ts +9 -0
  113. package/src/features/swap/components/swap-confirm-modal/swap-confirm-modal.tsx +75 -0
  114. package/src/features/swap/components/swap-widget-provider/swap-widget-provider.tsx +40 -6
  115. package/src/features/swap/components/swap-widget-provider/use-swap-validation.ts +17 -2
  116. package/src/features/swap/components/swap-widget-ui/swap-widget-ui.tsx +30 -3
  117. package/src/features/swap/utils/map-swap-error.ts +10 -8
  118. package/src/locales/en.ts +9 -0
  119. package/src/utils/map-defi-error.ts +7 -7
  120. package/dist/esm/features/balances/components/balance-badge/balance-badge.js +0 -33
  121. package/dist/esm/features/balances/components/balance-badge/balance-badge.js.map +0 -1
  122. package/dist/esm/features/balances/components/balance-badge/balance-badge.module.css +0 -21
  123. package/dist/esm/features/balances/components/balance-badge/index.js.map +0 -1
  124. package/dist/esm/tsconfig.build.tsbuildinfo +0 -1
  125. package/dist/types/features/balances/components/balance-badge/balance-badge.d.ts +0 -21
  126. package/dist/types/features/balances/components/balance-badge/balance-badge.d.ts.map +0 -1
  127. package/dist/types/features/balances/components/balance-badge/index.d.ts.map +0 -1
  128. package/src/features/balances/components/balance-badge/balance-badge.tsx +0 -47
@@ -17,6 +17,7 @@ import { ChevronDownIcon } from '../../../../components/ui/icons';
17
17
  import { useI18n } from '../../../settings/hooks/use-i18n';
18
18
  import { formatAmount } from '../staking-info/utils';
19
19
  import styles from './select-unstake-mode.module.css';
20
+ import { Button } from '../../../../components/ui/button';
20
21
 
21
22
  export interface SelectUnstakeModeProps extends ComponentProps<'div'> {
22
23
  value: UnstakeModes;
@@ -84,13 +85,20 @@ export const SelectUnstakeMode: FC<SelectUnstakeModeProps> = ({
84
85
 
85
86
  return (
86
87
  <div className={clsx(styles.root, className)} {...props}>
87
- <button type="button" className={styles.header} onClick={() => setOpen((v) => !v)}>
88
+ <div className={styles.header}>
88
89
  <span className={styles.headerLabel}>{t('staking.unstakeType')}</span>
89
- <span className={styles.headerValue}>
90
+
91
+ <Button
92
+ variant="gray"
93
+ size="s"
94
+ borderRadius="full"
95
+ className={styles.headerValue}
96
+ onClick={() => setOpen((v) => !v)}
97
+ >
90
98
  {selectedLabel}
91
99
  <ChevronDownIcon size={16} className={clsx(styles.chevron, open && styles.chevronOpen)} />
92
- </span>
93
- </button>
100
+ </Button>
101
+ </div>
94
102
 
95
103
  <Collapsible open={open}>
96
104
  <div className={styles.options}>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) TonTech.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ export * from './staking-confirm-modal';
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Copyright (c) TonTech.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type { FC } from 'react';
10
+ import type {
11
+ JettonInfo,
12
+ Network,
13
+ StakingProviderInfo,
14
+ StakingProviderMetadata,
15
+ StakingQuote,
16
+ StakingQuoteDirection,
17
+ StakingTokenInfo,
18
+ } from '@ton/appkit';
19
+
20
+ import { Modal } from '../../../../components/ui/modal/modal';
21
+ import { Button } from '../../../../components/ui/button';
22
+ import { AmountPreview } from '../../../../components/shared/amount-preview';
23
+ import { FlowPreview } from '../../../../components/shared/flow-preview';
24
+ import type { AppkitUIToken } from '../../../../types/appkit-ui-token';
25
+ import { useJettonInfo } from '../../../jettons';
26
+ import { useI18n } from '../../../settings/hooks/use-i18n';
27
+ import { StakingInfo } from '../staking-info';
28
+ import styles from './staking-confirm-modal.module.css';
29
+
30
+ export interface StakingConfirmModalProps {
31
+ open: boolean;
32
+ onClose: () => void;
33
+ onConfirm: () => void;
34
+ direction: StakingQuoteDirection;
35
+ network: Network | undefined;
36
+ quote: StakingQuote | undefined;
37
+ providerInfo: StakingProviderInfo | undefined;
38
+ providerMetadata: StakingProviderMetadata | undefined;
39
+ isProviderInfoLoading: boolean;
40
+ isQuoteLoading: boolean;
41
+ }
42
+
43
+ /**
44
+ * Adapter from staking-domain token shape (`StakingTokenInfo`) to the shared
45
+ * `AppkitUIToken` shape consumed by AmountPreview/FlowPreview. `name` is taken
46
+ * from the resolved jetton metadata when available, falling back to ticker.
47
+ */
48
+ const toUIToken = (
49
+ token: StakingTokenInfo | undefined,
50
+ jettonInfo: JettonInfo | null | undefined,
51
+ network: Network | undefined,
52
+ ): AppkitUIToken | undefined => {
53
+ if (!token || !network) return undefined;
54
+ return {
55
+ symbol: token.ticker,
56
+ name: jettonInfo?.name ?? token.ticker,
57
+ decimals: token.decimals,
58
+ address: token.address,
59
+ logo: token.address === 'ton' ? undefined : jettonInfo?.image,
60
+ network,
61
+ };
62
+ };
63
+
64
+ export const StakingConfirmModal: FC<StakingConfirmModalProps> = ({
65
+ open,
66
+ onClose,
67
+ onConfirm,
68
+ direction,
69
+ network,
70
+ quote,
71
+ providerInfo,
72
+ providerMetadata,
73
+ isProviderInfoLoading,
74
+ isQuoteLoading,
75
+ }) => {
76
+ const { t } = useI18n();
77
+
78
+ const stakeAddress = providerMetadata?.stakeToken.address;
79
+ const receiveAddress = providerMetadata?.receiveToken?.address;
80
+
81
+ const { data: stakeJettonInfo } = useJettonInfo({
82
+ address: stakeAddress,
83
+ query: { enabled: !!stakeAddress && stakeAddress !== 'ton' },
84
+ });
85
+ const { data: receiveJettonInfo } = useJettonInfo({
86
+ address: receiveAddress,
87
+ query: { enabled: !!receiveAddress && receiveAddress !== 'ton' },
88
+ });
89
+
90
+ const stakeToken = toUIToken(providerMetadata?.stakeToken, stakeJettonInfo, network);
91
+ const receiveToken = toUIToken(providerMetadata?.receiveToken, receiveJettonInfo, network);
92
+
93
+ const title = direction === 'stake' ? t('staking.confirmStakingTitle') : t('staking.confirmUnstakingTitle');
94
+
95
+ const amountIn = quote?.amountIn ?? '0';
96
+ const amountOut = quote?.amountOut ?? '0';
97
+
98
+ return (
99
+ <Modal open={open} onOpenChange={(isOpen) => !isOpen && onClose()} title={title}>
100
+ {direction === 'stake' ? (
101
+ <AmountPreview className={styles.singleAmount} amount={amountIn} token={stakeToken} />
102
+ ) : (
103
+ <FlowPreview fromAmount={amountIn} toAmount={amountOut} fromToken={receiveToken} toToken={stakeToken} />
104
+ )}
105
+
106
+ <StakingInfo
107
+ className={styles.info}
108
+ quote={quote}
109
+ isQuoteLoading={isQuoteLoading}
110
+ providerInfo={providerInfo}
111
+ providerMetadata={providerMetadata}
112
+ isProviderInfoLoading={isProviderInfoLoading}
113
+ direction={direction}
114
+ />
115
+
116
+ <Button className={styles.confirmButton} variant="fill" size="l" fullWidth onClick={onConfirm}>
117
+ {t('staking.confirm')}
118
+ </Button>
119
+ </Modal>
120
+ );
121
+ };
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import { createContext, useCallback, useContext, useMemo, useState } from 'react';
9
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
10
10
  import type { FC, PropsWithChildren } from 'react';
11
11
  import type { Network, StakingProvider, StakingQuoteDirection, TonShortfall } from '@ton/appkit';
12
12
  import {
@@ -224,8 +224,39 @@ export const StakingWidgetProvider: FC<StakingProviderProps> = ({ children, netw
224
224
  query: { refetchInterval: 5000 },
225
225
  });
226
226
 
227
- const { mutateAsync: buildTransaction } = useBuildStakeTransaction();
228
- const { mutateAsync: sendTransaction, isPending: isSendingTransaction } = useSendTransaction();
227
+ const {
228
+ mutateAsync: buildTransaction,
229
+ isPending: isBuildingTransaction,
230
+ error: buildError,
231
+ reset: resetBuild,
232
+ } = useBuildStakeTransaction({ mutation: { networkMode: 'always' } });
233
+ const {
234
+ mutateAsync: sendTransaction,
235
+ isPending: isSendingPending,
236
+ error: sendMutationError,
237
+ reset: resetSend,
238
+ } = useSendTransaction({ mutation: { networkMode: 'always' } });
239
+ const isSendingTransaction = isBuildingTransaction || isSendingPending;
240
+ const sendError = sendMutationError ?? buildError;
241
+
242
+ const resetSendError = useCallback(() => {
243
+ resetBuild();
244
+ resetSend();
245
+ }, [resetBuild, resetSend]);
246
+
247
+ // Drop the previous send error when the user changes anything that invalidates it —
248
+ // the next attempt is conceptually a new stake, no need to keep the old message on screen.
249
+ useEffect(() => {
250
+ resetSendError();
251
+ }, [direction, amount, isReversed, resetSendError]);
252
+
253
+ // Auto-clear the send error after a short delay so a stale failure doesn't linger in the
254
+ // submit button — the user is expected to act on it within seconds or move on.
255
+ useEffect(() => {
256
+ if (!sendError) return;
257
+ const id = setTimeout(resetSendError, 5000);
258
+ return () => clearTimeout(id);
259
+ }, [sendError, resetSendError]);
229
260
 
230
261
  const amountDecimals = useMemo(() => {
231
262
  const unstakeDecimals = isReversed
@@ -256,7 +287,10 @@ export const StakingWidgetProvider: FC<StakingProviderProps> = ({ children, netw
256
287
  data: quote,
257
288
  isFetching: isQuoteLoading,
258
289
  error: quoteError,
259
- } = useStakingQuote({ ...quoteParamsDebounced, query: { enabled: isNetworkSupported } });
290
+ } = useStakingQuote({
291
+ ...quoteParamsDebounced,
292
+ query: { enabled: isNetworkSupported, networkMode: 'always', retry: false, gcTime: 0 },
293
+ });
260
294
 
261
295
  const reversedAmount = useMemo(() => {
262
296
  if (direction === 'unstake' && isReversed) return quote?.amountIn || '0';
@@ -337,6 +371,7 @@ export const StakingWidgetProvider: FC<StakingProviderProps> = ({ children, netw
337
371
  amountDebounced: quoteParamsDebounced.amount || '',
338
372
  balance,
339
373
  quoteError,
374
+ sendError,
340
375
  direction,
341
376
  stakedBalance: stakedBalanceData?.stakedBalance,
342
377
  quote,
@@ -18,6 +18,8 @@ interface UseStakingValidationOptions {
18
18
  balance: string | undefined;
19
19
  quote?: StakingQuote;
20
20
  quoteError: Error | null;
21
+ /** Error from the build/send mutation. Takes priority over input validation but does not block submit. */
22
+ sendError: Error | null;
21
23
  direction: StakingQuoteDirection;
22
24
  amountDecimals?: number;
23
25
  isReversed: boolean;
@@ -31,13 +33,16 @@ export const useStakingValidation = ({
31
33
  balance,
32
34
  quote,
33
35
  quoteError,
36
+ sendError,
34
37
  direction,
35
38
  amountDecimals,
36
39
  isReversed,
37
40
  stakedBalance,
38
41
  isNetworkSupported,
39
42
  }: UseStakingValidationOptions) => {
40
- const error: string | null = useMemo(() => {
43
+ // Input-side validation that blocks submission. `sendError` is intentionally NOT considered
44
+ // here — a previous failed attempt shouldn't lock the button against a retry.
45
+ const blockingError: string | null = useMemo(() => {
41
46
  if (!isNetworkSupported) return 'defi.unsupportedNetwork';
42
47
 
43
48
  if ((parseFloat(amount) || 0) <= 0) return null;
@@ -73,7 +78,14 @@ export const useStakingValidation = ({
73
78
  amountDecimals,
74
79
  ]);
75
80
 
76
- const canSubmit = (parseFloat(amount) || 0) > 0 && error === null;
81
+ // The user-visible error: build/send failure (most recent user action) wins over background
82
+ // validation noise; falls back to validation when no send error is active.
83
+ const error = useMemo<string | null>(() => {
84
+ if (sendError) return mapStakingError(sendError, 'staking.sendFailed');
85
+ return blockingError;
86
+ }, [sendError, blockingError]);
87
+
88
+ const canSubmit = (parseFloat(amount) || 0) > 0 && blockingError === null && quote !== undefined;
77
89
 
78
90
  return { error, canSubmit };
79
91
  };
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import { useMemo, useState } from 'react';
9
+ import { useCallback, useMemo, useState } from 'react';
10
10
  import type { ComponentProps, FC, ReactNode } from 'react';
11
11
  import type { StakingQuoteDirection } from '@ton/appkit';
12
12
  import clsx from 'clsx';
@@ -15,6 +15,7 @@ import { CenteredAmountInput } from '../../../../components/ui/centered-amount-i
15
15
  import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/tabs';
16
16
  import { useI18n } from '../../../settings/hooks/use-i18n';
17
17
  import { StakingBalanceBlock } from '../staking-balance-block';
18
+ import { StakingConfirmModal } from '../staking-confirm-modal';
18
19
  import { StakingInfo } from '../staking-info';
19
20
  import { SelectUnstakeMode } from '../select-unstake-mode';
20
21
  import { StakingSettingsModal } from '../staking-settings-modal';
@@ -66,14 +67,25 @@ export const StakingWidgetUI: FC<StakingWidgetRenderProps> = ({
66
67
  const { t } = useI18n();
67
68
 
68
69
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
70
+ const [isConfirmOpen, setIsConfirmOpen] = useState(false);
69
71
 
70
72
  const receiveToken = providerMetadata?.receiveToken;
71
73
  const stakeToken = providerMetadata?.stakeToken;
72
74
 
73
75
  const buttonText = useMemo(() => {
76
+ if (isSendingTransaction || isQuoteLoading) return t('staking.loading');
74
77
  if (error) return t(error);
75
78
  return direction === 'stake' ? t('staking.continue') : t('staking.unstake');
76
- }, [error, direction, t]);
79
+ }, [isSendingTransaction, isQuoteLoading, error, direction, t]);
80
+
81
+ // Close the modal immediately; the build/send result (including errors) is surfaced
82
+ // back in the widget's main button via the `error` from the provider.
83
+ const handleConfirm = useCallback(() => {
84
+ setIsConfirmOpen(false);
85
+ sendTransaction().catch(() => {
86
+ // Error is captured by the mutation and shown through the validator's `error` output.
87
+ });
88
+ }, [sendTransaction]);
77
89
 
78
90
  const submitActions: ReactNode = (
79
91
  <div className={styles.actions}>
@@ -82,7 +94,7 @@ export const StakingWidgetUI: FC<StakingWidgetRenderProps> = ({
82
94
  size="l"
83
95
  fullWidth
84
96
  disabled={!canSubmit || isQuoteLoading || isSendingTransaction}
85
- onClick={sendTransaction}
97
+ onClick={() => setIsConfirmOpen(true)}
86
98
  >
87
99
  {buttonText}
88
100
  </ButtonWithConnect>
@@ -127,7 +139,7 @@ export const StakingWidgetUI: FC<StakingWidgetRenderProps> = ({
127
139
  </TabsContent>
128
140
 
129
141
  {/* ── UNSTAKE TAB ── */}
130
- <TabsContent className={styles.tab} value="unstake">
142
+ <TabsContent value="unstake">
131
143
  <div className={styles.content}>
132
144
  <div className={styles.inputSection}>
133
145
  <CenteredAmountInput
@@ -167,17 +179,18 @@ export const StakingWidgetUI: FC<StakingWidgetRenderProps> = ({
167
179
  />
168
180
  </div>
169
181
  </TabsContent>
170
-
171
- <StakingInfo
172
- quote={quote}
173
- isQuoteLoading={isQuoteLoading}
174
- providerInfo={providerInfo}
175
- providerMetadata={providerMetadata}
176
- isProviderInfoLoading={isProviderInfoLoading}
177
- direction={direction}
178
- />
179
182
  </Tabs>
180
183
 
184
+ <StakingInfo
185
+ className={styles.info}
186
+ quote={quote}
187
+ isQuoteLoading={isQuoteLoading}
188
+ providerInfo={providerInfo}
189
+ providerMetadata={providerMetadata}
190
+ isProviderInfoLoading={isProviderInfoLoading}
191
+ direction={direction}
192
+ />
193
+
181
194
  <LowBalanceModal
182
195
  open={isLowBalanceWarningOpen}
183
196
  mode={lowBalanceMode}
@@ -194,6 +207,19 @@ export const StakingWidgetUI: FC<StakingWidgetRenderProps> = ({
194
207
  onProviderChange={setStakingProviderId}
195
208
  network={network}
196
209
  />
210
+
211
+ <StakingConfirmModal
212
+ open={isConfirmOpen}
213
+ onClose={() => setIsConfirmOpen(false)}
214
+ onConfirm={handleConfirm}
215
+ direction={direction}
216
+ network={network}
217
+ quote={quote}
218
+ providerInfo={providerInfo}
219
+ providerMetadata={providerMetadata}
220
+ isProviderInfoLoading={isProviderInfoLoading}
221
+ isQuoteLoading={isQuoteLoading}
222
+ />
197
223
  </div>
198
224
  );
199
225
  };
@@ -11,12 +11,15 @@ import { buildStakeTransactionMutationOptions } from '@ton/appkit/queries';
11
11
  import type {
12
12
  BuildStakeTransactionData,
13
13
  BuildStakeTransactionErrorType,
14
+ BuildStakeTransactionMutationOptions,
14
15
  BuildStakeTransactionVariables,
15
16
  } from '@ton/appkit/queries';
16
17
 
17
18
  import { useAppKit } from '../../settings';
18
19
  import { useMutation } from '../../../libs/query';
19
20
 
21
+ export type UseBuildStakeTransactionParameters<context = unknown> = BuildStakeTransactionMutationOptions<context>;
22
+
20
23
  export type UseBuildStakeTransactionReturnType<context = unknown> = UseMutationResult<
21
24
  BuildStakeTransactionData,
22
25
  BuildStakeTransactionErrorType,
@@ -27,7 +30,9 @@ export type UseBuildStakeTransactionReturnType<context = unknown> = UseMutationR
27
30
  /**
28
31
  * Hook to build stake transaction
29
32
  */
30
- export const useBuildStakeTransaction = <context = unknown>(): UseBuildStakeTransactionReturnType<context> => {
33
+ export const useBuildStakeTransaction = <context = unknown>(
34
+ parameters?: UseBuildStakeTransactionParameters<context>,
35
+ ): UseBuildStakeTransactionReturnType<context> => {
31
36
  const appKit = useAppKit();
32
- return useMutation(buildStakeTransactionMutationOptions<context>(appKit));
37
+ return useMutation(buildStakeTransactionMutationOptions<context>(appKit, parameters));
33
38
  };
@@ -11,10 +11,12 @@ import { StakingError, StakingErrorCode } from '@ton/appkit';
11
11
  import { mapDefiError } from '../../../utils/map-defi-error';
12
12
 
13
13
  /**
14
- * Map a thrown staking error to an i18n key. Tries staking-specific codes first, falls back to the
15
- * shared {@link mapDefiError} for base DeFi codes, and finally to a generic `staking.quoteError`.
14
+ * Map a thrown staking error to an i18n key. Tries staking-specific codes first, falls back to
15
+ * the shared {@link mapDefiError} for base DeFi codes, and finally to the caller-provided
16
+ * {@link fallback} (defaults to `staking.quoteError`, but send-time callers should pass
17
+ * `staking.sendFailed`).
16
18
  */
17
- export const mapStakingError = (error: unknown): string => {
19
+ export const mapStakingError = (error: unknown, fallback: string = 'staking.quoteError'): string => {
18
20
  if (error instanceof StakingError) {
19
21
  switch (error.code) {
20
22
  case StakingErrorCode.InvalidParams:
@@ -24,5 +26,5 @@ export const mapStakingError = (error: unknown): string => {
24
26
  }
25
27
  }
26
28
 
27
- return mapDefiError(error) ?? 'staking.quoteError';
29
+ return mapDefiError(error) ?? fallback;
28
30
  };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) TonTech.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ export * from './swap-confirm-modal';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Copyright (c) TonTech.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type { FC } from 'react';
10
+ import type { SwapProvider, SwapQuote } from '@ton/appkit';
11
+
12
+ import { Modal } from '../../../../components/ui/modal/modal';
13
+ import { Button } from '../../../../components/ui/button';
14
+ import { FlowPreview } from '../../../../components/shared/flow-preview';
15
+ import { useI18n } from '../../../settings/hooks/use-i18n';
16
+ import type { AppkitUIToken } from '../../../../types/appkit-ui-token';
17
+ import { SwapInfo } from '../swap-info';
18
+ import styles from './swap-confirm-modal.module.css';
19
+
20
+ export interface SwapConfirmModalProps {
21
+ open: boolean;
22
+ onClose: () => void;
23
+ onConfirm: () => void;
24
+ fromToken: AppkitUIToken | null;
25
+ toToken: AppkitUIToken | null;
26
+ fromAmount: string;
27
+ toAmount: string;
28
+ fiatSymbol: string;
29
+ quote?: SwapQuote;
30
+ swapProvider?: SwapProvider;
31
+ slippage: number;
32
+ isQuoteLoading?: boolean;
33
+ }
34
+
35
+ export const SwapConfirmModal: FC<SwapConfirmModalProps> = ({
36
+ open,
37
+ onClose,
38
+ onConfirm,
39
+ fromToken,
40
+ toToken,
41
+ fromAmount,
42
+ toAmount,
43
+ fiatSymbol,
44
+ quote,
45
+ swapProvider,
46
+ slippage,
47
+ isQuoteLoading,
48
+ }) => {
49
+ const { t } = useI18n();
50
+
51
+ return (
52
+ <Modal open={open} onOpenChange={(isOpen) => !isOpen && onClose()} title={t('swap.confirmTitle')}>
53
+ <FlowPreview
54
+ fromAmount={fromAmount}
55
+ toAmount={toAmount}
56
+ fromToken={fromToken ?? undefined}
57
+ toToken={toToken ?? undefined}
58
+ fiatSymbol={fiatSymbol}
59
+ />
60
+
61
+ <SwapInfo
62
+ className={styles.info}
63
+ quote={quote}
64
+ provider={swapProvider}
65
+ toToken={toToken}
66
+ slippage={slippage}
67
+ isQuoteLoading={isQuoteLoading}
68
+ />
69
+
70
+ <Button className={styles.confirmButton} variant="fill" size="l" fullWidth onClick={onConfirm}>
71
+ {t('swap.confirm')}
72
+ </Button>
73
+ </Modal>
74
+ );
75
+ };
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import { createContext, useCallback, useContext, useMemo, useState } from 'react';
9
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
10
10
  import type { FC, PropsWithChildren } from 'react';
11
11
  import { formatUnits } from '@ton/appkit';
12
12
  import type { Network } from '@ton/appkit';
@@ -234,7 +234,7 @@ export const SwapWidgetProvider: FC<SwapProviderProps> = ({
234
234
  network,
235
235
  slippageBps: slippage,
236
236
  providerId: swapProvider?.providerId,
237
- query: { enabled: isNetworkSupported },
237
+ query: { enabled: isNetworkSupported, networkMode: 'always', retry: false, gcTime: 0 },
238
238
  });
239
239
  // Also show "loading" while the user is still typing (debounce in-flight) so the UI doesn't flash
240
240
  // the previous quote as if it were final.
@@ -247,6 +247,29 @@ export const SwapWidgetProvider: FC<SwapProviderProps> = ({
247
247
  });
248
248
  const { data: tonBalance } = useBalance({ network, query: { refetchInterval: 5000 } });
249
249
 
250
+ // 4. Mutations (hoisted above validation: the mutation `error` is one of its inputs)
251
+ const {
252
+ mutateAsync: buildTransaction,
253
+ isPending: isBuildingTransaction,
254
+ error: buildError,
255
+ reset: resetBuild,
256
+ } = useBuildSwapTransaction({ mutation: { networkMode: 'always' } });
257
+ const {
258
+ mutateAsync: sendTransaction,
259
+ isPending: isSendingPending,
260
+ error: sendMutationError,
261
+ reset: resetSend,
262
+ } = useSendTransaction({ mutation: { networkMode: 'always' } });
263
+ const isSendingTransaction = isBuildingTransaction || isSendingPending;
264
+ const sendError = sendMutationError ?? buildError;
265
+
266
+ // Drop the previous send error when the user changes anything that would invalidate it —
267
+ // the next attempt is conceptually a new swap, no need to keep the old message on screen.
268
+ const resetSendError = useCallback(() => {
269
+ resetBuild();
270
+ resetSend();
271
+ }, [resetBuild, resetSend]);
272
+
250
273
  // 3. Derivations
251
274
  const toAmount = quote?.toAmount ?? '';
252
275
  const { error, canSubmit } = useSwapValidation({
@@ -255,7 +278,9 @@ export const SwapWidgetProvider: FC<SwapProviderProps> = ({
255
278
  fromToken,
256
279
  toToken,
257
280
  fromBalance,
281
+ quote,
258
282
  quoteError,
283
+ sendError,
259
284
  isNetworkSupported,
260
285
  });
261
286
  const isLowBalanceWarningOpen = pendingSwap !== undefined;
@@ -265,10 +290,19 @@ export const SwapWidgetProvider: FC<SwapProviderProps> = ({
265
290
  return formatUnits(pendingSwap.requiredNanos, 9);
266
291
  }, [pendingSwap]);
267
292
 
268
- // 4. Mutations
269
- const { mutateAsync: buildTransaction, isPending: isBuildingTransaction } = useBuildSwapTransaction();
270
- const { mutateAsync: sendTransaction, isPending: isSendingPending } = useSendTransaction();
271
- const isSendingTransaction = isBuildingTransaction || isSendingPending;
293
+ // Drop the previous send error when the user changes anything that would invalidate it —
294
+ // the next attempt is conceptually a new swap, no need to keep the old message on screen.
295
+ useEffect(() => {
296
+ resetSendError();
297
+ }, [fromToken?.address, toToken?.address, fromAmount, resetSendError]);
298
+
299
+ // Auto-clear the send error after a short delay so a stale failure doesn't linger in the
300
+ // submit button — the user is expected to act on it within seconds or move on.
301
+ useEffect(() => {
302
+ if (!sendError) return;
303
+ const id = setTimeout(resetSendError, 5000);
304
+ return () => clearTimeout(id);
305
+ }, [sendError, resetSendError]);
272
306
 
273
307
  // 5. Callbacks
274
308
  const handleMaxClick = useCallback(() => {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { useMemo } from 'react';
10
+ import type { SwapQuote } from '@ton/appkit';
10
11
 
11
12
  import type { AppkitUIToken } from '../../../../types/appkit-ui-token';
12
13
  import { hasTooManyDecimals, isAmountExceedingBalance } from '../../../../utils/validate-amount';
@@ -18,7 +19,9 @@ interface UseSwapValidationOptions {
18
19
  fromToken: AppkitUIToken | null;
19
20
  toToken: AppkitUIToken | null;
20
21
  fromBalance: string | undefined;
22
+ quote: SwapQuote | undefined;
21
23
  quoteError: Error | null;
24
+ sendError: Error | null;
22
25
  isNetworkSupported: boolean;
23
26
  }
24
27
 
@@ -28,10 +31,12 @@ export function useSwapValidation({
28
31
  fromToken,
29
32
  toToken,
30
33
  fromBalance,
34
+ quote,
31
35
  quoteError,
36
+ sendError,
32
37
  isNetworkSupported,
33
38
  }: UseSwapValidationOptions) {
34
- const error: string | null = useMemo(() => {
39
+ const blockingError: string | null = useMemo(() => {
35
40
  if (!isNetworkSupported) return 'defi.unsupportedNetwork';
36
41
 
37
42
  if ((parseFloat(fromAmount) || 0) <= 0) return null;
@@ -45,7 +50,17 @@ export function useSwapValidation({
45
50
  return null;
46
51
  }, [isNetworkSupported, fromAmount, fromToken, fromBalance, quoteError, fromAmountDebounced]);
47
52
 
48
- const canSubmit = (parseFloat(fromAmount) || 0) > 0 && fromToken !== null && toToken !== null && error === null;
53
+ const error = useMemo<string | null>(() => {
54
+ if (sendError) return mapSwapError(sendError, 'swap.sendFailed');
55
+ return blockingError;
56
+ }, [sendError, blockingError]);
57
+
58
+ const canSubmit =
59
+ (parseFloat(fromAmount) || 0) > 0 &&
60
+ fromToken !== null &&
61
+ toToken !== null &&
62
+ blockingError === null &&
63
+ quote !== undefined;
49
64
 
50
65
  return { error, canSubmit };
51
66
  }