Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve UI on log-in modal. #350

Merged
merged 23 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7637725
improve UI on log in
gianfra-t Jan 3, 2025
c28850a
import from preact
gianfra-t Jan 6, 2025
ccefbe5
Merge branch 'polygon-prototype-staging' into improve-feedback-on-login
gianfra-t Jan 7, 2025
c0b37b0
force handleSignIn trigger on confirm click
gianfra-t Jan 7, 2025
0bc5aac
reuse signing box for login
gianfra-t Jan 13, 2025
f7c14d9
better error handling on wallet rejections
gianfra-t Jan 13, 2025
197a50e
Merge branch 'polygon-prototype-staging' into improve-feedback-on-login
gianfra-t Jan 13, 2025
932d4a7
log signature login raw error
gianfra-t Jan 13, 2025
9b96dfa
support different error message after wallet rejection
gianfra-t Jan 13, 2025
f41851f
Enable progress bar and footer also for `login` phase
ebma Jan 16, 2025
9c8f363
Merge branch 'polygon-prototype-staging' into improve-feedback-on-login
gianfra-t Jan 16, 2025
591190b
reset sep variables also when changing network
gianfra-t Jan 16, 2025
3f6551f
sign finished animation
gianfra-t Jan 16, 2025
b1520d5
improve animation code
gianfra-t Jan 17, 2025
9ae32d7
show toast error on sign rejection
gianfra-t Jan 17, 2025
17ebe3d
typo, small sign box improvement for assethub
gianfra-t Jan 17, 2025
67d4903
rollback on useless change
gianfra-t Jan 17, 2025
8d55f73
fix lint errors
gianfra-t Jan 17, 2025
680218b
move user reject detection to signChallenge component
gianfra-t Jan 17, 2025
c80dbdf
Improve SigningBox animation
Sharqiewicz Jan 20, 2025
c18b9db
Merge branch 'polygon-prototype-staging' into improve-feedback-on-login
gianfra-t Jan 21, 2025
5d5d74a
tweak signing box animation
gianfra-t Jan 21, 2025
f5af08a
Update src/hooks/offramp/useSubmitOfframp.ts
gianfra-t Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/assets/account-balance-wallet-blue.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 0 additions & 36 deletions src/components/SignIn/index.tsx

This file was deleted.

178 changes: 99 additions & 79 deletions src/components/SigningBox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,125 @@
import { Progress } from 'react-daisyui';
import { FC } from 'preact/compat';
import accountBalanceWalletIcon from '../../assets/account-balance-wallet.svg';
import { FC, useState, useEffect } from 'preact/compat';
import { motion, AnimatePresence } from 'framer-motion';

import { SigningPhase } from '../../hooks/offramp/useMainProcess';
import { isNetworkEVM, Networks } from '../../helpers/networks';
import accountBalanceWalletIcon from '../../assets/account-balance-wallet-blue.svg';
import { OfframpSigningPhase } from '../../types/offramp';
import { isNetworkEVM } from '../../helpers/networks';
import { useNetwork } from '../../contexts/network';
import { Spinner } from '../Spinner';

type ProgressStep = {
started: string;
signed: string;
finished: string;
approved: string;
type ProgressConfig = {
[key in OfframpSigningPhase]: number;
};

type SignatureConfig = {
maxSignatures: number;
getSignatureNumber: (step: SigningPhase) => string;
const PROGRESS_CONFIGS: Record<'EVM' | 'NON_EVM', ProgressConfig> = {
EVM: {
started: 25,
approved: 50,
signed: 75,
finished: 100,
login: 15,
},
NON_EVM: {
started: 33,
finished: 100,
signed: 0,
approved: 0,
login: 15,
},
};

const EVM_PROGRESS_CONFIG: ProgressStep = {
started: '25',
approved: '50',
signed: '75',
finished: '100',
};

const NON_EVM_PROGRESS_CONFIG: ProgressStep = {
started: '33',
finished: '100',
signed: '0',
approved: '0',
};

const EVM_SIGNATURE_CONFIG: SignatureConfig = {
maxSignatures: 2,
getSignatureNumber: (step: SigningPhase) => (step === 'started' ? '1' : '2'),
};

const NON_EVM_SIGNATURE_CONFIG: SignatureConfig = {
maxSignatures: 1,
getSignatureNumber: () => '1',
};

const getProgressConfig = (network: Networks): ProgressStep => {
return isNetworkEVM(network) ? EVM_PROGRESS_CONFIG : NON_EVM_PROGRESS_CONFIG;
};

const getSignatureConfig = (network: Networks): SignatureConfig => {
return isNetworkEVM(network) ? EVM_SIGNATURE_CONFIG : NON_EVM_SIGNATURE_CONFIG;
const getSignatureDetails = (step: OfframpSigningPhase, isEVM: boolean) => {
if (!isEVM) return { max: 1, current: 1 };
if (step === 'login') return { max: 1, current: 1 };
if (step === 'started') return { max: 2, current: 1 };
return { max: 2, current: 2 };
};

interface SigningBoxProps {
step?: SigningPhase;
step?: OfframpSigningPhase;
}

const isValidStep = (step: SigningPhase | undefined, network: Networks): step is SigningPhase => {
const isValidStep = (step: OfframpSigningPhase | undefined, isEVM: boolean): step is OfframpSigningPhase => {
if (!step) return false;
if (!['started', 'approved', 'signed'].includes(step)) return false;
if (!isNetworkEVM(network) && (step === 'approved' || step === 'signed')) return false;
if (step === 'finished' || step === 'login') return true;
if (!isEVM && (step === 'approved' || step === 'signed')) return false;
return true;
};

export const SigningBox: FC<SigningBoxProps> = ({ step }) => {
const { selectedNetwork } = useNetwork();
const isEVM = isNetworkEVM(selectedNetwork);
const progressConfig = isEVM ? PROGRESS_CONFIGS.EVM : PROGRESS_CONFIGS.NON_EVM;

if (!isValidStep(step, selectedNetwork)) return null;
const [progress, setProgress] = useState(0);
const [signatureState, setSignatureState] = useState({ max: 0, current: 0 });
const [shouldExit, setShouldExit] = useState(false);

const progressValue = getProgressConfig(selectedNetwork)[step];
const { maxSignatures, getSignatureNumber } = getSignatureConfig(selectedNetwork);
useEffect(() => {
if (!isValidStep(step, isEVM)) return;

return (
<section className="z-50 toast toast-end">
<div className="shadow-2xl">
<header className="bg-pink-500 rounded-t">
<h1 className="w-full py-2 text-center text-white">Action Required</h1>
</header>

<main className="px-8 bg-white">
<div className="flex items-center justify-center">
<div className="flex items-center justify-center w-10 h-10 border rounded-full border-primary">
<img src={accountBalanceWalletIcon} alt="wallet account button" />
</div>
<div className="mx-4 my-5 text-xs">
<p>Please sign the transaction in</p>
<p>your connected wallet to proceed</p>
</div>
</div>
if (step !== 'finished' && shouldExit) {
setShouldExit(false);
}

<div className="w-full pb-2.5">
<Progress value={progressValue} max="100" className="h-4 bg-white border progress-primary border-primary" />
if (step === 'finished') {
setProgress(100);
setTimeout(() => setShouldExit(true), 2500);
return;
}

setProgress(progressConfig[step]);
setSignatureState(getSignatureDetails(step, isEVM));
}, [step, isEVM, progressConfig, shouldExit]);

return (
<AnimatePresence mode="wait">
{!isValidStep(step, isEVM) || shouldExit ? null : (
<motion.section
className="z-50 toast toast-end"
initial={{ y: 150 }}
animate={{ y: 0, transition: { type: 'spring', bounce: 0.4 } }}
exit={{ y: 150 }}
transition={{ duration: 0.5 }}
key="signing-box"
>
<div className="shadow-2xl">
<motion.header className="bg-pink-500 rounded-t">
<h1 className="w-full py-2 text-center text-white">Action Required</h1>
</motion.header>

<main className="px-8 bg-white">
<motion.div className="flex items-center justify-center">
<div className="flex items-center justify-center w-10 h-10 border rounded-full border-primary">
<img src={accountBalanceWalletIcon} alt="wallet account button" />
</div>
<div className="mx-4 my-5 text-xs">
<p>Please sign the transaction in</p>
<p>your connected wallet to proceed</p>
</div>
</motion.div>

<motion.div className="w-full pb-2.5">
<div className="w-full h-4 overflow-hidden bg-white border rounded-full border-primary">
<motion.div
className="h-full rounded-full bg-primary"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5, ease: 'linear' }}
/>
</div>
</motion.div>
</main>

<motion.footer className="flex items-center justify-center bg-[#5E88D5] text-white rounded-b">
<Spinner />
<p className="ml-2.5 my-2 text-xs">
Waiting for signature {signatureState.current}/{signatureState.max}
</p>
</motion.footer>
</div>
</main>

<footer className="flex items-center justify-center bg-[#5E88D5] text-white rounded-b">
<Spinner />
<p className="ml-2.5 my-2 text-xs">
Waiting for signature {getSignatureNumber(step)}/{maxSignatures}
</p>
</footer>
</div>
</section>
</motion.section>
)}
</AnimatePresence>
);
};
5 changes: 4 additions & 1 deletion src/contexts/network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useLocalStorage, LocalStorageKeys } from '../hooks/useLocalStorage';
import { WALLETCONNECT_ASSETHUB_ID } from '../constants/constants';
import { useOfframpActions } from '../stores/offrampStore';
import { getNetworkId, isNetworkEVM, Networks } from '../helpers/networks';
import { useSep24Actions } from '../stores/sep24Store';

interface NetworkContextType {
walletConnectPolkadotSelectedNetworkId: string;
Expand Down Expand Up @@ -36,11 +37,13 @@ export const NetworkProvider = ({ children }: NetworkProviderProps) => {
const [networkSelectorDisabled, setNetworkSelectorDisabled] = useState(false);

const { resetOfframpState } = useOfframpActions();
const { cleanup: cleanupSep24Variables } = useSep24Actions();
const { switchChain } = useSwitchChain();

const setSelectedNetwork = useCallback(
(network: Networks) => {
resetOfframpState();
cleanupSep24Variables();
setSelectedNetworkState(network);
setSelectedNetworkLocalStorage(network);

Expand All @@ -49,7 +52,7 @@ export const NetworkProvider = ({ children }: NetworkProviderProps) => {
switchChain({ chainId: getNetworkId(network) });
}
},
[switchChain, setSelectedNetworkLocalStorage, resetOfframpState],
[switchChain, setSelectedNetworkLocalStorage, resetOfframpState, cleanupSep24Variables],
);

// Only run on first render
Expand Down
1 change: 0 additions & 1 deletion src/hooks/offramp/useMainProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { useOfframpActions, useOfframpState } from '../../stores/offrampStore';
import { useSep24UrlInterval, useSep24InitialResponse } from '../../stores/sep24Store';
import { useSep24Actions } from '../../stores/sep24Store';
import { useAnchorWindowHandler } from './useSEP24/useAnchorWindowHandler';
export type SigningPhase = 'started' | 'approved' | 'signed' | 'finished';

export interface ExecutionInput {
inputTokenType: InputTokenType;
Expand Down
8 changes: 5 additions & 3 deletions src/hooks/offramp/useSubmitOfframp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { useOfframpActions, useOfframpStarted, useOfframpState } from '../../sto
import { ExecutionInput } from './useMainProcess';
import { useSep24Actions } from '../../stores/sep24Store';

import { showToast, ToastMessage } from '../../helpers/notifications';

export const useSubmitOfframp = () => {
const { selectedNetwork } = useNetwork();
const { switchChainAsync, switchChain } = useSwitchChain();
Expand Down Expand Up @@ -124,10 +126,10 @@ export const useSubmitOfframp = () => {
setOfframpInitiating(false);
}
} catch (error) {
console.error('Error initializing the offramping process', error);
console.error('Error initializing the offramping process', (error as Error).message);
// Display error message, differentiating between user rejection and other errors
if ((error as Error).message.includes('User rejected the request')) {
setInitializeFailed('Please switch to the correct network and try again.');
if ((error as Error).message.includes('User rejected')) {
showToast(ToastMessage.ERROR, 'You must sign the login request to be able to sell Argentine Peso');
} else {
setInitializeFailed();
}
Expand Down
38 changes: 20 additions & 18 deletions src/hooks/useSignChallenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { DEFAULT_LOGIN_EXPIRATION_TIME_HOURS } from '../constants/constants';
import { SIGNING_SERVICE_URL } from '../constants/constants';
import { storageKeys } from '../constants/localStorage';
import { useVortexAccount } from './useVortexAccount';
import { useOfframpActions } from '../stores/offrampStore';
import { useEffect } from 'react';

export interface SiweSignatureData {
signatureSet: boolean;
Expand All @@ -24,9 +26,8 @@ function createSiweMessage(address: string, nonce: string) {
}

export function useSiweSignature() {
const [signingPending, setSigningPending] = useState(false);
const { address, getMessageSignature } = useVortexAccount();

const { setOfframpSigningPhase } = useOfframpActions();
// Used to wait for the modal interaction and/or return of the
// signing promise.
const [signPromise, setSignPromise] = useState<{
Expand Down Expand Up @@ -55,9 +56,9 @@ export function useSiweSignature() {
if (signPromise) return;
return new Promise((resolve, reject) => {
setSignPromise({ resolve, reject });
setSigningPending(true);
setOfframpSigningPhase?.('login');
});
}, [setSigningPending, setSignPromise, signPromise]);
}, [setOfframpSigningPhase, setSignPromise, signPromise]);

const handleSign = useCallback(async () => {
if (!address || !signPromise) return;
Expand Down Expand Up @@ -95,23 +96,27 @@ export function useSiweSignature() {

localStorage.setItem(storageKey, JSON.stringify(signatureData));
signPromise.resolve();
setOfframpSigningPhase?.('finished');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
signPromise.reject(new Error('Signing failed: ' + errorMessage));
setOfframpSigningPhase?.(undefined);

// First case Assethub, second case EVM
if (
(error as Error).message.includes('User rejected the request') ||
(error as Error).message.includes('Cancelled')
) {
return signPromise.reject(new Error('Signing failed: User rejected sign request'));
gianfra-t marked this conversation as resolved.
Show resolved Hide resolved
}
return signPromise.reject(new Error('Signing failed: Failed to sign login challenge. ' + errorMessage));
} finally {
setSigningPending(false);
setSignPromise(null);
}
}, [address, storageKey, signPromise, setSigningPending, setSignPromise, getMessageSignature]);
}, [address, storageKey, signPromise, setSignPromise, getMessageSignature, setOfframpSigningPhase]);

// Handler for modal cancellation
const handleCancel = useCallback(() => {
if (signPromise) {
signPromise.reject(new Error('User cancelled'));
setSignPromise(null);
}
setSigningPending(false);
}, [signPromise, setSigningPending, setSignPromise]);
useEffect(() => {
if (signPromise) handleSign();
}, [signPromise, handleSign]);

const checkAndWaitForSignature = useCallback(async (): Promise<void> => {
const stored = checkStoredSignature();
Expand All @@ -125,9 +130,6 @@ export function useSiweSignature() {
}, [storageKey, signMessage]);

return {
signingPending,
handleSign,
handleCancel,
checkAndWaitForSignature,
forceRefreshAndWaitForSignature,
};
Expand Down
Loading
Loading