Merge pull request #73 from andrerfneves/feature/deep-link

Feature/deep link
This commit is contained in:
George Lima 2019-02-19 09:58:37 -05:00 committed by GitHub
commit 7860f23d5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1621 additions and 3682 deletions

View File

@ -15,23 +15,25 @@ afterEach(() => app.stop());
describe('Send', () => { describe('Send', () => {
test('should load "Send Page"', async () => { test('should load "Send Page"', async () => {
expect(app.client.element('#send-wrapper') expect(app.client.element('#send-wrapper').isVisible()).resolves.toEqual(true);
.isVisible()).resolves.toEqual(true);
}); });
test('should show Additional Options click', async () => { test('should show Additional Options click', async () => {
expect(app.client.element('#send-wrapper #send-fee-wrapper') expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
.isVisible()).resolves.toEqual(false); false,
);
await app.client.element('#send-show-additional-options-button').click(); await app.client.element('#send-show-additional-options-button').click();
expect(app.client.element('#send-wrapper #send-fee-wrapper') expect(app.client.element('#send-wrapper #send-fee-wrapper').isVisible()).resolves.toEqual(
.isVisible()).resolves.toEqual(true); true,
);
}); });
test('should disable send button if required fields are empty', async () => { test('should disable send button if required fields are empty', async () => {
expect(app.client.element('#send-submit-button') expect(app.client.element('#send-submit-button').getAttribute('disabled')).resolves.toEqual(
.getAttribute('disabled')).resolves.toEqual(true); true,
);
}); });
test('should enable send button if required fields are filled', async () => { 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(); await app.client.element('#send-submit-button').click();
expect(app.client.element('#send-confirm-transaction-modal') expect(app.client.element('#send-confirm-transaction-modal').isVisible()).resolves.toEqual(
.isVisible()).resolves.toEqual(true); true,
);
}); });
test('should display a load indicator while the transaction is processed', async () => { 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(app.client.getAttribute('#send-confirm-transaction-modal img', 'src')).resolves.toEqual(
expect.stringContaining('/assets/sync_icon.png'), expect.stringContaining('/assets/sync_icon.png'),
); );
expect(app.client.getText('#send-confirm-transaction-modal p')) expect(app.client.getText('#send-confirm-transaction-modal p')).resolves.toEqual(
.resolves.toEqual('Processing transaction...'); 'Processing transaction...',
expect(app.client.element('#confirm-modal-button') );
.isVisible()).resolves.toEqual(false); expect(app.client.element('#confirm-modal-button').isVisible()).resolves.toEqual(false);
}); });
test('should show an error in invalid transaction', async () => { test('should show an error in invalid transaction', async () => {
expect(app.client.element('#send-error-text') expect(app.client.element('#send-error-text').isVisible()).resolves.toEqual(false);
.isVisible()).resolves.toEqual(false);
await app.client.element('#sidebar a:nth-child(1)').click(); await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').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 () => { test('should show a success screen after transaction and show a transaction item', async () => {
expect(app.client.element('#send-success-wrapper') expect(app.client.element('#send-success-wrapper').isVisible()).resolves.toEqual(false);
.isVisible()).resolves.toEqual(false);
await app.client.element('#sidebar a:nth-child(1)').click(); await app.client.element('#sidebar a:nth-child(1)').click();
await app.client.element('#sidebar a:nth-child(2)').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'); await app.client.waitUntilTextExists('#transaction-item-operation-id-1', 'Send');
expect(await app.client.element('#transaction-item-operation-id-1 img') expect(await app.client.element('#transaction-item-operation-id-1 img').isVisible()).toEqual(
.isVisible()).toEqual(true); true,
);
expect( expect(
await app.client.element('#transaction-item-operation-id-1 img').getAttribute('src'), 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');
}); });
}); });

View File

@ -1,7 +1,7 @@
// @flow // @flow
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import styled from 'styled-components'; import styled, { withTheme } from 'styled-components';
import { Transition, animated } from 'react-spring'; import { Transition, animated } from 'react-spring';
import CircleProgressComponent from 'react-circle'; import CircleProgressComponent from 'react-circle';
@ -9,8 +9,6 @@ import { TextComponent } from './text';
import zcashLogo from '../assets/images/zcash-simple-icon.svg'; import zcashLogo from '../assets/images/zcash-simple-icon.svg';
import { appTheme } from '../theme';
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@ -55,6 +53,7 @@ const LoadingText = styled(TextComponent)`
type Props = { type Props = {
progress: number, progress: number,
theme: AppTheme,
message: string, message: string,
}; };
@ -64,7 +63,7 @@ type State = {
const TIME_DELAY_ANIM = 100; const TIME_DELAY_ANIM = 100;
export class LoadingScreen extends PureComponent<Props, State> { class Component extends PureComponent<Props, State> {
state = { start: false }; state = { start: false };
componentDidMount() { componentDidMount() {
@ -75,7 +74,7 @@ export class LoadingScreen extends PureComponent<Props, State> {
render() { render() {
const { start } = this.state; const { start } = this.state;
const { progress, message } = this.props; const { progress, message, theme } = this.props;
return ( return (
<Wrapper data-testid='LoadingScreen'> <Wrapper data-testid='LoadingScreen'>
@ -109,8 +108,8 @@ export class LoadingScreen extends PureComponent<Props, State> {
progress={progress} progress={progress}
responsive responsive
showPercentage={false} showPercentage={false}
progressColor={appTheme.colors.activeItem} progressColor={theme.colors.activeItem}
bgColor={appTheme.colors.inactiveItem} bgColor={theme.colors.inactiveItem}
/> />
</CircleWrapper> </CircleWrapper>
<LoadingText value={message} /> <LoadingText value={message} />
@ -122,3 +121,5 @@ export class LoadingScreen extends PureComponent<Props, State> {
); );
} }
} }
export const LoadingScreen = withTheme(Component);

View File

@ -86,7 +86,9 @@ export const Component = ({
}: Props) => ( }: Props) => (
<Wrapper id='sidebar'> <Wrapper id='sidebar'>
{(options || []).map((item) => { {(options || []).map((item) => {
const isActive = location.pathname === item.route; const isActive = item.route === '/'
? location.pathname === item.route
: location.pathname.startsWith(item.route);
return ( return (
<StyledLink <StyledLink

View File

@ -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} />;
}
};

View File

@ -36,7 +36,17 @@ export type SendTransactionInput = {
memo: string, 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, balance: sendStatus.addressBalance,
zecPrice: sendStatus.zecPrice, zecPrice: sendStatus.zecPrice,
addresses: receive.addresses, addresses: receive.addresses,
@ -46,10 +56,19 @@ const mapStateToProps = ({ sendStatus, receive }: AppState) => ({
isToAddressValid: sendStatus.isToAddressValid, 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 ({ sendTransaction: async ({
from, to, amount, fee, memo, from, to, amount, fee, memo,
}: SendTransactionInput) => { }) => {
dispatch(sendTransaction()); dispatch(sendTransaction());
// $FlowFixMe // $FlowFixMe
@ -142,7 +161,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
if (zAddressesErr || tAddressesErr) return dispatch(loadAddressesError({ error: 'Something went wrong!' })); if (zAddressesErr || tAddressesErr) return dispatch(loadAddressesError({ error: 'Something went wrong!' }));
dispatch( return dispatch(
loadAddressesSuccess({ loadAddressesSuccess({
addresses: [...zAddresses, ...transparentAddresses], addresses: [...zAddresses, ...transparentAddresses],
}), }),

View File

@ -4,8 +4,10 @@ import { compose } from 'redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { RouterComponent } from './router'; import { RouterComponent } from './router';
import { withDaemonStatusCheck } from '../components/with-daemon-status-check'; import { withDaemonStatusCheck } from '../components/with-daemon-status-check';
import { withDeepLink } from '../components/with-deeplink';
export const Router = compose( export const Router = compose(
withRouter, withRouter,
withDaemonStatusCheck, withDaemonStatusCheck,
withDeepLink,
)(RouterComponent); )(RouterComponent);

View File

@ -42,7 +42,7 @@ const ContentWrapper = styled.div`
const getTitle = (path: string) => { const getTitle = (path: string) => {
if (path === '/') return 'Dashboard'; if (path === '/') return 'Dashboard';
return path.replace('/', ''); return path.split('/')[1];
}; };
export const RouterComponent = ({ export const RouterComponent = ({
@ -60,7 +60,7 @@ export const RouterComponent = ({
<ScrollTopComponent> <ScrollTopComponent>
<Switch> <Switch>
<Route exact path={DASHBOARD_ROUTE} component={DashboardContainer} /> <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={RECEIVE_ROUTE} component={ReceiveContainer} />
<Route path={SETTINGS_ROUTE} component={SettingsContainer} /> <Route path={SETTINGS_ROUTE} component={SettingsContainer} />
<Route path={CONSOLE_ROUTE} component={ConsoleView} /> <Route path={CONSOLE_ROUTE} component={ConsoleView} />

View File

@ -4,6 +4,7 @@ import React, { Fragment, PureComponent } from 'react';
import styled, { withTheme, keyframes } from 'styled-components'; import styled, { withTheme, keyframes } from 'styled-components';
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import { Transition, animated } from 'react-spring'; import { Transition, animated } from 'react-spring';
import { type Match } from 'react-router-dom';
import Tooltip from 'rc-tooltip'; import Tooltip from 'rc-tooltip';
import { FEES } from '../constants/fees'; 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 ArrowUpIconDark from '../assets/images/arrow_up_dark.png';
import ArrowUpIconLight from '../assets/images/arrow_up_light.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'; import type { State as SendState } from '../redux/modules/send';
const rotate = keyframes` const rotate = keyframes`
@ -264,7 +265,6 @@ const ValidateWrapper = styled(RowComponent)`
margin-top: 3px; margin-top: 3px;
`; `;
const ActionsWrapper = styled(RowComponent)` const ActionsWrapper = styled(RowComponent)`
padding: 30px 0; padding: 30px 0;
align-items: center; align-items: center;
@ -287,19 +287,9 @@ const HexadecimalText = styled(TextComponent)`
`; `;
type Props = { type Props = {
...SendState, match: Match,
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,
theme: AppTheme, theme: AppTheme,
}; } & MapStateToProps & MapDispatchToProps;
type State = { type State = {
showFee: boolean, showFee: boolean,
@ -329,11 +319,24 @@ class Component extends PureComponent<Props, State> {
state = initialState; state = initialState;
componentDidMount() { componentDidMount() {
const { resetSendView, loadAddresses, loadZECPrice } = this.props; const {
resetSendView, loadAddresses, loadZECPrice, match,
} = this.props;
resetSendView(); resetSendView();
loadAddresses(); loadAddresses();
loadZECPrice(); 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 }) => { updateTooltipVisibility = ({ balance, amount }: { balance: number, amount: number }) => {

View File

@ -5,10 +5,12 @@ set -eu
# Create default zcash.conf # Create default zcash.conf
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
if [ ! -e "$HOME/Library/Application Support/Zcash/zcash.conf" ] ; 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" echo "server=1" > "$HOME/Library/Application Support/Zcash/zcash.conf"
fi fi
else else
if [ ! -e "$HOME/.zcash/zcash.conf" ] ; then if [ ! -e "$HOME/.zcash/zcash.conf" ] ; then
mkdir -p "$HOME/.zcash"
echo "server=1" > "$HOME/.zcash/zcash.conf" echo "server=1" > "$HOME/.zcash/zcash.conf"
fi fi
fi fi

View File

@ -14,6 +14,7 @@ import runDaemon from './daemon/zcashd-child-process';
import zcashLog from './daemon/logger'; import zcashLog from './daemon/logger';
import getZecPrice from '../services/zec-price'; import getZecPrice from '../services/zec-price';
import store from './electron-store'; import store from './electron-store';
import { handleDeeplink } from './handle-deeplink';
dotenv.config(); dotenv.config();
@ -79,10 +80,38 @@ const createWindow = () => {
exports.mainWindow = mainWindow; 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 */ /* eslint-disable-next-line consistent-return */
app.on('ready', async () => { app.on('ready', async () => {
createWindow(); createWindow();
console.log('[Process Argv]', process.argv); // eslint-disable-line
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
zcashLog('Not running daemon, please run the mock API'); zcashLog('Not running daemon, please run the mock API');
return; return;

47
config/handle-deeplink.js Normal file
View File

@ -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);
}
}
};

View File

@ -118,10 +118,11 @@ type electron$app = {
getName(): string, getName(): string,
setName(name: string): void, setName(name: string): void,
getLocale(): string, getLocale(): string,
makeSingleInstance(callback: (argv: Array<string>, workingDirectory: string) => void): boolean, requestSingleInstanceLock(): boolean,
setAsDefaultProtocolClient(schema: string): void,
releaseSingleInstance(): void, releaseSingleInstance(): void,
disableHardwareAcceleration(): void, disableHardwareAcceleration(): void,
on(event: string, callback: () => any): void, on(event: string, callback: Function): void,
}; };
/** /**

View File

@ -66,7 +66,7 @@
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.12.4", "eslint-plugin-react": "^7.12.4",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
"flow-bin": "^0.92.1", "flow-bin": "^0.93.0",
"flow-coverage-report": "^0.6.1", "flow-coverage-report": "^0.6.1",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"glow": "^1.2.2", "glow": "^1.2.2",
@ -179,6 +179,12 @@
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "./build/icons/win/icon.ico" "icon": "./build/icons/win/icon.ico"
},
"protocols": {
"name": "zcash",
"schemes": [
"zcash"
]
} }
}, },
"jest": { "jest": {

View File

@ -4,7 +4,5 @@
import { globalShortcut, typeof BrowserWindow, typeof app as ElectronApp } from 'electron'; import { globalShortcut, typeof BrowserWindow, typeof app as ElectronApp } from 'electron';
export const registerDebugShortcut = (app: ElectronApp, mainWindow: BrowserWindow) => globalShortcut.register('CommandOrControl+Option+B', () => { export const registerDebugShortcut = (app: ElectronApp, mainWindow: BrowserWindow) => globalShortcut.register('CommandOrControl+Option+B', () => {
// $FlowFixMe
app.dock.show();
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
}); });

5026
yarn.lock

File diff suppressed because it is too large Load Diff