Skip to content

Commit

Permalink
WIP: Masa green UI ( no styles )
Browse files Browse the repository at this point in the history
  • Loading branch information
hide-on-bush-x committed Apr 4, 2023
1 parent 3a18ffc commit 0731466
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 28 deletions.
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -32,6 +32,7 @@
},
"author": "hide-on-bush",
"dependencies": {
"@acusti/react-code-input": "^3.11.0",
"@babel/preset-typescript": "^7.21.0",
"@masa-finance/masa-sdk": "^1.12.3",
"@metamask/providers": "^10.2.1",
Expand All @@ -43,8 +44,10 @@
"html-webpack-plugin": "^5.5.0",
"node-sass": "^8.0.0",
"react": "^18.2.0",
"react-phone-number-input": "^3.2.19",
"react-query": "^3.39.3",
"react-spinners": "^0.13.8",
"reaptcha": "^1.12.1",
"rodal": "^2.0.0",
"storybook-addon-sass-postcss": "^0.1.3"
},
Expand Down
40 changes: 31 additions & 9 deletions src/components/masa-interface/interface-subflow.tsx
@@ -1,34 +1,56 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
export interface SubflowPage {
next: () => void;
back: () => void;
setIndex: React.Dispatch<React.SetStateAction<number>>;
setIndex: React.Dispatch<React.SetStateAction<number | string>>;
context: any;
}

interface InterfaceSubflowProps {
pages: React.FunctionComponent<SubflowPage>[];
situationalPages: { [key: string]: React.FunctionComponent<SubflowPage> };
context: any;
}

export const InterfaceSubflow = ({ pages }: InterfaceSubflowProps) => {
const [index, setIndex] = useState(0);
const Renderer = ({ Render, params }) => {
return <Render {...params} />;
};

const length = useMemo(() => pages.length, [pages]);
export const InterfaceSubflow = ({
pages,
context,
situationalPages,
}: InterfaceSubflowProps) => {
const [index, setIndex] = useState<number | string>(0);

useEffect(() => {
setIndex(0);
}, [pages, setIndex]);
const length = useMemo(() => pages.length, [pages]);

const next = useCallback(() => {
if (typeof index === 'string') return setIndex(0);
if (index < length - 1) {
setIndex(index + 1);
}
}, [index, setIndex, length]);

const back = useCallback(() => {
if (typeof index === 'string') return setIndex(0);
if (index > 0) {
setIndex(index - 1);
}
}, [index, setIndex]);

return pages[index]({ next, back, setIndex });
if (situationalPages && typeof index === 'string') {
return (
<Renderer
Render={situationalPages[index]}
params={{ next, back, context, setIndex }}
/>
);
}
return (
<Renderer
Render={pages[index]}
params={{ next, back, context, setIndex }}
/>
);
};
2 changes: 1 addition & 1 deletion src/components/masa-interface/masa-interface.tsx
Expand Up @@ -26,7 +26,7 @@ const pages = {
authenticate: <InterfaceAuthenticate />,
createCreditScore: <InterfaceCreateCreditScore />,
switchNetwork: <InterfaceSwitchChain />,
masaGreen: <InterfaceMasaGreen/>
masaGreen: <InterfaceMasaGreen />,
};

export const MasaInterface = ({
Expand Down
24 changes: 17 additions & 7 deletions src/components/masa-interface/pages/masa-green/airdrop.tsx
@@ -1,15 +1,25 @@
import React from 'react';
import { SubflowPage } from '../../interface-subflow';

export const AirdropPage: React.FunctionComponent<SubflowPage> = ({
next,
back,
}) => {
export const AirdropPage: React.FunctionComponent<SubflowPage> = ({ next }) => {
return (
<div>
Hello! airdrop page
<button onClick={back}>Go back</button>
<button onClick={next}>Go next</button>
<h2>1,000,000 $Masa Token Airdrop</h2>
<div>
<p>
Earn 10 $MASA tokens for each new user you successfully refer to Masa
</p>
<p>
Each friend you refer must mint a Masa Green SBT to be considered a
successful referral
</p>
</div>

<div className="">
<button className={'masa-button'} onClick={() => next()}>
Get verified to start referrals
</button>
</div>
</div>
);
};
185 changes: 185 additions & 0 deletions src/components/masa-interface/pages/masa-green/code.tsx
@@ -0,0 +1,185 @@
import ReactCodeInput from '@acusti/react-code-input';
import { useLocalStorage, useMasa } from '../../../../provider';
import { SubflowPage } from 'components/masa-interface/interface-subflow';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';

const errorMsgs = {
expired: 'The code is expired. Please click Resend and try again!',
maxAttempts: 'Max attempts reached. Please try again in 10 minutes',
invalid: 'This code is not valid, please click Resend below and try again',
unexpedted: `We're sorry, an unexpected error has occured`,
};

const getRetryTimeout = (attemptNumber?: number): number => {
let result = 120;

const retryTimeouts: { [key: number]: number } = {
1: 15,
2: 30,
3: 40,
4: 60,
5: 90,
};

if (attemptNumber) {
result = retryTimeouts[attemptNumber];
}

return result;
};

export const CodeInterface: React.FunctionComponent<SubflowPage> = ({
next,
back,
context,
}) => {
const phoneNumber = useMemo(() => context.phoneNumber, [context]);
const codeRef = useRef(null);
const [code, setCode] = useState<string>('');
const [showCountDown, setShowCountDown] = useState<boolean>(false);
const [time, setTime] = useState<number>(0);
const { masa, handleGenerateGreen } = useMasa();

const isValid = true;

const [errorMsg, setErrorMsg] = useState<string | null>(null);

const { localStorageSet } = useLocalStorage();

useEffect(() => {
time > 0 && setTimeout(() => setTime(time - 1), 1000);
}, [time]);

useEffect(() => {
if (phoneNumber.length < 1) {
back();
}
}, [phoneNumber, back]);

const resendCode = useCallback(async () => {
setCode('');
setShowCountDown(true);
const result = await handleGenerateGreen?.(phoneNumber);

if (result) {
const attempts = result.data?.length;

if (attempts === 0) {
back();
} else {
setTime(getRetryTimeout(attempts));
}
}
}, [phoneNumber, handleGenerateGreen, back]);

const verify = useCallback(
async (completedCode: string, phone: string) => {
if (masa) {
const verifyResponse = await masa.green.verify(phone, completedCode);

if (verifyResponse?.status === 'approved') {
localStorageSet(`${phone}`, JSON.stringify(verifyResponse));
next();
} else {
switch (verifyResponse?.status) {
case 'pending':
setErrorMsg(errorMsgs.invalid);
break;
case '404':
setErrorMsg(errorMsgs.expired);
break;
case '429':
setErrorMsg(errorMsgs.maxAttempts);
break;
default:
setErrorMsg(errorMsgs.unexpedted);
}
}
}
},
[masa, next]
);

const inputStyle = {
borderRadius: '4px',
border: '1px solid',
paddingLeft: '22px',
margin: '6px',
width: '60px',
height: '60px',
fontSize: '22px',
color: 'black',
backgroundColor: 'white',
borderColor: 'lightgrey',
};

const invalidStyle = {
borderRadius: '4px',
border: '1px solid',
paddingLeft: '22px',
margin: '6px',
width: '60px',
height: '60px',
fontSize: '22px',
color: 'black',
backgroundColor: 'white',
borderColor: 'red',
};

const handleUpdateCode = useCallback(
async (code: string) => {
setCode(code);

if (code.length === 6) {
await verify(code, phoneNumber);
}
},
[setCode, phoneNumber, verify]
);

return (
<>
<h2>Enter 2FA 6-digit code</h2>
<h3>{`We sent a code to your phone number ending in ${phoneNumber.slice(
-4
)}.`}</h3>

<div className="w-full flex flex-col items-center">
<ReactCodeInput
ref={codeRef}
value={code}
onChange={handleUpdateCode}
inputStyle={inputStyle}
inputStyleInvalid={invalidStyle}
name="green"
type="number"
inputMode="numeric"
fields={6}
isValid={isValid}
/>
<p className={'text-errorRed text-sm mt-8'}>{errorMsg}</p>
<div className={'absolute -bottom-20'}>
<p className={'text-black font-ezra text-lg'}>
Didn&apos;t get the code?{' '}
{showCountDown && time !== 0 ? (
<span className={'font-bold ml-2'}>{time}</span>
) : (
<span
className={'font-bold cursor-pointer ml-2'}
onClick={() => resendCode()}
>
Resend
</span>
)}
</p>
</div>
</div>
</>
);
};
16 changes: 16 additions & 0 deletions src/components/masa-interface/pages/masa-green/error.tsx
@@ -0,0 +1,16 @@
import { SubflowPage } from 'components/masa-interface/interface-subflow';
import React from 'react';
export const Error: React.FunctionComponent<SubflowPage> = ({ setIndex }) => {
const handleReturn = () => {
setIndex(0);
};

return (
<div>
Error
<button className="masa=button" onClick={handleReturn}>
Return
</button>
</div>
);
};
33 changes: 30 additions & 3 deletions src/components/masa-interface/pages/masa-green/index.tsx
@@ -1,12 +1,39 @@
import React from 'react';
import React, { useState } from 'react';
import { InterfaceSubflow } from '../../interface-subflow';
import { AirdropPage } from './airdrop';
import { CodeInterface } from './code';
import { Error } from './error';
import { NotBotPage } from './not-a-bot';
import { PhoneInputInterface } from './phone-input';
import { Success } from './success';
import { VerifyAndMintInterface } from './verifiy-and-mint';

const InterfaceMasaGreen = () => {
const pages = [AirdropPage, NotBotPage];
const pages = [
AirdropPage,
NotBotPage,
PhoneInputInterface,
CodeInterface,
VerifyAndMintInterface,
Success,
];
const [phoneNumber, setPhoneNumber] = useState<string | undefined>();

return <InterfaceSubflow pages={pages} />;
const context = {
phoneNumber,
setPhoneNumber,
};

return (
<InterfaceSubflow
pages={pages}
context={context}
situationalPages={{
success: Success,
error: Error,
}}
/>
);
};

export default InterfaceMasaGreen;

0 comments on commit 0731466

Please sign in to comment.