Merge pull request #73 from andrerfneves/feature/deep-link
Feature/deep link
This commit is contained in:
commit
7860f23d5c
|
@ -15,23 +15,25 @@ afterEach(() => app.stop());
|
|||
|
||||
describe('Send', () => {
|
||||
test('should load "Send Page"', async () => {
|
||||
expect(app.client.element('#send-wrapper')
|
||||
.isVisible()).resolves.toEqual(true);
|
||||
expect(app.client.element('#send-wrapper').isVisible()).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
test('should show Additional Options click', async () => {
|
||||
expect(app.client.element('#send-wrapper #send-fee-wrapper')
|
||||
.isVisible()).resolves.toEqual(false);
|
||||
expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
|
||||
false,
|
||||
);
|
||||
|
||||
await app.client.element('#send-show-additional-options-button').click();
|
||||
|
||||
expect(app.client.element('#send-wrapper #send-fee-wrapper')
|
||||
.isVisible()).resolves.toEqual(true);
|
||||
expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('should disable send button if required fields are empty', async () => {
|
||||
expect(app.client.element('#send-submit-button')
|
||||
.getAttribute('disabled')).resolves.toEqual(true);
|
||||
expect(app.client.element('#send-submit-button').getAttribute('disabled')).resolves.toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('should enable send button if required fields are filled', async () => {
|
||||
|
@ -68,8 +70,9 @@ describe('Send', () => {
|
|||
|
||||
await app.client.element('#send-submit-button').click();
|
||||
|
||||
expect(app.client.element('#send-confirm-transaction-modal')
|
||||
.isVisible()).resolves.toEqual(true);
|
||||
expect(app.client.element('#send-confirm-transaction-modal').isVisible()).resolves.toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('should display a load indicator while the transaction is processed', async () => {
|
||||
|
@ -91,15 +94,14 @@ describe('Send', () => {
|
|||
expect(app.client.getAttribute('#send-confirm-transaction-modal img', 'src')).resolves.toEqual(
|
||||
expect.stringContaining('/assets/sync_icon.png'),
|
||||
);
|
||||
expect(app.client.getText('#send-confirm-transaction-modal p'))
|
||||
.resolves.toEqual('Processing transaction...');
|
||||
expect(app.client.element('#confirm-modal-button')
|
||||
.isVisible()).resolves.toEqual(false);
|
||||
expect(app.client.getText('#send-confirm-transaction-modal p')).resolves.toEqual(
|
||||
'Processing transaction...',
|
||||
);
|
||||
expect(app.client.element('#confirm-modal-button').isVisible()).resolves.toEqual(false);
|
||||
});
|
||||
|
||||
test('should show an error in invalid transaction', async () => {
|
||||
expect(app.client.element('#send-error-text')
|
||||
.isVisible()).resolves.toEqual(false);
|
||||
expect(app.client.element('#send-error-text').isVisible()).resolves.toEqual(false);
|
||||
|
||||
await app.client.element('#sidebar a:nth-child(1)').click();
|
||||
await app.client.element('#sidebar a:nth-child(2)').click();
|
||||
|
@ -124,8 +126,7 @@ describe('Send', () => {
|
|||
});
|
||||
|
||||
test('should show a success screen after transaction and show a transaction item', async () => {
|
||||
expect(app.client.element('#send-success-wrapper')
|
||||
.isVisible()).resolves.toEqual(false);
|
||||
expect(app.client.element('#send-success-wrapper').isVisible()).resolves.toEqual(false);
|
||||
|
||||
await app.client.element('#sidebar a:nth-child(1)').click();
|
||||
await app.client.element('#sidebar a:nth-child(2)').click();
|
||||
|
@ -149,10 +150,11 @@ describe('Send', () => {
|
|||
|
||||
await app.client.waitUntilTextExists('#transaction-item-operation-id-1', 'Send');
|
||||
|
||||
expect(await app.client.element('#transaction-item-operation-id-1 img')
|
||||
.isVisible()).toEqual(true);
|
||||
expect(await app.client.element('#transaction-item-operation-id-1 img').isVisible()).toEqual(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
await app.client.element('#transaction-item-operation-id-1 img').getAttribute('src'),
|
||||
).toEndWith('/assets/transaction_sent_icon.svg');
|
||||
).toEndWith('/assets/transaction_sent_icon_dark.svg');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import { Transition, animated } from 'react-spring';
|
||||
|
||||
import CircleProgressComponent from 'react-circle';
|
||||
|
@ -9,8 +9,6 @@ import { TextComponent } from './text';
|
|||
|
||||
import zcashLogo from '../assets/images/zcash-simple-icon.svg';
|
||||
|
||||
import { appTheme } from '../theme';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
@ -55,6 +53,7 @@ const LoadingText = styled(TextComponent)`
|
|||
|
||||
type Props = {
|
||||
progress: number,
|
||||
theme: AppTheme,
|
||||
message: string,
|
||||
};
|
||||
|
||||
|
@ -64,7 +63,7 @@ type State = {
|
|||
|
||||
const TIME_DELAY_ANIM = 100;
|
||||
|
||||
export class LoadingScreen extends PureComponent<Props, State> {
|
||||
class Component extends PureComponent<Props, State> {
|
||||
state = { start: false };
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -75,7 +74,7 @@ export class LoadingScreen extends PureComponent<Props, State> {
|
|||
|
||||
render() {
|
||||
const { start } = this.state;
|
||||
const { progress, message } = this.props;
|
||||
const { progress, message, theme } = this.props;
|
||||
|
||||
return (
|
||||
<Wrapper data-testid='LoadingScreen'>
|
||||
|
@ -109,8 +108,8 @@ export class LoadingScreen extends PureComponent<Props, State> {
|
|||
progress={progress}
|
||||
responsive
|
||||
showPercentage={false}
|
||||
progressColor={appTheme.colors.activeItem}
|
||||
bgColor={appTheme.colors.inactiveItem}
|
||||
progressColor={theme.colors.activeItem}
|
||||
bgColor={theme.colors.inactiveItem}
|
||||
/>
|
||||
</CircleWrapper>
|
||||
<LoadingText value={message} />
|
||||
|
@ -122,3 +121,5 @@ export class LoadingScreen extends PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const LoadingScreen = withTheme(Component);
|
||||
|
|
|
@ -86,7 +86,9 @@ export const Component = ({
|
|||
}: Props) => (
|
||||
<Wrapper id='sidebar'>
|
||||
{(options || []).map((item) => {
|
||||
const isActive = location.pathname === item.route;
|
||||
const isActive = item.route === '/'
|
||||
? location.pathname === item.route
|
||||
: location.pathname.startsWith(item.route);
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// @flow
|
||||
import React, { type ComponentType, Component } from 'react';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { ipcRenderer, remote } from 'electron';
|
||||
import { type RouterHistory, type Location } from 'react-router-dom';
|
||||
import { searchUriInArgv } from '../../config/handle-deeplink';
|
||||
import electronStore from '../../config/electron-store';
|
||||
|
||||
type PassedProps = {
|
||||
history: RouterHistory,
|
||||
location: Location,
|
||||
isRunning: boolean,
|
||||
};
|
||||
|
||||
const OSX_DEEPLINK_URL_KEY = 'OSX_DEEPLINK_URL';
|
||||
|
||||
export const withDeepLink = (
|
||||
WrappedComponent: ComponentType<PassedProps>,
|
||||
): ComponentType<$Diff<PassedProps, {}>> => class extends Component<PassedProps> {
|
||||
componentDidMount() {
|
||||
const arg = searchUriInArgv([
|
||||
...remote.process.argv,
|
||||
electronStore.get(OSX_DEEPLINK_URL_KEY) || '',
|
||||
]);
|
||||
|
||||
if (arg) this.redirect(arg);
|
||||
|
||||
remote.app.on('open-url', (event, url) => {
|
||||
this.redirect(url);
|
||||
});
|
||||
|
||||
ipcRenderer.on('on-deep-link', (event: Object, message: string) => {
|
||||
this.redirect(message);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ipcRenderer.removeAllListeners('on-deep-link');
|
||||
}
|
||||
|
||||
redirect(message: string) {
|
||||
const { history } = this.props;
|
||||
|
||||
// clean osx deeplink storage
|
||||
if (electronStore.has(OSX_DEEPLINK_URL_KEY)) {
|
||||
electronStore.delete(OSX_DEEPLINK_URL_KEY);
|
||||
}
|
||||
|
||||
history.replace(`/send/${message.replace(/zcash:(\/\/)?/, '')}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} {...this.state} />;
|
||||
}
|
||||
};
|
|
@ -36,7 +36,17 @@ export type SendTransactionInput = {
|
|||
memo: string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ sendStatus, receive }: AppState) => ({
|
||||
export type MapStateToProps = {|
|
||||
balance: number,
|
||||
zecPrice: number,
|
||||
addresses: string[],
|
||||
error: string | null,
|
||||
isSending: boolean,
|
||||
operationId: string | null,
|
||||
isToAddressValid: boolean,
|
||||
|};
|
||||
|
||||
const mapStateToProps = ({ sendStatus, receive }: AppState): MapStateToProps => ({
|
||||
balance: sendStatus.addressBalance,
|
||||
zecPrice: sendStatus.zecPrice,
|
||||
addresses: receive.addresses,
|
||||
|
@ -46,10 +56,19 @@ const mapStateToProps = ({ sendStatus, receive }: AppState) => ({
|
|||
isToAddressValid: sendStatus.isToAddressValid,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
export type MapDispatchToProps = {|
|
||||
sendTransaction: SendTransactionInput => Promise<void>,
|
||||
loadAddresses: () => Promise<void>,
|
||||
resetSendView: () => void,
|
||||
validateAddress: ({ address: string }) => Promise<void>,
|
||||
loadZECPrice: () => void,
|
||||
getAddressBalance: ({ address: string }) => Promise<void>,
|
||||
|};
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): MapDispatchToProps => ({
|
||||
sendTransaction: async ({
|
||||
from, to, amount, fee, memo,
|
||||
}: SendTransactionInput) => {
|
||||
}) => {
|
||||
dispatch(sendTransaction());
|
||||
|
||||
// $FlowFixMe
|
||||
|
@ -142,7 +161,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|||
|
||||
if (zAddressesErr || tAddressesErr) return dispatch(loadAddressesError({ error: 'Something went wrong!' }));
|
||||
|
||||
dispatch(
|
||||
return dispatch(
|
||||
loadAddressesSuccess({
|
||||
addresses: [...zAddresses, ...transparentAddresses],
|
||||
}),
|
||||
|
|
|
@ -4,8 +4,10 @@ import { compose } from 'redux';
|
|||
import { withRouter } from 'react-router-dom';
|
||||
import { RouterComponent } from './router';
|
||||
import { withDaemonStatusCheck } from '../components/with-daemon-status-check';
|
||||
import { withDeepLink } from '../components/with-deeplink';
|
||||
|
||||
export const Router = compose(
|
||||
withRouter,
|
||||
withDaemonStatusCheck,
|
||||
withDeepLink,
|
||||
)(RouterComponent);
|
||||
|
|
|
@ -42,7 +42,7 @@ const ContentWrapper = styled.div`
|
|||
const getTitle = (path: string) => {
|
||||
if (path === '/') return 'Dashboard';
|
||||
|
||||
return path.replace('/', '');
|
||||
return path.split('/')[1];
|
||||
};
|
||||
|
||||
export const RouterComponent = ({
|
||||
|
@ -60,7 +60,7 @@ export const RouterComponent = ({
|
|||
<ScrollTopComponent>
|
||||
<Switch>
|
||||
<Route exact path={DASHBOARD_ROUTE} component={DashboardContainer} />
|
||||
<Route path={SEND_ROUTE} component={SendContainer} />
|
||||
<Route path={`${SEND_ROUTE}/:to?`} component={SendContainer} />
|
||||
<Route path={RECEIVE_ROUTE} component={ReceiveContainer} />
|
||||
<Route path={SETTINGS_ROUTE} component={SettingsContainer} />
|
||||
<Route path={CONSOLE_ROUTE} component={ConsoleView} />
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, { Fragment, PureComponent } from 'react';
|
|||
import styled, { withTheme, keyframes } from 'styled-components';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { Transition, animated } from 'react-spring';
|
||||
import { type Match } from 'react-router-dom';
|
||||
import Tooltip from 'rc-tooltip';
|
||||
|
||||
import { FEES } from '../constants/fees';
|
||||
|
@ -31,7 +32,7 @@ import LoadingIcon from '../assets/images/sync_icon_dark.png';
|
|||
import ArrowUpIconDark from '../assets/images/arrow_up_dark.png';
|
||||
import ArrowUpIconLight from '../assets/images/arrow_up_light.png';
|
||||
|
||||
import type { SendTransactionInput } from '../containers/send';
|
||||
import type { SendTransactionInput, MapDispatchToProps, MapStateToProps } from '../containers/send';
|
||||
import type { State as SendState } from '../redux/modules/send';
|
||||
|
||||
const rotate = keyframes`
|
||||
|
@ -264,7 +265,6 @@ const ValidateWrapper = styled(RowComponent)`
|
|||
margin-top: 3px;
|
||||
`;
|
||||
|
||||
|
||||
const ActionsWrapper = styled(RowComponent)`
|
||||
padding: 30px 0;
|
||||
align-items: center;
|
||||
|
@ -287,19 +287,9 @@ const HexadecimalText = styled(TextComponent)`
|
|||
`;
|
||||
|
||||
type Props = {
|
||||
...SendState,
|
||||
balance: number,
|
||||
zecPrice: number,
|
||||
addresses: string[],
|
||||
sendTransaction: SendTransactionInput => void,
|
||||
loadAddresses: () => void,
|
||||
resetSendView: () => void,
|
||||
validateAddress: ({ address: string }) => void,
|
||||
loadAddresses: () => void,
|
||||
loadZECPrice: () => void,
|
||||
getAddressBalance: ({ address: string }) => void,
|
||||
match: Match,
|
||||
theme: AppTheme,
|
||||
};
|
||||
} & MapStateToProps & MapDispatchToProps;
|
||||
|
||||
type State = {
|
||||
showFee: boolean,
|
||||
|
@ -329,11 +319,24 @@ class Component extends PureComponent<Props, State> {
|
|||
state = initialState;
|
||||
|
||||
componentDidMount() {
|
||||
const { resetSendView, loadAddresses, loadZECPrice } = this.props;
|
||||
const {
|
||||
resetSendView, loadAddresses, loadZECPrice, match,
|
||||
} = this.props;
|
||||
|
||||
resetSendView();
|
||||
loadAddresses();
|
||||
loadZECPrice();
|
||||
|
||||
if (match.params.to) {
|
||||
this.handleChange('to')(match.params.to);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const previousToAddress = prevProps.match.params.to;
|
||||
const toAddress = this.props.match.params.to; // eslint-disable-line
|
||||
|
||||
if (toAddress && previousToAddress !== toAddress) this.handleChange('to')(toAddress);
|
||||
}
|
||||
|
||||
updateTooltipVisibility = ({ balance, amount }: { balance: number, amount: number }) => {
|
||||
|
|
|
@ -5,10 +5,12 @@ set -eu
|
|||
# Create default zcash.conf
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
if [ ! -e "$HOME/Library/Application Support/Zcash/zcash.conf" ] ; then
|
||||
mkdir -p "$HOME/Library/Application Support/Zcash"
|
||||
echo "server=1" > "$HOME/Library/Application Support/Zcash/zcash.conf"
|
||||
fi
|
||||
else
|
||||
if [ ! -e "$HOME/.zcash/zcash.conf" ] ; then
|
||||
mkdir -p "$HOME/.zcash"
|
||||
echo "server=1" > "$HOME/.zcash/zcash.conf"
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -14,6 +14,7 @@ import runDaemon from './daemon/zcashd-child-process';
|
|||
import zcashLog from './daemon/logger';
|
||||
import getZecPrice from '../services/zec-price';
|
||||
import store from './electron-store';
|
||||
import { handleDeeplink } from './handle-deeplink';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
@ -79,10 +80,38 @@ const createWindow = () => {
|
|||
exports.mainWindow = mainWindow;
|
||||
};
|
||||
|
||||
app.setAsDefaultProtocolClient('zcash');
|
||||
|
||||
const instanceLock = app.requestSingleInstanceLock();
|
||||
if (instanceLock) {
|
||||
app.on('second-instance', (event: Object, argv: string[]) => {
|
||||
handleDeeplink({
|
||||
app,
|
||||
mainWindow,
|
||||
argv,
|
||||
listenOpenUrl: false,
|
||||
});
|
||||
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
handleDeeplink({ app, mainWindow });
|
||||
|
||||
/* eslint-disable-next-line consistent-return */
|
||||
app.on('ready', async () => {
|
||||
createWindow();
|
||||
|
||||
console.log('[Process Argv]', process.argv); // eslint-disable-line
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
zcashLog('Not running daemon, please run the mock API');
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import { typeof app as ElectronApp, type electron$BrowserWindow, remote } from 'electron'; // eslint-disable-line
|
||||
import store from './electron-store';
|
||||
|
||||
const sendMessage = (mainWindow, url) => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.webContents.send('on-deep-link', url);
|
||||
} else {
|
||||
mainWindow.on('show', () => mainWindow.webContents.send('on-deep-link', url));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const searchUriInArgv = (argv: string[]): ?string => {
|
||||
const argIndex = argv.findIndex(item => /zcash:(\/\/)?/.test(item));
|
||||
return argv[argIndex];
|
||||
};
|
||||
|
||||
export const handleDeeplink = ({
|
||||
app,
|
||||
mainWindow,
|
||||
argv = process.argv,
|
||||
listenOpenUrl = true,
|
||||
}: {
|
||||
app: ElectronApp,
|
||||
mainWindow: electron$BrowserWindow,
|
||||
argv?: string[],
|
||||
listenOpenUrl?: boolean,
|
||||
}) => {
|
||||
if (listenOpenUrl) {
|
||||
app.on('open-url', (event: Object, url: string) => {
|
||||
event.preventDefault();
|
||||
// Save the url on electron-store, so we can get the value on withDeeplink HOC
|
||||
store.set('OSX_DEEPLINK_URL', url);
|
||||
sendMessage(mainWindow, url);
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
const arg = searchUriInArgv(argv);
|
||||
|
||||
if (arg) {
|
||||
sendMessage(mainWindow, arg);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -118,10 +118,11 @@ type electron$app = {
|
|||
getName(): string,
|
||||
setName(name: string): void,
|
||||
getLocale(): string,
|
||||
makeSingleInstance(callback: (argv: Array<string>, workingDirectory: string) => void): boolean,
|
||||
requestSingleInstanceLock(): boolean,
|
||||
setAsDefaultProtocolClient(schema: string): void,
|
||||
releaseSingleInstance(): void,
|
||||
disableHardwareAcceleration(): void,
|
||||
on(event: string, callback: () => any): void,
|
||||
on(event: string, callback: Function): void,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"file-loader": "^2.0.0",
|
||||
"flow-bin": "^0.92.1",
|
||||
"flow-bin": "^0.93.0",
|
||||
"flow-coverage-report": "^0.6.1",
|
||||
"flow-typed": "^2.5.1",
|
||||
"glow": "^1.2.2",
|
||||
|
@ -179,6 +179,12 @@
|
|||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "./build/icons/win/icon.ico"
|
||||
},
|
||||
"protocols": {
|
||||
"name": "zcash",
|
||||
"schemes": [
|
||||
"zcash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -4,7 +4,5 @@
|
|||
import { globalShortcut, typeof BrowserWindow, typeof app as ElectronApp } from 'electron';
|
||||
|
||||
export const registerDebugShortcut = (app: ElectronApp, mainWindow: BrowserWindow) => globalShortcut.register('CommandOrControl+Option+B', () => {
|
||||
// $FlowFixMe
|
||||
app.dock.show();
|
||||
mainWindow.webContents.openDevTools();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue