Page MenuHomePhabricator

D10443.id31122.diff
No OneTemporary

D10443.id31122.diff

diff --git a/web/cashtab/src/assets/fingerprint-solid.svg b/web/cashtab/src/assets/fingerprint-solid.svg
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/assets/fingerprint-solid.svg
@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="fingerprint" class="svg-inline--fa fa-fingerprint fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256.12 245.96c-13.25 0-24 10.74-24 24 1.14 72.25-8.14 141.9-27.7 211.55-2.73 9.72 2.15 30.49 23.12 30.49 10.48 0 20.11-6.92 23.09-17.52 13.53-47.91 31.04-125.41 29.48-224.52.01-13.25-10.73-24-23.99-24zm-.86-81.73C194 164.16 151.25 211.3 152.1 265.32c.75 47.94-3.75 95.91-13.37 142.55-2.69 12.98 5.67 25.69 18.64 28.36 13.05 2.67 25.67-5.66 28.36-18.64 10.34-50.09 15.17-101.58 14.37-153.02-.41-25.95 19.92-52.49 54.45-52.34 31.31.47 57.15 25.34 57.62 55.47.77 48.05-2.81 96.33-10.61 143.55-2.17 13.06 6.69 25.42 19.76 27.58 19.97 3.33 26.81-15.1 27.58-19.77 8.28-50.03 12.06-101.21 11.27-152.11-.88-55.8-47.94-101.88-104.91-102.72zm-110.69-19.78c-10.3-8.34-25.37-6.8-33.76 3.48-25.62 31.5-39.39 71.28-38.75 112 .59 37.58-2.47 75.27-9.11 112.05-2.34 13.05 6.31 25.53 19.36 27.89 20.11 3.5 27.07-14.81 27.89-19.36 7.19-39.84 10.5-80.66 9.86-121.33-.47-29.88 9.2-57.88 28-80.97 8.35-10.28 6.79-25.39-3.49-33.76zm109.47-62.33c-15.41-.41-30.87 1.44-45.78 4.97-12.89 3.06-20.87 15.98-17.83 28.89 3.06 12.89 16 20.83 28.89 17.83 11.05-2.61 22.47-3.77 34-3.69 75.43 1.13 137.73 61.5 138.88 134.58.59 37.88-1.28 76.11-5.58 113.63-1.5 13.17 7.95 25.08 21.11 26.58 16.72 1.95 25.51-11.88 26.58-21.11a929.06 929.06 0 0 0 5.89-119.85c-1.56-98.75-85.07-180.33-186.16-181.83zm252.07 121.45c-2.86-12.92-15.51-21.2-28.61-18.27-12.94 2.86-21.12 15.66-18.26 28.61 4.71 21.41 4.91 37.41 4.7 61.6-.11 13.27 10.55 24.09 23.8 24.2h.2c13.17 0 23.89-10.61 24-23.8.18-22.18.4-44.11-5.83-72.34zm-40.12-90.72C417.29 43.46 337.6 1.29 252.81.02 183.02-.82 118.47 24.91 70.46 72.94 24.09 119.37-.9 181.04.14 246.65l-.12 21.47c-.39 13.25 10.03 24.31 23.28 24.69.23.02.48.02.72.02 12.92 0 23.59-10.3 23.97-23.3l.16-23.64c-.83-52.5 19.16-101.86 56.28-139 38.76-38.8 91.34-59.67 147.68-58.86 69.45 1.03 134.73 35.56 174.62 92.39 7.61 10.86 22.56 13.45 33.42 5.86 10.84-7.62 13.46-22.59 5.84-33.43z"></path></svg>
\ No newline at end of file
diff --git a/web/cashtab/src/assets/styles/theme.js b/web/cashtab/src/assets/styles/theme.js
--- a/web/cashtab/src/assets/styles/theme.js
+++ b/web/cashtab/src/assets/styles/theme.js
@@ -65,11 +65,13 @@
'linear-gradient(270deg, #0074C2 0%, #273498 100%)',
color: '#fff',
hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)',
+ disabledOverlay: 'rgba(255, 255, 255, 0.5)',
},
secondary: {
background: '#e9eaed',
color: '#444',
hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)',
+ disabledOverlay: 'rgba(255, 255, 255, 0.5)',
},
},
collapses: {
@@ -77,4 +79,11 @@
border: '#eaedf3',
color: '#3e3f42',
},
+ generalSettings: {
+ item: {
+ icon: '#949494',
+ title: '#949494',
+ },
+ background: '#fff',
+ },
};
diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js
--- a/web/cashtab/src/components/App.js
+++ b/web/cashtab/src/components/App.js
@@ -34,6 +34,7 @@
import ABC from '@assets/logo_topright.png';
import { checkForTokenById } from '@utils/tokenMethods.js';
import { currency } from './Common/Ticker';
+import ProtectableComponentWrapper from './Authentication/ProtectableComponentWrapper';
const GlobalStyle = createGlobalStyle`
.ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button, #cropControlsConfirm {
@@ -307,42 +308,46 @@
</a>
{/*Begin component not included in extension as replaced by open in tab link*/}
</HeaderCtn>
- <WalletLabel name={wallet.name}></WalletLabel>
- <Switch>
- <Route path="/wallet">
- <Wallet />
- </Route>
- <Route path="/tokens">
- <Tokens
- passLoadingStatus={
- setLoadingUtxosAfterSend
- }
- />
- </Route>
- <Route path="/send">
- <Send
- passLoadingStatus={
- setLoadingUtxosAfterSend
- }
- />
- </Route>
- <Route
- path="/send-token/:tokenId"
- render={props => (
- <SendToken
- tokenId={props.match.params.tokenId}
+ <ProtectableComponentWrapper>
+ <WalletLabel name={wallet.name}></WalletLabel>
+ <Switch>
+ <Route path="/wallet">
+ <Wallet />
+ </Route>
+ <Route path="/tokens">
+ <Tokens
passLoadingStatus={
setLoadingUtxosAfterSend
}
/>
- )}
- />
- <Route path="/configure">
- <Configure />
- </Route>
- <Redirect exact from="/" to="/wallet" />
- <Route component={NotFound} />
- </Switch>
+ </Route>
+ <Route path="/send">
+ <Send
+ passLoadingStatus={
+ setLoadingUtxosAfterSend
+ }
+ />
+ </Route>
+ <Route
+ path="/send-token/:tokenId"
+ render={props => (
+ <SendToken
+ tokenId={
+ props.match.params.tokenId
+ }
+ passLoadingStatus={
+ setLoadingUtxosAfterSend
+ }
+ />
+ )}
+ />
+ <Route path="/configure">
+ <Configure />
+ </Route>
+ <Redirect exact from="/" to="/wallet" />
+ <Route component={NotFound} />
+ </Switch>
+ </ProtectableComponentWrapper>
</WalletCtn>
{wallet ? (
<Footer>
diff --git a/web/cashtab/src/components/Authentication/ProtectableComponentWrapper.js b/web/cashtab/src/components/Authentication/ProtectableComponentWrapper.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Authentication/ProtectableComponentWrapper.js
@@ -0,0 +1,32 @@
+import React, { useContext } from 'react';
+import { AuthenticationContext } from '@utils/context';
+import SignUp from './SignUp';
+import SignIn from './SignIn';
+
+const ProtectableComponentWrapper = ({ children }) => {
+ const authentication = useContext(AuthenticationContext);
+
+ if (authentication) {
+ const { loading, isAuthenticationRequired, isSignedIn } =
+ authentication;
+
+ if (loading) {
+ return <p>Loading authenticaion data...</p>;
+ }
+
+ // prompt if user would like to enable biometric lock when the app first run
+ if (isAuthenticationRequired === undefined) {
+ return <SignUp />;
+ }
+
+ // prompt user to sign in
+ if (isAuthenticationRequired && !isSignedIn) {
+ return <SignIn />;
+ }
+ }
+
+ // authentication = null => authentication is not supported
+ return <>{children}</>;
+};
+
+export default ProtectableComponentWrapper;
diff --git a/web/cashtab/src/components/Authentication/SignIn.js b/web/cashtab/src/components/Authentication/SignIn.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Authentication/SignIn.js
@@ -0,0 +1,155 @@
+import React, { useContext, useEffect, useState } from 'react';
+import { Modal, Spin } from 'antd';
+import styled from 'styled-components';
+import { AuthenticationContext } from '@utils/context';
+import { ThemedLockOutlined } from '@components/Common/CustomIcons';
+import PrimaryButton from '@components/Common/PrimaryButton';
+import { ReactComponent as FingerprintSVG } from '@assets/fingerprint-solid.svg';
+
+const StyledSignIn = styled.div`
+ h2 {
+ color: ${props => props.theme.wallet.text.primary};
+ font-size: 25px;
+ }
+ p {
+ color: ${props => props.theme.wallet.text.secondary};
+ }
+`;
+
+const UnlockButton = styled(PrimaryButton)`
+ position: relative;
+ width: auto;
+ margin: 30px auto;
+ padding: 20px 30px;
+
+ svg {
+ fill: ${props => props.theme.buttons.primary.color};
+ }
+
+ @media (max-width: 768px) {
+ font-size: 16px;
+ padding: 15px 20px;
+ }
+
+ :disabled {
+ cursor: not-allowed;
+ box-shadow: none;
+ ::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: ${props => props.theme.buttons.primary.disabledOverlay};
+ z-index: 10;
+ }
+ }
+`;
+
+const StyledFingerprintIcon = styled.div`
+ width: 48px;
+ height: 48px;
+ margin: auto;
+`;
+
+const SignIn = () => {
+ const authentication = useContext(AuthenticationContext);
+ const [isVisible, setIsVisible] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleDocVisibilityChange = () => {
+ document.visibilityState === 'visible'
+ ? setIsVisible(true)
+ : setIsVisible(false);
+ };
+
+ const handleSignIn = async () => {
+ try {
+ setIsLoading(true);
+ await authentication.signIn();
+ } catch (err) {
+ Modal.error({
+ title: 'Authentication Error',
+ content: 'Cannot get Credential from your device',
+ centered: true,
+ });
+ }
+ setIsLoading(false);
+ };
+
+ const handleSignInAndSuppressError = async () => {
+ try {
+ setIsLoading(true);
+ await authentication.signIn();
+ } catch (err) {
+ // fail silently
+ }
+ setIsLoading(false);
+ };
+
+ useEffect(() => {
+ if (document.visibilityState === 'visible') {
+ setIsVisible(true);
+ }
+ document.addEventListener(
+ 'visibilitychange',
+ handleDocVisibilityChange,
+ );
+
+ return () => {
+ document.removeEventListener(
+ 'visibilitychange',
+ handleDocVisibilityChange,
+ );
+ };
+ }, []);
+
+ useEffect(() => {
+ // This will trigger the plaform authenticator as soon as the component becomes visible
+ // (when switch back to this app), without any user gesture (such as clicking a button)
+ // In platforms that require user gesture in order to invoke the platform authenticator,
+ // this will fail. We want it to fail silently, and then show user a button to activate
+ // the platform authenticator
+ if (isVisible && authentication) {
+ handleSignInAndSuppressError();
+ }
+ }, [isVisible]);
+
+ let signInBody;
+ if (authentication) {
+ signInBody = (
+ <>
+ <p>
+ This wallet can be unlocked with your{' '}
+ <strong>fingerprint / device pin</strong>
+ </p>
+ <UnlockButton
+ onClick={handleSignIn}
+ disabled={isLoading ? true : false}
+ >
+ <StyledFingerprintIcon>
+ <FingerprintSVG />
+ </StyledFingerprintIcon>
+ Unlock
+ </UnlockButton>
+ <div>
+ {isLoading ? <Spin tip="loading authenticator" /> : ''}
+ </div>
+ </>
+ );
+ } else {
+ signInBody = <p>Authentication is not supported</p>;
+ }
+
+ return (
+ <StyledSignIn>
+ <h2>
+ <ThemedLockOutlined /> Wallet Unlock
+ </h2>
+ {signInBody}
+ </StyledSignIn>
+ );
+};
+
+export default SignIn;
diff --git a/web/cashtab/src/components/Authentication/SignUp.js b/web/cashtab/src/components/Authentication/SignUp.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Authentication/SignUp.js
@@ -0,0 +1,74 @@
+import React, { useContext } from 'react';
+import { Modal } from 'antd';
+import styled from 'styled-components';
+import { AuthenticationContext } from '@utils/context';
+import { ThemedLockOutlined } from '@components/Common/CustomIcons';
+import PrimaryButton, {
+ SecondaryButton,
+} from '@components/Common/PrimaryButton';
+
+const StyledSignUp = styled.div`
+ h2 {
+ color: ${props => props.theme.wallet.text.primary};
+ font-size: 25px;
+ }
+ p {
+ color: ${props => props.theme.wallet.text.secondary};
+ }
+`;
+
+const SignUp = () => {
+ const authentication = useContext(AuthenticationContext);
+
+ const handleSignUp = async () => {
+ try {
+ await authentication.signUp();
+ } catch (err) {
+ Modal.error({
+ title: 'Registration Error',
+ content: 'Cannot create Credential on your device',
+ centered: true,
+ });
+ }
+ };
+
+ let signUpBody;
+ if (authentication) {
+ signUpBody = (
+ <div>
+ <p>Enable wallet lock to protect your fund.</p>
+ <p>
+ You will need to unlock with your{' '}
+ <strong>fingerprint / device pin</strong> in order to access
+ the wallet.
+ </p>
+ <p>
+ This lock can also be enabled / disabled under
+ <br />
+ <strong>Settings / General Settings / App Lock</strong>
+ </p>
+ <PrimaryButton onClick={handleSignUp}>
+ Enable Lock
+ </PrimaryButton>
+ <SecondaryButton
+ onClick={() => authentication.turnOffAuthentication()}
+ >
+ Skip
+ </SecondaryButton>
+ </div>
+ );
+ } else {
+ signUpBody = <p>Authentication is not supported</p>;
+ }
+
+ return (
+ <StyledSignUp>
+ <h2>
+ <ThemedLockOutlined /> Wallet Lock
+ </h2>
+ {signUpBody}
+ </StyledSignUp>
+ );
+};
+
+export default SignUp;
diff --git a/web/cashtab/src/components/Common/CustomIcons.js b/web/cashtab/src/components/Common/CustomIcons.js
--- a/web/cashtab/src/components/Common/CustomIcons.js
+++ b/web/cashtab/src/components/Common/CustomIcons.js
@@ -6,6 +6,8 @@
LoadingOutlined,
WalletOutlined,
QrcodeOutlined,
+ SettingOutlined,
+ LockOutlined,
} from '@ant-design/icons';
import { Image } from 'antd';
import { currency } from '@components/Common/Ticker';
@@ -44,6 +46,12 @@
export const ThemedQrcodeOutlined = styled(QrcodeOutlined)`
color: ${props => props.theme.icons.outlined} !important;
`;
+export const ThemedSettingOutlined = styled(SettingOutlined)`
+ color: ${props => props.theme.icons.outlined} !important;
+`;
+export const ThemedLockOutlined = styled(LockOutlined)`
+ color: ${props => props.theme.icons.outlined} !important;
+`;
export const LoadingBlock = styled.div`
width: 100%;
diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js
--- a/web/cashtab/src/components/Configure/Configure.js
+++ b/web/cashtab/src/components/Configure/Configure.js
@@ -1,14 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
-import { Collapse, Form, Input, Modal, Alert } from 'antd';
+import { Collapse, Form, Input, Modal, Alert, Switch, Tag } from 'antd';
import {
PlusSquareOutlined,
WalletFilled,
ImportOutlined,
LockOutlined,
+ CheckOutlined,
+ CloseOutlined,
+ LockFilled,
+ ExclamationCircleFilled,
} from '@ant-design/icons';
-import { WalletContext } from '@utils/context';
+import { WalletContext, AuthenticationContext } from '@utils/context';
import { StyledCollapse } from '@components/Common/StyledCollapse';
import {
AntdFormWrapper,
@@ -22,6 +26,7 @@
ThemedCopyOutlined,
ThemedWalletOutlined,
ThemedDollarOutlined,
+ ThemedSettingOutlined,
} from '@components/Common/CustomIcons';
import { ReactComponent as Trashcan } from '@assets/trashcan.svg';
import { ReactComponent as Edit } from '@assets/edit.svg';
@@ -210,8 +215,30 @@
margin: 60px 0 50px;
`;
+const GeneralSettingsItem = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .title {
+ color: ${props => props.theme.generalSettings.item.title};
+ }
+ .anticon {
+ color: ${props => props.theme.generalSettings.item.icon};
+ }
+ .ant-switch {
+ background-color: ${props => props.theme.generalSettings.item.icon};
+ .anticon {
+ color: ${props => props.theme.generalSettings.background};
+ }
+ }
+ .ant-switch-checked {
+ background-color: ${props => props.theme.primary};
+ }
+`;
+
const Configure = () => {
const ContextValue = React.useContext(WalletContext);
+ const authentication = React.useContext(AuthenticationContext);
const { wallet, apiError } = ContextValue;
const {
@@ -433,6 +460,22 @@
setConfirmationOfWalletToBeDeleted(value);
};
+ const handleAppLockToggle = (checked, e) => {
+ if (checked) {
+ // if there is an existing credential, that means user has registered
+ // simply turn on the Authentication Required flag
+ if (authentication.credentialId) {
+ authentication.turnOnAuthentication();
+ } else {
+ // there is no existing credential, that means user has not registered
+ // user need to register
+ authentication.signUp();
+ }
+ } else {
+ authentication.turnOffAuthentication();
+ }
+ };
+
return (
<StyledConfigure>
{walletToBeRenamed !== null && (
@@ -663,6 +706,34 @@
}
/>
</AntdFormWrapper>
+ <StyledSpacer />
+ <h2>
+ <ThemedSettingOutlined /> General Settings
+ </h2>
+ <GeneralSettingsItem>
+ <div className="title">
+ <LockFilled /> Lock App
+ </div>
+ {authentication ? (
+ <Switch
+ size="small"
+ checkedChildren={<CheckOutlined />}
+ unCheckedChildren={<CloseOutlined />}
+ checked={
+ authentication.isAuthenticationRequired &&
+ authentication.credentialId
+ ? true
+ : false
+ }
+ // checked={false}
+ onChange={handleAppLockToggle}
+ />
+ ) : (
+ <Tag color="warning" icon={<ExclamationCircleFilled />}>
+ Not Supported
+ </Tag>
+ )}
+ </GeneralSettingsItem>
<StyledSpacer />[
<SettingsLink
type="link"
diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
--- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
+++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
@@ -2,7 +2,7 @@
exports[`Configure with a wallet 1`] = `
<div
- className="sc-iAyFgw hatKtw"
+ className="sc-eHgmQL dUpMKn"
>
<h2>
<span
@@ -72,7 +72,7 @@
</div>
</div>
<div
- className="ant-collapse ant-collapse-icon-position-left sc-EHOje iPpwBT"
+ className="ant-collapse ant-collapse-icon-position-left sc-gzVnrw IJTYA"
role={null}
>
<div
@@ -110,7 +110,7 @@
</div>
</div>
<div
- className="sc-hSdWYo iEyugW"
+ className="sc-cvbbAY JVsRj"
/>
<h2>
<span
@@ -135,7 +135,7 @@
Manage Wallets
</h2>
<button
- className="sc-jTzLTM iVNQiK"
+ className="sc-jzJRlG fiDUki"
onClick={[Function]}
>
<span
@@ -163,7 +163,7 @@
New Wallet
</button>
<button
- className="sc-fjdhpX eXuEIC"
+ className="sc-cSHVUG eNOONn"
onClick={[Function]}
>
<span
@@ -188,7 +188,7 @@
Import Wallet
</button>
<div
- className="sc-hSdWYo iEyugW"
+ className="sc-cvbbAY JVsRj"
/>
<h2>
<span
@@ -213,7 +213,7 @@
Fiat Currency
</h2>
<div
- className="sc-gZMcBi iFeyXx"
+ className="sc-VigVT ktfrOp"
>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
@@ -305,11 +305,99 @@
</div>
</div>
<div
- className="sc-hSdWYo iEyugW"
+ className="sc-cvbbAY JVsRj"
+ />
+ <h2>
+ <span
+ aria-label="setting"
+ className="anticon anticon-setting sc-ifAKCX cYbSut"
+ role="img"
+ >
+ <svg
+ aria-hidden="true"
+ data-icon="setting"
+ fill="currentColor"
+ focusable="false"
+ height="1em"
+ viewBox="64 64 896 896"
+ width="1em"
+ >
+ <path
+ d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"
+ />
+ </svg>
+ </span>
+ General Settings
+ </h2>
+ <div
+ className="sc-jWBwVP ibcxAa"
+ >
+ <div
+ className="title"
+ >
+ <span
+ aria-label="lock"
+ className="anticon anticon-lock"
+ role="img"
+ >
+ <svg
+ aria-hidden="true"
+ data-icon="lock"
+ fill="currentColor"
+ focusable="false"
+ height="1em"
+ viewBox="64 64 896 896"
+ width="1em"
+ >
+ <path
+ d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM540 701v53c0 4.4-3.6 8-8 8h-40c-4.4 0-8-3.6-8-8v-53a48.01 48.01 0 1156 0zm152-237H332V240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224z"
+ />
+ </svg>
+ </span>
+ Lock App
+ </div>
+ <button
+ aria-checked={false}
+ className="ant-switch ant-switch-small"
+ onClick={[Function]}
+ onKeyDown={[Function]}
+ role="switch"
+ type="button"
+ >
+ <div
+ className="ant-switch-handle"
+ />
+ <span
+ className="ant-switch-inner"
+ >
+ <span
+ aria-label="close"
+ className="anticon anticon-close"
+ role="img"
+ >
+ <svg
+ aria-hidden="true"
+ data-icon="close"
+ fill="currentColor"
+ focusable="false"
+ height="1em"
+ viewBox="64 64 896 896"
+ width="1em"
+ >
+ <path
+ d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
+ />
+ </svg>
+ </span>
+ </span>
+ </button>
+ </div>
+ <div
+ className="sc-cvbbAY JVsRj"
/>
[
<a
- className="sc-ckVGcZ bhwAJY"
+ className="sc-eNQAEJ jpqDcE"
href="https://docs.cashtab.com/docs/"
rel="noreferrer"
target="_blank"
@@ -323,7 +411,7 @@
exports[`Configure without a wallet 1`] = `
<div
- className="sc-iAyFgw hatKtw"
+ className="sc-eHgmQL dUpMKn"
>
<h2>
<span
@@ -393,7 +481,7 @@
</div>
</div>
<div
- className="sc-hSdWYo iEyugW"
+ className="sc-cvbbAY JVsRj"
/>
<h2>
<span
@@ -418,7 +506,7 @@
Manage Wallets
</h2>
<button
- className="sc-jTzLTM iVNQiK"
+ className="sc-jzJRlG fiDUki"
onClick={[Function]}
>
<span
@@ -446,7 +534,7 @@
New Wallet
</button>
<button
- className="sc-fjdhpX eXuEIC"
+ className="sc-cSHVUG eNOONn"
onClick={[Function]}
>
<span
@@ -471,7 +559,7 @@
Import Wallet
</button>
<div
- className="sc-hSdWYo iEyugW"
+ className="sc-cvbbAY JVsRj"
/>
<h2>
<span
@@ -496,7 +584,7 @@
Fiat Currency
</h2>
<div
- className="sc-gZMcBi iFeyXx"
+ className="sc-VigVT ktfrOp"
>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
@@ -588,11 +676,99 @@
</div>
</div>
<div
- className="sc-hSdWYo iEyugW"
+ className="sc-cvbbAY JVsRj"
+ />
+ <h2>
+ <span
+ aria-label="setting"
+ className="anticon anticon-setting sc-ifAKCX cYbSut"
+ role="img"
+ >
+ <svg
+ aria-hidden="true"
+ data-icon="setting"
+ fill="currentColor"
+ focusable="false"
+ height="1em"
+ viewBox="64 64 896 896"
+ width="1em"
+ >
+ <path
+ d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"
+ />
+ </svg>
+ </span>
+ General Settings
+ </h2>
+ <div
+ className="sc-jWBwVP ibcxAa"
+ >
+ <div
+ className="title"
+ >
+ <span
+ aria-label="lock"
+ className="anticon anticon-lock"
+ role="img"
+ >
+ <svg
+ aria-hidden="true"
+ data-icon="lock"
+ fill="currentColor"
+ focusable="false"
+ height="1em"
+ viewBox="64 64 896 896"
+ width="1em"
+ >
+ <path
+ d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM540 701v53c0 4.4-3.6 8-8 8h-40c-4.4 0-8-3.6-8-8v-53a48.01 48.01 0 1156 0zm152-237H332V240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224z"
+ />
+ </svg>
+ </span>
+ Lock App
+ </div>
+ <button
+ aria-checked={false}
+ className="ant-switch ant-switch-small"
+ onClick={[Function]}
+ onKeyDown={[Function]}
+ role="switch"
+ type="button"
+ >
+ <div
+ className="ant-switch-handle"
+ />
+ <span
+ className="ant-switch-inner"
+ >
+ <span
+ aria-label="close"
+ className="anticon anticon-close"
+ role="img"
+ >
+ <svg
+ aria-hidden="true"
+ data-icon="close"
+ fill="currentColor"
+ focusable="false"
+ height="1em"
+ viewBox="64 64 896 896"
+ width="1em"
+ >
+ <path
+ d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
+ />
+ </svg>
+ </span>
+ </span>
+ </button>
+ </div>
+ <div
+ className="sc-cvbbAY JVsRj"
/>
[
<a
- className="sc-ckVGcZ bhwAJY"
+ className="sc-eNQAEJ jpqDcE"
href="https://docs.cashtab.com/docs/"
rel="noreferrer"
target="_blank"
diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
--- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
+++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
@@ -3,7 +3,7 @@
exports[`Wallet with BCH balances 1`] = `
Array [
<div
- className="sc-kgoBCf fswuXg"
+ className="sc-kpOJdX GoEyB"
>
You currently have 0
XEC
@@ -30,7 +30,7 @@
}
>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -99,7 +99,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-EHOje jPIGGt"
+ className="sc-gzVnrw eqRDUd"
onClick={[Function]}
>
<span
@@ -140,7 +140,7 @@
</div>
</div>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -285,7 +285,7 @@
</span>
</div>
<span
- className="sc-iwsKbI bMOJU"
+ className="sc-gqjmRU cFeeUJ"
disabled={false}
onClick={[Function]}
style={
@@ -314,7 +314,7 @@
</div>
</div>
<div
- className="sc-dxgOiQ kbCOBL"
+ className="sc-jKJlTe hccxef"
>
=
@@ -328,7 +328,7 @@
}
>
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -379,7 +379,7 @@
}
>
<button
- className="sc-fjdhpX eXuEIC"
+ className="sc-cSHVUG eNOONn"
>
Send
</button>
@@ -388,12 +388,12 @@
</div>
</div>,
<div
- className="sc-hmzhuo fmIvlu"
+ className="sc-kvZOFW crFufP"
>
Signatures
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -441,7 +441,7 @@
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
<div
- className="sc-kgoBCf fswuXg"
+ className="sc-kpOJdX GoEyB"
>
You currently have 0
XEC
@@ -468,7 +468,7 @@
}
>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -537,7 +537,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-EHOje jPIGGt"
+ className="sc-gzVnrw eqRDUd"
onClick={[Function]}
>
<span
@@ -578,7 +578,7 @@
</div>
</div>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -723,7 +723,7 @@
</span>
</div>
<span
- className="sc-iwsKbI bMOJU"
+ className="sc-gqjmRU cFeeUJ"
disabled={false}
onClick={[Function]}
style={
@@ -752,7 +752,7 @@
</div>
</div>
<div
- className="sc-dxgOiQ kbCOBL"
+ className="sc-jKJlTe hccxef"
>
=
@@ -766,7 +766,7 @@
}
>
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -817,7 +817,7 @@
}
>
<button
- className="sc-fjdhpX eXuEIC"
+ className="sc-cSHVUG eNOONn"
>
Send
</button>
@@ -826,12 +826,12 @@
</div>
</div>,
<div
- className="sc-hmzhuo fmIvlu"
+ className="sc-kvZOFW crFufP"
>
Signatures
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -879,14 +879,14 @@
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
<div
- className="sc-kAzzGY kdOdTZ"
+ className="sc-kgoBCf kfjVlq"
>
0.06
XEC
</div>,
<div
- className="sc-chPdSV elAazB"
+ className="sc-kGXeez kiHoFA"
>
$
NaN
@@ -913,7 +913,7 @@
}
>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -982,7 +982,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-EHOje jPIGGt"
+ className="sc-gzVnrw eqRDUd"
onClick={[Function]}
>
<span
@@ -1023,7 +1023,7 @@
</div>
</div>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -1168,7 +1168,7 @@
</span>
</div>
<span
- className="sc-iwsKbI bMOJU"
+ className="sc-gqjmRU cFeeUJ"
disabled={false}
onClick={[Function]}
style={
@@ -1197,7 +1197,7 @@
</div>
</div>
<div
- className="sc-dxgOiQ kbCOBL"
+ className="sc-jKJlTe hccxef"
>
=
@@ -1211,7 +1211,7 @@
}
>
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -1262,7 +1262,7 @@
}
>
<button
- className="sc-jTzLTM iVNQiK"
+ className="sc-jzJRlG fiDUki"
onClick={[Function]}
>
Send
@@ -1272,12 +1272,12 @@
</div>
</div>,
<div
- className="sc-hmzhuo fmIvlu"
+ className="sc-kvZOFW crFufP"
>
Signatures
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -1325,7 +1325,7 @@
exports[`Wallet without BCH balance 1`] = `
Array [
<div
- className="sc-kgoBCf fswuXg"
+ className="sc-kpOJdX GoEyB"
>
You currently have 0
XEC
@@ -1352,7 +1352,7 @@
}
>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -1421,7 +1421,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-EHOje jPIGGt"
+ className="sc-gzVnrw eqRDUd"
onClick={[Function]}
>
<span
@@ -1462,7 +1462,7 @@
</div>
</div>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -1607,7 +1607,7 @@
</span>
</div>
<span
- className="sc-iwsKbI bMOJU"
+ className="sc-gqjmRU cFeeUJ"
disabled={false}
onClick={[Function]}
style={
@@ -1636,7 +1636,7 @@
</div>
</div>
<div
- className="sc-dxgOiQ kbCOBL"
+ className="sc-jKJlTe hccxef"
>
=
@@ -1650,7 +1650,7 @@
}
>
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -1701,7 +1701,7 @@
}
>
<button
- className="sc-fjdhpX eXuEIC"
+ className="sc-cSHVUG eNOONn"
>
Send
</button>
@@ -1710,12 +1710,12 @@
</div>
</div>,
<div
- className="sc-hmzhuo fmIvlu"
+ className="sc-kvZOFW crFufP"
>
Signatures
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -1763,7 +1763,7 @@
exports[`Without wallet defined 1`] = `
Array [
<div
- className="sc-kgoBCf fswuXg"
+ className="sc-kpOJdX GoEyB"
>
You currently have 0
XEC
@@ -1790,7 +1790,7 @@
}
>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -1859,7 +1859,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-EHOje jPIGGt"
+ className="sc-gzVnrw eqRDUd"
onClick={[Function]}
>
<span
@@ -1900,7 +1900,7 @@
</div>
</div>
<div
- className="sc-htoDjs ghcbdA"
+ className="sc-iwsKbI bwAByd"
>
<div
className="ant-row ant-form-item"
@@ -2045,7 +2045,7 @@
</span>
</div>
<span
- className="sc-iwsKbI bMOJU"
+ className="sc-gqjmRU cFeeUJ"
disabled={false}
onClick={[Function]}
style={
@@ -2074,7 +2074,7 @@
</div>
</div>
<div
- className="sc-dxgOiQ kbCOBL"
+ className="sc-jKJlTe hccxef"
>
=
@@ -2088,7 +2088,7 @@
}
>
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
@@ -2139,7 +2139,7 @@
}
>
<button
- className="sc-fjdhpX eXuEIC"
+ className="sc-cSHVUG eNOONn"
>
Send
</button>
@@ -2148,12 +2148,12 @@
</div>
</div>,
<div
- className="sc-hmzhuo fmIvlu"
+ className="sc-kvZOFW crFufP"
>
Signatures
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-VigVT idAASz"
+ className="ant-collapse ant-collapse-icon-position-left sc-fjdhpX ddFLEP"
role={null}
style={
Object {
diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
--- a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
+++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
@@ -5,7 +5,7 @@
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
<div
- className="sc-fjdhpX llHaIR"
+ className="sc-cSHVUG lalYdv"
>
6.001
@@ -78,7 +78,7 @@
}
>
<div
- className="sc-gZMcBi iFeyXx"
+ className="sc-VigVT ktfrOp"
>
<div
className="ant-row ant-form-item"
@@ -147,7 +147,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-htoDjs clindv"
+ className="sc-iwsKbI cVxKUE"
onClick={[Function]}
>
<span
@@ -188,7 +188,7 @@
</div>
</div>
<div
- className="sc-gZMcBi iFeyXx"
+ className="sc-VigVT ktfrOp"
>
<div
className="ant-row ant-form-item"
@@ -242,7 +242,7 @@
className="ant-input-group-addon"
>
<span
- className="sc-gqjmRU dcHtfz"
+ className="sc-jTzLTM PEluH"
disabled={false}
onClick={[Function]}
>
@@ -273,7 +273,7 @@
}
>
<button
- className="sc-EHOje bfWfmf"
+ className="sc-gzVnrw wXhZP"
onClick={[Function]}
>
Send
diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap
--- a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap
+++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap
@@ -2,7 +2,7 @@
exports[`Wallet with BCH balances and tokens and state field 1`] = `
<div
- className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU fLjHet"
+ className="ant-collapse ant-collapse-icon-position-left sc-jTzLTM RrYKP"
role={null}
style={
Object {
diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
--- a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
+++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
@@ -3,14 +3,14 @@
exports[`Wallet with BCH balances 1`] = `
Array [
<div
- className="sc-dxgOiQ mrtGb"
+ className="sc-jKJlTe klaRFe"
>
You need some
XEC
in your wallet to create tokens.
</div>,
<div
- className="sc-kGXeez fOxCHe"
+ className="sc-dxgOiQ eYoaKZ"
>
0
@@ -64,7 +64,7 @@
</div>
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU eryBsW"
+ className="ant-collapse ant-collapse-icon-position-left sc-jTzLTM fDvaHF"
role={null}
style={
Object {
@@ -107,7 +107,7 @@
</div>
</div>,
<p
- className="sc-jKJlTe iXuMkO"
+ className="sc-hMqMXs blALfb"
>
You need at least
@@ -130,14 +130,14 @@
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
<div
- className="sc-dxgOiQ mrtGb"
+ className="sc-jKJlTe klaRFe"
>
You need some
XEC
in your wallet to create tokens.
</div>,
<div
- className="sc-kGXeez fOxCHe"
+ className="sc-dxgOiQ eYoaKZ"
>
0
@@ -191,7 +191,7 @@
</div>
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU eryBsW"
+ className="ant-collapse ant-collapse-icon-position-left sc-jTzLTM fDvaHF"
role={null}
style={
Object {
@@ -234,7 +234,7 @@
</div>
</div>,
<p
- className="sc-jKJlTe iXuMkO"
+ className="sc-hMqMXs blALfb"
>
You need at least
@@ -257,14 +257,14 @@
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
<div
- className="sc-kGXeez fOxCHe"
+ className="sc-dxgOiQ eYoaKZ"
>
0.06
XEC
</div>,
<div
- className="sc-kpOJdX feFYke"
+ className="sc-ckVGcZ hCLjtP"
>
$
NaN
@@ -319,7 +319,7 @@
</div>
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU fLjHet"
+ className="ant-collapse ant-collapse-icon-position-left sc-jTzLTM RrYKP"
role={null}
style={
Object {
@@ -367,13 +367,13 @@
onClick={[Function]}
>
<div
- className="sc-kkGfuU etpeuT"
+ className="sc-hSdWYo tUbiI"
>
<div
- className="sc-hMqMXs kTHajp"
+ className="sc-kkGfuU dSxERO"
/>
<div
- className="sc-kEYyzF lnzmEY"
+ className="sc-iAyFgw clPeTt"
>
6.001
@@ -390,14 +390,14 @@
exports[`Wallet without BCH balance 1`] = `
Array [
<div
- className="sc-dxgOiQ mrtGb"
+ className="sc-jKJlTe klaRFe"
>
You need some
XEC
in your wallet to create tokens.
</div>,
<div
- className="sc-kGXeez fOxCHe"
+ className="sc-dxgOiQ eYoaKZ"
>
0
@@ -451,7 +451,7 @@
</div>
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU eryBsW"
+ className="ant-collapse ant-collapse-icon-position-left sc-jTzLTM fDvaHF"
role={null}
style={
Object {
@@ -494,7 +494,7 @@
</div>
</div>,
<p
- className="sc-jKJlTe iXuMkO"
+ className="sc-hMqMXs blALfb"
>
You need at least
@@ -517,14 +517,14 @@
exports[`Without wallet defined 1`] = `
Array [
<div
- className="sc-dxgOiQ mrtGb"
+ className="sc-jKJlTe klaRFe"
>
You need some
XEC
in your wallet to create tokens.
</div>,
<div
- className="sc-kGXeez fOxCHe"
+ className="sc-dxgOiQ eYoaKZ"
>
0
@@ -578,7 +578,7 @@
</div>
</div>,
<div
- className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU eryBsW"
+ className="ant-collapse ant-collapse-icon-position-left sc-jTzLTM fDvaHF"
role={null}
style={
Object {
@@ -621,7 +621,7 @@
</div>
</div>,
<p
- className="sc-jKJlTe iXuMkO"
+ className="sc-hMqMXs blALfb"
>
You need at least
diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
--- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
+++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
@@ -3,7 +3,7 @@
exports[`Wallet with BCH balances 1`] = `
Array [
<div
- className="sc-Rmtcm hmWARu"
+ className="sc-hzDkRC cnnkkE"
>
<span
aria-label="party emoji"
@@ -29,7 +29,7 @@
to send to others
</div>,
<div
- className="sc-gipzik brVdUh"
+ className="sc-Rmtcm doZmKW"
>
0
@@ -46,7 +46,7 @@
}
>
<div
- className="sc-cSHVUG ckubWs"
+ className="sc-chPdSV iDFVZe"
style={
Object {
"display": "none",
@@ -66,7 +66,7 @@
</span>
</div>
<svg
- className="sc-jzJRlG dYsuyj"
+ className="sc-kAzzGY lanDwf"
height={210}
id="borderedQRCode"
shapeRendering="crispEdges"
@@ -92,7 +92,7 @@
/>
</svg>
<div
- className="sc-kgoBCf iYHBff notranslate"
+ className="sc-kpOJdX eicPsf notranslate"
>
<input
readOnly={true}
@@ -101,34 +101,34 @@
value="ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd"
/>
<span
- className="sc-kAzzGY bGeWfk"
+ className="sc-kgoBCf bHwLKM"
>
ecash:
</span>
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
qzagy47m
</span>
vh6qxkvcn3acjnz73rkhkc6y7c
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
cxkrr6zd
</span>
</div>
</div>,
<div
- className="sc-fAjcbJ lkKmCi"
+ className="sc-gisBJw bgeSVQ"
>
<div
- className="sc-caSCKo kTdehG"
+ className="sc-kjoXOD bocqe"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-caSCKo kTdehG nonactiveBtn"
+ className="sc-kjoXOD bocqe nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -140,7 +140,7 @@
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
<div
- className="sc-Rmtcm hmWARu"
+ className="sc-hzDkRC cnnkkE"
>
<span
aria-label="party emoji"
@@ -166,7 +166,7 @@
to send to others
</div>,
<div
- className="sc-gipzik brVdUh"
+ className="sc-Rmtcm doZmKW"
>
0
@@ -183,7 +183,7 @@
}
>
<div
- className="sc-cSHVUG ckubWs"
+ className="sc-chPdSV iDFVZe"
style={
Object {
"display": "none",
@@ -203,7 +203,7 @@
</span>
</div>
<svg
- className="sc-jzJRlG dYsuyj"
+ className="sc-kAzzGY lanDwf"
height={210}
id="borderedQRCode"
shapeRendering="crispEdges"
@@ -229,7 +229,7 @@
/>
</svg>
<div
- className="sc-kgoBCf iYHBff notranslate"
+ className="sc-kpOJdX eicPsf notranslate"
>
<input
readOnly={true}
@@ -238,34 +238,34 @@
value="ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd"
/>
<span
- className="sc-kAzzGY bGeWfk"
+ className="sc-kgoBCf bHwLKM"
>
ecash:
</span>
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
qzagy47m
</span>
vh6qxkvcn3acjnz73rkhkc6y7c
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
cxkrr6zd
</span>
</div>
</div>,
<div
- className="sc-fAjcbJ lkKmCi"
+ className="sc-gisBJw bgeSVQ"
>
<div
- className="sc-caSCKo kTdehG"
+ className="sc-kjoXOD bocqe"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-caSCKo kTdehG nonactiveBtn"
+ className="sc-kjoXOD bocqe nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -277,14 +277,14 @@
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
<div
- className="sc-gipzik brVdUh"
+ className="sc-Rmtcm doZmKW"
>
0.06
XEC
</div>,
<div
- className="sc-csuQGl cLByja"
+ className="sc-bRBYWo itMbWO"
>
$
NaN
@@ -302,7 +302,7 @@
}
>
<div
- className="sc-cSHVUG ckubWs"
+ className="sc-chPdSV iDFVZe"
style={
Object {
"display": "none",
@@ -322,7 +322,7 @@
</span>
</div>
<svg
- className="sc-jzJRlG dYsuyj"
+ className="sc-kAzzGY lanDwf"
height={210}
id="borderedQRCode"
shapeRendering="crispEdges"
@@ -348,7 +348,7 @@
/>
</svg>
<div
- className="sc-kgoBCf iYHBff notranslate"
+ className="sc-kpOJdX eicPsf notranslate"
>
<input
readOnly={true}
@@ -357,34 +357,34 @@
value="ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd"
/>
<span
- className="sc-kAzzGY bGeWfk"
+ className="sc-kgoBCf bHwLKM"
>
ecash:
</span>
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
qzagy47m
</span>
vh6qxkvcn3acjnz73rkhkc6y7c
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
cxkrr6zd
</span>
</div>
</div>,
<div
- className="sc-fAjcbJ lkKmCi"
+ className="sc-gisBJw bgeSVQ"
>
<div
- className="sc-caSCKo kTdehG"
+ className="sc-kjoXOD bocqe"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-caSCKo kTdehG nonactiveBtn"
+ className="sc-kjoXOD bocqe nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -396,7 +396,7 @@
exports[`Wallet without BCH balance 1`] = `
Array [
<div
- className="sc-Rmtcm hmWARu"
+ className="sc-hzDkRC cnnkkE"
>
<span
aria-label="party emoji"
@@ -422,7 +422,7 @@
to send to others
</div>,
<div
- className="sc-gipzik brVdUh"
+ className="sc-Rmtcm doZmKW"
>
0
@@ -439,7 +439,7 @@
}
>
<div
- className="sc-cSHVUG ckubWs"
+ className="sc-chPdSV iDFVZe"
style={
Object {
"display": "none",
@@ -459,7 +459,7 @@
</span>
</div>
<svg
- className="sc-jzJRlG dYsuyj"
+ className="sc-kAzzGY lanDwf"
height={210}
id="borderedQRCode"
shapeRendering="crispEdges"
@@ -485,7 +485,7 @@
/>
</svg>
<div
- className="sc-kgoBCf iYHBff notranslate"
+ className="sc-kpOJdX eicPsf notranslate"
>
<input
readOnly={true}
@@ -494,34 +494,34 @@
value="ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd"
/>
<span
- className="sc-kAzzGY bGeWfk"
+ className="sc-kgoBCf bHwLKM"
>
ecash:
</span>
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
qzagy47m
</span>
vh6qxkvcn3acjnz73rkhkc6y7c
<span
- className="sc-chPdSV jcbRDb"
+ className="sc-kGXeez jNbIFN"
>
cxkrr6zd
</span>
</div>
</div>,
<div
- className="sc-fAjcbJ lkKmCi"
+ className="sc-gisBJw bgeSVQ"
>
<div
- className="sc-caSCKo kTdehG"
+ className="sc-kjoXOD bocqe"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-caSCKo kTdehG nonactiveBtn"
+ className="sc-kjoXOD bocqe nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -536,12 +536,12 @@
Welcome to Cashtab!
</h2>,
<p
- className="sc-jTzLTM iZNmqq"
+ className="sc-jzJRlG leTPdT"
>
Cashtab is an
<a
- className="sc-fjdhpX cfxeWn"
+ className="sc-cSHVUG dlQkwn"
href="https://github.com/bitcoin-abc/bitcoin-abc"
rel="noreferrer"
target="_blank"
@@ -557,7 +557,7 @@
Want to learn more?
<a
- className="sc-fjdhpX cfxeWn"
+ className="sc-cSHVUG dlQkwn"
href="https://docs.cashtabapp.com/docs/"
rel="noreferrer"
target="_blank"
@@ -566,7 +566,7 @@
</a>
</p>,
<button
- className="sc-gZMcBi JUqLQ"
+ className="sc-VigVT RpRhe"
onClick={[Function]}
>
<span
@@ -594,7 +594,7 @@
New Wallet
</button>,
<button
- className="sc-gqjmRU eEPviq"
+ className="sc-jTzLTM bLkbAy"
onClick={[Function]}
>
<span
diff --git a/web/cashtab/src/hooks/useWebAuthentication.js b/web/cashtab/src/hooks/useWebAuthentication.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/hooks/useWebAuthentication.js
@@ -0,0 +1,238 @@
+import { useState, useEffect } from 'react';
+import localforage from 'localforage';
+import { currency } from '@components/Common/Ticker';
+import {
+ convertBase64ToArrayBuffer,
+ convertArrayBufferToBase64,
+} from '@utils/convertArrBuffBase64';
+
+// return an Authentication Object
+// OR null if user device does not support Web Authentication
+const useWebAuthentication = () => {
+ const [isWebAuthnSupported, setIsWebAuthnSupported] = useState(false);
+ // Possible values of isAuthenticationRequired:
+ // true - YES, authentication is required
+ // false - NO, authentication is not required
+ // undefined - has not been set, this is the first time the app runs
+ const [isAuthenticationRequired, setIsAuthenticationRequired] =
+ useState(undefined);
+ const [credentialId, setCredentialId] = useState(null);
+ const [isSignedIn, setIsSignedIn] = useState(false);
+ const [userId, setUserId] = useState(Date.now().toString(16));
+ const [loading, setLoading] = useState(true);
+
+ const loadAuthenticationConfigFromLocalStorage = async () => {
+ // try to load authentication configuration from local storage
+ try {
+ return await localforage.getItem('authenticationConfig');
+ } catch (err) {
+ console.error(
+ 'Error is localforange.getItem("authenticatonConfig") in loadAuthenticationConfigFromLocalStorage() in useWebAuthentication()',
+ );
+ // Should stop when attempting to read from localstorage failed
+ // countinuing would prompt user to register new credential
+ // that would risk overrididing existing credential
+ throw err;
+ }
+ };
+
+ const saveAuthenticationConfigToLocalStorage = () => {
+ try {
+ return localforage.setItem('authenticationConfig', {
+ isAuthenticationRequired,
+ userId,
+ credentialId,
+ });
+ } catch (err) {
+ console.error(
+ 'Error is localforange.setItem("authenticatonConfig") in saveAuthenticationConfigToLocalStorage() in useWebAuthentication()',
+ );
+ throw err;
+ }
+ };
+
+ // Run Once
+ useEffect(async () => {
+ // check to see if user device supports User Verification
+ const available =
+ await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
+ // only attempt to save/load authentication configuration from local storage if web authetication is supported
+ if (available) {
+ const authenticationConfig =
+ await loadAuthenticationConfigFromLocalStorage();
+ // if this is the first time the app is run, then save the default config value
+ if (authenticationConfig === null) {
+ saveAuthenticationConfigToLocalStorage();
+ } else {
+ setUserId(authenticationConfig.userId);
+ setCredentialId(authenticationConfig.credentialId);
+ setIsAuthenticationRequired(
+ authenticationConfig.isAuthenticationRequired,
+ );
+ }
+ // signout the user when the app is not visible (minimize the browser, switch tab, switch app window)
+ const handleDocVisibilityChange = () => {
+ if (document.visibilityState !== 'visible')
+ setIsSignedIn(false);
+ };
+ document.addEventListener(
+ 'visibilitychange',
+ handleDocVisibilityChange,
+ );
+
+ setIsWebAuthnSupported(available);
+ setLoading(false);
+
+ return () => {
+ document.removeEventListener(
+ 'visibilitychange',
+ handleDocVisibilityChange,
+ );
+ };
+ }
+ }, []);
+
+ // save the config whenever it is changed
+ useEffect(async () => {
+ if (isAuthenticationRequired === undefined) return;
+ await saveAuthenticationConfigToLocalStorage();
+ }, [isAuthenticationRequired, credentialId]);
+
+ // options for PublicKeyCredentialCreation
+ const publicKeyCredentialCreationOptions = {
+ // hardcode for now
+ // consider generating random string and then verifying it against the reponse from authenticator
+ challenge: Uint8Array.from('cashtab-wallet-for-ecash', c =>
+ c.charCodeAt(0),
+ ),
+ rp: {
+ name: currency.name,
+ id: document.domain,
+ },
+ user: {
+ id: Uint8Array.from(userId, c => c.charCodeAt(0)),
+ name: `Local User`,
+ displayName: 'Local User',
+ },
+ pubKeyCredParams: [
+ { alg: -7, type: 'public-key' },
+ { alg: -35, type: 'public-key' },
+ { alg: -36, type: 'public-key' },
+ { alg: -257, type: 'public-key' },
+ { alg: -258, type: 'public-key' },
+ { alg: -259, type: 'public-key' },
+ { alg: -37, type: 'public-key' },
+ { alg: -38, type: 'public-key' },
+ { alg: -39, type: 'public-key' },
+ { alg: -8, type: 'public-key' },
+ ],
+ authenticatorSelection: {
+ userVerification: 'required',
+ authenticatorAttachment: 'platform',
+ requireResidentKey: false,
+ },
+ timeout: 60000,
+ attestation: 'none',
+ excludeCredentials: [],
+ extensions: {},
+ };
+
+ // options for PublicKeyCredentialRequest
+ const publickKeyRequestOptions = {
+ // hardcode for now
+ // consider generating random string and then verifying it against the reponse from authenticator
+ challenge: Uint8Array.from('cashtab-wallet-for-ecash', c =>
+ c.charCodeAt(0),
+ ),
+ timeout: 60000,
+ // rpId: document.domain,
+ allowCredentials: [
+ {
+ type: 'public-key',
+ // the credentialId is stored as base64
+ // need to convert it to ArrayBuffer
+ id: convertBase64ToArrayBuffer(credentialId),
+ transports: ['internal'],
+ },
+ ],
+ userVerification: 'required',
+ extensions: {},
+ };
+
+ const authentication = {
+ isAuthenticationRequired,
+ credentialId,
+ isSignedIn,
+ loading,
+ turnOnAuthentication: () => {
+ // Need to make sure that the credetialId is set
+ // before turning on the authentication requirement
+ // otherwise, user will be locked out of the app
+ // in other words, user must sign up / register first
+ if (credentialId) {
+ setIsAuthenticationRequired(true);
+ }
+ },
+ turnOffAuthentication: () => {
+ setIsAuthenticationRequired(false);
+ },
+
+ signUp: async () => {
+ try {
+ const publicKeyCredential = await navigator.credentials.create({
+ publicKey: publicKeyCredentialCreationOptions,
+ });
+ if (publicKeyCredential) {
+ // convert the rawId from ArrayBuffer to base64 String
+ const base64Id = convertArrayBufferToBase64(
+ publicKeyCredential.rawId,
+ );
+ setIsSignedIn(true);
+ setCredentialId(base64Id);
+ setIsAuthenticationRequired(true);
+ } else {
+ throw new Error(
+ 'Error: navigator.credentials.create() returns null, in signUp()',
+ );
+ }
+ } catch (err) {
+ throw err;
+ }
+ },
+
+ signIn: async () => {
+ try {
+ const assertion = await navigator.credentials.get({
+ publicKey: publickKeyRequestOptions,
+ });
+ if (assertion) {
+ // convert rawId from ArrayBuffer to base64 String
+ const base64Id = convertArrayBufferToBase64(
+ assertion.rawId,
+ );
+ if (base64Id === credentialId) setIsSignedIn(true);
+ } else {
+ throw new Error(
+ 'Error: navigator.credentials.get() returns null, signIn()',
+ );
+ }
+ } catch (err) {
+ throw err;
+ }
+ },
+
+ signOut: () => {
+ setIsSignedIn(false);
+ },
+ };
+
+ // Web Authentication support on a user's device may become unavailable due to various reasons
+ // (hardware failure, OS problems, the behaviour of some authenticators after several failed authentication attempts, etc)
+ // If this is the case, and user has previous enabled the lock, the decision here is to lock up the wallet.
+ // Otherwise, malicious user needs to simply disbale the platform authenticator to gain access to the wallet
+ return !isWebAuthnSupported && !isAuthenticationRequired
+ ? null
+ : authentication;
+};
+
+export default useWebAuthentication;
diff --git a/web/cashtab/src/index.js b/web/cashtab/src/index.js
--- a/web/cashtab/src/index.js
+++ b/web/cashtab/src/index.js
@@ -2,17 +2,19 @@
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
-import { WalletProvider } from './utils/context';
+import { AuthenticationProvider, WalletProvider } from './utils/context';
import { HashRouter as Router } from 'react-router-dom';
import GA from './utils/GoogleAnalytics';
ReactDOM.render(
- <WalletProvider>
- <Router>
- {GA.init() && <GA.RouteTracker />}
- <App />
- </Router>
- </WalletProvider>,
+ <AuthenticationProvider>
+ <WalletProvider>
+ <Router>
+ {GA.init() && <GA.RouteTracker />}
+ <App />
+ </Router>
+ </WalletProvider>
+ </AuthenticationProvider>,
document.getElementById('root'),
);
diff --git a/web/cashtab/src/utils/context.js b/web/cashtab/src/utils/context.js
--- a/web/cashtab/src/utils/context.js
+++ b/web/cashtab/src/utils/context.js
@@ -10,3 +10,17 @@
</WalletContext.Provider>
);
};
+
+// Authentication Context
+import useWebAuthentication from '../hooks/useWebAuthentication';
+export const AuthenticationContext = React.createContext();
+export const AuthenticationProvider = ({ children }) => {
+ // useWebAuthentication returns null if Web Authn is not supported
+ const authentication = useWebAuthentication();
+
+ return (
+ <AuthenticationContext.Provider value={authentication}>
+ {children}
+ </AuthenticationContext.Provider>
+ );
+};
diff --git a/web/cashtab/src/utils/convertArrBuffBase64.js b/web/cashtab/src/utils/convertArrBuffBase64.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/utils/convertArrBuffBase64.js
@@ -0,0 +1,19 @@
+export const convertArrayBufferToBase64 = buffer => {
+ // convert the buffer from ArrayBuffer to Array of 8-bit unsigned integers
+ const dataView = new Uint8Array(buffer);
+ // convert the Array of 8-bit unsigned integers to a String
+ const dataStr = dataView.reduce(
+ (str, cur) => str + String.fromCharCode(cur),
+ '',
+ );
+ // convert String to base64
+ return window.btoa(dataStr);
+};
+
+export const convertBase64ToArrayBuffer = base64Str => {
+ // convert base64 String to normal String
+ const dataStr = window.atob(base64Str);
+ // convert the String to an Array of 8-bit unsigned integers
+ const dataView = Uint8Array.from(dataStr, char => char.charCodeAt(0));
+ return dataView.buffer;
+};

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 26, 11:06 (12 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573359
Default Alt Text
D10443.id31122.diff (71 KB)

Event Timeline