diff --git a/src/common/extrinsicService/ExtrinsicProvider.tsx b/src/common/extrinsicService/ExtrinsicProvider.tsx index 5cea110a..5ce5cb45 100644 --- a/src/common/extrinsicService/ExtrinsicProvider.tsx +++ b/src/common/extrinsicService/ExtrinsicProvider.tsx @@ -1,29 +1,23 @@ import { createContext, PropsWithChildren, useContext } from 'react'; import { ChainId } from '@common/types'; -import { ExtrinsicBuilding, ExtrinsicBuildingOptions } from '@common/extrinsicService/types'; -import { Balance } from '@polkadot/types/interfaces'; -import { SubmittableResultResult } from '@polkadot/api-base/types/submittable'; +import { + EstimateFee, + ExtrinsicBuilding, + ExtrinsicBuildingOptions, + SubmitExtrinsic, +} from '@common/extrinsicService/types'; +import { Balance, Hash } from '@polkadot/types/interfaces'; import { useExtrinsicService } from '@common/extrinsicService/ExtrinsicService'; -import { KeyringPair } from '@polkadot/keyring/types'; +import { getKeyringPair } from '../wallet'; +import { FAKE_ACCOUNT_ID } from '../utils/constants'; type ExtrinsicProviderContextProps = { - estimateFee: ( - chainId: ChainId, - building: ExtrinsicBuilding, - options?: Partial, - ) => Promise; - - submitExtrinsic: ( - chainId: ChainId, - building: ExtrinsicBuilding, - options?: Partial, - ) => SubmittableResultResult<'promise'>; + estimateFee: EstimateFee; + submitExtrinsic: SubmitExtrinsic; }; const ExtrinsicProviderContext = createContext({} as ExtrinsicProviderContextProps); -export const FAKE_ACCOUNT_ID = '0x' + '1'.repeat(64); - export const ExtrinsicProvider = ({ children }: PropsWithChildren) => { const { prepareExtrinsic } = useExtrinsicService(); @@ -38,22 +32,20 @@ export const ExtrinsicProvider = ({ children }: PropsWithChildren) => { return paymentInfo.partialFee; } - function submitExtrinsic( + async function submitExtrinsic( chainId: ChainId, building: ExtrinsicBuilding, options?: Partial, - ): SubmittableResultResult<`promise`> { - const extrinsicPromise = prepareExtrinsic<'promise'>(chainId, building, options); + ): Promise { + const extrinsic = await prepareExtrinsic<'promise'>(chainId, building, options); - const keyringPromise = new Promise(function () {}); + const keyringPair = getKeyringPair(); + if (!keyringPair) return; - return extrinsicPromise.then(async (extrinsic) => { - const keyringPair = await keyringPromise; - await extrinsic.signAsync(keyringPair); - keyringPair.lock(); + await extrinsic.signAsync(keyringPair); + keyringPair.lock(); - return await extrinsic.send(); - }); + return await extrinsic.send(); } return ( diff --git a/src/common/extrinsicService/types/index.ts b/src/common/extrinsicService/types/index.ts index a52fcfeb..003b6459 100644 --- a/src/common/extrinsicService/types/index.ts +++ b/src/common/extrinsicService/types/index.ts @@ -1,6 +1,7 @@ import { ApiPromise } from '@polkadot/api'; import { SubmittableExtrinsic } from '@polkadot/api-base/types'; import { ChainId } from '@common/types'; +import { Balance, Hash } from '@polkadot/types/interfaces'; export interface ExtrinsicBuilder { api: ApiPromise; @@ -30,3 +31,15 @@ export interface ExtrinsicBuilderFactory { */ forChain(chainId: ChainId): Promise; } + +export type EstimateFee = ( + chainId: ChainId, + building: ExtrinsicBuilding, + options?: Partial, +) => Promise; + +export type SubmitExtrinsic = ( + chainId: ChainId, + building: ExtrinsicBuilding, + options?: Partial, +) => Promise; diff --git a/src/common/providers/contextProvider.tsx b/src/common/providers/contextProvider.tsx index 72c99648..47a6745b 100644 --- a/src/common/providers/contextProvider.tsx +++ b/src/common/providers/contextProvider.tsx @@ -5,14 +5,15 @@ import { getWallet } from '../wallet'; export interface IContext { assets: AssetAccount[] | []; publicKey?: HexString; - selectedAsset?: TrasferAsset; + selectedAsset?: TrasferAsset | null; setPublicKey: React.Dispatch>; setAssets: React.Dispatch>; - setSelectedAsset: React.Dispatch>; + setSelectedAsset: React.Dispatch>; } export const GlobalContext = createContext({ assets: [], + selectedAsset: null, setPublicKey: () => {}, setAssets: () => {}, setSelectedAsset: () => {}, @@ -22,7 +23,7 @@ export const GlobalContext = createContext({ export const GlobalStateProvider = ({ children }: { children: React.ReactNode }) => { const [publicKey, setPublicKey] = useState(); const [assets, setAssets] = useState([]); - const [selectedAsset, setSelectedAsset] = useState(); + const [selectedAsset, setSelectedAsset] = useState(null); useEffect(() => { if (!publicKey) { diff --git a/src/common/providers/telegramProvider.tsx b/src/common/providers/telegramProvider.tsx index 208d021c..fafd8075 100644 --- a/src/common/providers/telegramProvider.tsx +++ b/src/common/providers/telegramProvider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import React, { PropsWithChildren, createContext, useContext, useEffect, useMemo, useState } from 'react'; import Script from 'next/script'; import { WebApp, WebAppUser, MainButton, BackButton } from '@twa-dev/types'; @@ -11,7 +11,7 @@ export interface ITelegramContext { export const TelegramContext = createContext({}); -export const TelegramProvider = ({ children }: { children: React.ReactNode }) => { +export const TelegramProvider = ({ children }: PropsWithChildren) => { const [webApp, setWebApp] = useState(null); useEffect(() => { diff --git a/src/common/routing/paths.ts b/src/common/routing/paths.ts index e3fba068..3718acb8 100644 --- a/src/common/routing/paths.ts +++ b/src/common/routing/paths.ts @@ -12,6 +12,8 @@ export const Paths = { TRANSFER_SELECT_TOKEN: '/transfer/select-token', TRANSFER_ADDRESS: '/transfer/address', TRANSFER_AMOUNT: '/transfer/amount', + TRANSFER_CONFIRMATION: '/transfer/confirmation', + TRANSFER_RESULT: '/transfer/result', } as const; export type PathValue = (typeof Paths)[keyof typeof Paths]; diff --git a/src/common/types/index.ts b/src/common/types/index.ts index 623cec7a..c0d86329 100644 --- a/src/common/types/index.ts +++ b/src/common/types/index.ts @@ -9,6 +9,7 @@ export function unwrapHexString(string: string): HexString { export type ChainId = HexString; export type AssetId = number; export type Address = string; +export type AccountId = HexString; export type PublicKey = HexString; export type ChainAssetId = { @@ -32,8 +33,10 @@ export type AssetAccount = ChainAssetAccount & { }; export type TrasferAsset = AssetAccount & { - destination?: string; + destinationAddress?: string; amount?: string; + fee?: number; + transferAll?: boolean; }; export type StateResolution = { resolve: (value: T) => void; reject: () => void }; diff --git a/src/common/utils/address.ts b/src/common/utils/address.ts new file mode 100644 index 00000000..3e79a059 --- /dev/null +++ b/src/common/utils/address.ts @@ -0,0 +1,30 @@ +import { isHex, isU8a, u8aToU8a } from '@polkadot/util'; +import { base58Decode, checkAddressChecksum } from '@polkadot/util-crypto'; +import { AccountId, Address } from '../types'; + +const ADDRESS_ALLOWED_ENCODED_LENGTHS = [35, 36, 37, 38]; +const ACCOUNT_ID_LENGTH = 32; + +/** + * Check is account's address valid + * @param address account's address + * @return {Boolean} + */ +export const validateAddress = (address?: Address | AccountId): boolean => { + if (!address) return false; + + if (isU8a(address) || isHex(address)) { + return u8aToU8a(address).length === ACCOUNT_ID_LENGTH; + } + + try { + const decoded = base58Decode(address); + if (!ADDRESS_ALLOWED_ENCODED_LENGTHS.includes(decoded.length)) return false; + + const [isValid, endPos, ss58Length] = checkAddressChecksum(decoded); + + return isValid && Boolean(decoded.slice(ss58Length, endPos)); + } catch (error) { + return false; + } +}; diff --git a/src/common/utils/balance.ts b/src/common/utils/balance.ts index e3124f81..0bca504e 100644 --- a/src/common/utils/balance.ts +++ b/src/common/utils/balance.ts @@ -1,7 +1,13 @@ import BigNumber from 'bignumber.js'; -import { AssetAccount } from '../types'; +import { decodeAddress } from '@polkadot/keyring'; +import { BN, BN_TEN } from '@polkadot/util'; + +import { AssetAccount, ChainId, TrasferAsset } from '../types'; import { Chain } from '../chainRegistry/types'; import { IAssetBalance } from '../balances/types'; +import { EstimateFee, ExtrinsicBuilder, SubmitExtrinsic } from '../extrinsicService/types'; +import { Balance } from '@polkadot/types/interfaces'; +import { FAKE_ACCOUNT_ID } from './constants'; const ZERO_BALANCE = '0'; @@ -138,28 +144,50 @@ export const updateAssetsBalance = (prevAssets: AssetAccount[], chain: Chain, ba ); }; -// async function handleSign() { -// extrinsicService -// .submitExtrinsic(polkadot.chainId, (builder) => builder.addCall(builder.api.tx.system.remark('Hello'))) -// .then( -// (hash) => { -// alert('Success: ' + hash); -// }, -// (failure) => { -// alert('Failed: ' + failure); -// }, -// ); -// } - -// export async function handleFee(estimateFee, chainId: ChainId, address: Address) { -// estimateFee(chainId, (builder: ExtrinsicBuilder) => -// builder.addCall(builder.api.tx.balances.transferKeepAlive(decodeAddress(address), '0')), -// ).then( -// (fee) => { -// alert('Fee: ' + fee); -// }, -// (failure) => { -// alert('Failed to calculate fee: ' + failure); -// }, -// ); -// } +export const formatAmount = (amount: string, precision: number): string => { + if (!amount) return ZERO_BALANCE; + + const isDecimalValue = amount.match(/^(\d+)\.(\d+)$/); + const bnPrecision = new BN(precision); + if (isDecimalValue) { + const div = new BN(amount.replace(/\.\d*$/, '')); + const modString = amount.replace(/^\d+\./, '').slice(0, precision); + const mod = new BN(modString); + + return div + .mul(BN_TEN.pow(bnPrecision)) + .add(mod.mul(BN_TEN.pow(new BN(precision - modString.length)))) + .toString(); + } + + return new BN(amount.replace(/\D/g, '')).mul(BN_TEN.pow(bnPrecision)).toString(); +}; + +export async function handleSend( + submitExtrinsic: SubmitExtrinsic, + { destinationAddress, chainId, amount, transferAll, precision }: TrasferAsset, +) { + const decodedAddress = decodeAddress(destinationAddress); + + return await submitExtrinsic(chainId, (builder) => { + const transferFunction = transferAll + ? builder.api.tx.balances.transferAll(decodedAddress, false) + : builder.api.tx.balances.transferKeepAlive(decodedAddress, formatAmount(amount as string, precision)); + + builder.addCall(transferFunction); + }).then((hash) => { + console.log('Success, Hash:', hash?.toString()); + }); +} + +const FAKE_AMMOUNT = '1'; + +export async function handleFee(estimateFee: EstimateFee, chainId: ChainId, precision: number): Promise { + return await estimateFee(chainId, (builder: ExtrinsicBuilder) => + builder.addCall(builder.api.tx.balances.transferKeepAlive(decodeAddress(FAKE_ACCOUNT_ID), FAKE_AMMOUNT)), + ).then((fee: Balance) => { + const { formattedValue } = formatBalance(fee.toString(), precision); + + return Number(formattedValue); + }); +} diff --git a/src/common/utils/constants.ts b/src/common/utils/constants.ts index b76b0e29..2a1ba5c5 100644 --- a/src/common/utils/constants.ts +++ b/src/common/utils/constants.ts @@ -1,2 +1,3 @@ export const PUBLIC_KEY_STORE = 'publicKey'; export const MNEMONIC_STORE = 'mnemonic'; +export const FAKE_ACCOUNT_ID = '0x' + '1'.repeat(64); diff --git a/src/common/wallet/store.ts b/src/common/wallet/store.ts index 8fe333f7..d788930d 100644 --- a/src/common/wallet/store.ts +++ b/src/common/wallet/store.ts @@ -44,16 +44,8 @@ export const backupMnemonic = (mnemonic: string, password: string): void => { window.Telegram.WebApp.CloudStorage.setItem(MNEMONIC_STORE, encryptedMnemonic); }; -export const getMnemonic = (password: string): string | null => { - const encryptedMnemonic = secureLocalStorage.getItem(MNEMONIC_STORE); - - if (encryptedMnemonic) { - const mnemonic = AES.decrypt(encryptedMnemonic, password); - - return mnemonic.toString(CryptoJS.enc.Utf8); - } else { - return null; - } +export const getMnemonic = (): string | null => { + return (secureLocalStorage.getItem(MNEMONIC_STORE) as string) || null; }; /** @@ -61,14 +53,15 @@ export const getMnemonic = (password: string): string | null => { * Make sure to call lock() after pair was used to clean up secret! * @param password */ -export const getKeyringPair = (password: string): KeyringPair | undefined => { +export const getKeyringPair = (): KeyringPair | undefined => { try { - const mnemonic = getMnemonic(password); - if (mnemonic === null) return undefined; + const mnemonic = getMnemonic(); + + if (mnemonic === null) return; return keyring.createFromUri(mnemonic); } catch (e) { - return undefined; + console.warn(e); } }; diff --git a/src/components/Assets/AssetsList.tsx b/src/components/Assets/AssetsList.tsx index 84f28822..2dd1f35e 100644 --- a/src/components/Assets/AssetsList.tsx +++ b/src/components/Assets/AssetsList.tsx @@ -1,14 +1,22 @@ import { useGlobalContext } from '@/common/providers/contextProvider'; import AssetBalance from './AssetBalance'; +import { Button } from '@nextui-org/react'; const AssetsList = () => { const { assets } = useGlobalContext(); + // TODO: delete it later + const copyAddress = (address: string) => { + navigator.clipboard.writeText(address); + }; + return (
{assets.map((asset) => ( - + ))}
); diff --git a/src/components/Layout/layout.tsx b/src/components/Layout/layout.tsx new file mode 100644 index 00000000..6345974f --- /dev/null +++ b/src/components/Layout/layout.tsx @@ -0,0 +1,13 @@ +import { PropsWithChildren } from 'react'; +import { ExtrinsicProvider } from '@/common/extrinsicService/ExtrinsicProvider'; +import { ChainRegistry } from '@/common/chainRegistry'; + +export default function Layout({ children }: PropsWithChildren) { + return ( + + +
{children}
+
+
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 59e100c8..96198dec 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,6 +17,7 @@ import Plate from './Plate/Plate'; import PasswordForm from './PasswordForm/PasswordForm'; import Shimmering from './Shimmering/Shimmering'; import Identicon from './Identicon/Identicon'; +import Layout from './Layout/layout'; export { TextBase, @@ -37,4 +38,5 @@ export { PasswordForm, Shimmering, Identicon, + Layout, }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f2c2146f..b5c9534e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,22 +1,28 @@ import { AppProps } from 'next/app'; -import { useEffect, useState } from 'react'; +import { ReactElement, ReactNode, useEffect, useState } from 'react'; import { NextUIProvider } from '@nextui-org/react'; import { TelegramProvider } from '@common/providers/telegramProvider'; import { GlobalStateProvider } from '@/common/providers/contextProvider'; import './globals.css'; +import { NextPage } from 'next'; -const App = ({ Component, pageProps }: AppProps) => { +export type NextPageWithLayout

= NextPage & { + getLayout?: (page: ReactElement) => ReactNode; +}; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +}; + +const App = ({ Component, pageProps }: AppPropsWithLayout) => { const [render, setRender] = useState(false); useEffect(() => setRender(true), []); + const getLayout = Component.getLayout || ((page) => page); return ( - {render && ( - - - - )} + {render && {getLayout()} } ); diff --git a/src/pages/onboarding/create-wallet/index.tsx b/src/pages/onboarding/create-wallet/index.tsx index e1780199..cb9d71b9 100644 --- a/src/pages/onboarding/create-wallet/index.tsx +++ b/src/pages/onboarding/create-wallet/index.tsx @@ -15,6 +15,10 @@ export default function CreateWalletPage() { const [isLoading, setIsLoading] = useState(true); useEffect(() => { + const callback = () => { + router.push(Paths.DASHBOARD); + }; + (async () => { if (!publicKey) { console.error("Can't create wallet when public key is missing"); @@ -31,9 +35,7 @@ export default function CreateWalletPage() { // TODO: Handle errors here and display retry page maybe await completeOnboarding(publicKey, webApp); - MainButton?.onClick(() => { - router.push(Paths.DASHBOARD); - }); + MainButton?.onClick(callback); setTimeout(() => { setIsLoading(false); @@ -47,6 +49,7 @@ export default function CreateWalletPage() { MainButton?.setText('Continue'); MainButton?.hide(); MainButton?.hideProgress(); + MainButton?.offClick(callback); }; }, []); diff --git a/src/pages/transfer/address.tsx b/src/pages/transfer/address.tsx index ef14fadc..96310789 100644 --- a/src/pages/transfer/address.tsx +++ b/src/pages/transfer/address.tsx @@ -5,6 +5,7 @@ import { Button, Input } from '@nextui-org/react'; import { useTelegram } from '@common/providers/telegramProvider'; import { useGlobalContext } from '@/common/providers/contextProvider'; +import { validateAddress } from '@/common/utils/address'; import { Icon, HelpText, BodyText, Identicon } from '@/components'; import { Paths } from '@/common/routing'; @@ -16,38 +17,47 @@ export default function AddressPage() { const [isAddressValid, setIsAddressValid] = useState(true); useEffect(() => { - BackButton?.show(); - BackButton?.onClick(() => { + router.prefetch(Paths.TRANSFER_AMOUNT); + const callback = () => { router.push(Paths.TRANSFER_SELECT_TOKEN); - }); + }; + + BackButton?.show(); + BackButton?.onClick(callback); + + return () => { + BackButton?.offClick(callback); + }; }, []); useEffect(() => { + const callback = () => { + setSelectedAsset((prev) => ({ ...prev!, destinationAddress: address })); + router.push(Paths.TRANSFER_AMOUNT); + }; + if (address.length) { MainButton?.show(); - MainButton?.onClick(() => { - setSelectedAsset((prev) => ({ ...prev!, destination: address })); - router.push(Paths.TRANSFER_AMOUNT); - }); isAddressValid ? MainButton?.enable() : MainButton?.disable(); + MainButton?.onClick(callback); } else { MainButton?.hide(); } + + return () => { + MainButton?.offClick(callback); + }; }, [address, isAddressValid]); const handleChange = (value: string) => { setAddress(value); - - // TODO: validate address - setIsAddressValid(value.length > 10); + setIsAddressValid(validateAddress(value)); }; const handleQrCode = () => { webApp?.showScanQrPopup({ text: 'Scan QR code' }, (value) => { setAddress(value); - - // TODO: validate address - setIsAddressValid(true); + setIsAddressValid(validateAddress(value)); webApp.closeScanQrPopup(); }); }; diff --git a/src/pages/transfer/amount.tsx b/src/pages/transfer/amount.tsx index 67b12fbe..b890149c 100644 --- a/src/pages/transfer/amount.tsx +++ b/src/pages/transfer/amount.tsx @@ -1,58 +1,125 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { ReactElement, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; +// @ts-expect-error no types import MiddleEllipsis from 'react-middle-ellipsis'; -import { Input } from '@nextui-org/react'; +import { Button, CircularProgress, Input } from '@nextui-org/react'; import { useTelegram } from '@common/providers/telegramProvider'; import { useGlobalContext } from '@/common/providers/contextProvider'; import { Paths } from '@/common/routing'; -import { HeadlineText, Icon, Identicon, CaptionText, LargeTitleText, TextBase } from '@/components'; +import { HeadlineText, Icon, Identicon, CaptionText, LargeTitleText, TextBase, Layout } from '@/components'; import { IconNames } from '@/components/Icon/types'; +import { useExtrinsicProvider } from '@/common/extrinsicService/ExtrinsicProvider'; +import { formatBalance, handleFee } from '@/common/utils/balance'; export default function AmountPage() { const router = useRouter(); + const { estimateFee } = useExtrinsicProvider(); const { BackButton, MainButton } = useTelegram(); - const { selectedAsset } = useGlobalContext(); + const { selectedAsset, setSelectedAsset } = useGlobalContext(); + const [amount, setAmount] = useState('0'); + const [transferAll, setTransferAll] = useState(false); + const [maxAmountToSend, setMaxAmountToSend] = useState(); + const [isAmountValid, setIsAmountValid] = useState(true); useEffect(() => { + router.prefetch(Paths.TRANSFER_CONFIRMATION); + MainButton?.setText('Continue'); BackButton?.show(); + MainButton?.show(); MainButton?.disable(); - BackButton?.onClick(() => { + + const callback = () => { router.push(Paths.TRANSFER_ADDRESS); - }); - }, []); + }; + BackButton?.onClick(callback); + + if (!selectedAsset) return; + + (async () => { + const fee = selectedAsset.fee || (await handleFee(estimateFee, selectedAsset.chainId, selectedAsset.precision)); + const formattedBalance = Number( + formatBalance(selectedAsset.transferableBalance, selectedAsset.precision).formattedValue, + ); + + const max = Number(Math.max(formattedBalance - fee, 0).toFixed(5)); + setMaxAmountToSend(max); + setSelectedAsset((prev) => ({ ...prev!, fee })); + })(); + + return () => { + BackButton?.offClick(callback); + }; + }, [BackButton, MainButton]); + + useEffect(() => { + const callback = () => { + setSelectedAsset((prev) => ({ ...prev!, transferAll, amount })); + router.push(Paths.TRANSFER_CONFIRMATION); + }; + + if (!isAmountValid || !Number(amount)) return; + MainButton?.enable(); + MainButton?.onClick(callback); + + return () => { + MainButton?.offClick(callback); + }; + }, [amount, isAmountValid, maxAmountToSend]); + + const handleMaxSend = () => { + if (maxAmountToSend === undefined) return; + setTransferAll(true); + setAmount(String(maxAmountToSend)); + setIsAmountValid(Boolean(maxAmountToSend)); + }; + + const handleChange = (value: string) => { + setTransferAll(false); + setIsAmountValid(!!Number(value) && Number(value) <= (maxAmountToSend || 0)); + setAmount(value); + }; return ( -

+ <>
- + Send to -
+ - {selectedAsset?.destination} + {selectedAsset?.destinationAddress} -
+
- - Max: {selectedAsset?.transferableBalance} {selectedAsset?.symbol} - +
-
+
{selectedAsset?.symbol}
-
+ ); } + +AmountPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/src/pages/transfer/confirmation.tsx b/src/pages/transfer/confirmation.tsx new file mode 100644 index 00000000..7b899f19 --- /dev/null +++ b/src/pages/transfer/confirmation.tsx @@ -0,0 +1,118 @@ +'use client'; +import { ReactElement, useEffect } from 'react'; +import { useRouter } from 'next/router'; +// @ts-expect-error no types +import MiddleEllipsis from 'react-middle-ellipsis'; +import { Divider } from '@nextui-org/react'; + +import { useTelegram } from '@common/providers/telegramProvider'; +import { useGlobalContext } from '@/common/providers/contextProvider'; +import { Paths } from '@/common/routing'; +import { + HeadlineText, + Icon, + Identicon, + LargeTitleText, + TextBase, + Plate, + BodyText, + CaptionText, + Layout, +} from '@/components'; +import { IconNames } from '@/components/Icon/types'; +import { useExtrinsicProvider } from '@/common/extrinsicService/ExtrinsicProvider'; +import { handleSend } from '@/common/utils/balance'; + +export default function ConfirmationPage() { + const router = useRouter(); + const { submitExtrinsic } = useExtrinsicProvider(); + + const { BackButton, MainButton } = useTelegram(); + const { selectedAsset } = useGlobalContext(); + + useEffect(() => { + if (!selectedAsset) return; + + const mainCallback = async () => { + MainButton?.showProgress(false); + await handleSend(submitExtrinsic, selectedAsset).then(() => { + router.push(Paths.TRANSFER_RESULT); + }); + }; + const backCallback = () => { + router.push(Paths.TRANSFER_AMOUNT); + }; + + BackButton?.show(); + BackButton?.onClick(backCallback); + + MainButton?.show(); + MainButton?.setText('Confirm'); + MainButton?.onClick(mainCallback); + + return () => { + MainButton?.hideProgress(); + MainButton?.offClick(mainCallback); + BackButton?.offClick(backCallback); + }; + }, [selectedAsset]); + + const details = [ + { + title: 'Recipients address', + value: selectedAsset?.destinationAddress, + }, + { + title: 'Fee', + value: `${selectedAsset?.fee} ${selectedAsset?.symbol}`, + }, + { + title: 'Total amount', + value: `${(Number(selectedAsset?.amount) + (selectedAsset?.fee as number)).toFixed(5)} ${selectedAsset?.symbol}`, + }, + { + title: 'Network', + value: selectedAsset?.name, + }, + ]; + + return ( + <> +
+ + + Send to + + + + {selectedAsset?.destinationAddress} + + + + +
+
+ + {selectedAsset?.symbol} + {selectedAsset?.amount} +
+ + {details.map(({ title, value }, index) => ( +
+ {index !== 0 && } +
+ + {title} + + {value} +
+
+ ))} +
+ + ); +} + +ConfirmationPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/src/pages/transfer/index.tsx b/src/pages/transfer/index.tsx index 53ac5c66..7bd2295e 100644 --- a/src/pages/transfer/index.tsx +++ b/src/pages/transfer/index.tsx @@ -12,13 +12,15 @@ export default function TransferPage() { const { BackButton } = useTelegram(); useEffect(() => { - BackButton?.show(); - BackButton?.onClick(() => { + const callback = () => { router.push(Paths.DASHBOARD); - }); + }; + BackButton?.show(); + BackButton?.onClick(callback); return () => { BackButton?.hide(); + BackButton?.offClick(callback); }; }, []); diff --git a/src/pages/transfer/result.tsx b/src/pages/transfer/result.tsx new file mode 100644 index 00000000..258f7c60 --- /dev/null +++ b/src/pages/transfer/result.tsx @@ -0,0 +1,45 @@ +'use client'; +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; + +import { useTelegram } from '@common/providers/telegramProvider'; +import { useGlobalContext } from '@/common/providers/contextProvider'; +import { Paths } from '@/common/routing'; +import { HeadlineText, TitleText } from '@/components'; + +export default function ResultPage() { + const router = useRouter(); + const { BackButton, MainButton } = useTelegram(); + const { selectedAsset, setSelectedAsset } = useGlobalContext(); + + useEffect(() => { + MainButton?.setText('Done'); + BackButton?.hide(); + MainButton?.show(); + const callback = () => { + router.replace(Paths.DASHBOARD); + }; + MainButton?.onClick(callback); + + return () => { + MainButton?.hide(); + MainButton?.setText('Continue'); + MainButton?.offClick(callback); + setSelectedAsset(null); + }; + }, []); + + return ( +
+ + {selectedAsset?.amount} {selectedAsset?.symbol} Sent to + + + {selectedAsset?.destinationAddress} + + + Your transaction has been sent to the network and will be processed in a few seconds. + +
+ ); +} diff --git a/src/pages/transfer/select-token.tsx b/src/pages/transfer/select-token.tsx index d7b9b18c..c0a41588 100644 --- a/src/pages/transfer/select-token.tsx +++ b/src/pages/transfer/select-token.tsx @@ -14,10 +14,17 @@ export default function SelectTokenPage() { const { assets, setSelectedAsset } = useGlobalContext(); useEffect(() => { - BackButton?.show(); - BackButton?.onClick(() => { + router.prefetch(Paths.TRANSFER_ADDRESS); + + const callback = () => { router.push(Paths.TRANSFER); - }); + }; + BackButton?.show(); + BackButton?.onClick(callback); + + return () => { + BackButton?.offClick(callback); + }; }, []); return ( diff --git a/src/screens/dashboard/main/DashboardMain.tsx b/src/screens/dashboard/main/DashboardMain.tsx index c6e35a28..ee2111dc 100644 --- a/src/screens/dashboard/main/DashboardMain.tsx +++ b/src/screens/dashboard/main/DashboardMain.tsx @@ -20,10 +20,11 @@ export const DashboardMain = () => { const { getAllChains } = useChainRegistry(); const { subscribeBalance } = useBalances(); const { publicKey, setAssets, assets } = useGlobalContext(); - const { user, MainButton } = useTelegram(); + const { user, MainButton, BackButton } = useTelegram(); useEffect(() => { MainButton?.hide(); + BackButton?.hide(); if (!publicKey) { return; } @@ -86,12 +87,6 @@ export const DashboardMain = () => { - {/* - */}
); }; diff --git a/src/screens/onboarding/restore/RestoreWallet.tsx b/src/screens/onboarding/restore/RestoreWallet.tsx index 1d405172..941ff4a9 100644 --- a/src/screens/onboarding/restore/RestoreWallet.tsx +++ b/src/screens/onboarding/restore/RestoreWallet.tsx @@ -27,26 +27,30 @@ export const RestoreWalletPage = ({ mnemonic }: Props) => { }, []); useEffect(() => { - if (password.length) { - MainButton?.enable(); - - MainButton?.onClick(() => { - if (password.length < 8) { - setIsPasswordValid(false); + const callback = () => { + if (password.length < 8) { + setIsPasswordValid(false); - return; - } - const wallet = initializeWalletFromCloud(password, mnemonic); - setIsPasswordValid(Boolean(wallet)); + return; + } + const wallet = initializeWalletFromCloud(password, mnemonic); + setIsPasswordValid(Boolean(wallet)); + if (wallet) { + setPublicKey(wallet?.publicKey); + router.push(Paths.DASHBOARD); + } + }; - if (wallet) { - setPublicKey(wallet?.publicKey); - router.push(Paths.DASHBOARD); - } - }); + if (password.length) { + MainButton?.enable(); + MainButton?.onClick(callback); } else { MainButton?.disable(); } + + return () => { + MainButton?.offClick(callback); + }; }, [password]); return (