mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-02-22 14:22:33 +01:00
feat: add account sync
This commit is contained in:
parent
d45560481a
commit
a2aba0bd9c
23 changed files with 645 additions and 159 deletions
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
`;
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
|
|
162
src/components/auth/views/QRLogin.tsx
Normal file
162
src/components/auth/views/QRLogin.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
|
|
43
src/components/loadingBar/LoadingBar.tsx
Normal file
43
src/components/loadingBar/LoadingBar.tsx
Normal 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>
|
||||
);
|
|
@ -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
17
src/hooks/UseInterval.tsx
Normal 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]);
|
||||
};
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -110,7 +110,6 @@ export const AccountSettings = () => {
|
|||
type={isType}
|
||||
status={status}
|
||||
setStatus={setStatus}
|
||||
withRedirect={true}
|
||||
callback={() => setStatus('none')}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
169
src/views/settings/Sync.tsx
Normal 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} />;
|
||||
};
|
41
yarn.lock
41
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue