Skip to content

Commit

Permalink
Backup flow - 12 words backup self test (#83)
Browse files Browse the repository at this point in the history
* test paper backup

* test paper backup - remove prod files

* test paper backup - remove prod files

* some styling changes

* code review changes

* Merge branch 'develop' of https://github.com/spacemeshos/smapp into wallet-backup-test-me

# Conflicts:
#	app/routes.js
  • Loading branch information
yhaspel authored and Ilya Vilensky committed Apr 15, 2019
1 parent 62b8f8b commit 3c89cfd
Show file tree
Hide file tree
Showing 12 changed files with 1,157 additions and 1,425 deletions.
1 change: 1 addition & 0 deletions app/components/wallet/index.js
@@ -1,3 +1,4 @@
export { AccountCard, BackupReminder, InitialLeftPane } from './overview';
export { SendCoinsHeader, TxParams, TxFeeSelector, TxTotal, TxConfirmation } from './sendCoins';
export { ReceiveCoins } from './ReceiveCoins';
export { DropContainer, DragItem } from './testMe';
45 changes: 45 additions & 0 deletions app/components/wallet/testMe/DragItem.js
@@ -0,0 +1,45 @@
// @flow
import React from 'react';
import { DragSource } from 'react-dnd';
import styled from 'styled-components';
import { smColors } from '/vars';

// $FlowStyledIssue
const Wrapper = styled.div`
height: 32px;
line-height: 32px;
text-align: center;
width: 140px;
border-radius: 2px;
background-color: ${({ isDropped }) => (isDropped ? smColors.borderGray : smColors.green)};
margin-right: 10px;
color: ${smColors.white};
opacity: ${({ isDragging }) => (isDragging ? 0.4 : 1)};
cursor: pointer;
`;

type Props = {
word: string,
isDropped: boolean,
isDragging: boolean,
connectDragSource: any
};

const DragItem = (props: Props) => {
const { word, isDropped, isDragging, connectDragSource } = props;
return (
<Wrapper ref={connectDragSource} isDragging={isDragging} isDropped={isDropped}>
{isDropped ? '' : word}
</Wrapper>
);
};
export default DragSource(
'item',
{
beginDrag: (props: Props) => ({ word: props.word })
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
})
)(DragItem);
59 changes: 59 additions & 0 deletions app/components/wallet/testMe/DropContainer.js
@@ -0,0 +1,59 @@
// @flow
import React from 'react';
import { DropTarget } from 'react-dnd';
import styled from 'styled-components';
import { smColors } from '/vars';

// $FlowStyledIssue
const Item = styled.div`
height: 32px;
line-height: 32px;
text-align: center;
width: 140px;
border: 1px solid ${smColors.green};
border-radius: 2px;
background-color: ${({ backgroundColor }) => backgroundColor};
color: white;
`;

type Props = {
canDrop: boolean,
isOver: boolean,
connectDropTarget: any,
droppedWord: string
};

const DropContainer = (props: Props) => {
const { canDrop, isOver, droppedWord, connectDropTarget } = props;

const isActive = canDrop && isOver;
let backgroundColor = smColors.white;

if (droppedWord) {
backgroundColor = smColors.green;
} else if (isActive) {
backgroundColor = smColors.green30alpha;
} else if (canDrop) {
backgroundColor = smColors.green10alpha;
}
return (
<Item ref={connectDropTarget} backgroundColor={backgroundColor}>
{droppedWord}
</Item>
);
};

export default DropTarget(
'item',
{
drop: (props, monitor) => {
const item = monitor.getItem();
props.onDrop({ droppedWord: item.word });
}
},
(connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
})
)(DropContainer);
2 changes: 2 additions & 0 deletions app/components/wallet/testMe/index.js
@@ -0,0 +1,2 @@
export { default as DropContainer } from './DropContainer';
export { default as DragItem } from './DragItem';
6 changes: 5 additions & 1 deletion app/redux/auth/actions.js
@@ -1,6 +1,10 @@
// @flow
import { Action } from '/types';
import { localStorageService } from '/infra/storageServices';

export const LOGOUT: string = 'LOGOUT';

export const logout = (): Action => ({ type: LOGOUT });
export const logout = (): Action => {
localStorageService.clear();
return { type: LOGOUT };
};
6 changes: 5 additions & 1 deletion app/routes.js
@@ -1,5 +1,5 @@
// @flow
import { Auth, Main, LocalNode, Wallet, Overview, SendCoins, Backup, TwelveWordsBackup, Transactions, Settings } from '/screens';
import { Auth, Main, LocalNode, Wallet, Overview, SendCoins, Backup, TwelveWordsBackup, Transactions, TestMe, Settings } from '/screens';

const app = [
{
Expand Down Expand Up @@ -47,6 +47,10 @@ const wallet = [
{
path: '/main/wallet/twelve-words-backup',
component: TwelveWordsBackup
},
{
path: '/main/wallet/test-twelve-words-backup',
component: TestMe
}
];

Expand Down
2 changes: 1 addition & 1 deletion app/screens/index.js
@@ -1,6 +1,6 @@
export { Auth } from './auth';
export { Main } from './main';
export { LocalNode } from './localNode';
export { Wallet, Overview, SendCoins, Backup, TwelveWordsBackup } from './wallet';
export { Wallet, Overview, SendCoins, Backup, TwelveWordsBackup, TestMe } from './wallet';
export { Transactions } from './transactions';
export { Settings } from './Settings';
231 changes: 231 additions & 0 deletions app/screens/wallet/TestMe.js
@@ -0,0 +1,231 @@
// @flow
import React, { Component } from 'react';
import styled from 'styled-components';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { connect } from 'react-redux';
import type { RouterHistory } from 'react-router-dom';
import { DropContainer, DragItem } from '/components/wallet';
import { SmButton } from '/basicComponents';
import { smColors } from '/vars';

const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
flex: 1;
flex-direction: column;
flex-direction: flex-start;
padding: 50px;
`;

const Header = styled.span`
font-size: 31px;
font-weight: bold;
line-height: 42px;
color: ${smColors.lighterBlack};
margin-bottom: 20px;
`;

const HeaderExplanation = styled.div`
font-size: 16px;
color: ${smColors.lighterBlack};
line-height: 30px;
margin-bottom: 30px;
`;

const BaseText = styled.span`
font-size: 16px;
font-weight: normal;
line-height: 22px;
color: ${smColors.lighterBlack};
`;

const ActionLink = styled(BaseText)`
user-select: none;
color: ${smColors.darkGreen};
cursor: pointer;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
`;

const Text = styled.span`
font-size: 18px;
color: ${smColors.darkGray};
line-height: 32px;
`;

const IndexWrapper = styled.div`
height: 32px;
line-height: 32px;
width: 28px;
margin-right: 50px;
text-align: right;
`;

const Index = styled(Text)`
color: ${smColors.darkGray50Alpha};
`;

const TwelveWordsContainer = styled.div`
border: 1px solid ${smColors.darkGreen};
padding: 28px;
margin-bottom: 22px;
column-count: 3;
`;

const WordWrapper = styled.div`
height: 50px;
line-height: 50px;
display: flex;
`;

const NotificationSection = styled.div`
display: flex;
flex-direction: column;
height: 25px;
margin-bottom: 22px;
`;

// $FlowStyledIssue
const Notification = styled(BaseText)`
font-weight: bold;
line-height: 30px;
color: ${({ color }) => color};
`;

const WordOptionsContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 42px;
`;

const ButtonsRow = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 30px;
`;

const LeftButtonsContainer = styled.div`
display: flex;
`;

const getTestWords = (mnemonic: string) => {
const twelveWords = mnemonic.split(' ');
const indices = [];
while (indices.length < 4) {
const idx = Math.floor(Math.random() * 12);
if (!indices.includes(idx)) {
indices.push(idx);
}
}
const testWords: string[] = [];
indices.forEach((index: number) => {
testWords.push(twelveWords[index]);
});
return testWords;
};

type Props = {
history: RouterHistory,
mnemonic: string
};

type State = {
testWords: string[],
twelveWords: Array<Object>,
dropsCounter: number,
matchCounter: number
};

class TestMe extends Component<Props, State> {
constructor(props: Props) {
super(props);
const { mnemonic } = props;
this.state = {
testWords: getTestWords(mnemonic),
twelveWords: mnemonic.split(' ').map((word) => ({ word, droppedWord: '' })),
dropsCounter: 0,
matchCounter: 0
};
}

render() {
const { history } = this.props;
const { testWords, twelveWords, dropsCounter, matchCounter } = this.state;
const isTestSuccess = matchCounter === 4 && dropsCounter === 4;
const isTestFailed = matchCounter < 4 && dropsCounter === 4;
return (
<Wrapper>
<DragDropContextProvider backend={HTML5Backend}>
<Header>Create a 12 Words Backup</Header>
<HeaderExplanation>Drag each of the 4 words below to its matching number in your paper backup words list.</HeaderExplanation>
<WordOptionsContainer>
{testWords.map((word: string) => (
<DragItem key={word} word={word} isDropped={this.checkIfWasDropped(word)} />
))}
</WordOptionsContainer>
<TwelveWordsContainer>
{twelveWords.map(({ word, droppedWord }: { word: string, droppedWord: string }, index: number) => (
<WordWrapper key={word}>
<IndexWrapper>
<Index>{`${index + 1}`}</Index>
</IndexWrapper>
<DropContainer droppedWord={droppedWord} onDrop={this.handleDrop({ word, index })} />
</WordWrapper>
))}
</TwelveWordsContainer>
<NotificationSection>
{isTestSuccess && <Notification color={smColors.orange}>All right! Your 12 Words Backup is confirmed</Notification>}
{isTestFailed && <Notification color={smColors.red}>No match. Check your 12 Words Paper Backup and try again.</Notification>}
</NotificationSection>
<ButtonsRow>
<LeftButtonsContainer>
<SmButton text="Try Again" theme="green" onPress={this.tryAgain} style={{ width: 150, marginRight: 20 }} />
<SmButton text="Go Back" theme="green" onPress={history.goBack} style={{ width: 150 }} />
</LeftButtonsContainer>
<SmButton text="Done" isDisabled={!isTestSuccess} theme="orange" onPress={this.navigateToWallet} style={{ width: 150 }} />
</ButtonsRow>
<ActionLink onClick={history.goBack}>Stuck? Go back and write down the numbered words on paper.</ActionLink>
</DragDropContextProvider>
</Wrapper>
);
}

handleDrop = ({ word, index }: { word: string, index: number }) => ({ droppedWord }: { droppedWord: string }) => {
const { twelveWords, dropsCounter, matchCounter } = this.state;
this.setState({
twelveWords: [...twelveWords.slice(0, index), { word, droppedWord }, ...twelveWords.slice(index + 1)],
dropsCounter: dropsCounter + 1,
matchCounter: word === droppedWord ? matchCounter + 1 : matchCounter
});
};

tryAgain = () => {
const { twelveWords } = this.state;
this.setState({ twelveWords: twelveWords.map(({ word }) => ({ word, droppedWord: '' })), dropsCounter: 0, matchCounter: 0 });
};

checkIfWasDropped = (word: string): boolean => {
const { twelveWords } = this.state;
return !!twelveWords.find(({ droppedWord }) => droppedWord === word);
};

navigateToWallet = () => {
const { history } = this.props;
history.push('/main/wallet/overview');
};
}

const mapStateToProps = (state) => ({
mnemonic: state.wallet.mnemonic
});

TestMe = connect(mapStateToProps)(TestMe);
export default TestMe;

0 comments on commit 3c89cfd

Please sign in to comment.