diff --git a/.gitignore b/.gitignore index 06629127..96b70249 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ storybook-static/* .sentryclirc .env*.local +pr-review-report.md \ No newline at end of file diff --git a/components/HeaderWithMenu/index.tsx b/components/HeaderWithMenu/index.tsx index 9f71f741..fe4c5388 100644 --- a/components/HeaderWithMenu/index.tsx +++ b/components/HeaderWithMenu/index.tsx @@ -1,51 +1,47 @@ -import { useIntercom } from "react-use-intercom" import IconButton from "@/components/buttons/iconButton" import GoHomeButton from "@/components/utils/GoHome" import { ArrowLeft } from 'lucide-react' -import ChatIcon from "@/components/Icons/ChatIcon" -import dynamic from "next/dynamic" import LayerswapMenu from "@/components/LayerswapMenu" import { useQueryState } from "@/context/query" +import { UserStatusHeader } from "../SecretDerivation" +import useWindowDimensions from "@/hooks/useWindowDimensions" +import dynamic from "next/dynamic" const WalletsHeader = dynamic(() => import("../Wallet/ConnectedWallets.tsx").then((comp) => comp.WalletsHeader), { loading: () => <> }) - function HeaderWithMenu({ goBack }: { goBack: (() => void) | undefined | null }) { - const { boot, show, update } = useIntercom() const query = useQueryState() + const { isMobile } = useWindowDimensions() return ( -
- { - goBack && - - }> - - } - { - !query.hideLogo &&
- -
- } -
+
+
+ { + goBack && +
+ + } /> +
+ } + { + !query.hideLogo &&
+ +
+ } +
+
+ { + isMobile + ? + : null + } - { - boot(); - show(); - update() - }} - icon={ - - }> - -
- -
+
) diff --git a/components/LayerswapMenu/Menu.tsx b/components/LayerswapMenu/Menu.tsx index f863c6a8..2a7c8905 100644 --- a/components/LayerswapMenu/Menu.tsx +++ b/components/LayerswapMenu/Menu.tsx @@ -131,7 +131,7 @@ const Footer = ({ children, hidden, sticky = true }: FooterProps) => { bg-secondary-900 shadow-widget-footer p-4 - px-6 + px-4 w-full ${hidden ? 'animation-slide-out' : ''}`}> {children} diff --git a/components/LayerswapMenu/MenuList.tsx b/components/LayerswapMenu/MenuList.tsx index 28a446c2..50484449 100644 --- a/components/LayerswapMenu/MenuList.tsx +++ b/components/LayerswapMenu/MenuList.tsx @@ -13,6 +13,7 @@ import Menu from "./Menu"; import dynamic from "next/dynamic"; import { MenuStep } from "@/Models/Wizard"; import useWindowDimensions from "@/hooks/useWindowDimensions"; +import { UserStatusMenu } from "../SecretDerivation"; const WalletsMenu = dynamic(() => import("../Wallet/ConnectedWallets.tsx").then((comp) => comp.WalletsMenu), { loading: () => <> @@ -35,6 +36,7 @@ const MenuList: FC<{ goToStep: (step: MenuStep, path?: string) => void }> = ({ g return
+ diff --git a/components/LayerswapMenu/index.tsx b/components/LayerswapMenu/index.tsx index 11f2ff1d..185a252c 100644 --- a/components/LayerswapMenu/index.tsx +++ b/components/LayerswapMenu/index.tsx @@ -52,11 +52,9 @@ const Comp = () => { return <>
-
- setIsOpen(true)} icon={ - - } /> -
+ setIsOpen(true)} icon={ + + } /> -
-
+
+
{title}
-
- - }> - -
+ + }> +
+ className='select-text max-h-full overflow-y-auto overflow-x-hidden styled-scroll px-4 h-full' id="virtualListContainer"> {children}
diff --git a/components/Modal/modalWithoutAnimation.tsx b/components/Modal/modalWithoutAnimation.tsx index d5ee76cf..c260956d 100644 --- a/components/Modal/modalWithoutAnimation.tsx +++ b/components/Modal/modalWithoutAnimation.tsx @@ -75,18 +75,16 @@ export const ModalContent = (props: ModalContentProps) => { {header}
{showCloseButton && ( -
- - }> - -
+ + }> + )}
)} -
+
{typeof children === 'function' ? children({ closeModal, shouldFocus }) : children}
diff --git a/components/SecretDerivation/LoginModal/OptionSelect.tsx b/components/SecretDerivation/LoginModal/OptionSelect.tsx new file mode 100644 index 00000000..4ae37326 --- /dev/null +++ b/components/SecretDerivation/LoginModal/OptionSelect.tsx @@ -0,0 +1,55 @@ +import { useConnectModal } from '@/components/WalletModal'; +import useWallet from '@/hooks/useWallet'; +import { Wallet } from '@/Models/WalletProvider'; +import { Fingerprint, Wallet as WalletIcon } from 'lucide-react'; + +const OptionSelect = ({ goToStep, onConnectFinish }: { goToStep: (step: string) => void, onConnectFinish: (wallet?: Wallet) => void }) => { + const { connect } = useConnectModal() + const { providers } = useWallet(); + + const evmProvider = providers.find(p => p.name.toLowerCase() === 'evm'); + const connectedWallets = evmProvider?.connectedWallets || []; + + const selectWallet = async() => { + if(connectedWallets.length < 1) { + const wallet = await connect(evmProvider); + + if(wallet) { + onConnectFinish(wallet); + return + } + } + goToStep('wallet_select') + } + + return ( + <> +

Choose how to login.

+
+ goToStep('passkey_choice')} icon={Fingerprint} title="Passkey" description="Face ID, Touch ID, or Windows Hello" /> + +
+ + ) +} + + +const OptionItem = ({ onClick, icon: Icon, title, description }: { onClick: () => void, icon: (props: { className: string }) => React.ReactNode, title: string, description: string }) => { + return ( + + ) +} + +export default OptionSelect; \ No newline at end of file diff --git a/components/SecretDerivation/LoginModal/PasskeyChoice.tsx b/components/SecretDerivation/LoginModal/PasskeyChoice.tsx new file mode 100644 index 00000000..29dc503b --- /dev/null +++ b/components/SecretDerivation/LoginModal/PasskeyChoice.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { KeyRound, AlertTriangle } from 'lucide-react'; +import SubmitButton from '../../buttons/submitButton'; + +interface PasskeyChoiceProps { + onUseExisting: () => void; + onCreateNew: (label?: string) => void; + noPasskeyHint?: boolean; +} + +export function PasskeyChoice({ onUseExisting, onCreateNew, noPasskeyHint }: PasskeyChoiceProps) { + + const handleCreateNew = () => { + onCreateNew('Train'); + }; + + return ( +
+
+
+ +
+

Sign in with passkey

+
+ +
+ + Use existing passkey + + {noPasskeyHint && ( +
+ +

+ No passkey found for any related origin +

+
+ )} +
+ +
+
+ OR +
+
+ + Create new passkey + +
+ ); +} diff --git a/components/SecretDerivation/LoginModal/SelectWallet.tsx b/components/SecretDerivation/LoginModal/SelectWallet.tsx new file mode 100644 index 00000000..76f7a972 --- /dev/null +++ b/components/SecretDerivation/LoginModal/SelectWallet.tsx @@ -0,0 +1,58 @@ +import WalletIcon from "@/components/Icons/WalletIcon"; +import shortenAddress from "@/components/utils/ShortenAddress"; +import { useConnectModal } from "@/components/WalletModal"; +import useWallet from "@/hooks/useWallet"; +import { Wallet } from "@/Models/WalletProvider"; +import { Plus } from "lucide-react"; + +interface WalletSelectProps { + startWalletLogin: (wallet: Wallet) => void; +} + +const WalletSelect = ({ startWalletLogin }: WalletSelectProps) => { + const { providers } = useWallet(); + const evmProvider = providers.find(p => p.name.toLowerCase() === 'evm'); + const connectedWallets = evmProvider?.connectedWallets || []; + + const { connect } = useConnectModal(); + + return ( + <> +

Select a connected EVM wallet

+ +
+ {connectedWallets.length === 0 && ( +
+ No EVM wallets connected. +
+ )} + {connectedWallets.map((wallet) => ( + + ))} +
+ + + ); +}; + +export default WalletSelect; \ No newline at end of file diff --git a/components/SecretDerivation/LoginModal/index.tsx b/components/SecretDerivation/LoginModal/index.tsx new file mode 100644 index 00000000..6aa4a918 --- /dev/null +++ b/components/SecretDerivation/LoginModal/index.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from 'react'; +import { useConfig } from 'wagmi'; +import { Loader2, ChevronLeft } from 'lucide-react'; +import toast from 'react-hot-toast'; +import VaulModal from '../../Modal/vaulModal'; +import { useSecretDerivation } from '@/context/secretDerivationContext'; +import { PasskeyChoice } from './PasskeyChoice'; +import { Wallet } from '@/Models/WalletProvider'; +import { useSteps } from '@/hooks/useSteps'; +import { Steps, Step } from '@/components/Step'; +import OptionSelect from './OptionSelect'; +import IconButton from '@/components/buttons/iconButton'; +import WalletSelect from './SelectWallet'; + +type LoginStep = 'pick' | 'passkey_choice' | 'wallet_select' | 'signing'; + +const getErrorMessage = (error: unknown, fallback: string): string => { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return fallback; +}; + +interface LoginModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function LoginModal({ isOpen, onClose }: LoginModalProps) { + const config = useConfig(); + const { loginWithPasskey, loginWithNewPasskey, loginWithWallet, derivationMessage } = useSecretDerivation(); + const { currentStep, goToStep, goBack, canGoBack, reset, isStep } = useSteps({ initial: 'pick' }); + const [noPasskeyHint, setNoPasskeyHint] = useState(false); + + useEffect(() => { + if (isOpen) { + reset(); + setNoPasskeyHint(false); + } + }, [isOpen, reset]); + + const closeAndReset = () => { + onClose(); + }; + + const startUseExistingPasskey = async () => { + goToStep('signing'); + setNoPasskeyHint(false); + try { + await loginWithPasskey({ createIfMissing: false }); + toast.success('Logged in with passkey'); + closeAndReset(); + } catch (e) { + toast.error(getErrorMessage(e, 'Passkey login failed')); + setNoPasskeyHint(true); + goToStep('passkey_choice', 'back'); + } + }; + + const startCreateNewPasskey = async (label?: string) => { + goToStep('signing'); + try { + await loginWithNewPasskey(label); + toast.success('Logged in with passkey'); + closeAndReset(); + } catch (e) { + toast.error(getErrorMessage(e, 'Passkey login failed')); + goToStep('passkey_choice', 'back'); + } + }; + + const startWalletLogin = async (wallet: Wallet) => { + goToStep('signing'); + try { + await loginWithWallet(config, wallet); + toast.success('Logged in with wallet'); + closeAndReset(); + } catch (e) { + toast.error(getErrorMessage(e, 'Wallet login failed')); + goToStep('wallet_select', 'back'); + } + }; + + const onConnectFinish = async (wallet?: Wallet) => { + if (!wallet) { + goToStep('wallet_select', 'back'); + return; + } + await startWalletLogin(wallet); + }; + + const handleBack = () => { + if (isStep('passkey_choice')) { + setNoPasskeyHint(false); + } + goBack(); + }; + + return ( + { + if (!show) closeAndReset(); + }} + header={ +
+ { + (canGoBack || currentStep === 'signing') && +
+ + }> + +
+ } +

{currentStep === 'signing' ? 'Signing' : 'Login to continue'}

+
+ } + modalId="secret-derivation-login-modal" + > + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + + +const Signing = ({ derivationMessage }: { derivationMessage: string }) => { + return ( +
+
+ +
+

+ {derivationMessage || 'Please sign…'} +

+

+ Complete the action in your passkey or wallet. Do not close this window. +

+
+ ) +} diff --git a/components/SecretDerivation/SignFlowModal.tsx b/components/SecretDerivation/SignFlowModal.tsx new file mode 100644 index 00000000..58a981dc --- /dev/null +++ b/components/SecretDerivation/SignFlowModal.tsx @@ -0,0 +1,66 @@ +// components/SecretDerivation/SignFlowModal.tsx +// Single flow: choose method (if needed) → signing state until commit completes + +import { useSecretDerivation } from '@/context/secretDerivationContext'; +import { useEffect, useState } from 'react'; +import VaulModal from '../Modal/vaulModal'; +import { Loader2 } from 'lucide-react'; + +interface SignFlowModalProps { + isOpen: boolean; + onClose: () => void; + onComplete: () => void; + performCommit: () => Promise; +} + +export function SignFlowModal({ isOpen, onClose, onComplete, performCommit }: SignFlowModalProps) { + const { derivationMessage } = useSecretDerivation(); + const [commitStarted, setCommitStarted] = useState(false); + + useEffect(() => { + if (isOpen) { + setCommitStarted(false); + } + }, [isOpen]); + + // useEffect(() => { + // if (!isOpen || commitStarted) return; + + // setCommitStarted(true); + // performCommit() + // .then(() => { + // onComplete(); + // onClose(); + // }) + // .catch(() => { + // setCommitStarted(false); + // }); + // }, [isOpen, commitStarted, performCommit, onComplete, onClose]); + + return ( + { + if (!show) onClose(); + }} + header="Signing" + modalId="secret-derivation-sign-flow" + > + +
+
+
+ +
+

+ {derivationMessage || 'Please sign…'} +

+

+ Complete the action in your passkey or wallet. Do not close this window. +

+
+
+
+
+ ); +} diff --git a/components/SecretDerivation/UserStatus.tsx b/components/SecretDerivation/UserStatus.tsx new file mode 100644 index 00000000..05e06cbc --- /dev/null +++ b/components/SecretDerivation/UserStatus.tsx @@ -0,0 +1,298 @@ +import { useState } from "react" +import { Fingerprint, LogOut } from "lucide-react" +import toast from "react-hot-toast" +import VaulDrawer from "../Modal/vaulModal" +import { Popover, PopoverContent, PopoverTrigger } from "../shadcn/popover" +import { useSecretDerivationStore } from "@/stores/secretDerivationStore" +import { usePasskeyCredentialId } from "@/stores/secretDerivationStore" +import shortenAddress from "../utils/ShortenAddress" +import useWindowDimensions from "@/hooks/useWindowDimensions" +import { formatPasskeyIdForDisplay } from "@/lib/htlc/secretDerivation/passkeyService" +import WalletIcon from "../Icons/WalletIcon" + +interface LoginWallet { + address: string + chainId?: string | number + providerName: string + displayName?: string +} + +interface LoginDataCardProps { + method: 'passkey' | 'wallet_sign' | null + loginWallet: LoginWallet | null + passkeyDisplayId: string | null + onCopyAddress: () => void + className?: string +} + +const LoginDataCard = ({ + method, + loginWallet, + passkeyDisplayId, + onCopyAddress, + className = "flex items-center gap-3 p-3 bg-secondary-700 rounded-xl", +}: LoginDataCardProps) => ( +
+ {method === 'passkey' ? ( + <> +
+ +
+
+ Passkey + {passkeyDisplayId && ( + {passkeyDisplayId} + )} +
+ + ) : ( + <> +
+ +
+
+ + {loginWallet?.displayName || 'EVM Wallet'} + + {loginWallet?.address && ( + + )} +
+ + )} +
+) + +interface UserStatusContentProps { + method: 'passkey' | 'wallet_sign' | null + loginWallet: LoginWallet | null + logout: () => void + onClose?: () => void + showHeader?: boolean + showPasskeyWarning?: boolean +} + +const UserStatusContent = ({ + method, + loginWallet, + logout, + onClose, + showHeader = true, + showPasskeyWarning = true, +}: UserStatusContentProps) => { + const handleLogout = () => { + logout() + onClose?.() + toast.success('Logged out successfully') + } + + const handleCopyAddress = () => { + if (loginWallet?.address) { + navigator.clipboard.writeText(loginWallet.address) + toast.success('Address copied') + } + } + + const storedPasskeyCredId = usePasskeyCredentialId() + const passkeyDisplayId = method === 'passkey' && storedPasskeyCredId + ? formatPasskeyIdForDisplay(storedPasskeyCredId) + : null + + return ( +
+ {showHeader && ( +

Connected with

+ )} + + + {showPasskeyWarning && method === 'passkey' && ( +
+

+ Store your passkeys securely. Losing your passkey means losing access to your account and any associated funds permanently. +

+
+ )} + + +
+ ) +} + +export const UserStatusHeader = () => { + const { + method, + isLoggedIn, + loginWallet, + logout, + } = useSecretDerivationStore() + const [openDrawer, setOpenDrawer] = useState(false) + const [openPopover, setOpenPopover] = useState(false) + const { isMobile } = useWindowDimensions() + const storedPasskeyCredId = usePasskeyCredentialId() + const passkeyDisplayId = method === 'passkey' && storedPasskeyCredId + ? formatPasskeyIdForDisplay(storedPasskeyCredId) + : null + + if (!isLoggedIn) return null + + const pillLabel = method === 'passkey' + ? "Passkey" + : (loginWallet?.displayName || shortenAddress(loginWallet?.address ?? '') || 'Wallet') + + const pillContent = ( + <> + {method === 'passkey' ? ( + + ) : ( + + )} + {pillLabel} + + ) + + const pillClassName = "inline-flex items-center gap-2 py-2 px-3 rounded-full bg-secondary-500 text-primary-text hover:bg-secondary-400 focus:outline-none transition-colors active:animate-press-down" + + return ( + <> + {isMobile ? ( + <> + + setOpenDrawer(false)} + method={method} + loginWallet={loginWallet} + logout={logout} + /> + + ) : ( + + + + + + setOpenPopover(false)} + /> + + + )} + + ) +} + +export const UserStatusMenu = () => { + const { isLoggedIn, method, loginWallet, logout } = useSecretDerivationStore() + const [openModal, setOpenModal] = useState(false) + const storedPasskeyCredId = usePasskeyCredentialId() + const passkeyDisplayId = method === 'passkey' && storedPasskeyCredId + ? formatPasskeyIdForDisplay(storedPasskeyCredId) + : null + + if (!isLoggedIn) return null + + const menuLabel = method === 'passkey' + ? (passkeyDisplayId ? `Passkey · ${passkeyDisplayId}` : 'Passkey') + : `${loginWallet?.displayName || 'Wallet'}${loginWallet?.address ? ` · ${shortenAddress(loginWallet.address)}` : ''}` + + return ( + <> + + setOpenModal(false)} + method={method} + loginWallet={loginWallet} + logout={logout} + /> + + ) +} + +interface UserStatusDrawerProps { + isOpen: boolean + onClose: () => void + method: 'passkey' | 'wallet_sign' | null + loginWallet: LoginWallet | null + logout: () => void +} + +const UserStatusDrawer = ({ isOpen, onClose, method, loginWallet, logout }: UserStatusDrawerProps) => { + return ( + + + + + + ) +} diff --git a/components/SecretDerivation/index.ts b/components/SecretDerivation/index.ts new file mode 100644 index 00000000..1a697980 --- /dev/null +++ b/components/SecretDerivation/index.ts @@ -0,0 +1,5 @@ +// components/SecretDerivation/index.ts + +export { LoginModal } from './LoginModal'; +export { SignFlowModal } from './SignFlowModal'; +export { UserStatusHeader, UserStatusMenu } from './UserStatus'; diff --git a/components/Step/index.tsx b/components/Step/index.tsx new file mode 100644 index 00000000..1344d194 --- /dev/null +++ b/components/Step/index.tsx @@ -0,0 +1,27 @@ +import { createContext, useContext, ReactNode } from 'react'; + +const StepsContext = createContext(null); + +interface StepsProps { + currentStep: T; + children: ReactNode; +} + +export function Steps({ currentStep, children }: StepsProps) { + return ( + + {children} + + ); +} + +interface StepProps { + name: T; + children: ReactNode; +} + +export function Step({ name, children }: StepProps) { + const currentStep = useContext(StepsContext); + if (currentStep !== name) return null; + return <>{children}; +} diff --git a/components/Swap/Atomic/Form.tsx b/components/Swap/Atomic/Form.tsx index 3ab9bf9e..e668db71 100644 --- a/components/Swap/Atomic/Form.tsx +++ b/components/Swap/Atomic/Form.tsx @@ -1,52 +1,31 @@ import { Form, useFormikContext } from "formik"; -import { FC, useCallback, useEffect } from "react"; +import { FC, useEffect } from "react"; import React from "react"; import NetworkFormField from "../../Input/NetworkFormField"; -import LayerSwapApiClient from "../../../lib/trainApiClient"; import { SwapFormValues } from "../../DTOs/SwapFormValues"; -import useSWR from "swr"; -import { ApiResponse } from "../../../Models/ApiResponse"; -import { motion, useCycle } from "framer-motion"; -import { ArrowUpDown, Loader2 } from 'lucide-react' import { Widget } from "../../Widget/Index"; -import { classNames } from "../../utils/classNames"; import { useQueryState } from "../../../context/query"; import FeeDetailsComponent from "../../FeeDetails"; import { useFee } from "../../../context/feeContext"; import AmountField from "../../Input/Amount" -import dynamic from "next/dynamic"; -import { Balance } from "../../../Models/Balance"; import ResizablePanel from "../../ResizablePanel"; -import { Network } from "../../../Models/Network"; -import { resolveRoutesURLForSelectedToken } from "../../../helpers/routes"; import useWallet from "../../../hooks/useWallet"; import FormButton from "../FormButton"; -import { useAtomicState } from "../../../context/atomicContext"; import { hasRequiredDestinationWallet } from "../../../lib/wallets/utils/destinationWalletUtils"; -// const ReserveGasNote = dynamic(() => import("../../ReserveGasNote"), { -// loading: () => <>, -// }); const SwapForm: FC = () => { const { values, - setValues, - errors, isValid, isSubmitting, setFieldValue + errors, isValid, isSubmitting } = useFormikContext(); const { to: destination, - fromCurrency, - toCurrency, - from: source, } = values - const { selectedSourceAccount, setSelectedSourceAccount } = useAtomicState() const { providers, wallets } = useWallet() const { valuesChanger } = useFee() - const layerswapApiClient = new LayerSwapApiClient() const query = useQueryState(); - let valuesSwapperDisabled = false; const { fee, isFeeLoading } = useFee() const actionDisplayName = query?.buttonTextColor || "Swap now" @@ -55,145 +34,27 @@ const SwapForm: FC = () => { valuesChanger(values) }, [values]) - // useEffect(() => { - // if (values.refuel && minAllowedAmount && (Number(values.amount) < minAllowedAmount)) { - // setFieldValue('amount', minAllowedAmount) - // } - // }, [values.refuel, destination, minAllowedAmount]) - - const [animate, cycle] = useCycle( - { rotate: 0 }, - { rotate: 180 } - ); - - // const sourceRoutesEndpoint = (source || destination) ? resolveRoutesURLForSelectedToken({ direction: 'from', network: source?.name, token: fromCurrency?.symbol, includes: { unavailable: true, unmatched: true } }) : null - // const destinationRoutesEndpoint = (source || destination) ? resolveRoutesURLForSelectedToken({ direction: 'to', network: destination?.name, token: toCurrency?.symbol, includes: { unavailable: true, unmatched: true } }) : null - - // const { data: sourceRoutesRes, isLoading: sourceLoading } = useSWR>(sourceRoutesEndpoint, layerswapApiClient.fetcher, { keepPreviousData: true }) - // const { data: destinationRoutesRes, isLoading: destinationLoading } = useSWR>(destinationRoutesEndpoint, layerswapApiClient.fetcher, { keepPreviousData: true }) - // const sourceRoutes = sourceRoutesRes?.data - // const destinationRoutes = destinationRoutesRes?.data - // const sourceCanBeSwapped = !source ? true : (destinationRoutes?.some(l => l.name === source?.name && l.tokens.some(t => t.symbol === fromCurrency?.symbol - // // && t.status === 'active' - // )) ?? false) - // const destinationCanBeSwapped = !destination ? true : (sourceRoutes?.some(l => l.name === destination?.name && l.tokens.some(t => t.symbol === toCurrency?.symbol - // // && t.status === 'active' - // )) ?? false) - - // if (query.lockTo || query.lockFrom || query.hideTo || query.hideFrom) { - // valuesSwapperDisabled = true; - // } - // if (!sourceCanBeSwapped || !destinationCanBeSwapped) { - // valuesSwapperDisabled = true; - // } else if (!source && !destination) { - // valuesSwapperDisabled = true; - // } - - // const valuesSwapper = useCallback(() => { - - // const newFrom = sourceRoutes?.find(l => l.name === destination?.name) - // const newTo = destinationRoutes?.find(l => l.name === source?.name) - // const newFromToken = newFrom?.tokens.find(t => t.symbol === toCurrency?.symbol) - // const newToToken = newTo?.tokens.find(t => t.symbol === fromCurrency?.symbol) - - // const destinationProvider = destination - // ? providers.find(p => p.autofillSupportedNetworks?.includes(destination?.name) && p.connectedWallets?.some(w => !w.isNotAvailable && w.addresses.some(a => a.toLowerCase() === values.destination_address?.toLowerCase()))) - // : undefined - - // const newDestinationProvider = newTo ? providers.find(p => p.autofillSupportedNetworks?.includes(newTo.name) && p.connectedWallets?.some(w => !w.isNotAvailable && w.addresses.some(a => a.toLowerCase() === selectedSourceAccount?.address.toLowerCase()))) - // : undefined - // const oldDestinationWallet = newDestinationProvider?.connectedWallets?.find(w => w.autofillSupportedNetworks?.some(n => n.toLowerCase() === newTo?.name.toLowerCase()) && w.addresses.some(a => a.toLowerCase() === values.destination_address?.toLowerCase())) - // const oldDestinationWalletIsNotCompatible = destinationProvider && (destinationProvider?.name !== newDestinationProvider?.name || !(newTo && oldDestinationWallet?.autofillSupportedNetworks?.some(n => n.toLowerCase() === newTo?.name.toLowerCase()))) - // const destinationWalletIsAvailable = newTo ? newDestinationProvider?.connectedWallets?.some(w => w.autofillSupportedNetworks?.some(n => n.toLowerCase() === newTo.name.toLowerCase()) && w.addresses.some(a => a.toLowerCase() === selectedSourceAccount?.address.toLowerCase())) : undefined - // const oldSourceWalletIsNotCompatible = destinationProvider && (selectedSourceAccount?.wallet.providerName !== destinationProvider?.name || !(newFrom && selectedSourceAccount?.wallet.withdrawalSupportedNetworks?.some(n => n.toLowerCase() === newFrom.name.toLowerCase()))) - - // const changeDestinationAddress = newTo && (oldDestinationWalletIsNotCompatible || oldSourceWalletIsNotCompatible) && destinationWalletIsAvailable - - // const newVales: SwapFormValues = { - // ...values, - // from: newFrom, - // to: newTo, - // fromCurrency: newFromToken, - // toCurrency: newToToken, - // destination_address: values.destination_address, - // depositMethod: undefined - // } - - // if (changeDestinationAddress) { - // newVales.destination_address = selectedSourceAccount?.address - // } - - // setValues(newVales, true); - - // const changeSourceAddress = newFrom && values.depositMethod === 'wallet' && destinationProvider && (oldSourceWalletIsNotCompatible || changeDestinationAddress) - // if (changeSourceAddress && values.destination_address) { - // const sourceAvailableWallet = destinationProvider?.connectedWallets?.find(w => w.withdrawalSupportedNetworks?.some(n => n.toLowerCase() === newFrom.name.toLowerCase()) && w.addresses.some(a => a.toLowerCase() === values.destination_address?.toLowerCase())) - // if (sourceAvailableWallet) { - // setSelectedSourceAccount({ - // wallet: sourceAvailableWallet, - // address: values.destination_address - // }) - // } - // else { - // setSelectedSourceAccount(undefined) - // } - - // } - // }, [values, sourceRoutes, destinationRoutes, sourceCanBeSwapped, selectedSourceAccount]) - - const handleReserveGas = useCallback((walletBalance: Balance, networkGas: number) => { - if (walletBalance && networkGas) - setFieldValue('amount', walletBalance?.amount - networkGas) - }, [values.amount]) - - - const sourceWalletNetwork = values.from - - const shouldConnectWallet = (sourceWalletNetwork && !selectedSourceAccount) || - (!values.from && !wallets.length); + const shouldConnectWallet = !wallets.length; const shouldConnectDestinationWallet = !hasRequiredDestinationWallet(destination, providers); return <> -
+ -
+
{!(query?.hideFrom && values?.from) &&
} - {/* {!query?.hideFrom && !query?.hideTo && - } */} {!(query?.hideTo && values?.to) &&
}
-
+
- {/* { - values.amount && - handleReserveGas(walletBalance, networkGas)} /> - } */}
diff --git a/components/Swap/Atomic/index.tsx b/components/Swap/Atomic/index.tsx index 62e3f4d3..b0809ab2 100644 --- a/components/Swap/Atomic/index.tsx +++ b/components/Swap/Atomic/index.tsx @@ -21,6 +21,7 @@ import { generateSwapInitialValues } from "../../../lib/generateSwapInitialValue import { useSettingsState } from "../../../context/settings"; import { resolvePersistantQueryParams } from "../../../helpers/querryHelper"; import toast from "react-hot-toast"; +import { useSecretDerivation } from "../../../context/secretDerivationContext"; const AtomicPage = dynamicWithRetries( () => import("../AtomicChat/index.tsx") as unknown as Promise<{ default: React.ComponentType }>, @@ -41,6 +42,7 @@ export default function Form() { const { goToStep } = useFormWizardaUpdate() const { currentStepName } = useFormWizardState() const query = useQueryState() + const { isLoggedIn } = useSecretDerivation() const { updatePolling: pollFee, fee } = useFee() const { getProvider } = useWallet() @@ -53,6 +55,11 @@ export default function Form() { const handleSubmit = useCallback(async (values: SwapFormValues) => { try { + // Check if user has logged in (chosen a derivation method) + if (!isLoggedIn) { + throw new Error("Please login first") + } + if (!values.amount) { throw new Error("No amount specified") } @@ -99,7 +106,7 @@ export default function Form() { console.log(error) toast.error(error) } - }, [query, router, getProvider]) + }, [query, router, getProvider, isLoggedIn]) const initialValues: SwapFormValues = generateSwapInitialValues(settings, query) @@ -133,9 +140,7 @@ export default function Form() { > -
- -
+
handleWizardRouting(AtomicSteps.Form, 'back')}> diff --git a/components/Swap/AtomicChat/Actions/UserActions.tsx b/components/Swap/AtomicChat/Actions/UserActions.tsx index 7f77f3cd..026458a7 100644 --- a/components/Swap/AtomicChat/Actions/UserActions.tsx +++ b/components/Swap/AtomicChat/Actions/UserActions.tsx @@ -9,12 +9,14 @@ import { useFee } from "../../../../context/feeContext"; import useCommitDetailsPolling from "../../../../hooks/htlc/useCommitDetailsPolling"; import useLockDetailsPolling from "../../../../hooks/htlc/useLockDetailsPolling"; import useRefundStatusPolling from "../../../../hooks/htlc/useRefundStatusPolling"; +import { SignFlowModal } from "@/components/SecretDerivation"; export const UserCommitAction: FC = () => { const { source_network, destination_network, amount, address, source_asset, destination_asset, onCommit, commitId, updateCommit, srcAtomicContract } = useAtomicState(); const { provider } = useWallet(source_network, 'withdrawal') const wallet = provider?.activeWallet const { fee } = useFee() + const [signFlowOpen, setSignFlowOpen] = useState(false) const atomicContract = srcAtomicContract const destLpAddress = fee?.quote?.destinationSolverAddress @@ -83,6 +85,10 @@ export const UserCommitAction: FC = () => { } } + const onConfirmClick = async () => { + setSignFlowOpen(true) + } + // Poll for commit details using SWR useCommitDetailsPolling({ network: source_network, @@ -105,15 +111,23 @@ export const UserCommitAction: FC = () => { Confirm in wallet : - - Confirm in wallet - + <> + + Confirm in wallet + + setSignFlowOpen(false)} + onComplete={() => setSignFlowOpen(false)} + performCommit={handleCommit} + /> + }
} diff --git a/components/Swap/AtomicChat/AtomicContent/index.tsx b/components/Swap/AtomicChat/AtomicContent/index.tsx index b0c5d968..eca6417b 100644 --- a/components/Swap/AtomicChat/AtomicContent/index.tsx +++ b/components/Swap/AtomicChat/AtomicContent/index.tsx @@ -157,7 +157,7 @@ const ReleasingAssets: FC<{ commitStatus: CommitStatus, isManualClaimable: boole opacity: show ? 1 : 0, height: show ? 'auto' : '172px', }} - className="flex flex-col gap-6 pt-10 pb-6 transition-all duration-500" + className="flex flex-col gap-6 pt-10 pb-4 transition-all duration-500" > {ResolvedIcon}
diff --git a/components/Swap/AtomicChat/index.tsx b/components/Swap/AtomicChat/index.tsx index b8993b59..c5e6c2da 100644 --- a/components/Swap/AtomicChat/index.tsx +++ b/components/Swap/AtomicChat/index.tsx @@ -2,15 +2,23 @@ import { FC } from "react"; import { Widget } from "../../Widget/Index"; import { Actions } from "./Actions"; import AtomicContent from "./AtomicContent"; +import { useSecretDerivation } from "../../../context/secretDerivationContext"; type ContainerProps = { type: "widget" | "contained", } const Commitment: FC = ({ type }) => { + const { isLoggedIn } = useSecretDerivation(); + + // Early return for safety (login already validated by FormButton) + if (!isLoggedIn) { + return null; + } + return ( <> - + diff --git a/components/Swap/FormButton.tsx b/components/Swap/FormButton.tsx index 386ad9c9..caac3bec 100644 --- a/components/Swap/FormButton.tsx +++ b/components/Swap/FormButton.tsx @@ -5,10 +5,13 @@ import SwapButton from "../buttons/swapButton"; import { FormikErrors } from "formik"; import { SwapFormValues } from "../DTOs/SwapFormValues"; import KnownInternalNames from "../../lib/knownIds"; -import { FC } from "react"; +import { FC, useState } from "react"; import { useFormikContext } from "formik"; import useWallet from "../../hooks/useWallet"; import { useConnectModal } from "../WalletModal"; +import { useSecretDerivation } from "../../context/secretDerivationContext"; +import { LoginModal } from "../SecretDerivation"; +import SubmitButton from "../buttons/submitButton"; const Address = dynamic( () => import("../Input/Address/index.tsx").then((mod) => mod.default), @@ -26,6 +29,26 @@ const FormButton = ({ actionDisplayName, shouldConnectDestinationWallet }) => { + const { isLoggedIn } = useSecretDerivation(); + const [loginOpen, setLoginOpen] = useState(false); + + // Check derivation method first (before any other checks) + if (!isLoggedIn) { + return ( + <> + setLoginOpen(true)} + > + Login to continue + + setLoginOpen(false)} + /> + + ); + } if (values.from && values.to && values.fromCurrency && values.toCurrency && values.amount && !quote && !isQuoteLoading) { return = ({ children, basePath, themeData, appName }) => { return ( - - - - - - - - - - - {children} - - - - - - - - - - + + + + + + + + + + + + {children} + + + + + + + + + + + ) } diff --git a/components/Widget/Content.tsx b/components/Widget/Content.tsx index 462a3b5b..8d4dbc43 100644 --- a/components/Widget/Content.tsx +++ b/components/Widget/Content.tsx @@ -1,17 +1,16 @@ type ContetProps = { center?: boolean, children?: JSX.Element | JSX.Element[]; - className?: string; } -const Content = ({ children, center, className }: ContetProps) => { +const Content = ({ children, center }: ContetProps) => { return center ? -
-
-
+
+
+
{children}
- :
{children}
+ :
{children}
} export default Content \ No newline at end of file diff --git a/components/Widget/Footer.tsx b/components/Widget/Footer.tsx index edf462f7..b6705da2 100644 --- a/components/Widget/Footer.tsx +++ b/components/Widget/Footer.tsx @@ -1,5 +1,6 @@ import { motion } from "framer-motion"; import { useMeasure } from "@uidotdev/usehooks"; +import useWindowDimensions from "@/hooks/useWindowDimensions"; const variants = { enter: () => { @@ -31,6 +32,7 @@ type FooterProps = { const Footer = ({ children, hidden, sticky = true }: FooterProps) => { let [footerRef, { height }] = useMeasure(); + const { isMobile } = useWindowDimensions() return ( sticky ? @@ -42,33 +44,32 @@ const Footer = ({ children, hidden, sticky = true }: FooterProps) => { }} custom={{ direction: -1, width: 100 }} variants={variants} - className={!!(height && height !== 0) ? `text-primary-text text-base mt-3 + className={`text-primary-text text-base max-sm:fixed max-sm:inset-x-0 max-sm:bottom-0 max-sm:z-30 - max-sm:bg-secondary-900 + max-sm:bg-secondary-transparent max-sm:shadow-widget-footer max-sm:p-4 - max-sm:px-6 - max-sm:w-full ${hidden ? 'animation-slide-out' : ''}` - : ''}> -
- {children} -
+ max-sm:px-4 + max-sm:w-full ${hidden ? 'animation-slide-out' : ''} w-full`}> + {children} -
-
- + max-sm:w-full invisible sm:hidden w-full`}> +
+ : null + } + : -
- {children} -
+ children ) } export default Footer; \ No newline at end of file diff --git a/components/Widget/Index.tsx b/components/Widget/Index.tsx index 9d40f448..b9abc04c 100644 --- a/components/Widget/Index.tsx +++ b/components/Widget/Index.tsx @@ -42,10 +42,10 @@ const Widget = ({ children, className, hideMenu }: Props) => { !hideMenu && } -
+
-
+
{children}
diff --git a/components/Wizard/Wizard.tsx b/components/Wizard/Wizard.tsx index e88d8656..bd87184b 100644 --- a/components/Wizard/Wizard.tsx +++ b/components/Wizard/Wizard.tsx @@ -15,7 +15,7 @@ const Wizard: FC = ({ children, wizardId, className }) => { const wrapper = useRef(null); const { setWrapperWidth } = useFormWizardaUpdate() - const { wrapperWidth, positionPercent, moving, goBack, noToolBar, hideMenu } = useFormWizardState() + const { wrapperWidth, moving, goBack, noToolBar, hideMenu } = useFormWizardState() useEffect(() => { function handleResize() { @@ -29,7 +29,7 @@ const Wizard: FC = ({ children, wizardId, className }) => { return () => window.removeEventListener("resize", handleResize); }, []); - const width = positionPercent || 0 + return <>
@@ -44,11 +44,11 @@ const Wizard: FC = ({ children, wizardId, className }) => { !hideMenu && } -
+
-
+
{children}
diff --git a/components/Wizard/WizardItem.tsx b/components/Wizard/WizardItem.tsx index 71f7926a..896d28ac 100644 --- a/components/Wizard/WizardItem.tsx +++ b/components/Wizard/WizardItem.tsx @@ -16,7 +16,7 @@ type Props = { const WizardItem: FC = (({ StepName, children, GoBack, PositionPercent, fitHeight = false, className, inModal }: Props) => { const { currentStepName, wrapperWidth, moving } = useFormWizardState() const { setGoBack, setPositionPercent } = useFormWizardaUpdate() - const styleConfigs = fitHeight ? { width: `${wrapperWidth}px`, height: '100%' } : { width: `${wrapperWidth}px`, minHeight: inModal ? 'inherit' : '400px', height: '100%' } + const styleConfigs = fitHeight ? { width: `${wrapperWidth}px`, height: '100%' } : { width: `${wrapperWidth}px`, minHeight: inModal ? 'inherit' : '380px', height: '100%' } useEffect(() => { if (currentStepName === StepName) { diff --git a/components/buttons/iconButton.tsx b/components/buttons/iconButton.tsx index d76f7def..952aa19c 100644 --- a/components/buttons/iconButton.tsx +++ b/components/buttons/iconButton.tsx @@ -5,19 +5,21 @@ interface IconButtonProps extends Omit, 'color' | 'ref' icon?: React.ReactNode } -const IconButton = forwardRef(function IconButton({ className, icon, ...props }, ref){ +const IconButton = forwardRef(function IconButton({ className, icon, ...props }, ref) { const theirProps = props as object; return ( -
- Icon description - + Icon description + +
) }) diff --git a/components/buttons/submitButton.tsx b/components/buttons/submitButton.tsx index 6c17979c..48e854ad 100644 --- a/components/buttons/submitButton.tsx +++ b/components/buttons/submitButton.tsx @@ -32,7 +32,7 @@ const SubmitButton: FC = ({ isDisabled, isSubmitting, icon, c style={style} className={clsx('navigation-focus-ring-text-bold-lg enabled:active:animate-press-down text-primary focus:outline-none focus:ring-0 items-center space-x-1 disabled:bg-primary-900 disabled:text-primary-buttonTextColor/50 disabled:cursor-not-allowed relative w-full flex justify-center font-medium rounded-xl transform hover:brightness-125 transition duration-200 ease-in-out', { className, - 'text-primary-buttonTextColor bg-primary-500': buttonStyle === 'filled', + 'text-primary-buttonTextColor bg-primary-500 hover:bg-primary-500/80': buttonStyle === 'filled', 'text-primary-text bg-secondary-300 hover:bg-secondary-400': buttonStyle === 'secondary', 'py-4 px-4': size === 'large', 'py-3 px-2 md:px-3': size === 'medium', diff --git a/components/globalFooter.tsx b/components/globalFooter.tsx index e71aa53b..60fa25b8 100644 --- a/components/globalFooter.tsx +++ b/components/globalFooter.tsx @@ -40,7 +40,7 @@ const GLobalFooter = () => { } return ( -