Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711237
D10443.id31121.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
72 KB
Subscribers
None
D10443.id31121.diff
View Options
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
@@ -1,9 +1,9 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
+// Jest Snapshot v1, https://goo.gl/fbAQLP
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
@@ -1,16 +1,16 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
+// Jest Snapshot v1, https://goo.gl/fbAQLP
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
@@ -1,9 +1,9 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
+// Jest Snapshot v1, https://goo.gl/fbAQLP
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
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 26, 11:06 (5 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573358
Default Alt Text
D10443.id31121.diff (72 KB)
Attached To
D10443: add the ability to lock / unlock the wallet with user's device platform autheticator
Event Timeline
Log In to Comment