Skip to content

Commit

Permalink
feat: transaction stepper with all states
Browse files Browse the repository at this point in the history
  • Loading branch information
Addminus committed Mar 17, 2022
1 parent ec58843 commit 34a2278
Show file tree
Hide file tree
Showing 20 changed files with 962 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/widget/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SettingsDrawerBase } from './components/SettingsDrawer';
import { WalletHeader } from './components/WalletHeader';
import { queryClient } from './config/queryClient';
import { SwapPage } from './pages/SwapPage';
import { TransactionPage } from './pages/TransactionPage';
import { SwapFormProvider } from './providers/SwapFormProvider';
import { WidgetProvider } from './providers/WidgetProvider';
import { theme } from './theme';
Expand Down Expand Up @@ -47,6 +48,10 @@ export const App: React.FC<AppProps> = ({ config }) => {
<Route path={routes.settings} element={null} />
<Route path={routes.selectWallet} element={null} />
</Route>
<Route
path={routes.transaction}
element={<TransactionPage />}
/>
</Routes>
</SwapFormProvider>
</WidgetProvider>
Expand Down
15 changes: 15 additions & 0 deletions packages/widget/src/components/EmailInput/EmailInput.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { styled } from '@mui/material/styles';
import { Input as InputBase } from '../Input';

export const Input = styled(InputBase)({
border: '1px solid #E4E7E9',
borderRadius: '4px',
'& input[type="number"]::-webkit-outer-spin-button, & input[type="number"]::-webkit-inner-spin-button':
{
WebkitAppearance: 'none',
margin: 0,
},
'& input[type="number"]': {
MozAppearance: 'textfield',
},
});
79 changes: 79 additions & 0 deletions packages/widget/src/components/EmailInput/EmailInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Button, FormControl } from '@mui/material';
import { Box } from '@mui/system';
import { ChangeEvent, useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
SwapFormKeyHelper,
SwapFormTypeProps,
} from '../../providers/SwapFormProvider';
import { formatAmount } from '../../utils/format';
import { SwapInputAdornment } from '../SwapInputAdornment';
import { Input } from './EmailInput.style';

const InputEndAdornment = () => {
return <span style={{ color: 'grey' }}>@</span>;
};

export const EmailInput = () => {
const { t } = useTranslation();
const {
register,
setValue,
formState: { isSubmitting },
} = useFormContext();
// const amountKey = SwapFormKeyHelper.getAmountKey(formType);
// const value = useWatch({
// name: amountKey,
// });

// useEffect(() => {
// register(amountKey, { required: true });
// }, [amountKey, register]);

// const handleChange = (
// event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
// ) => {
// const { value } = event.target;
// setValue(amountKey, formatAmount(value, true));
// };

// const handleBlur = (
// event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
// ) => {
// const { value } = event.target;
// setValue(amountKey, formatAmount(value));
// };

return (
<Box
px={3}
component="form"
sx={{
margin: '0 auto',
'& > :not(style)': { m: 1, width: '25ch' },
}}
>
<FormControl disabled={isSubmitting}>
<Input
autoComplete="off"
placeholder={t(`transaction.footer.emailForm.inputPlaceholder`)}
endAdornment={<InputEndAdornment />}
inputProps={{
inputMode: 'email',
}}
// onChange={handleChange}
// onBlur={handleBlur}
// value={value}
// name={amountKey}
required
/>

{/* <FormHelperText id="swap-from-helper-text">Text</FormHelperText> */}
</FormControl>
<Button size="large" variant="contained">
{t(`transaction.footer.emailForm.submitBtn`)}
</Button>
</Box>
);
};
1 change: 1 addition & 0 deletions packages/widget/src/components/EmailInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './EmailInput';
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
return t(`swap.to`);
case routes.selectWallet:
return t(`header.selectWallet`);
case routes.transaction:
return t(`header.processIsOn`);
default:
return t(`header.swap`);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/widget/src/components/SwapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { LoadingButton } from '@mui/lab';
import { styled } from '@mui/material/styles';
import { useRef } from 'react';
import { useWatch } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ChainId, getChainById } from '..';
import { SwapFormKeyHelper } from '../providers/SwapFormProvider';
import { useWalletInterface } from '../services/walletInterface';
import { SelectWalletDrawer } from './SelectWalletDrawer/SelectWalletDrawer';
import { SelectWalletDrawerBase } from './SelectWalletDrawer/types';
import { routes } from '../utils/routes';

export const Button = styled(LoadingButton)({
textTransform: 'none',
Expand All @@ -16,6 +18,7 @@ export const Button = styled(LoadingButton)({
});

export const SwapButton = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const { accountInformation, switchChain } = useWalletInterface();
const [chainId] = useWatch({
Expand All @@ -36,6 +39,8 @@ export const SwapButton = () => {
getChainById(chainId || ChainId.ETH).id !== accountInformation.chainId
) {
await switchChain(chainId!);
} else {
navigate(routes.transaction, { replace: true });
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import StepConnector, {
stepConnectorClasses,
} from '@mui/material/StepConnector';
import Step, { stepClasses } from '@mui/material/Step';
import StepLabel, { stepLabelClasses } from '@mui/material/StepLabel';
import { styled } from '@mui/system';
import { StepIconProps, Theme } from '@mui/material';
import { SignIcon } from '@lifinance/widget/components/TransactionStepper/icons/signIcon';
import { TickIcon } from './icons/tickIcon';

const MIN_STEPPER_LINE_HEIGHT = 128;

interface StepperIconBubbleOptions {
active?: boolean;
completed?: boolean;
error?: boolean;
}

export const TransactionStep = styled(Step)(({ theme }) => ({
position: 'relative',
}));

export const TransactionStepLabel = styled(StepLabel)(({ theme }) => ({
[`&.${stepLabelClasses.root}`]: {
margin: '-20px 0 -2px -3px',
padding: 0,
},
}));

export const TransactionStepperConnector = styled(StepConnector)(
({ theme }) => ({
[`&.${stepConnectorClasses.alternativeLabel}`]: {},
[`&.${stepConnectorClasses.active}`]: {
[`& .${stepConnectorClasses.line}`]: {
background: theme.palette.primary.main,
},
[`& .${stepConnectorClasses.line}:before`]: {
content: '" "',
position: 'absolute',
height: MIN_STEPPER_LINE_HEIGHT,
top: 0,
left: -64,
width: 64,
// background: `linear-gradient(0deg, ${theme.palette.primary.main} 0%, rgba(255,255,255,1) 100%)`,
background: `linear-gradient(0deg, ${theme.palette.primary.main} 0%, rgba(255,255,255,0) 95%)`,
opacity: 0.3,
},
[`& .${stepConnectorClasses.line}:after`]: {
content: '" "',
position: 'absolute',
bottom: 0,
left: -64,
height: 1.5,
width: '160px',

background: theme.palette.primary.main,

// backgroundColor:
// theme.palette.mode === 'dark' ? theme.palette.grey[200] : '#eaeaf0',
},
},
[`&.${stepConnectorClasses.completed}`]: {
[`& .${stepConnectorClasses.line}`]: {
background: theme.palette.primary.main,
},
},
[`& .${stepConnectorClasses.line}`]: {
position: 'relative',
height: MIN_STEPPER_LINE_HEIGHT,
width: 10,

border: 0,
backgroundColor:
theme.palette.mode === 'dark' ? theme.palette.grey[200] : '#eaeaf0',
},
}),
);

export const StepperIconBubble = styled('div')(
({
theme,
options,
}: {
theme?: Theme;
options: StepperIconBubbleOptions;
}) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: 40,
height: 40,
borderRadius: 30,
zIndex: 999,
boxShadow: '0px 4px 6px -8px rgba(0, 0, 0, 0.1)',
boxSizing: 'border-box',

border: options.completed
? '1px solid white'
: options.active
? '2px solid #3F49E1'
: '1px solid rgba(0, 0, 0, 0.05)',
color: options.completed ? 'white' : options.active ? '#3F49E1' : 'black',
background: options.completed ? '#3F49E1' : 'white',
}),
);

export function StepperIcons(props: StepIconProps) {
const { active, completed, error } = props;

return (
<StepperIconBubble options={{ active, completed, error }}>
<SignIcon completed={completed} />
</StepperIconBubble>
);
}

export function DoneStepperIcon(props: StepIconProps) {
const { active, completed, error } = props;
return (
<StepperIconBubble options={{ active, completed, error }}>
<TickIcon completed={completed} />
</StepperIconBubble>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Step } from '@lifinance/sdk';
import { Box, Button, Stepper, Typography } from '@mui/material';
import { useState } from 'react';
import { TFunction, useTranslation } from 'react-i18next';
import {
TransactionStepperConnector,
TransactionStep,
TransactionStepLabel,
DoneStepperIcon,
} from './TransactionStepper.style';
import { Props, TransactionStepperProps } from './types';

function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

const RouteStepLabel: React.FC<Props> = ({
children,
active,
action,
error,
}) => {
return (
<TransactionStepLabel
error={error}
color={error ? 'error' : 'primary'}
StepIconComponent={action ? DoneStepperIcon : undefined}
>
{children}
</TransactionStepLabel>
);
};

const LabelContent: React.FC<Props> = ({ children, active }) => {
let sx;
if (active) {
sx = { position: 'absolute', left: 158, top: -12 };
}
return <Box sx={sx}>{children}</Box>;
};

const renderStep = (
step: Step,
index: number,
t: TFunction<'translation', undefined>,
) => {
const isSigning = false;
const isWaiting = true;
const isFailed = false;

const SignButton = (
<Button size="large" variant="contained">
{t(`transaction.signPrompt`)}
</Button>
);

const details = (
<Typography>
<u>{t(`transaction.txDetails`)}</u>
</Typography>
);

const waitingText = (
<Typography>
4:34 / {(step.estimate.executionDuration / 60).toFixed(0)}{' '}
{capitalizeFirstLetter(step.tool)}
</Typography>
);

return [
<TransactionStep key={`${step.id}_action`}>
<RouteStepLabel action active={isSigning} error={isFailed}>
<LabelContent active={isSigning}>
{isSigning ? SignButton : details}
</LabelContent>
</RouteStepLabel>
</TransactionStep>,
<TransactionStep key={`${step.id}_wait`}>
<LabelContent active={isWaiting}>
{isWaiting ? waitingText : undefined}
</LabelContent>
</TransactionStep>,
];
};

export const TransactionStepper: React.FC<TransactionStepperProps> = ({
route,
}) => {
const { t } = useTranslation();
const [activeStep, setActiveStep] = useState<number>(2);

return (
<Stepper
activeStep={activeStep}
orientation="vertical"
connector={<TransactionStepperConnector />}
>
<TransactionStep key={-1} />
{route.steps.map((step, index) => renderStep(step, index, t))}
<TransactionStep key={route.steps.length}>
<TransactionStepLabel StepIconComponent={DoneStepperIcon}>
{(
route.steps
.map((step) => step.estimate.executionDuration)
.reduce((cumulated, x) => cumulated + x) / 60
).toFixed(1)}{' '}
min
</TransactionStepLabel>
</TransactionStep>
</Stepper>
);
};
Loading

0 comments on commit 34a2278

Please sign in to comment.