Skip to content

Commit

Permalink
Merge pull request #350 from pendulum-chain/improve-feedback-on-login
Browse files Browse the repository at this point in the history
Improve UI on log-in modal.
  • Loading branch information
gianfra-t authored Jan 21, 2025
2 parents a20a037 + f5af08a commit 6252890
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 148 deletions.
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'));
}
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

0 comments on commit 6252890

Please sign in to comment.