feat: add account sync

This commit is contained in:
AP 2020-02-24 07:31:24 +01:00
parent d45560481a
commit a2aba0bd9c
23 changed files with 645 additions and 159 deletions

View file

@ -1,6 +1,6 @@
{
"name": "app",
"version": "0.1.5",
"version": "0.1.6",
"private": true,
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
@ -16,6 +16,7 @@
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "16.9.5",
"@types/react-modal": "^3.10.5",
"@types/react-qr-reader": "^2.1.2",
"@types/react-router-dom": "^5.1.2",
"@types/react-tooltip": "^3.11.0",
"@types/styled-components": "^4.4.3",
@ -37,6 +38,7 @@
"react": "^16.11.0",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "^16.11.0",
"react-qr-reader": "^2.2.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.0",
"react-spinners": "^0.8.0",

View file

@ -8,7 +8,6 @@ import { useSettings } from './context/SettingsContext';
import { ModalProvider } from 'styled-react-modal';
import { useAccount } from './context/AccountContext';
import { toast } from 'react-toastify';
import { FadingBackground } from './components/modal/ReactModal';
import 'react-toastify/dist/ReactToastify.css';
import { Header } from './sections/header/Header';
import { Footer } from './sections/footer/Footer';
@ -17,6 +16,7 @@ import { ScrollToTop } from 'components/scrollToTop/ScrollToTop';
import { ContextProvider } from 'context/ContextProvider';
import { ConnectionCheck } from 'components/connectionCheck/ConnectionCheck';
import { StatusCheck } from 'components/statusCheck/StatusCheck';
import { BaseModalBackground } from 'styled-react-modal';
const EntryView = React.lazy(() => import('./views/entry/Entry'));
const ContentView = React.lazy(() => import('./sections/content/Content'));
@ -54,7 +54,7 @@ const ContextApp: React.FC = () => {
return (
<ThemeProvider theme={{ mode: theme }}>
<ModalProvider backgroundComponent={FadingBackground}>
<ModalProvider backgroundComponent={BaseModalBackground}>
<ScrollToTop />
<GlobalStyles />
<Header />

View file

@ -33,3 +33,11 @@ export const FixedWidth = styled.div`
margin: 0px;
margin-right: 8px;
`;
export const QRTextWrapper = styled.div`
display: flex;
margin: 16px 0;
flex-direction: column;
align-items: center;
justify-content: center;
`;

View file

@ -3,6 +3,7 @@ import { getNextAvailable } from 'utils/storage';
import { LoginForm } from './views/NormalLogin';
import { ConnectLoginForm } from './views/ConnectLogin';
import { BTCLoginForm } from './views/BTCLogin';
import { QRLogin } from './views/QRLogin';
import { ViewCheck } from './checks/ViewCheck';
import CryptoJS from 'crypto-js';
import { useAccount } from 'context/AccountContext';
@ -15,18 +16,11 @@ import { useStatusDispatch } from 'context/StatusContext';
type AuthProps = {
type: string;
status: string;
withRedirect?: boolean;
callback: () => void;
setStatus: (state: string) => void;
};
export const Auth = ({
type,
status,
withRedirect,
callback,
setStatus,
}: AuthProps) => {
export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
const next = getNextAvailable();
const { changeAccount } = useAccount();
@ -50,6 +44,34 @@ export const Auth = ({
admin,
viewOnly,
cert,
skipCheck,
}: {
name?: string;
host?: string;
admin?: string;
viewOnly?: string;
cert?: string;
skipCheck?: boolean;
}) => {
if (skipCheck) {
quickSave({ name, cert, admin, viewOnly, host });
} else {
name && setName(name);
host && setHost(host);
admin && setAdmin(admin);
viewOnly && setViewOnly(viewOnly);
cert && setCert(cert);
setStatus('confirmNode');
}
};
const quickSave = ({
name,
host,
admin,
viewOnly,
cert,
}: {
name?: string;
host?: string;
@ -57,13 +79,20 @@ export const Auth = ({
viewOnly?: string;
cert?: string;
}) => {
name && setName(name);
host && setHost(host);
admin && setAdmin(admin);
viewOnly && setViewOnly(viewOnly);
cert && setCert(cert);
saveUserAuth({
available: next,
name,
host: host || '',
admin,
viewOnly,
cert,
});
setStatus('confirmNode');
dispatch({ type: 'disconnected' });
dispatchState({ type: 'disconnected' });
changeAccount(next);
push('/');
};
const handleSave = () => {
@ -85,7 +114,7 @@ export const Auth = ({
dispatchState({ type: 'disconnected' });
changeAccount(next);
withRedirect && push('/');
push('/');
};
const handleConnect = () => {
@ -99,13 +128,13 @@ export const Auth = ({
const renderView = () => {
switch (type) {
case 'login':
return <LoginForm handleSet={handleSet} available={next} />;
return <LoginForm handleSet={handleSet} />;
case 'qrcode':
return <QRLogin handleSet={handleSet} />;
case 'connect':
return (
<ConnectLoginForm handleSet={handleSet} available={next} />
);
return <ConnectLoginForm handleSet={handleSet} />;
default:
return <BTCLoginForm handleSet={handleSet} available={next} />;
return <BTCLoginForm handleSet={handleSet} />;
}
};

View file

@ -6,8 +6,7 @@ import { Line, StyledTitle } from '../Auth.styled';
import { RiskCheckboxAndConfirm } from './Checkboxes';
interface AuthProps {
available: number;
handleSet?: ({
handleSet: ({
name,
host,
admin,
@ -22,7 +21,7 @@ interface AuthProps {
}) => void;
}
export const BTCLoginForm = ({ available, handleSet }: AuthProps) => {
export const BTCLoginForm = ({ handleSet }: AuthProps) => {
const [name, setName] = useState('');
const [json, setJson] = useState('');
const [checked, setChecked] = useState(false);
@ -31,13 +30,13 @@ export const BTCLoginForm = ({ available, handleSet }: AuthProps) => {
try {
JSON.parse(json);
const { cert, admin, viewOnly, host } = getConfigLnd(json);
handleSet && handleSet({ name, host, admin, viewOnly, cert });
handleSet({ name, host, admin, viewOnly, cert });
} catch (error) {
toast.error('Invalid JSON Object');
toast.error('Invalid JSON');
}
};
const canConnect = json !== '' && !!available && checked;
const canConnect = json !== '' && checked;
return (
<>
<Line>

View file

@ -5,8 +5,7 @@ import { Line, StyledTitle } from '../Auth.styled';
import { RiskCheckboxAndConfirm } from './Checkboxes';
interface AuthProps {
available: number;
handleSet?: ({
handleSet: ({
name,
host,
admin,
@ -21,7 +20,7 @@ interface AuthProps {
}) => void;
}
export const ConnectLoginForm = ({ available, handleSet }: AuthProps) => {
export const ConnectLoginForm = ({ handleSet }: AuthProps) => {
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [checked, setChecked] = useState(false);
@ -30,16 +29,15 @@ export const ConnectLoginForm = ({ available, handleSet }: AuthProps) => {
const { cert, macaroon, socket } = getAuthLnd(url);
const base64Cert = getBase64CertfromDerFormat(cert) || '';
handleSet &&
handleSet({
name,
host: socket,
admin: macaroon,
cert: base64Cert,
});
handleSet({
name,
host: socket,
admin: macaroon,
cert: base64Cert,
});
};
const canConnect = url !== '' && !!available && checked;
const canConnect = url !== '' && checked;
return (
<>

View file

@ -9,8 +9,7 @@ import {
import { RiskCheckboxAndConfirm } from './Checkboxes';
interface AuthProps {
available: number;
handleSet?: ({
handleSet: ({
name,
host,
admin,
@ -25,7 +24,7 @@ interface AuthProps {
}) => void;
}
export const LoginForm = ({ available, handleSet }: AuthProps) => {
export const LoginForm = ({ handleSet }: AuthProps) => {
const [isViewOnly, setIsViewOnly] = useState(true);
const [checked, setChecked] = useState(false);
@ -36,14 +35,13 @@ export const LoginForm = ({ available, handleSet }: AuthProps) => {
const [cert, setCert] = useState('');
const handleClick = () => {
handleSet && handleSet({ name, host, admin, viewOnly, cert });
handleSet({ name, host, admin, viewOnly, cert });
};
const canConnect =
name !== '' &&
host !== '' &&
(admin !== '' || viewOnly !== '') &&
!!available &&
checked;
return (
<>

View file

@ -1,45 +1,10 @@
import React from 'react';
import { Sub4Title, SubTitle } from '../../generic/Styled';
import zxcvbn from 'zxcvbn';
import styled from 'styled-components';
import { progressBackground } from '../../../styles/Themes';
import { ColorButton } from '../../buttons/colorButton/ColorButton';
import { Input } from 'components/input/Input';
import { Line } from '../Auth.styled';
const Progress = styled.div`
width: 100%;
background: ${progressBackground};
`;
interface ProgressBar {
percent: number;
barColor?: string;
}
const ProgressBar = styled.div`
height: 10px;
background-color: ${({ barColor }: ProgressBar) =>
barColor ? barColor : 'blue'};
width: ${({ percent }: ProgressBar) => `${percent}%`};
`;
const getColor = (percent: number) => {
switch (true) {
case percent < 20:
return '#ff4d4f';
case percent < 40:
return '#ff7a45';
case percent < 60:
return '#ffa940';
case percent < 80:
return '#bae637';
case percent <= 100:
return '#73d13d';
default:
return '';
}
};
import { LoadingBar } from 'components/loadingBar/LoadingBar';
interface PasswordProps {
isPass: string;
@ -65,12 +30,7 @@ export const PasswordInput = ({
</Line>
<Line>
<Sub4Title>Strength:</Sub4Title>
<Progress>
<ProgressBar
percent={strength}
barColor={getColor(strength)}
/>
</Progress>
<LoadingBar percent={strength} />
</Line>
<ColorButton
disabled={strength < needed}

View file

@ -0,0 +1,162 @@
import React, { useState, useEffect } from 'react';
import QrReader from 'react-qr-reader';
import Modal from '../../../components/modal/ReactModal';
import { toast } from 'react-toastify';
import { getQRConfig } from 'utils/auth';
import { Line, QRTextWrapper } from '../Auth.styled';
import sortBy from 'lodash.sortby';
import { LoadingBar } from 'components/loadingBar/LoadingBar';
import { SubTitle } from 'components/generic/Styled';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
type QRLoginProps = {
handleSet: ({
name,
host,
admin,
viewOnly,
cert,
skipCheck,
}: {
name?: string;
host?: string;
admin?: string;
viewOnly?: string;
cert?: string;
skipCheck?: boolean;
}) => void;
};
export const QRLogin = ({ handleSet }: QRLoginProps) => {
const [qrData, setQrData] = useState<any>([]);
const [modalOpen, setModalOpen] = useState(true);
const [modalClosed, setModalClosed] = useState('none');
const [total, setTotal] = useState(0);
const [missing, setMissing] = useState<number[]>();
useEffect(() => {
if (qrData.length >= total && total !== 0) {
setModalOpen(false);
const sorted = sortBy(qrData, 'index');
const strings = sorted.map((code: { auth: string }) => code.auth);
const completeString = strings.join('');
try {
const { name, cert, admin, viewOnly, host } = getQRConfig(
completeString,
);
handleSet({
name,
host,
admin,
viewOnly,
cert,
skipCheck: true,
});
} catch (error) {
toast.error('Error reading QR codes.');
}
}
}, [qrData, handleSet, total]);
const handleScan = (data: string | null) => {
if (data) {
try {
const parsed = JSON.parse(data);
!total && setTotal(parsed.total);
!missing && setMissing([...Array(parsed.total).keys()]);
if (
missing &&
missing.length >= 0 &&
missing.includes(parsed.index)
) {
const remaining = missing.filter((value: number) => {
const number = parseInt(parsed.index);
return value !== number;
});
const data = [...qrData, parsed];
setQrData(data);
setMissing(remaining);
}
} catch (error) {
setModalOpen(false);
toast.error('Error reading QR codes.');
}
}
};
const handleError = () => {
setModalOpen(false);
setModalClosed('error');
};
const handleClose = () => {
setModalClosed('forced');
setModalOpen(false);
setMissing(undefined);
setTotal(0);
setQrData([]);
};
const renderInfo = () => {
switch (modalClosed) {
case 'forced':
return (
<>
<QRTextWrapper>
<SubTitle>
No information read from QR Codes.
</SubTitle>
</QRTextWrapper>
<ColorButton
fullWidth={true}
onClick={() => {
setModalClosed('none');
setModalOpen(true);
}}
>
Try Again
</ColorButton>
</>
);
case 'error':
return (
<QRTextWrapper>
<SubTitle>
Make sure you have given ThunderHub the correct
permissions to use the camara.
</SubTitle>
</QRTextWrapper>
);
default:
return null;
}
};
return (
<>
{renderInfo()}
<Modal isOpen={modalOpen} closeCallback={handleClose}>
<Line>
<LoadingBar
percent={
missing
? 100 * ((total - missing.length) / total)
: 0
}
/>
</Line>
<QrReader
delay={500}
onError={handleError}
onScan={handleScan}
style={{ width: '100%' }}
/>
</Modal>
</>
);
};

View file

@ -50,7 +50,7 @@ export const SecureButton = ({
>
{children}
</ColorButton>
<Modal isOpen={modalOpen} setIsOpen={setModalOpen}>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<LoginModal
color={color}
macaroon={admin}

View file

@ -0,0 +1,43 @@
import React from 'react';
import styled from 'styled-components';
import { progressBackground } from 'styles/Themes';
const Progress = styled.div`
width: 100%;
background: ${progressBackground};
`;
interface ProgressBar {
percent: number;
barColor?: string;
}
const ProgressBar = styled.div`
height: 10px;
background-color: ${({ barColor }: ProgressBar) =>
barColor ? barColor : 'blue'};
width: ${({ percent }: ProgressBar) => `${percent}%`};
`;
const getColor = (percent: number) => {
switch (true) {
case percent < 20:
return '#ff4d4f';
case percent < 40:
return '#ff7a45';
case percent < 60:
return '#ffa940';
case percent < 80:
return '#bae637';
case percent <= 100:
return '#73d13d';
default:
return '';
}
};
export const LoadingBar = ({ percent }: { percent: number }) => (
<Progress>
<ProgressBar percent={percent} barColor={getColor(percent)} />
</Progress>
);

View file

@ -1,45 +1,59 @@
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { css } from 'styled-components';
import { cardColor, mediaWidths } from '../../styles/Themes';
import ReactModal, { BaseModalBackground } from 'styled-react-modal';
export const FadingBackground = styled(BaseModalBackground)``;
import ReactModal from 'styled-react-modal';
interface ModalProps {
children: ReactNode;
isOpen: boolean;
setIsOpen: (set: boolean) => void;
noMinWidth?: boolean;
closeCallback: () => void;
}
const StyleModal = ReactModal.styled`
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
background-color: ${cardColor};
padding: 20px;
border-radius: 5px;
outline: none;
min-width: 578px;
const generalCSS = css`
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
background-color: ${cardColor};
padding: 20px;
border-radius: 5px;
outline: none;
@media (${mediaWidths.mobile}) {
top: 100%;
border-radius: 0px;
transform: translateY(-100%) translateX(-50%);
width: 100%;
min-width: 325px;
}
@media (${mediaWidths.mobile}) {
top: 100%;
border-radius: 0px;
transform: translateY(-100%) translateX(-50%);
width: 100%;
min-width: 325px;
}
`;
const Modal = ({ children, isOpen, setIsOpen }: ModalProps) => {
const StyleModal = ReactModal.styled`
${generalCSS}
min-width: 578px;
`;
const StyleModalSmall = ReactModal.styled`
${generalCSS}
`;
const Modal = ({
children,
isOpen,
noMinWidth = false,
closeCallback,
}: ModalProps) => {
const Styled = noMinWidth ? StyleModalSmall : StyleModal;
return (
<StyleModal
<Styled
isOpen={isOpen}
onBackgroundClick={() => setIsOpen(!isOpen)}
onEscapeKeydown={() => setIsOpen(!isOpen)}
onBackgroundClick={closeCallback}
onEscapeKeydown={closeCallback}
>
{children}
</StyleModal>
</Styled>
);
};

17
src/hooks/UseInterval.tsx Normal file
View file

@ -0,0 +1,17 @@
import { useEffect, useRef } from 'react';
export const useInterval = (callback: any, delay: number) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
};
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
};

View file

@ -134,3 +134,21 @@ export const getConfigLnd = (json: string) => {
return emptyObject;
};
export const getQRConfig = (json: string) => {
const config = JSON.parse(json);
if (config) {
const { name = '', cert = '', admin, viewOnly, host } = config;
return {
name,
cert,
admin,
viewOnly,
host,
};
}
return { ...emptyObject, name: undefined };
};

View file

@ -242,7 +242,7 @@ export const ChannelCard = ({
<div>{`Received: ${formatReceived}`}</div>
<div>{`Sent: ${formatSent}`}</div>
</ReactTooltip>
<Modal isOpen={modalOpen} setIsOpen={setModalOpen}>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<CloseChannel
setModalOpen={setModalOpen}
channelId={id}

View file

@ -40,34 +40,19 @@ export const LoginView = () => {
>
BTCPayServer
</SingleButton>
<SingleButton
selected={isType === 'qrcode'}
onClick={() => setIsType('qrcode')}
>
QR Code
</SingleButton>
</MultiButton>
</>
);
const renderText = () => {
switch (isType) {
case 'login':
return null;
case 'connect':
return (
<>
<Separation />
<Text>
To connect via LNDConnect paste the LNDConnectUrl
down below.
{' Find the url format specification '}
<Link
href={
'https://github.com/LN-Zap/lndconnect/blob/master/lnd_connect_uri.md'
}
>
here.
</Link>
</Text>
<Separation />
</>
);
default:
case 'btcpay':
return (
<>
<Separation />
@ -88,6 +73,27 @@ export const LoginView = () => {
<Separation />
</>
);
case 'connect':
return (
<>
<Separation />
<Text>
To connect via LNDConnect paste the LNDConnectUrl
down below.
{' Find the url format specification '}
<Link
href={
'https://github.com/LN-Zap/lndconnect/blob/master/lnd_connect_uri.md'
}
>
here.
</Link>
</Text>
<Separation />
</>
);
default:
return null;
}
};
@ -101,7 +107,6 @@ export const LoginView = () => {
type={isType}
status={status}
setStatus={setStatus}
withRedirect={true}
callback={() => setStatus('none')}
/>
</Card>

View file

@ -105,7 +105,7 @@ export const PayCard = ({ setOpen }: { setOpen: () => void }) => {
Send Sats
</ColorButton>
</ResponsiveLine>
<Modal isOpen={modalOpen} setIsOpen={setModalOpen}>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
{renderData()}
<SecureButton
callback={makePayment}

View file

@ -230,7 +230,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
>
Send
</ColorButton>
<Modal isOpen={modalOpen} setIsOpen={setModalOpen}>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<SingleLine>
<SubTitle>Send to Address</SubTitle>
</SingleLine>

View file

@ -110,7 +110,6 @@ export const AccountSettings = () => {
type={isType}
status={status}
setStatus={setStatus}
withRedirect={true}
callback={() => setStatus('none')}
/>
</>

View file

@ -44,27 +44,49 @@ export const SettingsButton = styled(SimpleButton)`
export const DangerView = () => {
const { refreshAccount } = useAccount();
const renderButton = () => {
const saved = getStorageSaved();
if (saved.length > 1) {
return (
<MultiButton>
{saved.map((entry, index) => {
return (
<SingleButton
color={'red'}
onClick={() => {
deleteAuth(entry.index);
refreshAccount();
}}
>
{entry.name}
</SingleButton>
);
})}
</MultiButton>
);
} else if (saved.length === 1) {
return (
<ColorButton
color={'red'}
onClick={() => {
deleteAuth(saved[0].index);
refreshAccount();
}}
>
{saved[0].name}
</ColorButton>
);
}
return null;
};
return (
<CardWithTitle>
<SubTitle>Danger Zone</SubTitle>
<OutlineCard>
<SettingsLine>
<Sub4Title>Delete Account:</Sub4Title>
<MultiButton>
{getStorageSaved().map((entry, index) => {
return (
<SingleButton
color={'red'}
onClick={() => {
deleteAuth(entry.index);
refreshAccount();
}}
>
{entry.name}
</SingleButton>
);
})}
</MultiButton>
{renderButton()}
</SettingsLine>
<SettingsLine>
<Sub4Title>Delete all Accounts and Settings:</Sub4Title>

View file

@ -6,6 +6,7 @@ import { textColor } from '../../styles/Themes';
import { AccountSettings } from './Account';
import { DangerView } from './Danger';
import { CurrentSettings } from './Current';
import { SyncSettings } from './Sync';
export const ButtonRow = styled.div`
width: auto;
@ -13,11 +14,11 @@ export const ButtonRow = styled.div`
`;
export const SettingsLine = styled(SingleLine)`
margin: 10px 0;
margin: 8px 0;
`;
export const SettingsButton = styled(SimpleButton)`
padding: 10px;
padding: 8px;
&:hover {
border: 1px solid ${textColor};
@ -28,6 +29,7 @@ export const SettingsView = () => {
return (
<>
<InterfaceSettings />
<SyncSettings />
<CurrentSettings />
<AccountSettings />
<DangerView />

169
src/views/settings/Sync.tsx Normal file
View file

@ -0,0 +1,169 @@
import React, { useState } from 'react';
import {
CardWithTitle,
SubTitle,
Card,
Sub4Title,
Separation,
} from '../../components/generic/Styled';
import { SettingsLine } from './Settings';
import {
MultiButton,
SingleButton,
} from 'components/buttons/multiButton/MultiButton';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
import { XSvg } from 'components/generic/Icons';
import { useAccount } from 'context/AccountContext';
import QRCode from 'qrcode.react';
import styled from 'styled-components';
import { useInterval } from 'hooks/UseInterval';
import Modal from 'components/modal/ReactModal';
const QRWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
`;
export const SyncSettings = () => {
const { name, host, admin, viewOnly, cert } = useAccount();
const getValue = () => {
switch (true) {
case !!viewOnly:
return 'viewOnly';
default:
return 'adminOnly';
}
};
const [state, setState] = useState('none');
const [type, setType] = useState(getValue());
const getObject = () => {
switch (type) {
case 'complete':
return { viewOnly, admin };
case 'adminOnly':
return { admin };
default:
return { viewOnly };
}
};
const renderSettings = () => (
<>
<Separation />
<SettingsLine>
<Sub4Title>Sync Type</Sub4Title>
<MultiButton>
{viewOnly && (
<SingleButton
selected={type === 'viewOnly'}
onClick={() => setType('viewOnly')}
>
View-Only
</SingleButton>
)}
{admin && (
<SingleButton
selected={type === 'adminOnly'}
onClick={() => setType('adminOnly')}
>
Admin-Only
</SingleButton>
)}
{viewOnly && admin && (
<SingleButton
selected={type === 'complete'}
onClick={() => setType('complete')}
>
Admin and View
</SingleButton>
)}
</MultiButton>
</SettingsLine>
<SettingsLine>
<ColorButton
withMargin={'16px 0 0'}
fullWidth={true}
onClick={() => setState('finish')}
>
Generate QR
</ColorButton>
</SettingsLine>
</>
);
const renderQRCode = () => {
const connection = JSON.stringify({
name,
host,
cert,
...getObject(),
});
return (
<Modal
isOpen={true}
noMinWidth={true}
closeCallback={() => setState('none')}
>
<QRWrapper>
<SubTitle>Scan with ThunderHub</SubTitle>
<QRLoop connection={connection} />
</QRWrapper>
</Modal>
);
};
return (
<CardWithTitle>
<SubTitle>Sync</SubTitle>
<Card>
<SettingsLine>
<Sub4Title>Sync account to another device</Sub4Title>
<ColorButton
onClick={() =>
setState((prev: string) =>
prev !== 'none' ? 'none' : 'generate',
)
}
>
{state !== 'none' ? <XSvg /> : 'Sync'}
</ColorButton>
</SettingsLine>
{state === 'generate' && renderSettings()}
{state === 'finish' && renderQRCode()}
</Card>
</CardWithTitle>
);
};
const QRLoop = ({ connection }: { connection: string }) => {
const textArray = connection.match(/.{1,100}/g) ?? [];
const length = textArray.length;
const objectArray = textArray.map((value: string, index: number) =>
JSON.stringify({
index: index,
total: length,
auth: value,
}),
);
const [count, setCount] = useState(0);
useInterval(() => {
setCount((prev: number) => {
if (prev < length - 1) {
return prev + 1;
}
return 0;
});
}, 1000);
return <QRCode value={objectArray[count]} renderAs={'svg'} size={200} />;
};

View file

@ -2383,6 +2383,13 @@
dependencies:
"@types/react" "*"
"@types/react-qr-reader@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@types/react-qr-reader/-/react-qr-reader-2.1.2.tgz#da7a674cf9e3ceb1c026e89d5aa41fb1587fc66b"
integrity sha512-tDLqqiQMZlTKBKtWedcf7QZ3L9fyz8nPHNq/j+1mcJoEvrdM3Y89i3/FKwdmZ4H7G6uUk4JqShpWXLCuYpZK+w==
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.1.2":
version "5.1.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196"
@ -9117,6 +9124,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jsqr@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.2.0.tgz#f93fc65fa7d1ded78b1bcb020fa044352b04261a"
integrity sha512-wKcQS9QC2VHGk7aphWCp1RrFyC0CM6fMgC5prZZ2KV/Lk6OKNoCod9IR6bao+yx3KPY0gZFC5dc+h+KFzCI0Wg==
jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"
@ -12403,6 +12415,15 @@ react-popper@^1.3.6:
typed-styles "^0.0.7"
warning "^4.0.2"
react-qr-reader@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.2.1.tgz#dc89046d1c1a1da837a683dd970de5926817d55b"
integrity sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA==
dependencies:
jsqr "^1.2.0"
prop-types "^15.7.2"
webrtc-adapter "^7.2.1"
react-router-dom@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
@ -13090,6 +13111,13 @@ rsvp@^4.8.4:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
rtcpeerconnection-shim@^1.2.15:
version "1.2.15"
resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz#e7cc189a81b435324c4949aa3dfb51888684b243"
integrity sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==
dependencies:
sdp "^2.6.0"
run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@ -13224,6 +13252,11 @@ scss-tokenizer@^0.2.3:
js-base64 "^2.1.8"
source-map "^0.4.2"
sdp@^2.12.0, sdp@^2.6.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.12.0.tgz#338a106af7560c86e4523f858349680350d53b22"
integrity sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==
secure-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/secure-keys/-/secure-keys-1.0.0.tgz#f0c82d98a3b139a8776a8808050b824431087fca"
@ -15618,6 +15651,14 @@ webpack@4.41.5, webpack@^4.33.0, webpack@^4.38.0:
watchpack "^1.6.0"
webpack-sources "^1.4.1"
webrtc-adapter@^7.2.1:
version "7.5.0"
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.5.0.tgz#b870f6ff5e87191276e66f9521ac457c81801954"
integrity sha512-cUqlw310uLLSYvO8FTNCVmGWSMlMt6vuSDkcYL1nW+RUvAILJ3jEIvAUgFQU5EFGnU+mf9/No14BFv3U+hoxBQ==
dependencies:
rtcpeerconnection-shim "^1.2.15"
sdp "^2.12.0"
websocket-driver@>=0.5.1:
version "0.7.3"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"