Page MenuHomePhabricator

No OneTemporary

diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js
index 0c4a49607..a8558f239 100644
--- a/web/cashtab/src/components/App.js
+++ b/web/cashtab/src/components/App.js
@@ -1,662 +1,679 @@
import React, { useState, useEffect } from 'react';
import 'antd/dist/antd.less';
import PropTypes from 'prop-types';
-import { Spin } from 'antd';
+import { Spin, Modal } from 'antd';
import {
CashLoadingIcon,
HomeIcon,
SendIcon,
ReceiveIcon,
SettingsIcon,
AirdropIcon,
ThemedSignAndVerifyMsg,
} from 'components/Common/CustomIcons';
import '../index.css';
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import { theme } from 'assets/styles/theme';
import Home from 'components/Home/Home';
import Receive from 'components/Receive/Receive';
import Tokens from 'components/Tokens/Tokens';
import Send from 'components/Send/Send';
import SendToken from 'components/Send/SendToken';
import Airdrop from 'components/Airdrop/Airdrop';
import Configure from 'components/Configure/Configure';
import SignVerifyMsg from 'components/SignVerifyMsg/SignVerifyMsg';
import NotFound from 'components/NotFound';
import CashTab from 'assets/cashtab_xec.png';
import './App.css';
import { WalletContext } from 'utils/context';
import { isValidStoredWallet } from 'utils/cashMethods';
import {
Route,
Redirect,
Switch,
useLocation,
useHistory,
} from 'react-router-dom';
// Easter egg imports not used in extension/src/components/App.js
import TabCash from 'assets/tabcash.png';
import { checkForTokenById } from 'utils/tokenMethods.js';
// Biometric security import not used in extension/src/components/App.js
import ProtectableComponentWrapper from './Authentication/ProtectableComponentWrapper';
import ServiceWorkerWrapper from './Common/ServiceWorkerWrapper';
const GlobalStyle = createGlobalStyle`
*::placeholder {
color: ${props => props.theme.forms.placeholder} !important;
}
*::selection {
background: ${props => props.theme.eCashBlue} !important;
}
.ant-modal-content, .ant-modal-header, .ant-modal-title {
background-color: ${props => props.theme.modal.background} !important;
color: ${props => props.theme.modal.color} !important;
}
.ant-modal-content svg {
fill: ${props => props.theme.modal.color};
}
.ant-modal-footer button {
background-color: ${props =>
props.theme.modal.buttonBackground} !important;
color: ${props => props.theme.modal.color} !important;
border-color: ${props => props.theme.modal.border} !important;
:hover {
background-color: ${props => props.theme.eCashBlue} !important;
}
}
.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 {
border-radius: 3px;
background-color: ${props =>
props.theme.modal.buttonBackground} !important;
color: ${props => props.theme.modal.color} !important;
border-color: ${props => props.theme.modal.border} !important;
:hover {
background-color: ${props => props.theme.eCashBlue} !important;
}
text-shadow: none !important;
}
.ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover, #cropControlsConfirm:hover {
color: ${props => props.theme.contrast};
transition: all 0.3s;
background-color: ${props => props.theme.eCashBlue};
border-color: ${props => props.theme.eCashBlue};
}
.selectedCurrencyOption, .ant-select-dropdown {
text-align: left;
color: ${props => props.theme.contrast} !important;
background-color: ${props =>
props.theme.collapses.expandedBackground} !important;
}
.cashLoadingIcon {
color: ${props => props.theme.eCashBlue} !important;
font-size: 48px !important;
}
.selectedCurrencyOption:hover {
color: ${props => props.theme.contrast} !important;
background-color: ${props => props.theme.eCashBlue} !important;
}
#addrSwitch, #cropSwitch {
.ant-switch-checked {
background-color: white !important;
}
}
#addrSwitch.ant-switch-checked, #cropSwitch.ant-switch-checked {
background-image: ${props =>
props.theme.buttons.primary.backgroundImage} !important;
}
.ant-slider-rail {
background-color: ${props => props.theme.forms.border} !important;
}
.ant-slider-track {
background-color: ${props => props.theme.eCashBlue} !important;
}
.ant-descriptions-bordered .ant-descriptions-row {
background: ${props => props.theme.contrast};
}
.ant-modal-confirm-content, .ant-modal-confirm-title {
color: ${props => props.theme.contrast} !important;
}
.ant-form-item-explain {
div {
color: ${props => props.theme.forms.text};
}
}
.ant-input-prefix {
color: ${props => props.theme.eCashBlue};
}
.ant-spin-nested-loading>div>.ant-spin .ant-spin-dot {
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
margin: auto !important;
}
.ant-spin-nested-loading>div>.ant-spin {
position: fixed !important;
}
`;
const CustomApp = styled.div`
text-align: center;
font-family: 'Poppins', sans-serif;
background-color: ${props => props.theme.backgroundColor};
background-size: 100px 171px;
background-image: ${props => props.theme.backgroundImage};
background-attachment: fixed;
`;
const Footer = styled.div`
z-index: 2;
height: 80px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
background-color: ${props => props.theme.footerBackground};
position: fixed;
bottom: 0;
width: 500px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 50px;
@media (max-width: 768px) {
width: 100%;
padding: 0 20px;
}
`;
const NavWrapper = styled.div`
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 1.3rem;
margin-bottom: 5px;
`;
const NavIcon = styled.span`
position: relative;
background-color: ${props =>
props.clicked ? 'transparent' : props.theme.buttons.primary.color};
width: 2rem;
height: 2px;
display: inline-block;
transition: transform 300ms, top 300ms, background-color 300ms;
&::before,
&::after {
content: '';
background-color: ${props => props.theme.buttons.primary.color};
width: 2rem;
height: 2px;
display: inline-block;
position: absolute;
left: 0;
transition: transform 300ms, top 300ms, background-color 300ms;
}
&::before {
top: ${props => (props.clicked ? '0' : '-0.8rem')};
transform: ${props => (props.clicked ? 'rotate(135deg)' : 'rotate(0)')};
}
&::after {
top: ${props => (props.clicked ? '0' : '0.8rem')};
transform: ${props =>
props.clicked ? 'rotate(-135deg)' : 'rotate(0)'};
}
`;
const NavMenu = styled.div`
position: fixed;
float: right;
margin-right: 1px;
bottom: 5rem;
display: flex;
width: 8.23rem;
flex-direction: column;
border: ${props => (props.open ? '1px solid' : '0px solid')};
border-color: ${props =>
props.open ? props.theme.contrast : 'transparent'};
justify-content: center;
align-items: center;
@media (max-width: 768px) {
right: 0;
margin-right: 0;
}
overflow: hidden;
transition: ${props =>
props.open
? 'max-height 1000ms ease-in-out , border-color 800ms ease-in-out, border-width 800ms ease-in-out'
: 'max-height 300ms cubic-bezier(0, 1, 0, 1), border-color 600ms ease-in-out, border-width 800ms ease-in-out'};
max-height: ${props => (props.open ? '100rem' : '0')};
`;
const NavItem = styled.button`
display: flex;
justify-content: right;
align-items: center;
width: 100%;
white-space: nowrap;
height: 3rem;
background-color: ${props => props.theme.walletBackground};
border: none;
color: ${props => props.theme.contrast};
gap: 6px;
cursor: pointer;
&:hover {
color: ${props => props.theme.navActive};
svg {
fill: ${props => props.theme.navActive};
}
}
svg {
fill: ${props => props.theme.contrast};
max-width: 26px;
height: auto;
flex: 1;
}
p {
flex: 2;
margin: 0;
}
${({ active, ...props }) =>
active &&
`
color: ${props.theme.navActive};
svg {
fill: ${props.theme.navActive};
}
`}
`;
export const NavButton = styled.button`
:focus,
:active {
outline: none;
}
cursor: pointer;
padding: 0;
background: none;
border: none;
font-size: 10px;
svg {
fill: ${props => props.theme.contrast};
width: 26px;
height: auto;
}
${({ active, ...props }) =>
active &&
`
color: ${props.theme.navActive};
svg {
fill: ${props.theme.navActive};
}
`}
`;
export const WalletBody = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 100vh;
`;
export const WalletCtn = styled.div`
position: relative;
width: 500px;
min-height: 100vh;
padding: 0 0 100px;
background: ${props => props.theme.walletBackground};
-webkit-box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow};
-moz-box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow};
box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow};
@media (max-width: 768px) {
width: 100%;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
`;
export const HeaderCtn = styled.div`
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
justify-content: center;
width: 100%;
padding: 15px 0;
`;
export const CashTabLogo = styled.img`
width: 120px;
@media (max-width: 768px) {
width: 110px;
}
`;
// AbcLogo styled component not included in extension, replaced by open in new tab link
export const AbcLogo = styled.img`
width: 150px;
@media (max-width: 768px) {
width: 120px;
}
`;
// Easter egg styled component not used in extension/src/components/App.js
export const EasterEgg = styled.img`
position: fixed;
bottom: -195px;
margin: 0;
right: 10%;
transition-property: bottom;
transition-duration: 1.5s;
transition-timing-function: ease-out;
:hover {
bottom: 0;
}
@media screen and (max-width: 1250px) {
display: none;
}
`;
const NavHeader = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 1rem;
color: ${props => props.theme.navActive};
svg {
padding: 0.2rem;
fill: ${props => props.theme.navActive};
height: 33px;
width: 30px;
}
`;
const App = () => {
const ContextValue = React.useContext(WalletContext);
const { wallet, loading } = ContextValue;
const [loadingUtxosAfterSend, setLoadingUtxosAfterSend] = useState(false);
const [updatingWalletInfo, setUpdatingWalletInfo] = useState(false);
const [navMenuClicked, setNavMenuClicked] = useState(false);
const handleNavMenuClick = () => setNavMenuClicked(!navMenuClicked);
// If wallet is unmigrated, do not show page until it has migrated
// An invalid wallet will be validated/populated after the next API call, ETA 10s
const validWallet = isValidStoredWallet(wallet);
const location = useLocation();
const history = useHistory();
const selectedKey =
location && location.pathname ? location.pathname.substr(1) : '';
/*
See https://web.dev/push-notifications-subscribing-a-user/
note that Safari still uses callback syntax, hence this method which handles both syntaxes
https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission
*/
const askPermissionForNotifications = () => {
return new Promise(function (resolve, reject) {
const permissionResult = Notification.requestPermission(function (
result,
) {
resolve(result);
});
if (permissionResult) {
permissionResult.then(resolve, reject);
}
}).then(function (permissionResult) {
console.log(`permissionResult`, permissionResult);
if (permissionResult !== 'granted') {
- throw new Error("We weren't granted permission.");
- // TODO in this case, we should display a modal saying we'll keep asking
+ // Warning modal if user does not accept notifications
+ Modal.warn({
+ title: 'Notification permission refused',
+ content:
+ 'Please enable notifications from Cashtab.com to help prevent your browser from deleting Cashtab wallet info.',
+ });
} else {
+ console.log(`Notification permission granted`);
// If it is granted, ask again for persistent storage
// Best way to do this is to show a modal that refreshes the page
+
+ // Success modal when user accepts notifications, prompting a refresh
+ Modal.success({
+ title: 'Notification permission granted',
+ content: 'Please refresh the page',
+ onOk: () => {
+ console.log(
+ `Reloading page to retry for persistent storage permission`,
+ );
+ window.location.reload(true);
+ },
+ });
}
});
};
const checkForPersistentStorage = async () => {
// Request persistent storage for site
if (navigator.storage && navigator.storage.persist) {
// Check if storage is persistent
const isPersisted = await navigator.storage.persisted();
console.log(`Persisted storage status: ${isPersisted}`);
// If not, request persistent storage
if (!isPersisted) {
console.log(`Requesting persistent storage`);
const persistanceRequestResult =
await navigator.storage.persist();
console.log(
`Persistent storage granted: ${persistanceRequestResult}`,
);
// If the request was not granted, ask the user to approve notification permissions
if (!persistanceRequestResult) {
// Check if the browser supports notifications
const browserSupportsNotifications =
'Notification' in window;
console.log(
`browserSupportsNotifications`,
browserSupportsNotifications,
);
// If the browser supports notifications, check the permission status
if (browserSupportsNotifications) {
const cashtabNotificationPermission =
Notification.permission;
console.log(
`cashtabNotificationPermission`,
cashtabNotificationPermission,
);
// If the permission is not 'granted', ask for notification permissions
if (cashtabNotificationPermission !== 'granted') {
console.log(
`User has not granted notification permission and persistent storage status has not been granted. Requesting notification permission.`,
);
return askPermissionForNotifications();
}
}
}
}
}
};
// Easter egg boolean not used in extension/src/components/App.js
const hasTab = validWallet
? checkForTokenById(
wallet.state.tokens,
'50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e',
)
: false;
useEffect(() => {
checkForPersistentStorage();
}, []);
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<ServiceWorkerWrapper />
<Spin
spinning={
loading ||
loadingUtxosAfterSend ||
updatingWalletInfo ||
(wallet && !validWallet)
}
indicator={CashLoadingIcon}
>
<CustomApp>
<WalletBody>
<WalletCtn>
<HeaderCtn>
<CashTabLogo src={CashTab} alt="cashtab" />
{selectedKey === 'airdrop' && (
<NavHeader>
Airdrop
<AirdropIcon />
</NavHeader>
)}
{selectedKey === 'configure' && (
<NavHeader>
Settings
<SettingsIcon />
</NavHeader>
)}
{selectedKey === 'signverifymsg' && (
<NavHeader>
{' '}
Sign & Verify Msg
<ThemedSignAndVerifyMsg />
</NavHeader>
)}
{/*Begin component not included in extension as desktop only*/}
{hasTab && (
<EasterEgg src={TabCash} alt="tabcash" />
)}
{/*End component not included in extension as desktop only*/}
</HeaderCtn>
<ProtectableComponentWrapper>
<Switch>
<Route path="/wallet">
<Home />
</Route>
<Route path="/receive">
<Receive
passLoadingStatus={
setLoadingUtxosAfterSend
}
/>
</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
}
passLoadingStatus={
setLoadingUtxosAfterSend
}
/>
)}
/>
<Route path="/airdrop">
<Airdrop
passLoadingStatus={
setLoadingUtxosAfterSend
}
/>
</Route>
<Route path="/signverifymsg">
<SignVerifyMsg />
</Route>
<Route path="/configure">
<Configure
passLoadingStatus={
setUpdatingWalletInfo
}
/>
</Route>
<Redirect exact from="/" to="/wallet" />
<Route component={NotFound} />
</Switch>
</ProtectableComponentWrapper>
</WalletCtn>
{wallet ? (
<Footer>
<NavButton
active={selectedKey === 'wallet'}
onClick={() => history.push('/wallet')}
>
<HomeIcon />
</NavButton>
<NavButton
active={selectedKey === 'send'}
onClick={() => history.push('/send')}
>
<SendIcon
style={{
marginTop: '-9px',
}}
/>
</NavButton>
<NavButton
active={selectedKey === 'receive'}
onClick={() => history.push('receive')}
>
<ReceiveIcon />
</NavButton>
<NavWrapper onClick={handleNavMenuClick}>
<NavIcon clicked={navMenuClicked} />
<NavMenu open={navMenuClicked}>
<NavItem
active={selectedKey === 'airdrop'}
onClick={() =>
history.push('/airdrop')
}
>
{' '}
<p>Airdrop</p>
<AirdropIcon />
</NavItem>
<NavItem
active={
selectedKey === 'signverifymsg'
}
onClick={() =>
history.push('/signverifymsg')
}
>
<p>Sign & Verify</p>
<ThemedSignAndVerifyMsg />
</NavItem>
<NavItem
active={selectedKey === 'configure'}
onClick={() =>
history.push('/configure')
}
>
<p>Settings</p>
<SettingsIcon />
</NavItem>
</NavMenu>
</NavWrapper>
</Footer>
) : null}
</WalletBody>
</CustomApp>
</Spin>
</ThemeProvider>
);
};
App.propTypes = {
match: PropTypes.string,
};
export default App;

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 21, 18:42 (22 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865716
Default Alt Text
(25 KB)

Event Timeline