@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.
- package/README.md +7 -6
- package/dist/esm/components/shared/amount-preview/amount-preview.js +17 -0
- package/dist/esm/components/shared/amount-preview/amount-preview.js.map +1 -0
- package/dist/esm/components/shared/amount-preview/amount-preview.module.css +40 -0
- package/dist/esm/{features/balances/components/balance-badge → components/shared/amount-preview}/index.js +1 -1
- package/dist/esm/components/shared/amount-preview/index.js.map +1 -0
- package/dist/esm/components/shared/flow-preview/flow-preview.js +24 -0
- package/dist/esm/components/shared/flow-preview/flow-preview.js.map +1 -0
- package/dist/esm/components/shared/flow-preview/flow-preview.module.css +37 -0
- package/dist/esm/components/shared/flow-preview/index.js +9 -0
- package/dist/esm/components/shared/flow-preview/index.js.map +1 -0
- package/dist/esm/components/shared/settings-button/settings-button.js +1 -1
- package/dist/esm/components/shared/settings-button/settings-button.js.map +1 -1
- package/dist/esm/components/ui/button/button.module.css +1 -1
- package/dist/esm/components/ui/logo/logo.module.css +1 -3
- package/dist/esm/components/ui/modal/modal.module.css +1 -1
- package/dist/esm/components/ui/tabs/tabs.module.css +1 -1
- package/dist/esm/features/balances/components/send-jetton-button/send-jetton-button.js +3 -3
- package/dist/esm/features/balances/components/send-jetton-button/send-jetton-button.js.map +1 -1
- package/dist/esm/features/balances/index.js +0 -1
- package/dist/esm/features/balances/index.js.map +1 -1
- package/dist/esm/features/staking/components/select-unstake-mode/select-unstake-mode.js +2 -1
- package/dist/esm/features/staking/components/select-unstake-mode/select-unstake-mode.js.map +1 -1
- package/dist/esm/features/staking/components/select-unstake-mode/select-unstake-mode.module.css +2 -6
- package/dist/esm/features/staking/components/staking-confirm-modal/index.js +9 -0
- package/dist/esm/features/staking/components/staking-confirm-modal/index.js.map +1 -0
- package/dist/esm/features/staking/components/staking-confirm-modal/staking-confirm-modal.js +46 -0
- package/dist/esm/features/staking/components/staking-confirm-modal/staking-confirm-modal.js.map +1 -0
- package/dist/esm/features/staking/components/staking-confirm-modal/staking-confirm-modal.module.css +11 -0
- package/dist/esm/features/staking/components/staking-widget-provider/staking-widget-provider.js +27 -4
- package/dist/esm/features/staking/components/staking-widget-provider/staking-widget-provider.js.map +1 -1
- package/dist/esm/features/staking/components/staking-widget-provider/use-staking-validation.js +12 -3
- package/dist/esm/features/staking/components/staking-widget-provider/use-staking-validation.js.map +1 -1
- package/dist/esm/features/staking/components/staking-widget-ui/staking-widget-ui.js +16 -4
- package/dist/esm/features/staking/components/staking-widget-ui/staking-widget-ui.js.map +1 -1
- package/dist/esm/features/staking/components/staking-widget-ui/staking-widget-ui.module.css +4 -0
- package/dist/esm/features/staking/hooks/use-build-stake-transaction.js +2 -2
- package/dist/esm/features/staking/hooks/use-build-stake-transaction.js.map +1 -1
- package/dist/esm/features/staking/utils/map-staking-error.js +6 -4
- package/dist/esm/features/staking/utils/map-staking-error.js.map +1 -1
- package/dist/esm/features/swap/components/swap-confirm-modal/index.js +9 -0
- package/dist/esm/features/swap/components/swap-confirm-modal/index.js.map +1 -0
- package/dist/esm/features/swap/components/swap-confirm-modal/swap-confirm-modal.js +12 -0
- package/dist/esm/features/swap/components/swap-confirm-modal/swap-confirm-modal.js.map +1 -0
- package/dist/esm/features/swap/components/swap-confirm-modal/swap-confirm-modal.module.css +7 -0
- package/dist/esm/features/swap/components/swap-widget-provider/swap-widget-provider.js +28 -6
- package/dist/esm/features/swap/components/swap-widget-provider/swap-widget-provider.js.map +1 -1
- package/dist/esm/features/swap/components/swap-widget-provider/use-swap-validation.js +12 -3
- package/dist/esm/features/swap/components/swap-widget-provider/use-swap-validation.js.map +1 -1
- package/dist/esm/features/swap/components/swap-widget-ui/swap-widget-ui.js +16 -4
- package/dist/esm/features/swap/components/swap-widget-ui/swap-widget-ui.js.map +1 -1
- package/dist/esm/features/swap/utils/map-swap-error.js +10 -8
- package/dist/esm/features/swap/utils/map-swap-error.js.map +1 -1
- package/dist/esm/locales/en.js +9 -0
- package/dist/esm/locales/en.js.map +1 -1
- package/dist/esm/styles/index.css +3 -3
- package/dist/esm/utils/map-defi-error.js +7 -7
- package/dist/esm/utils/map-defi-error.js.map +1 -1
- package/dist/types/components/shared/amount-preview/amount-preview.d.ts +24 -0
- package/dist/types/components/shared/amount-preview/amount-preview.d.ts.map +1 -0
- package/dist/types/{features/balances/components/balance-badge → components/shared/amount-preview}/index.d.ts +1 -1
- package/dist/types/components/shared/amount-preview/index.d.ts.map +1 -0
- package/dist/types/components/shared/flow-preview/flow-preview.d.ts +18 -0
- package/dist/types/components/shared/flow-preview/flow-preview.d.ts.map +1 -0
- package/dist/types/components/shared/flow-preview/index.d.ts +9 -0
- package/dist/types/components/shared/flow-preview/index.d.ts.map +1 -0
- package/dist/types/features/balances/index.d.ts +0 -1
- package/dist/types/features/balances/index.d.ts.map +1 -1
- package/dist/types/features/staking/components/select-unstake-mode/select-unstake-mode.d.ts.map +1 -1
- package/dist/types/features/staking/components/staking-confirm-modal/index.d.ts +9 -0
- package/dist/types/features/staking/components/staking-confirm-modal/index.d.ts.map +1 -0
- package/dist/types/features/staking/components/staking-confirm-modal/staking-confirm-modal.d.ts +23 -0
- package/dist/types/features/staking/components/staking-confirm-modal/staking-confirm-modal.d.ts.map +1 -0
- package/dist/types/features/staking/components/staking-widget-provider/staking-widget-provider.d.ts.map +1 -1
- package/dist/types/features/staking/components/staking-widget-provider/use-staking-validation.d.ts +3 -1
- package/dist/types/features/staking/components/staking-widget-provider/use-staking-validation.d.ts.map +1 -1
- package/dist/types/features/staking/components/staking-widget-ui/staking-widget-ui.d.ts.map +1 -1
- package/dist/types/features/staking/hooks/use-build-stake-transaction.d.ts +3 -2
- package/dist/types/features/staking/hooks/use-build-stake-transaction.d.ts.map +1 -1
- package/dist/types/features/staking/utils/map-staking-error.d.ts +5 -3
- package/dist/types/features/staking/utils/map-staking-error.d.ts.map +1 -1
- package/dist/types/features/swap/components/swap-confirm-modal/index.d.ts +9 -0
- package/dist/types/features/swap/components/swap-confirm-modal/index.d.ts.map +1 -0
- package/dist/types/features/swap/components/swap-confirm-modal/swap-confirm-modal.d.ts +26 -0
- package/dist/types/features/swap/components/swap-confirm-modal/swap-confirm-modal.d.ts.map +1 -0
- package/dist/types/features/swap/components/swap-widget-provider/swap-widget-provider.d.ts.map +1 -1
- package/dist/types/features/swap/components/swap-widget-provider/use-swap-validation.d.ts +4 -1
- package/dist/types/features/swap/components/swap-widget-provider/use-swap-validation.d.ts.map +1 -1
- package/dist/types/features/swap/components/swap-widget-ui/swap-widget-ui.d.ts.map +1 -1
- package/dist/types/features/swap/utils/map-swap-error.d.ts +4 -2
- package/dist/types/features/swap/utils/map-swap-error.d.ts.map +1 -1
- package/dist/types/libs/i18n.d.ts +9 -0
- package/dist/types/libs/i18n.d.ts.map +1 -1
- package/dist/types/locales/en.d.ts +9 -0
- package/dist/types/locales/en.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/components/shared/amount-preview/amount-preview.tsx +74 -0
- package/src/{features/balances/components/balance-badge → components/shared/amount-preview}/index.ts +1 -1
- package/src/components/shared/flow-preview/flow-preview.tsx +64 -0
- package/src/components/shared/flow-preview/index.ts +9 -0
- package/src/components/shared/settings-button/settings-button.tsx +1 -1
- package/src/features/balances/components/send-jetton-button/send-jetton-button.tsx +3 -3
- package/src/features/balances/index.ts +0 -1
- package/src/features/staking/components/select-unstake-mode/select-unstake-mode.tsx +12 -4
- package/src/features/staking/components/staking-confirm-modal/index.ts +9 -0
- package/src/features/staking/components/staking-confirm-modal/staking-confirm-modal.tsx +121 -0
- package/src/features/staking/components/staking-widget-provider/staking-widget-provider.tsx +39 -4
- package/src/features/staking/components/staking-widget-provider/use-staking-validation.ts +14 -2
- package/src/features/staking/components/staking-widget-ui/staking-widget-ui.tsx +39 -13
- package/src/features/staking/hooks/use-build-stake-transaction.ts +7 -2
- package/src/features/staking/utils/map-staking-error.ts +6 -4
- package/src/features/swap/components/swap-confirm-modal/index.ts +9 -0
- package/src/features/swap/components/swap-confirm-modal/swap-confirm-modal.tsx +75 -0
- package/src/features/swap/components/swap-widget-provider/swap-widget-provider.tsx +40 -6
- package/src/features/swap/components/swap-widget-provider/use-swap-validation.ts +17 -2
- package/src/features/swap/components/swap-widget-ui/swap-widget-ui.tsx +30 -3
- package/src/features/swap/utils/map-swap-error.ts +10 -8
- package/src/locales/en.ts +9 -0
- package/src/utils/map-defi-error.ts +7 -7
- package/dist/esm/features/balances/components/balance-badge/balance-badge.js +0 -33
- package/dist/esm/features/balances/components/balance-badge/balance-badge.js.map +0 -1
- package/dist/esm/features/balances/components/balance-badge/balance-badge.module.css +0 -21
- package/dist/esm/features/balances/components/balance-badge/index.js.map +0 -1
- package/dist/esm/tsconfig.build.tsbuildinfo +0 -1
- package/dist/types/features/balances/components/balance-badge/balance-badge.d.ts +0 -21
- package/dist/types/features/balances/components/balance-badge/balance-badge.d.ts.map +0 -1
- package/dist/types/features/balances/components/balance-badge/index.d.ts.map +0 -1
- 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
|
-
<
|
|
88
|
+
<div className={styles.header}>
|
|
88
89
|
<span className={styles.headerLabel}>{t('staking.unstakeType')}</span>
|
|
89
|
-
|
|
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
|
-
</
|
|
93
|
-
</
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
94
102
|
|
|
95
103
|
<Collapsible open={open}>
|
|
96
104
|
<div className={styles.options}>
|
|
@@ -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 {
|
|
228
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
|
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>(
|
|
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
|
|
15
|
-
* shared {@link mapDefiError} for base DeFi codes, and finally to
|
|
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) ??
|
|
29
|
+
return mapDefiError(error) ?? fallback;
|
|
28
30
|
};
|
|
@@ -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
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
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
|
|
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
|
}
|