Browse Source

feat(onboarding): implement new Onboarding process

next
Tom Kirkpatrick 6 years ago
parent
commit
e95796fc89
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 23
      app/components/Onboarding/Alias/Alias.js
  2. 17
      app/components/Onboarding/Alias/Alias.scss
  3. 3
      app/components/Onboarding/Alias/index.js
  4. 39
      app/components/Onboarding/Autopilot/Autopilot.js
  5. 53
      app/components/Onboarding/Autopilot/Autopilot.scss
  6. 3
      app/components/Onboarding/Autopilot/index.js
  7. 7
      app/components/Onboarding/Autopilot/messages.js
  8. 63
      app/components/Onboarding/BtcPayServer/BtcPayServer.js
  9. 58
      app/components/Onboarding/BtcPayServer/BtcPayServer.scss
  10. 3
      app/components/Onboarding/BtcPayServer/index.js
  11. 10
      app/components/Onboarding/BtcPayServer/messages.js
  12. 23
      app/components/Onboarding/ConnectionConfirm/ConnectionConfirm.js
  13. 15
      app/components/Onboarding/ConnectionConfirm/ConnectionConfirm.scss
  14. 3
      app/components/Onboarding/ConnectionConfirm/index.js
  15. 7
      app/components/Onboarding/ConnectionConfirm/messages.js
  16. 89
      app/components/Onboarding/ConnectionDetails/ConnectionDetails.js
  17. 57
      app/components/Onboarding/ConnectionDetails/ConnectionDetails.scss
  18. 3
      app/components/Onboarding/ConnectionDetails/index.js
  19. 10
      app/components/Onboarding/ConnectionDetails/messages.js
  20. 57
      app/components/Onboarding/ConnectionType/ConnectionType.js
  21. 61
      app/components/Onboarding/ConnectionType/ConnectionType.scss
  22. 3
      app/components/Onboarding/ConnectionType/index.js
  23. 14
      app/components/Onboarding/ConnectionType/messages.js
  24. 71
      app/components/Onboarding/FormContainer/FormContainer.js
  25. 64
      app/components/Onboarding/FormContainer/FormContainer.scss
  26. 3
      app/components/Onboarding/FormContainer/index.js
  27. 8
      app/components/Onboarding/FormContainer/messages.js
  28. 19
      app/components/Onboarding/InitWallet/InitWallet.js
  29. 0
      app/components/Onboarding/InitWallet/InitWallet.scss
  30. 3
      app/components/Onboarding/InitWallet/index.js
  31. 60
      app/components/Onboarding/Login/Login.js
  32. 44
      app/components/Onboarding/Login/Login.scss
  33. 3
      app/components/Onboarding/Login/index.js
  34. 8
      app/components/Onboarding/Login/messages.js
  35. 69
      app/components/Onboarding/NewWalletPassword/NewWalletPassword.js
  36. 48
      app/components/Onboarding/NewWalletPassword/NewWalletPassword.scss
  37. 3
      app/components/Onboarding/NewWalletPassword/index.js
  38. 10
      app/components/Onboarding/NewWalletPassword/messages.js
  39. 26
      app/components/Onboarding/NewWalletSeed/NewWalletSeed.js
  40. 31
      app/components/Onboarding/NewWalletSeed/NewWalletSeed.scss
  41. 3
      app/components/Onboarding/NewWalletSeed/index.js
  42. 536
      app/components/Onboarding/Onboarding.js
  43. 51
      app/components/Onboarding/ReEnterSeed/ReEnterSeed.js
  44. 59
      app/components/Onboarding/ReEnterSeed/ReEnterSeed.scss
  45. 3
      app/components/Onboarding/ReEnterSeed/index.js
  46. 39
      app/components/Onboarding/RecoverForm/RecoverForm.js
  47. 60
      app/components/Onboarding/RecoverForm/RecoverForm.scss
  48. 3
      app/components/Onboarding/RecoverForm/index.js
  49. 6
      app/components/Onboarding/RecoverForm/messages.js
  50. 36
      app/components/Onboarding/Signup/Signup.js
  51. 51
      app/components/Onboarding/Signup/Signup.scss
  52. 3
      app/components/Onboarding/Signup/index.js
  53. 7
      app/components/Onboarding/Signup/messages.js
  54. 83
      app/components/Onboarding/Steps/Alias.js
  55. 74
      app/components/Onboarding/Steps/Autopilot.js
  56. 126
      app/components/Onboarding/Steps/BtcPayServer.js
  57. 139
      app/components/Onboarding/Steps/ConnectionConfirm.js
  58. 211
      app/components/Onboarding/Steps/ConnectionDetails.js
  59. 112
      app/components/Onboarding/Steps/ConnectionType.js
  60. 112
      app/components/Onboarding/Steps/Login.js
  61. 85
      app/components/Onboarding/Steps/Password.js
  62. 105
      app/components/Onboarding/Steps/Recover.js
  63. 115
      app/components/Onboarding/Steps/SeedConfirm.js
  64. 102
      app/components/Onboarding/Steps/SeedView.js
  65. 69
      app/components/Onboarding/Steps/WalletCreate.js
  66. 69
      app/components/Onboarding/Steps/WalletRecover.js
  67. 13
      app/components/Onboarding/Steps/index.js
  68. 73
      app/components/Onboarding/Steps/messages.js
  69. 4
      app/components/Onboarding/index.js
  70. 33
      app/components/Onboarding/messages.js
  71. 210
      app/containers/Onboarding.js
  72. 2
      app/lib/lnd/walletUnlockerMethods/index.js
  73. 26
      app/lib/zap/controller.js
  74. 6
      app/reducers/ipc.js
  75. 560
      app/reducers/onboarding.js
  76. 1
      package.json
  77. 155
      stories/containers/onboarding.stories.js
  78. 96
      stories/containers/onboarding/components.stories.js
  79. 5
      yarn.lock

23
app/components/Onboarding/Alias/Alias.js

@ -1,23 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Alias.scss'
const Alias = ({ alias, updateAlias }) => (
<div className={styles.container}>
<input
type="text"
placeholder="Satoshi"
className={styles.alias}
ref={input => input && input.focus()}
value={alias}
onChange={event => updateAlias(event.target.value)}
/>
</div>
)
Alias.propTypes = {
alias: PropTypes.string.isRequired,
updateAlias: PropTypes.func.isRequired
}
export default Alias

17
app/components/Onboarding/Alias/Alias.scss

@ -1,17 +0,0 @@
@import 'styles/variables.scss';
.alias {
background: transparent;
outline: none;
border: 1px solid #404040;
border-radius: 4px;
padding: 15px;
color: var(--lightningOrange);
-webkit-text-fill-color: var(--primaryText);
font-size: 22px;
}
.alias::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}

3
app/components/Onboarding/Alias/index.js

@ -1,3 +0,0 @@
import Alias from './Alias'
export default Alias

39
app/components/Onboarding/Autopilot/Autopilot.js

@ -1,39 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import FaCircle from 'react-icons/lib/fa/circle'
import FaCircleThin from 'react-icons/lib/fa/circle-thin'
import { FormattedMessage } from 'react-intl'
import messages from './messages'
import styles from './Autopilot.scss'
const Autopilot = ({ autopilot, setAutopilot }) => (
<div className={styles.container}>
<section className={`${styles.enable} ${autopilot ? styles.active : undefined}`}>
<div onClick={() => setAutopilot(true)}>
{autopilot ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>
<FormattedMessage {...messages.enable} />
</span>
</div>
</section>
<section
className={`${styles.disable} ${
!autopilot && autopilot !== null ? styles.active : undefined
}`}
>
<div onClick={() => setAutopilot(false)}>
{!autopilot && autopilot !== null ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>
<FormattedMessage {...messages.disable} />
</span>
</div>
</section>
</div>
)
Autopilot.propTypes = {
autopilot: PropTypes.bool,
setAutopilot: PropTypes.func.isRequired
}
export default Autopilot

53
app/components/Onboarding/Autopilot/Autopilot.scss

@ -1,53 +0,0 @@
@import 'styles/variables.scss';
.container {
color: var(--primaryText);
section {
margin-bottom: 0;
&.enable {
&.active {
div {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
div:hover {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
&.disable {
&.active {
div {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
div:hover {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
div {
width: 30%;
text-align: center;
display: flex;
padding: 20px;
border: 1px solid var(--primaryText);
border-radius: 5px;
cursor: pointer;
transition: all 0.25s;
margin: 15px 20px 10px 0;
}
.label {
margin-left: 15px;
}
}
}

3
app/components/Onboarding/Autopilot/index.js

@ -1,3 +0,0 @@
import Autopilot from './Autopilot'
export default Autopilot

7
app/components/Onboarding/Autopilot/messages.js

@ -1,7 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
enable: 'Enable Autopilot',
disable: 'Disable Autopilot'
})

63
app/components/Onboarding/BtcPayServer/BtcPayServer.js

@ -1,63 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import messages from './messages'
import styles from './BtcPayServer.scss'
const BtcPayServer = ({
connectionString,
connectionStringIsValid,
setConnectionString,
startLndHostError,
intl
}) => (
<div className={styles.container}>
<section className={styles.input}>
<label htmlFor="connectionString">
<FormattedMessage {...messages.connection_string_label} />:
</label>
<textarea
type="text"
id="connectionString"
rows="10"
placeholder={intl.formatMessage({ ...messages.connection_string_placeholder })}
className={
connectionString && (startLndHostError || !connectionStringIsValid)
? styles.error
: undefined
}
ref={input => input}
value={connectionString}
onChange={event => setConnectionString(event.target.value)}
/>
<p className={styles.description}>
<FormattedMessage {...messages.btcpay_description} />
</p>
<p
className={`${styles.errorMessage} ${
connectionString && !connectionStringIsValid ? styles.visible : undefined
}`}
>
<FormattedMessage {...messages.btcpay_error} />
</p>
<p
className={`${styles.errorMessage} ${
connectionString && startLndHostError ? styles.visible : undefined
}`}
>
{startLndHostError}
</p>
</section>
</div>
)
BtcPayServer.propTypes = {
connectionString: PropTypes.string.isRequired,
connectionStringIsValid: PropTypes.bool.isRequired,
setConnectionString: PropTypes.func.isRequired,
startLndHostError: PropTypes.string
}
export default injectIntl(BtcPayServer)

58
app/components/Onboarding/BtcPayServer/BtcPayServer.scss

@ -1,58 +0,0 @@
@import 'styles/variables.scss';
.container {
color: var(--primaryText);
.input {
margin-bottom: 15px;
}
label {
display: block;
font-size: 12px;
line-height: 14px;
padding-bottom: 5px;
min-height: 14px;
}
textarea {
background: transparent;
outline: none;
border: 1px solid #404040;
border-radius: 4px;
padding: 10px;
color: var(--lightningOrange);
-webkit-text-fill-color: var(--primaryText);
font-size: 14px;
width: 95%;
transition: all 0.25s;
&.error {
border: 1px solid var(--superRed);
}
}
textarea::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.description {
margin-top: 8px;
font-size: 12px;
line-height: 18px;
opacity: 0.5;
}
.errorMessage {
margin-top: 8px;
font-size: 12px;
line-height: 18px;
color: var(--superRed);
display: none;
&.visible {
display: block;
}
}
}

3
app/components/Onboarding/BtcPayServer/index.js

@ -1,3 +0,0 @@
import BtcPayServer from './BtcPayServer'
export default BtcPayServer

10
app/components/Onboarding/BtcPayServer/messages.js

@ -1,10 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
btcpay_description:
"Paste the full content of your BTCPay Server connection config file. This can be found by clicking the link entitled 'Click here to open the configuration file' in your BTCPay Server gRPC settings.",
btcpay_error: 'Invalid connection string.',
connection_string_label: 'Connection String',
connection_string_placeholder: 'BTCPay Server Connection String'
})

23
app/components/Onboarding/ConnectionConfirm/ConnectionConfirm.js

@ -1,23 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import messages from './messages'
import styles from './ConnectionConfirm.scss'
const ConnectionConfirm = ({ connectionHost }) => (
<div className={styles.container}>
<h2>
<FormattedMessage {...messages.verify_host_title} />{' '}
<span className={styles.host}>{connectionHost.split(':')[0]}</span>?{' '}
</h2>
<p>
<FormattedMessage {...messages.verify_host_description} />
</p>
</div>
)
ConnectionConfirm.propTypes = {
connectionHost: PropTypes.string.isRequired
}
export default ConnectionConfirm

15
app/components/Onboarding/ConnectionConfirm/ConnectionConfirm.scss

@ -1,15 +0,0 @@
@import 'styles/variables.scss';
.container {
color: var(--primaryText);
h2 {
font-size: 16px;
margin-bottom: 20px;
font-weight: 300;
}
.host {
color: var(--superGreen);
}
}

3
app/components/Onboarding/ConnectionConfirm/index.js

@ -1,3 +0,0 @@
import ConnectionConfirm from './ConnectionConfirm'
export default ConnectionConfirm

7
app/components/Onboarding/ConnectionConfirm/messages.js

@ -1,7 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
verify_host_title: 'Are you sure you want to connect to',
verify_host_description: 'Please check the hostname carefully.'
})

89
app/components/Onboarding/ConnectionDetails/ConnectionDetails.js

@ -1,89 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import messages from './messages'
import styles from './ConnectionDetails.scss'
const ConnectionDetails = ({
connectionHost,
connectionCert,
connectionMacaroon,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
startLndHostError,
startLndCertError,
startLndMacaroonError
}) => (
<div className={styles.container}>
<section className={styles.input}>
<label htmlFor="connectionHost">
<FormattedMessage {...messages.hostname_title} />:
</label>
<input
type="text"
id="connectionHost"
className={`${styles.host} ${startLndHostError ? styles.error : undefined}`}
ref={input => input}
value={connectionHost}
onChange={event => setConnectionHost(event.target.value)}
/>
<p className={styles.description}>
<FormattedMessage {...messages.hostname_description} />
</p>
<p className={`${startLndHostError ? styles.visible : undefined} ${styles.errorMessage}`}>
{startLndHostError}
</p>
</section>
<section className={styles.input}>
<label htmlFor="connectionCert">
<FormattedMessage {...messages.cert_title} />:
</label>
<input
type="text"
id="connectionCert"
className={`${styles.cert} ${startLndCertError ? styles.error : undefined}`}
ref={input => input}
value={connectionCert}
onChange={event => setConnectionCert(event.target.value)}
/>
<p className={styles.description}>
<FormattedMessage {...messages.cert_description} />
</p>
<p className={`${startLndCertError ? styles.visible : undefined} ${styles.errorMessage}`}>
{startLndCertError}
</p>
</section>
<section className={styles.input}>
<label htmlFor="connectionMacaroon">Macaroon:</label>
<input
type="text"
id="connectionMacaroon"
className={`${styles.macaroon} ${startLndMacaroonError ? styles.error : undefined}`}
ref={input => input}
value={connectionMacaroon}
onChange={event => setConnectionMacaroon(event.target.value)}
/>
<p className={styles.description}>
<FormattedMessage {...messages.macaroon_description} />
</p>
<p className={`${startLndMacaroonError ? styles.visible : undefined} ${styles.errorMessage}`}>
{startLndMacaroonError}
</p>
</section>
</div>
)
ConnectionDetails.propTypes = {
connectionHost: PropTypes.string.isRequired,
connectionCert: PropTypes.string.isRequired,
connectionMacaroon: PropTypes.string.isRequired,
setConnectionHost: PropTypes.func.isRequired,
setConnectionCert: PropTypes.func.isRequired,
setConnectionMacaroon: PropTypes.func.isRequired,
startLndHostError: PropTypes.string,
startLndCertError: PropTypes.string,
startLndMacaroonError: PropTypes.string
}
export default ConnectionDetails

57
app/components/Onboarding/ConnectionDetails/ConnectionDetails.scss

@ -1,57 +0,0 @@
@import 'styles/variables.scss';
.container {
color: var(--primaryText);
.input {
margin-bottom: 10px;
}
label {
display: block;
font-size: 14px;
line-height: 18px;
margin-bottom: 5px;
}
input {
background: transparent;
outline: none;
border: 1px solid var(--primaryText);
border-radius: 4px;
padding: 8px;
color: var(--lightningOrange);
-webkit-text-fill-color: var(--primaryText);
font-size: 18px;
font-weight: 400;
width: 600px;
transition: all 0.25s;
margin-bottom: 5px;
&.error {
border: 1px solid var(--superRed);
}
}
input::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.description {
font-size: 12px;
line-height: 18px;
opacity: 0.5;
}
.errorMessage {
font-size: 12px;
line-height: 18px;
color: var(--superRed);
display: none;
&.visible {
display: block;
}
}
}

3
app/components/Onboarding/ConnectionDetails/index.js

@ -1,3 +0,0 @@
import ConnectionDetails from './ConnectionDetails'
export default ConnectionDetails

10
app/components/Onboarding/ConnectionDetails/messages.js

@ -1,10 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
hostname_title: 'Host',
hostname_description: 'Hostname and port of the Lnd gRPC interface. Example: localhost:10009',
cert_title: 'TLS Certificate',
cert_description: 'Path to the lnd tls cert. Example: /path/to/tls.cert',
macaroon_description: 'Path to the lnd macaroon file. Example: /path/to/admin.macaroon'
})

57
app/components/Onboarding/ConnectionType/ConnectionType.js

@ -1,57 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import FaCircle from 'react-icons/lib/fa/circle'
import FaCircleThin from 'react-icons/lib/fa/circle-thin'
import { FormattedMessage } from 'react-intl'
import messages from './messages'
import styles from './ConnectionType.scss'
const ConnectionType = ({ connectionType, setConnectionType }) => (
<div className={styles.container}>
<section
className={`${styles.option} ${connectionType === 'local' ? styles.active : undefined}`}
>
<div className={`${styles.button}`} onClick={() => setConnectionType('local')}>
{connectionType === 'local' ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>
<FormattedMessage {...messages.default} />{' '}
<span className={styles.superscript}>testnet</span>
</span>
</div>
<div className={`${styles.description}`}>
<FormattedMessage {...messages.default_description} />
<br />
(testnet <FormattedMessage {...messages.only} />)
</div>
</section>
<section
className={`${styles.option} ${connectionType === 'custom' ? styles.active : undefined}`}
>
<div className={`${styles.button}`} onClick={() => setConnectionType('custom')}>
{connectionType === 'custom' ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>
<FormattedMessage {...messages.custom} />
</span>
</div>
<div className={`${styles.description}`}>
<FormattedMessage {...messages.custom_description} />
</div>
</section>
<section className={`${styles.option} ${connectionType === 'btcpayserver' && styles.active}`}>
<div className={`${styles.button}`} onClick={() => setConnectionType('btcpayserver')}>
{connectionType === 'btcpayserver' ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>BTCPay Server</span>
</div>
<div className={`${styles.description}`}>
<FormattedMessage {...messages.btcpay_description} />
</div>
</section>
</div>
)
ConnectionType.propTypes = {
connectionType: PropTypes.string.isRequired,
setConnectionType: PropTypes.func.isRequired
}
export default ConnectionType

61
app/components/Onboarding/ConnectionType/ConnectionType.scss

@ -1,61 +0,0 @@
@import 'styles/variables.scss';
.container {
color: var(--primaryText);
section {
margin: 0;
display: flex;
align-items: center;
line-height: 20px;
font-size: 16px;
.description {
width: 80%;
// opacity: 0.25;
transition: all 0.25s;
}
&:hover .description {
opacity: 0.5;
}
.button {
width: 30%;
text-align: center;
display: flex;
padding: 20px;
border: 1px solid var(--primaryText);
border-radius: 5px;
cursor: pointer;
transition: all 0.25s;
margin: 15px 20px 10px 0;
}
&.active {
.button {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
.description {
opacity: 0.8;
color: var(--lightningOrange);
}
}
.button:hover {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
.label {
margin-left: 15px;
}
.superscript {
vertical-align: super;
font-size: 10px;
}
}
}

3
app/components/Onboarding/ConnectionType/index.js

@ -1,3 +0,0 @@
import ConnectionType from './ConnectionType'
export default ConnectionType

14
app/components/Onboarding/ConnectionType/messages.js

@ -1,14 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
default: 'Default',
default_description:
'By selecting the default mode we will do everything for you. Just click and go!',
only: 'only',
custom: 'Custom',
custom_description:
'Connect to your own node. You will need to provide your own connection settings so this is for advanced users only.',
btcpay_description:
'Connect to your own BTCPay Server instance to access your BTCPay Server wallet.'
})

71
app/components/Onboarding/FormContainer/FormContainer.js

@ -1,71 +0,0 @@
import { shell } from 'electron'
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import FaAngleLeft from 'react-icons/lib/fa/angle-left'
import FaAngleRight from 'react-icons/lib/fa/angle-right'
import ZapLogo from 'components/Icon/ZapLogo'
import Button from 'components/UI/Button'
import messages from './messages'
import styles from './FormContainer.scss'
const FormContainer = ({ title, description, back, next, children, theme }) => (
<div className={`${styles.container} ${theme}`}>
<header className={styles.header}>
<section>
<ZapLogo width="70px" height="32px" />
</section>
<section>
<div
className={styles.help}
onClick={() =>
shell.openExternal('https://ln-zap.github.io/zap-tutorials/zap-desktop-getting-started')
}
>
<FormattedMessage {...messages.help} />
</div>
</section>
</header>
<div className={styles.info}>
<h1>{title}</h1>
<p>{description}</p>
</div>
<div className={styles.content}>{children}</div>
<footer className={styles.footer}>
<div className={styles.buttonsContainer}>
<section>
{back && (
<Button variant="secondary" onClick={back} px={0}>
<FaAngleLeft />
<FormattedMessage {...messages.back} />
</Button>
)}
</section>
<section>
{next && (
<Button onClick={next}>
<FormattedMessage {...messages.next} />
<FaAngleRight />
</Button>
)}
</section>
</div>
</footer>
</div>
)
FormContainer.propTypes = {
title: PropTypes.object.isRequired,
description: PropTypes.object.isRequired,
theme: PropTypes.string.isRequired,
back: PropTypes.func,
next: PropTypes.func,
children: PropTypes.object.isRequired
}
export default FormContainer

64
app/components/Onboarding/FormContainer/FormContainer.scss

@ -1,64 +0,0 @@
@import 'styles/variables.scss';
.container {
position: relative;
height: 100vh;
background: var(--tertiaryColor);
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 40px 40px 20px 40px;
color: var(--primaryText);
.help {
text-decoration: underline;
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
.info {
color: var(--primaryText);
margin-bottom: 20px;
padding: 10px 40px 20px;
height: 16vh;
h1 {
font-size: 22px;
margin-bottom: 10px;
}
p {
font-size: 12px;
line-height: 1.5;
width: 70%;
}
}
.content {
position: relative;
background: var(--primaryColor);
height: 100vh;
padding: 20px 40px;
}
.footer {
position: absolute;
bottom: 0;
padding: 25px 40px;
color: var(--primaryText);
width: 100%;
.buttonsContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}

3
app/components/Onboarding/FormContainer/index.js

@ -1,3 +0,0 @@
import FormContainer from './FormContainer'
export default FormContainer

8
app/components/Onboarding/FormContainer/messages.js

@ -1,8 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
help: 'Need Help?',
next: 'Next',
back: 'back'
})

19
app/components/Onboarding/InitWallet/InitWallet.js

@ -1,19 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Signup from 'components/Onboarding/Signup'
import Login from 'components/Onboarding/Login'
import styles from './InitWallet.scss'
const InitWallet = ({ hasSeed, loginProps, signupProps }) => (
<div className={styles.container}>
{hasSeed ? <Login {...loginProps} /> : <Signup {...signupProps} />}
</div>
)
InitWallet.propTypes = {
hasSeed: PropTypes.bool.isRequired,
loginProps: PropTypes.object.isRequired,
signupProps: PropTypes.object.isRequired
}
export default InitWallet

0
app/components/Onboarding/InitWallet/InitWallet.scss

3
app/components/Onboarding/InitWallet/index.js

@ -1,3 +0,0 @@
import InitWallet from './InitWallet'
export default InitWallet

60
app/components/Onboarding/Login/Login.js

@ -1,60 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import Button from 'components/UI/Button'
import messages from './messages'
import styles from './Login.scss'
const Login = ({
password,
passwordIsValid,
updatePassword,
unlockingWallet,
unlockWallet,
unlockWalletError,
intl
}) => (
<div className={styles.container}>
<input
type="password"
placeholder={intl.formatMessage({ ...messages.password_placeholder })}
className={`${styles.password} ${unlockWalletError.isError ? styles.inputError : undefined}`}
ref={input => input && input.focus()}
value={password}
onChange={event => updatePassword(event.target.value)}
onKeyPress={event => {
if (event.key === 'Enter') {
unlockWallet(password)
}
}}
/>
<p className={`${unlockWalletError.isError ? styles.active : undefined} ${styles.error}`}>
{unlockWalletError.message}
</p>
<section className={styles.buttons}>
<Button
disabled={!passwordIsValid || unlockingWallet}
onClick={() => unlockWallet(password)}
processing={unlockingWallet}
>
{unlockingWallet ? (
<FormattedMessage {...messages.unlocking} />
) : (
<FormattedMessage {...messages.unlock} />
)}
</Button>
</section>
</div>
)
Login.propTypes = {
password: PropTypes.string.isRequired,
passwordIsValid: PropTypes.bool.isRequired,
updatePassword: PropTypes.func.isRequired,
unlockingWallet: PropTypes.bool.isRequired,
unlockWallet: PropTypes.func.isRequired,
unlockWalletError: PropTypes.object.isRequired
}
export default injectIntl(Login)

44
app/components/Onboarding/Login/Login.scss

@ -1,44 +0,0 @@
@import 'styles/variables.scss';
.container {
position: relative;
}
.password {
background: transparent;
outline: none;
border: 1px solid #404040;
border-radius: 4px;
padding: 15px;
color: var(--lightningOrange);
-webkit-text-fill-color: var(--primaryText);
font-size: 22px;
transition: all 0.25s;
&.inputError {
border: 1px solid var(--superRed);
}
}
.password::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.error {
margin-top: 20px;
height: 20px;
color: var(--superRed);
visibility: hidden;
font-size: 12px;
transition: all 0.25s;
&.active {
visibility: visible;
}
}
.buttons {
margin-top: 15%;
text-align: center;
}

3
app/components/Onboarding/Login/index.js

@ -1,3 +0,0 @@
import Login from './Login'
export default Login

8
app/components/Onboarding/Login/messages.js

@ -1,8 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
password_placeholder: 'Password',
unlock: 'Unlock',
unlocking: 'Unlocking'
})

69
app/components/Onboarding/NewWalletPassword/NewWalletPassword.js

@ -1,69 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import messages from './messages'
import styles from './NewWalletPassword.scss'
const NewWalletPassword = ({
createWalletPassword,
createWalletPasswordConfirmation,
showCreateWalletPasswordConfirmationError,
passwordMinCharsError,
updateCreateWalletPassword,
updateCreateWalletPasswordConfirmation,
intl
}) => (
<div className={styles.container}>
<section className={styles.input}>
<input
type="password"
placeholder={intl.formatMessage({ ...messages.password_placeholder })}
className={`${styles.password} ${
showCreateWalletPasswordConfirmationError ? styles.error : undefined
}
${passwordMinCharsError && styles.error}`}
value={createWalletPassword}
onChange={event => updateCreateWalletPassword(event.target.value)}
/>
</section>
<section className={styles.input}>
<input
type="password"
placeholder={intl.formatMessage({ ...messages.password_confirm_placeholder })}
className={`${styles.password} ${
showCreateWalletPasswordConfirmationError ? styles.error : undefined
}
${passwordMinCharsError && styles.error}`}
value={createWalletPasswordConfirmation}
onChange={event => updateCreateWalletPasswordConfirmation(event.target.value)}
/>
<p
className={`${styles.errorMessage} ${
showCreateWalletPasswordConfirmationError ? styles.visible : undefined
}`}
>
<FormattedMessage {...messages.password_error_match} />
</p>
<p className={`${styles.helpMessage} ${passwordMinCharsError ? styles.red : undefined}`}>
<FormattedMessage
{...messages.password_error_length}
values={{
passwordMinLength: '8'
}}
/>
</p>
</section>
</div>
)
NewWalletPassword.propTypes = {
createWalletPassword: PropTypes.string.isRequired,
createWalletPasswordConfirmation: PropTypes.string.isRequired,
showCreateWalletPasswordConfirmationError: PropTypes.bool.isRequired,
passwordMinCharsError: PropTypes.bool.isRequired,
updateCreateWalletPassword: PropTypes.func.isRequired,
updateCreateWalletPasswordConfirmation: PropTypes.func.isRequired
}
export default injectIntl(NewWalletPassword)

48
app/components/Onboarding/NewWalletPassword/NewWalletPassword.scss

@ -1,48 +0,0 @@
@import 'styles/variables.scss';
.input:nth-child(2) {
margin-top: 30px;
}
.password {
background: transparent;
outline: none;
border: 1px solid var(--primaryText);
border-radius: 4px;
padding: 15px;
color: var(--lightningOrange);
-webkit-text-fill-color: var(--primaryText);
font-size: 22px;
transition: all 0.25s;
&.error {
border: 1px solid var(--superRed);
}
}
.password::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.errorMessage {
color: var(--superRed);
margin-top: 10px;
font-size: 10px;
visibility: hidden;
&.visible {
visibility: visible;
}
}
.helpMessage {
color: var(--primaryText);
opacity: 0.87;
padding-top: 10px;
font-size: 14px;
&.red {
color: var(--superRed);
}
}

3
app/components/Onboarding/NewWalletPassword/index.js

@ -1,3 +0,0 @@
import NewWalletPassword from './NewWalletPassword'
export default NewWalletPassword

10
app/components/Onboarding/NewWalletPassword/messages.js

@ -1,10 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
password_placeholder: 'Password',
password_confirm_placeholder: 'Confirm Password',
password_error_match: 'Passwords do not match',
password_error_length: 'Password must be at least {passwordMinLength} characters long',
unlock: 'Unlock'
})

26
app/components/Onboarding/NewWalletSeed/NewWalletSeed.js

@ -1,26 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './NewWalletSeed.scss'
const NewWalletSeed = ({ seed }) => (
<div className={styles.container}>
<ul className={styles.seedContainer}>
{seed.map((word, index) => (
<li key={index}>
<section>
<label htmlFor={word}>{index + 1}</label>
</section>
<section>
<span>{word}</span>
</section>
</li>
))}
</ul>
</div>
)
NewWalletSeed.propTypes = {
seed: PropTypes.array.isRequired
}
export default NewWalletSeed

31
app/components/Onboarding/NewWalletSeed/NewWalletSeed.scss

@ -1,31 +0,0 @@
@import 'styles/variables.scss';
.container {
font-size: 14px;
color: var(--primaryText);
letter-spacing: 1.5px;
li {
display: inline-block;
margin: 5px 0;
width: 25%;
font-family: 'Courier', courier, sans-serif;
section {
display: inline-block;
vertical-align: middle;
color: var(--primaryText);
&:nth-child(1) {
width: 15%;
text-align: center;
opacity: 0.5;
}
&:nth-child(2) {
width: calc(85% - 10px);
margin: 0 5px;
}
}
}
}

3
app/components/Onboarding/NewWalletSeed/index.js

@ -1,3 +0,0 @@
import NewWalletSeed from './NewWalletSeed'
export default NewWalletSeed

536
app/components/Onboarding/Onboarding.js

@ -1,319 +1,297 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Redirect } from 'react-router-dom'
import { FormattedMessage } from 'react-intl'
import { Box } from 'rebass'
import { Flex, Box } from 'rebass'
import { Panel, Wizard } from 'components/UI'
import {
Alias,
Autopilot,
BtcPayServer,
ConnectionType,
ConnectionDetails,
ConnectionConfirm,
Login,
Password,
Recover,
SeedConfirm,
SeedView,
WalletCreate,
WalletRecover
} from './Steps'
import messages from './messages'
import FormContainer from './FormContainer'
import ConnectionType from './ConnectionType'
import ConnectionDetails from './ConnectionDetails'
import ConnectionConfirm from './ConnectionConfirm'
import BtcPayServer from './BtcPayServer'
import Alias from './Alias'
import Autopilot from './Autopilot'
import Login from './Login'
import Signup from './Signup'
import RecoverForm from './RecoverForm'
import NewWalletSeed from './NewWalletSeed'
import ReEnterSeed from './ReEnterSeed'
import NewWalletPassword from './NewWalletPassword'
class Onboarding extends React.Component {
static propTypes = {
// STATE
alias: PropTypes.string,
autopilot: PropTypes.bool,
connectionType: PropTypes.string,
connectionHost: PropTypes.string,
connectionCert: PropTypes.string,
connectionMacaroon: PropTypes.string,
connectionString: PropTypes.string,
lndWalletStarted: PropTypes.bool,
lndWalletUnlockerStarted: PropTypes.bool,
seed: PropTypes.array,
startLndHostError: PropTypes.string,
startLndCertError: PropTypes.string,
startLndMacaroonError: PropTypes.string,
onboarded: PropTypes.bool,
fetchingSeed: PropTypes.bool,
// DISPATCH
createNewWallet: PropTypes.func.isRequired,
generateSeed: PropTypes.func.isRequired,
recoverOldWallet: PropTypes.func.isRequired,
resetOnboarding: PropTypes.func.isRequired,
setAlias: PropTypes.func.isRequired,
setAutopilot: PropTypes.func.isRequired,
setConnectionType: PropTypes.func.isRequired,
setConnectionHost: PropTypes.func.isRequired,
setConnectionCert: PropTypes.func.isRequired,
setConnectionMacaroon: PropTypes.func.isRequired,
setConnectionString: PropTypes.func.isRequired,
setPassword: PropTypes.func.isRequired,
startLnd: PropTypes.func.isRequired,
stopLnd: PropTypes.func.isRequired
}
componentWillUnmount() {
const { resetOnboarding } = this.props
resetOnboarding()
}
const Onboarding = ({
onboarding: {
step,
previousStep,
/**
* Dynamically generte form steps to use in the onboarding Wizzard.
* @return {[Wizzard.Step]} A list of WizardSteps.
*/
getSteps = () => {
const {
// STATE
alias,
autopilot,
connectionType,
connectionString,
connectionHost,
connectionCert,
connectionMacaroon,
alias,
autopilot,
createWalletPassword,
connectionString,
lndWalletStarted,
lndWalletUnlockerStarted,
seed,
onboarded
},
theme,
connectionTypeProps,
connectionDetailProps,
connectionConfirmProps,
changeStep,
startLndHostError,
startLndCertError,
startLndMacaroonError,
unlockWalletError,
fetchingSeed,
// DISPATCH
setAlias,
setAutopilot,
setConnectionType,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
setConnectionString,
setUnlockWalletError,
setPassword,
startLnd,
submitNewWallet,
validateHost,
validateCert,
validateMacaroon,
generateSeed,
resetOnboarding,
createNewWallet,
recoverOldWallet,
aliasProps,
initWalletProps,
autopilotProps,
recoverFormProps,
newWalletSeedProps,
newWalletPasswordProps,
reEnterSeedProps
}) => {
const renderStep = () => {
switch (step) {
case 0.1:
return (
<FormContainer
title={<FormattedMessage {...messages.connection_title} />}
description={<FormattedMessage {...messages.connection_description} />}
theme={theme}
back={null}
next={() => {
stopLnd,
unlockWallet
} = this.props
let formSteps = []
switch (connectionType) {
case 'custom':
changeStep(0.2)
break
case 'btcpayserver':
changeStep(0.3)
/**
* Form steps for create flow.
*/
case 'create':
formSteps = [
...formSteps,
<Wizard.Step
key="SeedView"
component={SeedView}
{...{ seed, generateSeed, fetchingSeed }}
/>,
<Wizard.Step key="SeedConfirm" component={SeedConfirm} {...{ seed }} />,
<Wizard.Step key="Password" component={Password} {...{ setPassword }} />,
<Wizard.Step key="Alias" component={Alias} {...{ alias, setAlias }} />,
<Wizard.Step key="Autopilot" component={Autopilot} {...{ autopilot, setAutopilot }} />,
<Wizard.Step key="WalletCreate" component={WalletCreate} {...{ createNewWallet }} />
]
break
default:
changeStep(1)
}
}}
>
<ConnectionType {...connectionTypeProps} />
</FormContainer>
)
case 0.2:
return (
<FormContainer
title={<FormattedMessage {...messages.connection_details_custom_title} />}
description={<FormattedMessage {...messages.connection_details_custom_description} />}
theme={theme}
back={() => changeStep(0.1)}
next={() => {
// dont allow the user to move on if we don't at least have a hostname.
if (!connectionDetailProps.connectionHostIsValid) {
return
}
/**
* Form steps for import flow.
*/
case 'import':
formSteps = [
...formSteps,
<Wizard.Step key="Recover" component={Recover} {...{ seed }} />,
<Wizard.Step key="Password" component={Password} {...{ setPassword }} />,
<Wizard.Step key="Alias" component={Alias} {...{ alias, setAlias }} />,
<Wizard.Step key="Autopilot" component={Autopilot} {...{ autopilot, setAutopilot }} />,
<Wizard.Step key="WalletRecover" component={WalletRecover} {...{ recoverOldWallet }} />
]
break
changeStep(0.4)
/**
* Form steps for custom connection flow.
*/
case 'custom':
formSteps = [
<Wizard.Step
key="ConnectionDetails"
component={ConnectionDetails}
{...{
connectionHost,
connectionCert,
connectionMacaroon,
startLndHostError,
startLndCertError,
startLndMacaroonError,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
validateHost,
validateCert,
validateMacaroon
}}
>
<ConnectionDetails {...connectionDetailProps} />
</FormContainer>
)
case 0.3:
return (
<FormContainer
title={<FormattedMessage {...messages.btcpay_title} />}
description={<FormattedMessage {...messages.btcpay_description} />}
theme={theme}
back={() => changeStep(0.1)}
next={() => {
// dont allow the user to move on if the connection string is invalid.
if (!connectionDetailProps.connectionStringIsValid) {
return
}
changeStep(0.4)
/>,
<Wizard.Step
key="ConnectionConfirm"
component={ConnectionConfirm}
{...{
connectionType,
connectionHost,
connectionCert,
connectionMacaroon,
lndWalletStarted,
lndWalletUnlockerStarted,
startLndHostError,
startLndCertError,
startLndMacaroonError,
startLnd
}}
>
<BtcPayServer {...connectionDetailProps} />
</FormContainer>
)
/>,
<Wizard.Step
key="Login"
component={Login}
{...{ unlockWallet, setUnlockWalletError, unlockWalletError }}
/>
]
break
case 0.4:
return (
<FormContainer
title={<FormattedMessage {...messages.confirm_connection_title} />}
description={<FormattedMessage {...messages.confirm_connection_description} />}
theme={theme}
back={() => changeStep(previousStep)}
next={() => {
startLnd({
type: connectionType,
string: connectionString,
host: connectionHost,
cert: connectionCert,
macaroon: connectionMacaroon
})
/**
* Form steps for BTCPay Server connection flow.
*/
case 'btcpayserver':
formSteps = [
<Wizard.Step
key="BtcPayServer"
component={BtcPayServer}
{...{
connectionString,
startLndHostError,
setConnectionString
}}
>
<ConnectionConfirm {...connectionConfirmProps} />
</FormContainer>
)
case 1:
return (
<FormContainer
title={<FormattedMessage {...messages.alias_title} />}
description={<FormattedMessage {...messages.alias_description} />}
theme={theme}
back={() => changeStep(0.1)}
next={() => changeStep(2)}
>
<Alias {...aliasProps} />
</FormContainer>
)
case 2:
return (
<FormContainer
title={<FormattedMessage {...messages.autopilot_title} />}
description={<FormattedMessage {...messages.autopilot_description} />}
theme={theme}
back={() => changeStep(1)}
next={() => startLnd({ type: connectionType, alias, autopilot })}
>
<Autopilot {...autopilotProps} />
</FormContainer>
)
case 3:
return (
<FormContainer
title={<FormattedMessage {...messages.login_title} />}
description={
<FormattedMessage
{...messages.login_description}
values={{
walletDir:
initWalletProps.loginProps.existingWalletDir && connectionType === 'local'
? initWalletProps.loginProps.existingWalletDir
: connectionHost.split(':')[0]
/>,
<Wizard.Step
key="ConnectionConfirm"
component={ConnectionConfirm}
{...{
connectionType,
connectionString,
lndWalletStarted,
lndWalletUnlockerStarted,
startLndHostError,
startLndCertError,
startLndMacaroonError,
startLnd
}}
/>,
<Wizard.Step
key="Login"
component={Login}
{...{ unlockWallet, setUnlockWalletError, unlockWalletError }}
/>
}
theme={theme}
back={null}
next={null}
>
<Login {...initWalletProps.loginProps} />
</FormContainer>
)
case 4:
return (
<FormContainer
title={<FormattedMessage {...messages.create_wallet_password_title} />}
description={<FormattedMessage {...messages.create_wallet_password_description} />}
theme={theme}
back={null}
next={() => {
// dont allow the user to move on if the confirmation password doesnt match the original password
// if the password is less than 8 characters or empty dont allow users to proceed
if (
newWalletPasswordProps.passwordMinCharsError ||
!newWalletPasswordProps.createWalletPassword ||
!newWalletPasswordProps.createWalletPasswordConfirmation ||
newWalletPasswordProps.showCreateWalletPasswordConfirmationError
) {
return
]
break
}
changeStep(5)
}}
>
<NewWalletPassword {...newWalletPasswordProps} />
</FormContainer>
)
case 5:
return (
<FormContainer
title={<FormattedMessage {...messages.signup_title} />}
description={<FormattedMessage {...messages.signup_description} />}
theme={theme}
back={() => changeStep(4)}
next={() => {
// require the user to select create wallet or import wallet
if (
!initWalletProps.signupProps.signupForm.create &&
!initWalletProps.signupProps.signupForm.import
) {
return
const steps = [
<Wizard.Step
key="ConnectionType"
component={ConnectionType}
{...{ connectionType, setConnectionType, resetOnboarding, stopLnd }}
/>,
...formSteps
]
return steps
}
changeStep(initWalletProps.signupProps.signupForm.create ? 6 : 5.1)
}}
>
<Signup {...initWalletProps.signupProps} />
</FormContainer>
/**
* If we have already started the create new wallet process and generated a seed, change the text on the back button
* since it will act as a reset button in this case.
*/
getBackButtonText = () => {
const { seed } = this.props
return seed.length > 0 ? (
<FormattedMessage {...messages.start_over} />
) : (
<FormattedMessage {...messages.previous} />
)
case 5.1:
return (
<FormContainer
title={<FormattedMessage {...messages.import_title} />}
description={<FormattedMessage {...messages.import_description} />}
theme={theme}
back={() => changeStep(5)}
next={() => {
const recoverySeed = recoverFormProps.recoverSeedInput.map(input => input.word)
recoverOldWallet(createWalletPassword, recoverySeed)
}}
>
<RecoverForm {...recoverFormProps} />
</FormContainer>
)
case 6:
return (
<FormContainer
title={<FormattedMessage {...messages.save_seed_title} />}
description={<FormattedMessage {...messages.save_seed_description} />}
theme={theme}
back={() => changeStep(5)}
next={() => changeStep(7)}
>
<NewWalletSeed {...newWalletSeedProps} />
</FormContainer>
)
case 7:
return (
<FormContainer
title={<FormattedMessage {...messages.retype_seed_title} />}
description={
<FormattedMessage
{...messages.retype_seed_description}
values={{
word1: reEnterSeedProps.seedIndexesArr[0],
word2: reEnterSeedProps.seedIndexesArr[1],
word3: reEnterSeedProps.seedIndexesArr[2]
}}
/>
}
theme={theme}
back={() => changeStep(6)}
next={() => {
// don't allow them to move on if they havent re-entered the seed correctly
if (!reEnterSeedProps.reEnterSeedChecker) {
return
}
submitNewWallet(createWalletPassword, seed)
}}
>
<ReEnterSeed {...reEnterSeedProps} />
</FormContainer>
)
/**
* If we have already started the create new wallet process and generated a seed, configure the back button to
* navigate back to step 1.
*/
getPreviousStep = () => {
const { seed } = this.props
return seed.length > 0 ? 0 : null
}
render() {
const { connectionType, onboarded } = this.props
const steps = this.getSteps()
const previousStep = this.getPreviousStep()
const backButtonText = this.getBackButtonText()
if (onboarded) {
return <Redirect to={['create', 'import'].includes(connectionType) ? '/syncing' : '/app'} />
}
return (
<Box width={1}>
{renderStep()}
{onboarded && <Redirect to={connectionType === 'local' ? '/syncing' : '/app'} />}
<Wizard steps={steps}>
<Flex css={{ height: '100%' }}>
<Panel width={1}>
<Panel.Body width={9 / 16} mx="auto">
<Wizard.Steps />
</Panel.Body>
<Panel.Footer>
<Flex justifyContent="space-between">
<Box>
<Wizard.BackButton navigateTo={previousStep}>{backButtonText}</Wizard.BackButton>
</Box>
<Box ml="auto">
<Wizard.NextButton>
<FormattedMessage {...messages.next} />
</Wizard.NextButton>
</Box>
</Flex>
</Panel.Footer>
</Panel>
</Flex>
</Wizard>
)
}
Onboarding.propTypes = {
onboarding: PropTypes.object.isRequired,
connectionTypeProps: PropTypes.object.isRequired,
connectionDetailProps: PropTypes.object.isRequired,
connectionConfirmProps: PropTypes.object.isRequired,
aliasProps: PropTypes.object.isRequired,
autopilotProps: PropTypes.object.isRequired,
initWalletProps: PropTypes.object.isRequired,
newWalletSeedProps: PropTypes.object.isRequired,
newWalletPasswordProps: PropTypes.object.isRequired,
recoverFormProps: PropTypes.object.isRequired,
reEnterSeedProps: PropTypes.object.isRequired,
changeStep: PropTypes.func.isRequired,
startLnd: PropTypes.func.isRequired,
submitNewWallet: PropTypes.func.isRequired,
recoverOldWallet: PropTypes.func.isRequired
}
}
export default Onboarding

51
app/components/Onboarding/ReEnterSeed/ReEnterSeed.js

@ -1,51 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './ReEnterSeed.scss'
class ReEnterSeed extends React.Component {
componentWillMount() {
const { setReEnterSeedIndexes } = this.props
setReEnterSeedIndexes()
}
render() {
const { seed, reEnterSeedInput, updateReEnterSeedInput, seedIndexesArr } = this.props
return (
<div className={styles.container}>
<ul className={styles.seedContainer}>
{seedIndexesArr.map(index => (
<li key={index}>
<section>
<label htmlFor={index}>{index}</label>
</section>
<section>
<input
type="text"
id={index}
value={reEnterSeedInput[index] ? reEnterSeedInput[index] : ''}
onChange={event => updateReEnterSeedInput({ word: event.target.value, index })}
className={`${styles.word} ${
reEnterSeedInput[index] && seed[index - 1] === reEnterSeedInput[index]
? styles.valid
: styles.invalid
}`}
/>
</section>
</li>
))}
</ul>
</div>
)
}
}
ReEnterSeed.propTypes = {
seed: PropTypes.array.isRequired,
reEnterSeedInput: PropTypes.object.isRequired,
updateReEnterSeedInput: PropTypes.func.isRequired,
setReEnterSeedIndexes: PropTypes.func.isRequired,
seedIndexesArr: PropTypes.array.isRequired
}
export default ReEnterSeed

59
app/components/Onboarding/ReEnterSeed/ReEnterSeed.scss

@ -1,59 +0,0 @@
@import 'styles/variables.scss';
.seedContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 12px;
margin-top: 10%;
li {
display: inline-block;
margin: 10px;
width: 25%;
border: 0.2px solid var(--primaryText);
padding: 5px 10px;
section {
display: inline-block;
color: var(--primaryText);
margin: 0;
&:nth-child(1) {
text-align: center;
opacity: 0.5;
}
}
}
}
.word {
margin: 0 3px;
background-color: inherit;
outline: 0;
border: none;
padding: 8px 10px 6px 10px;
color: var(--primaryText);
font-family: 'Courier', courier, sans-serif;
font-size: 14px;
line-height: 18px;
&.valid {
color: var(--superGreen);
}
&.invalid {
color: var(--superRed);
}
}
.word::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.contentEditable {
width: 100px;
background: red;
}

3
app/components/Onboarding/ReEnterSeed/index.js

@ -1,3 +0,0 @@
import ReEnterSeed from './ReEnterSeed'
export default ReEnterSeed

39
app/components/Onboarding/RecoverForm/RecoverForm.js

@ -1,39 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { injectIntl } from 'react-intl'
import messages from './messages'
import styles from './RecoverForm.scss'
const RecoverForm = ({ recoverSeedInput, updateRecoverSeedInput, intl }) => (
<div className={styles.container}>
<ul className={styles.seedContainer}>
{Array(24)
.fill('')
.map((word, index) => (
<li key={index}>
<section>
<label htmlFor={index}>{index + 1}</label>
</section>
<section>
<input
type="text"
id={index}
placeholder={intl.formatMessage({ ...messages.word_placeholder })}
value={recoverSeedInput[index] ? recoverSeedInput[index].word : ''}
onChange={event => updateRecoverSeedInput({ word: event.target.value, index })}
className={styles.word}
/>
</section>
</li>
))}
</ul>
</div>
)
RecoverForm.propTypes = {
recoverSeedInput: PropTypes.array.isRequired,
updateRecoverSeedInput: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
}
export default injectIntl(RecoverForm)

60
app/components/Onboarding/RecoverForm/RecoverForm.scss

@ -1,60 +0,0 @@
@import 'styles/variables.scss';
.seedContainer {
position: relative;
display: inline-block;
font-size: 12px;
li {
display: inline-block;
margin: 5px 0;
width: 25%;
section {
display: inline-block;
color: var(--primaryText);
margin: 0;
&:nth-child(1) {
width: 10%;
text-align: center;
opacity: 0.5;
}
&:nth-child(2) {
width: calc(90% - 10px);
margin-right: 10px;
}
}
}
}
.word {
margin: 0 3px;
background-color: var(--primaryText);
outline: 0;
border: none;
padding: 8px 10px 6px 10px;
color: var(--primaryText);
font-family: 'Courier', courier, sans-serif;
font-size: 14px;
line-height: 18px;
&.valid {
color: var(--superGreen);
}
&.invalid {
color: var(--superRed);
}
}
.word::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.contentEditable {
width: 100px;
background: red;
}

3
app/components/Onboarding/RecoverForm/index.js

@ -1,3 +0,0 @@
import RecoverForm from './RecoverForm'
export default RecoverForm

6
app/components/Onboarding/RecoverForm/messages.js

@ -1,6 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
word_placeholder: 'word'
})

36
app/components/Onboarding/Signup/Signup.js

@ -1,36 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import FaCircle from 'react-icons/lib/fa/circle'
import FaCircleThin from 'react-icons/lib/fa/circle-thin'
import { FormattedMessage } from 'react-intl'
import messages from './messages'
import styles from './Signup.scss'
const Signup = ({ signupForm, setSignupCreate, setSignupImport }) => (
<div className={styles.container}>
<section className={`${styles.enable} ${signupForm.create ? styles.active : undefined}`}>
<div onClick={setSignupCreate}>
{signupForm.create ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>
<FormattedMessage {...messages.signup_create} />
</span>
</div>
</section>
<section className={`${styles.disable} ${signupForm.import ? styles.active : undefined}`}>
<div onClick={setSignupImport}>
{signupForm.import ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>
<FormattedMessage {...messages.signup_import} />
</span>
</div>
</section>
</div>
)
Signup.propTypes = {
signupForm: PropTypes.object.isRequired,
setSignupCreate: PropTypes.func.isRequired,
setSignupImport: PropTypes.func.isRequired
}
export default Signup

51
app/components/Onboarding/Signup/Signup.scss

@ -1,51 +0,0 @@
@import 'styles/variables.scss';
.container {
color: var(--primaryText);
section {
margin-bottom: 20px;
&.enable {
&.active {
div {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
div:hover {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
&.disable {
&.active {
div {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
div:hover {
color: var(--lightningOrange);
border-color: var(--lightningOrange);
}
}
div {
width: 30%;
display: inline-block;
padding: 20px;
border: 1px solid var(--primaryText);
border-radius: 5px;
cursor: pointer;
transition: all 0.25s;
}
.label {
margin-left: 15px;
}
}
}

3
app/components/Onboarding/Signup/index.js

@ -1,3 +0,0 @@
import Signup from './Signup'
export default Signup

7
app/components/Onboarding/Signup/messages.js

@ -1,7 +0,0 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
signup_create: 'Create new wallet',
signup_import: 'Import existing wallet'
})

83
app/components/Onboarding/Steps/Alias.js

@ -0,0 +1,83 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import { Box } from 'rebass'
import { Bar, Form, Header, Input } from 'components/UI'
import messages from './messages'
class Alias extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
alias: PropTypes.string,
setAlias: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {},
alias: ''
}
setFormApi = formApi => {
this.formApi = formApi
}
handleSubmit = values => {
const { setAlias } = this.props
setAlias(values.alias)
}
render() {
const { wizardApi, wizardState, alias, setAlias, intl, ...rest } = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
const shouldValidateInline = formState.submits > 0
return (
<>
<Header
title={<FormattedMessage {...messages.alias_title} />}
subtitle={<FormattedMessage {...messages.alias_description} />}
align="left"
/>
<Bar my={4} />
<Box>
<Input
field="alias"
name="alias"
label={<FormattedMessage {...messages.nickname} />}
initialValue={alias}
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
/>
</Box>
</>
)
}}
</Form>
)
}
}
export default injectIntl(Alias)

74
app/components/Onboarding/Steps/Autopilot.js

@ -0,0 +1,74 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import { Bar, Form, Header, RadioGroup, Radio } from 'components/UI'
import messages from './messages'
class Autopilot extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
autopilot: PropTypes.bool,
setAutopilot: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {},
autopilot: 'enable'
}
handleSubmit = values => {
const { setAutopilot } = this.props
setAutopilot(values.autopilot === 'enable' ? true : false)
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const { wizardApi, wizardState, autopilot, setAutopilot, ...rest } = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
<Header
title={<FormattedMessage {...messages.autopilot_title} />}
subtitle={<FormattedMessage {...messages.autopilot_description} />}
align="left"
/>
<Bar my={4} />
<RadioGroup
required
field="autopilot"
name="autopilot"
initialValue={autopilot ? 'enable' : 'disable'}
>
<Radio value="enable" label={<FormattedMessage {...messages.enable} />} />
<Radio value="disable" label={<FormattedMessage {...messages.disable} />} />
</RadioGroup>
</Form>
)
}
}
export default Autopilot

126
app/components/Onboarding/Steps/BtcPayServer.js

@ -0,0 +1,126 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import get from 'lodash.get'
import { Bar, Form, Header, TextArea } from 'components/UI'
import messages from './messages'
class BtcPayServer extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
connectionString: PropTypes.string.isRequired,
setConnectionString: PropTypes.func.isRequired,
startLndHostError: PropTypes.string
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
componentDidUpdate(prevProps) {
const { startLndHostError } = this.props
if (startLndHostError && startLndHostError !== prevProps.startLndHostError) {
this.formApi.setError('connectionString', startLndHostError)
}
}
handleSubmit = values => {
const { setConnectionString } = this.props
setConnectionString(values.connectionString)
}
handleConnectionStringChange = () => {
this.formApi.setError('connectionString', null)
}
validateConnectionString = value => {
const { intl } = this.props
let config = {}
try {
config = JSON.parse(value)
} catch (e) {
return intl.formatMessage({ ...messages.btcpay_error })
}
const configs = get(config, 'configurations', [])
const params = configs.find(c => c.type === 'grpc' && c.cryptoCode === 'BTC') || {}
const { host, port, macaroon } = params
if (!host || !port || !macaroon) {
return intl.formatMessage({ ...messages.btcpay_error })
}
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const {
wizardApi,
wizardState,
intl,
connectionString,
setConnectionString,
startLndHostError,
...rest
} = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
const shouldValidateInline = formState.submits > 0
return (
<>
<Header
title={<FormattedMessage {...messages.btcpay_title} />}
subtitle={<FormattedMessage {...messages.btcpay_description} />}
align="left"
/>
<Bar my={4} />
<TextArea
field="connectionString"
name="connectionString"
label={<FormattedMessage {...messages.connection_string_label} />}
description={
<FormattedMessage {...messages.btcpay_connection_string_description} />
}
placeholder={intl.formatMessage({ ...messages.connection_string_placeholder })}
initialValue={connectionString}
onValueChange={this.handleConnectionStringChange}
validate={this.validateConnectionString}
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
required
rows="10"
/>
</>
)
}}
</Form>
)
}
}
export default injectIntl(BtcPayServer)

139
app/components/Onboarding/Steps/ConnectionConfirm.js

@ -0,0 +1,139 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import get from 'lodash.get'
import { Bar, Form, Header, Span, Text } from 'components/UI'
import messages from './messages'
const parseConnectionString = value => {
let config = {}
try {
config = JSON.parse(value)
} catch (e) {
return new Error('Invalid connection string')
}
const configs = get(config, 'configurations', [])
const params = configs.find(c => c.type === 'grpc' && c.cryptoCode === 'BTC') || {}
const { host, port, macaroon } = params
if (!host || !port || !macaroon) {
return new Error('Invalid connection string')
}
return { host, port, macaroon }
}
class ConnectionConfirm extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
connectionType: PropTypes.string.isRequired,
connectionHost: PropTypes.string,
connectionCert: PropTypes.string,
connectionMacaroon: PropTypes.string,
connectionString: PropTypes.string,
startLndHostError: PropTypes.string,
startLndCertError: PropTypes.string,
startLndMacaroonError: PropTypes.string,
startLnd: PropTypes.func.isRequired,
lndWalletUnlockerStarted: PropTypes.bool,
lndWalletStarted: PropTypes.bool
}
static defaultProps = {
wizardApi: {},
wizardState: {},
connectionHost: '',
connectionCert: '',
connectionMacaroon: '',
connectionString: '',
startLndHostError: '',
startLndCertError: '',
startLndMacaroonError: ''
}
handleSubmit = async () => {
let {
connectionType,
connectionHost,
connectionCert,
connectionMacaroon,
connectionString,
startLnd
} = this.props
let options = {
type: connectionType,
host: connectionHost,
cert: connectionCert,
macaroon: connectionMacaroon
}
if (connectionString) {
const { host, port, macaroon } = parseConnectionString(connectionString)
options = { type: connectionType, host: `${host}:${port}`, macaroon }
}
return startLnd(options)
}
render() {
const {
wizardApi,
wizardState,
connectionType,
connectionHost,
connectionCert,
connectionMacaroon,
connectionString,
lndWalletStarted,
lndWalletUnlockerStarted,
startLndHostError,
startLndCertError,
startLndMacaroonError,
startLnd,
...rest
} = this.props
const { getApi, preSubmit, onSubmit, onSubmitFailure } = wizardApi
// Determine the hostname.
let hostname = connectionHost.split(':')[0]
if (connectionString) {
const { host } = parseConnectionString(connectionString)
hostname = host
}
return (
<Form
{...rest}
getApi={getApi}
preSubmit={preSubmit}
onSubmit={async values => {
try {
await this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
} catch (e) {
wizardApi.onSubmitFailure()
wizardApi.previous()
}
}}
onSubmitFailure={onSubmitFailure}
>
<Header
title={<FormattedMessage {...messages.confirm_connection_title} />}
subtitle={<FormattedMessage {...messages.confirm_connection_description} />}
align="left"
/>
<Bar my={4} />
<Text>
<FormattedMessage {...messages.verify_host_title} />{' '}
<Span color="superGreen">{hostname}</Span>?{' '}
</Text>
<Text mt={2}>
<FormattedMessage {...messages.verify_host_description} />
</Text>
</Form>
)
}
}
export default ConnectionConfirm

211
app/components/Onboarding/Steps/ConnectionDetails.js

@ -0,0 +1,211 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import { Box } from 'rebass'
import { Bar, Form, Header, Input } from 'components/UI'
import messages from './messages'
class ConnectionDetails extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
connectionHost: PropTypes.string,
connectionCert: PropTypes.string,
connectionMacaroon: PropTypes.string,
startLndHostError: PropTypes.string,
startLndCertError: PropTypes.string,
startLndMacaroonError: PropTypes.string,
setConnectionHost: PropTypes.func.isRequired,
setConnectionCert: PropTypes.func.isRequired,
setConnectionMacaroon: PropTypes.func.isRequired,
validateHost: PropTypes.func.isRequired,
validateCert: PropTypes.func.isRequired,
validateMacaroon: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {},
connectionHost: '',
connectionCert: '',
connectionMacaroon: '',
startLndHostError: '',
startLndCertError: '',
startLndMacaroonError: ''
}
componentDidMount() {
const { startLndHostError, startLndCertError, startLndMacaroonError } = this.props
if (startLndHostError) {
this.formApi.setError('connectionHost', startLndHostError)
}
if (startLndCertError) {
this.formApi.setError('connectionCert', startLndCertError)
}
if (startLndMacaroonError) {
this.formApi.setError('connectionMacaroon', startLndMacaroonError)
}
}
handleConnectionHostChange = () => {
const formState = this.formApi.getState()
delete formState.asyncErrors.connectionHost
this.formApi.setState(formState)
}
handleConnectionCertChange = () => {
const formState = this.formApi.getState()
delete formState.asyncErrors.connectionCert
this.formApi.setState(formState)
}
handleConnectionMacaroonChange = () => {
const formState = this.formApi.getState()
delete formState.asyncErrors.connectionMacaroon
this.formApi.setState(formState)
}
handleSubmit = values => {
const { setConnectionHost, setConnectionCert, setConnectionMacaroon } = this.props
setConnectionHost(values.connectionHost)
setConnectionCert(values.connectionCert)
setConnectionMacaroon(values.connectionMacaroon)
}
validateHost = async value => {
const { validateHost } = this.props
try {
await validateHost(value)
} catch (e) {
return e.toString()
}
}
validateCert = async value => {
const { validateCert } = this.props
try {
await validateCert(value)
} catch (e) {
return e.toString()
}
}
validateMacaroon = async value => {
const { validateMacaroon } = this.props
try {
await validateMacaroon(value)
} catch (e) {
return e.toString()
}
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const {
wizardApi,
wizardState,
connectionHost,
connectionCert,
connectionMacaroon,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
startLndHostError,
startLndCertError,
startLndMacaroonError,
validateHost,
validateCert,
validateMacaroon,
...rest
} = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
const shouldValidateInline =
formState.submits > 0 || startLndHostError || startLndCertError || startLndMacaroonError
return (
<>
<Header
title={<FormattedMessage {...messages.connection_details_custom_title} />}
subtitle={<FormattedMessage {...messages.connection_details_custom_description} />}
align="left"
/>
<Bar my={4} />
<Box mb={3}>
<Input
field="connectionHost"
name="connectionHost"
label={<FormattedMessage {...messages.hostname_title} />}
description={<FormattedMessage {...messages.hostname_description} />}
initialValue={connectionHost}
onValueChange={this.handleConnectionHostChange}
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
asyncValidate={this.validateHost}
required
/>
</Box>
<Box mb={3}>
<Input
field="connectionCert"
name="connectionCert"
label={<FormattedMessage {...messages.cert_title} />}
description={<FormattedMessage {...messages.cert_description} />}
initialValue={connectionCert}
onValueChange={this.handleConnectionCertChange}
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
asyncValidate={this.validateCert}
required
/>
</Box>
<Box mb={3}>
<Input
field="connectionMacaroon"
name="connectionMacaroon"
label="Macaroon"
description={<FormattedMessage {...messages.macaroon_description} />}
initialValue={connectionMacaroon}
onValueChange={this.handleConnectionMacaroonChange}
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
asyncValidate={this.validateMacaroon}
required
/>
</Box>
</>
)
}}
</Form>
)
}
}
export default ConnectionDetails

112
app/components/Onboarding/Steps/ConnectionType.js

@ -0,0 +1,112 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import { Bar, Form, Header, RadioGroup, Radio } from 'components/UI'
import messages from './messages'
class ConnectionType extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
resetOnboarding: PropTypes.func.isRequired,
setConnectionType: PropTypes.func.isRequired,
stopLnd: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
componentDidMount() {
const { resetOnboarding, stopLnd } = this.props
stopLnd()
resetOnboarding()
}
handleSubmit = values => {
const { setConnectionType } = this.props
setConnectionType(values.connectionType)
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const {
wizardApi,
wizardState,
connectionType,
setConnectionType,
resetOnboarding,
stopLnd,
...rest
} = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<>
<Header
title={<FormattedMessage {...messages.connection_title} />}
subtitle={<FormattedMessage {...messages.connection_description} />}
align="left"
/>
<Bar my={4} />
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
<RadioGroup
required
field="connectionType"
name="connectionType"
initialValue={connectionType}
>
<Radio
value="create"
label={<FormattedMessage {...messages.signup_create} />}
description="Let Zap crearte a new bitcoin wallet and lightning node for you."
/>
<Radio
value="import"
label={<FormattedMessage {...messages.signup_import} />}
description="Import your own priivate key to recover an existing wallet."
/>
<Radio
value="custom"
label="Connect your own node"
description={<FormattedMessage {...messages.custom_description} />}
/>
<Radio
value="btcpayserver"
label="BTCPay Server"
description={<FormattedMessage {...messages.btcpay_description} />}
/>
</RadioGroup>
</Form>
</>
)
}
}
export default ConnectionType

112
app/components/Onboarding/Steps/Login.js

@ -0,0 +1,112 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Box } from 'rebass'
import { FormattedMessage, injectIntl } from 'react-intl'
import { Bar, Form, Header, PasswordInput } from 'components/UI'
import messages from './messages'
class Login extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
walletDir: PropTypes.string.isRequired,
unlockWalletError: PropTypes.string,
setUnlockWalletError: PropTypes.func.isRequired,
unlockWallet: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {},
unlockWalletError: null
}
componentDidUpdate(prevProps) {
const { setUnlockWalletError, unlockWalletError } = this.props
// Set the form error if we got an error unlocking.
if (unlockWalletError && !prevProps.unlockWalletError) {
this.formApi.setError('password', unlockWalletError)
setUnlockWalletError(null)
}
}
handleSubmit = async values => {
const { unlockWallet } = this.props
await unlockWallet(values.password)
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const {
wizardApi,
wizardState,
walletDir,
unlockWallet,
unlockWalletError,
setUnlockWalletError,
intl,
...rest
} = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={async values => {
try {
await this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
} catch (e) {
wizardApi.onSubmitFailure()
}
}}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
const shouldValidateInline = formState.submits > 0
return (
<>
<Header
title={<FormattedMessage {...messages.login_title} />}
subtitle={<FormattedMessage {...messages.login_description} />}
align="left"
/>
<Bar my={4} />
<Box>
<PasswordInput
field="password"
name="password"
label={<FormattedMessage {...messages.password_label} />}
description={<FormattedMessage {...messages.password_description} />}
required
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
placeholder={intl.formatMessage({ ...messages.password_placeholder })}
autoComplete="current-password"
/>
</Box>
</>
)
}}
</Form>
)
}
}
export default injectIntl(Login)

85
app/components/Onboarding/Steps/Password.js

@ -0,0 +1,85 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Box } from 'rebass'
import { FormattedMessage, injectIntl } from 'react-intl'
import { Bar, Form, Header, PasswordInput } from 'components/UI'
import messages from './messages'
class Password extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
setPassword: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
handleSubmit = async values => {
const { setPassword } = this.props
await setPassword(values.password)
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const { wizardApi, wizardState, setPassword, intl, ...rest } = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={async values => {
await this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
const shouldValidateInline = formState.submits > 0
return (
<>
<Header
title={<FormattedMessage {...messages.create_wallet_password_title} />}
subtitle={<FormattedMessage {...messages.create_wallet_password_description} />}
align="left"
/>
<Bar my={4} />
<Box>
<PasswordInput
field="password"
name="password"
label={<FormattedMessage {...messages.password_label} />}
required
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
placeholder={intl.formatMessage({ ...messages.password_placeholder })}
autoComplete="current-password"
/>
</Box>
</>
)
}}
</Form>
)
}
}
export default injectIntl(Password)

105
app/components/Onboarding/Steps/Recover.js

@ -0,0 +1,105 @@
/* eslint-disable react/no-multi-comp */
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import bip39 from 'bip39-en'
import { Flex } from 'rebass'
import { Bar, Form, Header, Input, Label } from 'components/UI'
import messages from './messages'
class SeedWord extends React.Component {
static propTypes = {
index: PropTypes.number.isRequired,
intl: PropTypes.object.isRequired,
word: PropTypes.string
}
validateWord = value => {
return !value || !bip39.includes(value) ? 'incorrect' : null
}
render() {
const { index, intl, word } = this.props
return (
<Flex key={`word${index}`} justifyContent="flex-start" alignItems="center" as="li" my={1}>
<Label htmlFor={`word${index}`} width={35} mb={0}>
{index}
</Label>
<Input
initialValue={word}
field={`word${index}`}
validate={this.validateWord}
variant="thin"
validateOnChange
placeholder={intl.formatMessage({ ...messages.word_placeholder })}
showMessage={false}
/>
</Flex>
)
}
}
const SeedWordWithIntl = injectIntl(SeedWord)
class SeedView extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
seed: PropTypes.array.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
render() {
const { wizardApi, wizardState, seed, intl, ...rest } = this.props
const { getApi, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const indexes = Array.from(Array(24).keys())
return (
<Form
{...rest}
getApi={getApi}
preSubmit={preSubmit}
onSubmit={onSubmit}
onSubmitFailure={onSubmitFailure}
>
<Header
title={<FormattedMessage {...messages.import_title} />}
subtitle={<FormattedMessage {...messages.import_description} />}
align="left"
/>
<Bar my={4} />
<Flex justifyContent="space-between">
<Flex flexDirection="column" as="ul" mr={2}>
{indexes.slice(0, 6).map(word => (
<SeedWordWithIntl key={word + 1} index={word + 1} word={seed[word]} />
))}
</Flex>
<Flex flexDirection="column" as="ul" mx={2}>
{indexes.slice(6, 12).map(word => (
<SeedWordWithIntl key={word + 1} index={word + 1} word={seed[word]} />
))}
</Flex>
<Flex flexDirection="column" as="ul" mx={2}>
{indexes.slice(12, 18).map(word => (
<SeedWordWithIntl key={word + 1} index={word + 1} word={seed[word]} />
))}
</Flex>
<Flex flexDirection="column" as="ul" ml={2}>
{indexes.slice(18, 24).map(word => (
<SeedWordWithIntl key={word + 1} index={word + 1} word={seed[word]} />
))}
</Flex>
</Flex>
</Form>
)
}
}
export default injectIntl(SeedView)

115
app/components/Onboarding/Steps/SeedConfirm.js

@ -0,0 +1,115 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import { Flex } from 'rebass'
import { Bar, Form, Header, Input, Label } from 'components/UI'
import messages from './messages'
class SeedConfirm extends React.Component {
state = {
seedWordIndexes: []
}
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
seed: PropTypes.array.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
componentDidMount() {
this.generateSeedWordIndexes()
}
generateSeedWordIndexes = () => {
const seedWordIndexes = []
while (seedWordIndexes.length < 3) {
const r = Math.floor(Math.random() * 24) + 1
if (seedWordIndexes.indexOf(r) === -1) {
seedWordIndexes.push(r)
}
}
this.setState({ seedWordIndexes })
}
setFormApi = formApi => {
this.formApi = formApi
}
validateWord = (index, word) => {
const { seed } = this.props
return !word || word !== seed[index] ? 'incorrect' : null
}
render() {
const { wizardApi, wizardState, seed, intl, ...rest } = this.props
const { seedWordIndexes } = this.state
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={onSubmit}
onSubmitFailure={onSubmitFailure}
>
{({ formState }) => {
const shouldValidateInline = formState.submits > 0
return (
<>
<Header
title={<FormattedMessage {...messages.retype_seed_title} />}
subtitle={
<FormattedMessage
{...messages.retype_seed_description}
values={{
word1: seedWordIndexes[0],
word2: seedWordIndexes[1],
word3: seedWordIndexes[2]
}}
/>
}
align="left"
/>
<Bar my={4} />
{seedWordIndexes.map((wordIndex, index) => {
return (
<Flex key={`word${index}`} justifyContent="flex-start" mb={3}>
<Label htmlFor="alias" width={25} mt={18}>
{wordIndex}
</Label>
<Input
field={`word${index}`}
name={`word${index}`}
validateOnBlur={shouldValidateInline}
validateOnChange={shouldValidateInline}
validate={value => this.validateWord.call(null, wordIndex - 1, value)}
placeholder={intl.formatMessage({ ...messages.word_placeholder })}
required
/>
</Flex>
)
})}
</>
)
}}
</Form>
)
}
}
export default injectIntl(SeedConfirm)

102
app/components/Onboarding/Steps/SeedView.js

@ -0,0 +1,102 @@
/* eslint-disable react/no-multi-comp */
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl } from 'react-intl'
import { Flex } from 'rebass'
import { Bar, Form, Header, Spinner, Text } from 'components/UI'
import messages from './messages'
const SeedWord = ({ index, word }) => (
<Text as="li" my={2}>
<Flex>
<Text fontWeight="normal" width={25}>
{index}
</Text>
<Text>{word}</Text>
</Flex>
</Text>
)
SeedWord.propTypes = {
index: PropTypes.number.isRequired,
word: PropTypes.string.isRequired
}
class SeedView extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
seed: PropTypes.array,
fetchingSeed: PropTypes.bool,
generateSeed: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {},
seed: [],
fetchingSeed: false
}
async componentDidMount() {
const { seed, generateSeed } = this.props
if (seed.length === 0) {
generateSeed()
}
}
render() {
const { wizardApi, wizardState, seed, generateSeed, fetchingSeed, intl, ...rest } = this.props
const { getApi, preSubmit, onSubmit, onSubmitFailure } = wizardApi
return (
<Form
{...rest}
getApi={getApi}
preSubmit={preSubmit}
onSubmit={onSubmit}
onSubmitFailure={onSubmitFailure}
>
<Header
title={<FormattedMessage {...messages.save_seed_title} />}
subtitle={<FormattedMessage {...messages.save_seed_description} />}
align="left"
/>
<Bar my={4} />
{fetchingSeed && (
<Text textAlign="center">
<Spinner />
Generating Seed...
</Text>
)}
{!fetchingSeed &&
seed.length > 0 && (
<Flex justifyContent="space-between">
<Flex flexDirection="column" as="ul">
{seed.slice(0, 6).map((word, index) => (
<SeedWord key={index} index={index + 1} word={word} />
))}
</Flex>
<Flex flexDirection="column" as="ul">
{seed.slice(6, 12).map((word, index) => (
<SeedWord key={index} index={index + 7} word={word} />
))}
</Flex>
<Flex flexDirection="column" as="ul">
{seed.slice(12, 18).map((word, index) => (
<SeedWord key={index} index={index + 13} word={word} />
))}
</Flex>
<Flex flexDirection="column" as="ul">
{seed.slice(18, 24).map((word, index) => (
<SeedWord key={index} index={index + 19} word={word} />
))}
</Flex>
</Flex>
)}
</Form>
)
}
}
export default injectIntl(SeedView)

69
app/components/Onboarding/Steps/WalletCreate.js

@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Spinner, Text } from 'components/UI'
class WalletCreate extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
createNewWallet: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
componentDidMount() {
this.formApi.submitForm()
}
handleSubmit = () => {
const { createNewWallet } = this.props
createNewWallet()
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const {
wizardApi,
wizardState,
autopilot,
setWalletCreate,
createNewWallet,
...rest
} = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
<Text textAlign="center">
<Spinner />
Creating wallet...
</Text>
</Form>
)
}
}
export default WalletCreate

69
app/components/Onboarding/Steps/WalletRecover.js

@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Spinner, Text } from 'components/UI'
class WalletRecover extends React.Component {
static propTypes = {
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
recoverOldWallet: PropTypes.func.isRequired
}
static defaultProps = {
wizardApi: {},
wizardState: {}
}
componentDidMount() {
this.formApi.submitForm()
}
handleSubmit = () => {
const { recoverOldWallet } = this.props
recoverOldWallet()
}
setFormApi = formApi => {
this.formApi = formApi
}
render() {
const {
wizardApi,
wizardState,
autopilot,
setWalletRecover,
recoverOldWallet,
...rest
} = this.props
const { getApi, onChange, preSubmit, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState
return (
<Form
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
preSubmit={preSubmit}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
<Text textAlign="center">
<Spinner />
Importing wallet...
</Text>
</Form>
)
}
}
export default WalletRecover

13
app/components/Onboarding/Steps/index.js

@ -0,0 +1,13 @@
export Alias from './Alias'
export Autopilot from './Autopilot'
export BtcPayServer from './BtcPayServer'
export ConnectionConfirm from './ConnectionConfirm'
export ConnectionDetails from './ConnectionDetails'
export ConnectionType from './ConnectionType'
export Login from './Login'
export Password from './Password'
export Recover from './Recover'
export SeedConfirm from './SeedConfirm'
export SeedView from './SeedView'
export WalletCreate from './WalletCreate'
export WalletRecover from './WalletRecover'

73
app/components/Onboarding/Steps/messages.js

@ -0,0 +1,73 @@
import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
alias_description: 'Set your nickname to help others connect with you on the Lightning Network',
alias_title: 'What should we call you?',
autopilot_description:
'Autopilot is an automatic network manager. Instead of manually adding people to build your network to make payments, enable autopilot to automatically connect you to the Lightning Network using 60% of your balance.',
autopilot_title: 'Autopilot',
back: 'back',
btcpay_connection_string_description:
"Paste the full content of your BTCPay Server connection config file. This can be found by clicking the link entitled 'Click here to open the configuration file' in your BTCPay Server gRPC settings.",
btcpay_connection_type_description:
'Connect to your own BTCPay Server instance to access your BTCPay Server wallet.',
btcpay_description: 'Enter the connection details for your BTCPay Server node.',
btcpay_error: 'Invalid connection string.',
btcpay_title: 'Connect to BTCPay Server',
cert_description: 'Path to the lnd tls cert. Example: /path/to/tls.cert',
cert_title: 'TLS Certificate',
confirm_connection_description: 'Confirm the connection details for your Lightning node.',
confirm_connection_title: 'Confirm connection',
connection_description: 'Let Zap spin up and manage a node for you, or connect to your own node.',
connection_details_custom_description: 'Enter the connection details for your Lightning node.',
connection_details_custom_title: 'Connection details',
connection_string_label: 'Connection String',
connection_string_placeholder: 'BTCPay Server Connection String',
connection_title: 'How do you want to connect to the Lightning Network?',
create_wallet_password_description:
'Looks like you are new here. Set a password to encrypt your wallet. This password will be needed to unlock Zap in the future',
create_wallet_password_title: 'Welcome!',
custom: 'Custom',
custom_description:
'Connect to your own node. You will need to provide your own connection settings so this is for advanced users only.',
default: 'Default',
default_description:
'By selecting the defualt mode we will do everything for you. Just click and go!',
disable: 'Disable Autopilot',
enable: 'Enable Autopilot',
help: 'Need Help?',
hostname_description: 'Hostname and port of the Lnd gRPC interface. Example: localhost:10009',
hostname_title: 'Host',
import_description: "Recovering a wallet, nice. You don't need anyone else, you got yourself :)",
import_title: 'Import your seed',
login_description:
'It looks like you already have a wallet (wallet found at: `{walletDir}`). Please enter your wallet password to unlock it.',
login_title: 'Welcome back!',
macaroon_description: 'Path to the lnd macaroon file. Example: /path/to/admin.macaroon',
next: 'Next',
nickname: 'Nickname',
only: 'only',
password_confirm_placeholder: 'Confirm Password',
password_error_length: 'Password must be at least {passwordMinLength} characters long',
password_error_match: 'Passwords do not match',
password_label: 'Password',
password_placeholder: 'Enter your password',
password_description:
'You would have set your password when first creating your walet. This is separate from your 24 word seed.',
retype_seed_description:
"Your seed is important! If you lose your seed you'll have no way to recover your wallet. To make sure that you have properly saved your seed, please retype words {word1}, {word2} & {word3}",
retype_seed_title: 'Retype your seed',
save_seed_description:
'Please save these 24 words securely! This will allow you to recover your wallet in the future',
save_seed_title: 'Save your wallet seed',
signup_create: 'Create new wallet',
signup_description: 'Would you like to create a new wallet or import an existing one?',
signup_import: 'Import existing wallet',
signup_title: "Alright, let's get set up",
unlock: 'Unlock',
unlocking: 'Unlocking',
verify_host_description: 'Please check the hostname carefully.',
verify_host_title: 'Are you sure you want to connect to',
word_placeholder: 'word'
})

4
app/components/Onboarding/index.js

@ -1,3 +1 @@
import Onboarding from './Onboarding'
export default Onboarding
export Onboarding from './Onboarding'

33
app/components/Onboarding/messages.js

@ -2,34 +2,7 @@ import { defineMessages } from 'react-intl'
/* eslint-disable max-len */
export default defineMessages({
connection_title: 'How do you want to connect to the Lightning Network?',
connection_description:
'By default, Zap will spin up a node for you and handle all the nerdy stuff in the background. However, you can also set up a custom node connection and use Zap to control a remote node if you desire (for advanced users).',
connection_details_custom_title: 'Connection details',
connection_details_custom_description: 'Enter the connection details for your Lightning node.',
btcpay_title: 'BTCPay Server',
btcpay_description: 'Enter the connection details for your BTCPay Server node.',
confirm_connection_title: 'Confirm connection',
confirm_connection_description: 'Confirm the connection details for your Lightning node.',
alias_title: 'What should we call you?',
alias_description: 'Set your nickname to help others connect with you on the Lightning Network',
autopilot_title: 'Autopilot',
autopilot_description:
'Autopilot is an automatic network manager. Instead of manually adding people to build your network to make payments, enable autopilot to automatically connect you to the Lightning Network using 60% of your balance.',
create_wallet_password_title: 'Welcome!',
create_wallet_password_description:
'Looks like you are new here. Set a password to encrypt your wallet. This password will be needed to unlock Zap in the future',
signup_title: "Alright, let's get set up",
signup_description: 'Would you like to create a new wallet or import an existing one?',
login_title: 'Welcome back!',
login_description:
'It looks like you already have a wallet (wallet found at: `{walletDir}`). Please enter your wallet password to unlock it.',
import_title: 'Import your seed',
import_description: "Recovering a wallet, nice. You don't need anyone else, you got yourself :)",
save_seed_title: 'Save your wallet seed',
save_seed_description:
'Please save these 24 words securely! This will allow you to recover your wallet in the future',
retype_seed_title: 'Retype your seed',
retype_seed_description:
"Your seed is important! If you lose your seed you'll have no way to recover your wallet. To make sure that you have properly saved your seed, please retype words {word1}, {word2} & {word3}"
next: 'Next',
previous: 'Back',
start_over: 'Start over'
})

210
app/containers/Onboarding.js

@ -1,186 +1,70 @@
import { connect } from 'react-redux'
import { themeSelectors } from 'reducers/theme'
import { Onboarding } from 'components/Onboarding'
import {
setAlias,
setAutopilot,
setConnectionType,
setConnectionString,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
updateAlias,
updatePassword,
setAutopilot,
changeStep,
setConnectionString,
setPassword,
setUnlockWalletError,
startLnd,
createWallet,
updateCreateWalletPassword,
updateCreateWalletPasswordConfirmation,
submitNewWallet,
stopLnd,
validateHost,
validateCert,
validateMacaroon,
generateSeed,
createNewWallet,
recoverOldWallet,
onboardingSelectors,
unlockWallet,
setSignupCreate,
setSignupImport,
updateReEnterSeedInput,
updateRecoverSeedInput,
setReEnterSeedIndexes
resetOnboarding,
unlockWallet
} from 'reducers/onboarding'
import Onboarding from 'components/Onboarding'
const mapStateToProps = state => ({
alias: state.onboarding.alias,
autopilot: state.onboarding.autopilot,
connectionType: state.onboarding.connectionType,
connectionHost: state.onboarding.connectionHost,
connectionCert: state.onboarding.connectionCert,
connectionMacaroon: state.onboarding.connectionMacaroon,
connectionString: state.onboarding.connectionString,
lndWalletStarted: state.onboarding.lndWalletStarted,
lndWalletUnlockerStarted: state.onboarding.lndWalletUnlockerStarted,
startLndHostError: state.onboarding.startLndHostError,
startLndCertError: state.onboarding.startLndCertError,
startLndMacaroonError: state.onboarding.startLndMacaroonError,
seed: state.onboarding.seed,
signupMode: state.onboarding.signupMode,
unlockWalletError: state.onboarding.unlockWalletError,
onboarded: state.onboarding.onboarded,
fetchingSeed: state.onboarding.fetchingSeed
})
const mapDispatchToProps = {
setAlias,
setAutopilot,
setConnectionType,
setConnectionString,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
updateAlias,
updatePassword,
updateCreateWalletPassword,
updateCreateWalletPasswordConfirmation,
setAutopilot,
changeStep,
setConnectionString,
setPassword,
setUnlockWalletError,
startLnd,
createWallet,
submitNewWallet,
stopLnd,
validateHost,
validateCert,
validateMacaroon,
generateSeed,
createNewWallet,
recoverOldWallet,
unlockWallet,
setSignupCreate,
setSignupImport,
updateReEnterSeedInput,
updateRecoverSeedInput,
setReEnterSeedIndexes
}
const mapStateToProps = state => ({
currentTheme: themeSelectors.currentTheme(state),
onboarding: state.onboarding,
passwordIsValid: onboardingSelectors.passwordIsValid(state),
passwordMinCharsError: onboardingSelectors.passwordMinCharsError(state),
showCreateWalletPasswordConfirmationError: onboardingSelectors.showCreateWalletPasswordConfirmationError(
state
),
reEnterSeedChecker: onboardingSelectors.reEnterSeedChecker(state),
connectionStringIsValid: onboardingSelectors.connectionStringIsValid(state),
connectionHostIsValid: onboardingSelectors.connectionHostIsValid(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const connectionTypeProps = {
connectionType: stateProps.onboarding.connectionType,
setConnectionType: dispatchProps.setConnectionType
}
const connectionDetailProps = {
connectionHostIsValid: stateProps.connectionHostIsValid,
connectionStringIsValid: stateProps.connectionStringIsValid,
connectionString: stateProps.onboarding.connectionString,
connectionHost: stateProps.onboarding.connectionHost,
connectionCert: stateProps.onboarding.connectionCert,
connectionMacaroon: stateProps.onboarding.connectionMacaroon,
setConnectionString: dispatchProps.setConnectionString,
setConnectionHost: dispatchProps.setConnectionHost,
setConnectionCert: dispatchProps.setConnectionCert,
setConnectionMacaroon: dispatchProps.setConnectionMacaroon,
startLndHostError: stateProps.onboarding.startLndHostError,
startLndCertError: stateProps.onboarding.startLndCertError,
startLndMacaroonError: stateProps.onboarding.startLndMacaroonError
}
const connectionConfirmProps = {
connectionHost: stateProps.onboarding.connectionHost
}
const aliasProps = {
updateAlias: dispatchProps.updateAlias,
alias: stateProps.onboarding.alias
}
const autopilotProps = {
autopilot: stateProps.onboarding.autopilot,
setAutopilot: dispatchProps.setAutopilot
}
const initWalletProps = {
hasSeed: stateProps.onboarding.hasSeed,
loginProps: {
password: stateProps.onboarding.password,
passwordIsValid: stateProps.passwordIsValid,
hasSeed: stateProps.onboarding.hasSeed,
existingWalletDir: stateProps.onboarding.existingWalletDir,
unlockingWallet: stateProps.onboarding.unlockingWallet,
unlockWalletError: stateProps.onboarding.unlockWalletError,
updatePassword: dispatchProps.updatePassword,
createWallet: dispatchProps.createWallet,
unlockWallet: dispatchProps.unlockWallet
},
signupProps: {
signupForm: stateProps.onboarding.signupForm,
setSignupCreate: dispatchProps.setSignupCreate,
setSignupImport: dispatchProps.setSignupImport
}
}
const newWalletSeedProps = {
seed: stateProps.onboarding.seed
}
const newWalletPasswordProps = {
createWalletPassword: stateProps.onboarding.createWalletPassword,
createWalletPasswordConfirmation: stateProps.onboarding.createWalletPasswordConfirmation,
showCreateWalletPasswordConfirmationError: stateProps.showCreateWalletPasswordConfirmationError,
passwordMinCharsError: stateProps.passwordMinCharsError,
updateCreateWalletPassword: dispatchProps.updateCreateWalletPassword,
updateCreateWalletPasswordConfirmation: dispatchProps.updateCreateWalletPasswordConfirmation
}
const recoverFormProps = {
recoverSeedInput: stateProps.onboarding.recoverSeedInput,
updateRecoverSeedInput: dispatchProps.updateRecoverSeedInput
}
const reEnterSeedProps = {
seed: stateProps.onboarding.seed,
reEnterSeedInput: stateProps.onboarding.reEnterSeedInput,
seedIndexesArr: stateProps.onboarding.seedIndexesArr,
reEnterSeedChecker: stateProps.reEnterSeedChecker,
updateReEnterSeedInput: dispatchProps.updateReEnterSeedInput,
setReEnterSeedIndexes: dispatchProps.setReEnterSeedIndexes
}
const onboardingProps = {
onboarding: stateProps.onboarding,
theme: stateProps.currentTheme,
changeStep: dispatchProps.changeStep,
startLnd: dispatchProps.startLnd,
submitNewWallet: dispatchProps.submitNewWallet,
recoverOldWallet: dispatchProps.recoverOldWallet,
connectionTypeProps,
connectionDetailProps,
connectionConfirmProps,
aliasProps,
autopilotProps,
initWalletProps,
newWalletSeedProps,
newWalletPasswordProps,
recoverFormProps,
reEnterSeedProps
}
return {
...stateProps,
...dispatchProps,
...ownProps,
...onboardingProps
}
resetOnboarding,
unlockWallet
}
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
mapDispatchToProps
)(Onboarding)

2
app/lib/lnd/walletUnlockerMethods/index.js

@ -25,7 +25,7 @@ export default function(walletUnlocker, log, event, msg, data, lndConfig) {
walletController
.unlockWallet(walletUnlocker, data)
.then(() => event.sender.send('walletUnlocked'))
.catch(() => event.sender.send('unlockWalletError'))
.catch(e => event.sender.send('setUnlockWalletError', e.message))
break
case 'initWallet':
walletController

26
app/lib/zap/controller.js

@ -69,11 +69,11 @@ class ZapController {
{ name: 'startOnboarding', from: '*', to: 'onboarding' },
{ name: 'startLocalLnd', from: 'onboarding', to: 'running' },
{ name: 'startRemoteLnd', from: 'onboarding', to: 'connected' },
{ name: 'stopLnd', from: '*', to: 'onboarding' },
{ name: 'terminate', from: '*', to: 'terminated' }
],
methods: {
onOnboarding: this.onOnboarding.bind(this),
onStartOnboarding: this.onStartOnboarding.bind(this),
onBeforeStartLocalLnd: this.onBeforeStartLocalLnd.bind(this),
onBeforeStartRemoteLnd: this.onBeforeStartRemoteLnd.bind(this),
onTerminated: this.onTerminated.bind(this),
@ -134,6 +134,9 @@ class ZapController {
startRemoteLnd(...args: any[]) {
return this.fsm.startRemoteLnd(...args)
}
stopLnd(...args: any[]) {
return this.fsm.stopLnd(...args)
}
terminate(...args: any[]) {
return this.fsm.terminate(...args)
}
@ -168,14 +171,9 @@ class ZapController {
}
// Give the grpc connections a chance to be properly closed out.
return new Promise(resolve => setTimeout(resolve, 200))
}
onStartOnboarding() {
mainLog.debug('[FSM] onStartOnboarding...')
// Notify the app to start the onboarding process.
return new Promise(resolve => setTimeout(resolve, 200)).then(() =>
this.sendMessage('startOnboarding')
)
}
onBeforeStartLocalLnd() {
@ -195,9 +193,7 @@ class ZapController {
mainLog.info(' > cert:', this.lndConfig.cert)
mainLog.info(' > macaroon:', this.lndConfig.macaroon)
return this.startLightningWallet()
.then(() => this.sendMessage('walletConnected'))
.catch(e => {
return this.startLightningWallet().catch(e => {
const errors = {}
// There was a problem connecting to the host.
if (e.code === 'LND_GRPC_HOST_ERROR') {
@ -208,7 +204,10 @@ class ZapController {
errors.cert = e.message
}
// There was a problem accessing the macaroon file.
else if (e.code === 'LND_GRPC_MACAROON_ERROR') {
else if (
e.code === 'LND_GRPC_MACAROON_ERROR' ||
e.message.includes('cannot determine data format of binary-encoded macaroon')
) {
errors.macaroon = e.message
}
// Other error codes such as UNAVAILABLE most likely indicate that there is a problem with the host.
@ -221,6 +220,7 @@ class ZapController {
// which indicates that the requested operation is not implemented or not supported/enabled in the service.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-native-core/src/constants.js#L129
if (e.code === 12) {
this.sendMessage('startLndSuccess')
return this.startWalletUnlocker()
}
@ -458,6 +458,7 @@ class ZapController {
return this.startOnboarding()
})
)
ipcMain.on('stopLnd', () => this.stopLnd())
}
/**
@ -465,6 +466,7 @@ class ZapController {
*/
_removeIpcListeners() {
ipcMain.removeAllListeners('startLnd')
ipcMain.removeAllListeners('stopLnd')
ipcMain.removeAllListeners('startLightningWallet')
ipcMain.removeAllListeners('walletUnlocker')
ipcMain.removeAllListeners('lnd')

6
app/reducers/ipc.js

@ -47,8 +47,7 @@ import {
receiveSeedError,
walletCreated,
walletUnlocked,
walletConnected,
unlockWalletError
setUnlockWalletError
} from './onboarding'
// Import all receiving IPC event handlers and pass them into createIpc
@ -119,8 +118,7 @@ const ipc = createIpc({
receiveSeedError,
walletCreated,
walletUnlocked,
walletConnected,
unlockWalletError
setUnlockWalletError
})
export default ipc

560
app/reducers/onboarding.js

@ -1,7 +1,10 @@
import crypto from 'crypto'
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import get from 'lodash.get'
import db from 'store/db'
import { validateHost as doHostValidation } from 'lib/utils/validateHost'
import { fileExists } from 'lib/utils/fileExists'
import { fetchInfo } from './info'
import { setError } from './error'
@ -13,23 +16,14 @@ export const SET_CONNECTION_STRING = 'SET_CONNECTION_STRING'
export const SET_CONNECTION_HOST = 'SET_CONNECTION_HOST'
export const SET_CONNECTION_CERT = 'SET_CONNECTION_CERT'
export const SET_CONNECTION_MACAROON = 'SET_CONNECTION_MACAROON'
export const UPDATE_ALIAS = 'UPDATE_ALIAS'
export const UPDATE_PASSWORD = 'UPDATE_PASSWORD'
export const UPDATE_CREATE_WALLET_PASSWORD = 'UPDATE_CREATE_WALLET_PASSWORD'
export const UPDATE_CREATE_WALLET_PASSWORD_CONFIRMATION =
'UPDATE_CREATE_WALLET_PASSWORD_CONFIRMATION'
export const UPDATE_RE_ENTER_SEED_INPUT = 'UPDATE_RE_ENTER_SEED_INPUT'
export const UPDATE_RECOVER_SEED_INPUT = 'UPDATE_RECOVER_SEED_INPUT'
export const CHANGE_STEP = 'CHANGE_STEP'
export const SET_ALIAS = 'SET_ALIAS'
export const SET_AUTOPILOT = 'SET_AUTOPILOT'
export const SET_PASSWORD = 'SET_PASSWORD'
export const SET_LND_WALLET_UNLOCKER_STARTED = 'SET_LND_WALLET_UNLOCKER_STARTED'
export const SET_LND_WALLET_STARTED = 'SET_LND_WALLET_STARTED'
export const FETCH_SEED = 'FETCH_SEED'
export const SET_SEED = 'SET_SEED'
export const SET_HAS_SEED = 'SET_HAS_SEED'
export const SET_RE_ENTER_SEED_INDEXES = 'SET_RE_ENTER_SEED_INDEXES'
export const ONBOARDING_STARTED = 'ONBOARDING_STARTED'
export const ONBOARDING_FINISHED = 'ONBOARDING_FINISHED'
@ -38,18 +32,22 @@ export const STARTING_LND = 'STARTING_LND'
export const LND_STARTED = 'LND_STARTED'
export const SET_START_LND_ERROR = 'SET_START_LND_ERROR'
export const LOADING_EXISTING_WALLET = 'LOADING_EXISTING_WALLET'
export const STOPPING_LND = 'STOPPING_LND'
export const LND_STOPPED = 'LND_STOPPED'
export const LOADING_EXISTING_WALLET = 'LOADING_EXISTING_WALLET'
export const CREATING_NEW_WALLET = 'CREATING_NEW_WALLET'
export const RECOVERING_OLD_WALLET = 'RECOVERING_OLD_WALLET'
export const UNLOCKING_WALLET = 'UNLOCKING_WALLET'
export const WALLET_UNLOCKED = 'WALLET_UNLOCKED'
export const SET_UNLOCK_WALLET_ERROR = 'SET_UNLOCK_WALLET_ERROR'
export const SET_SIGNUP_CREATE = 'SET_SIGNUP_CREATE'
export const SET_SIGNUP_IMPORT = 'SET_SIGNUP_IMPORT'
export const VALIDATING_HOST = 'VALIDATING_HOST'
export const VALIDATING_CERT = 'VALIDATING_CERT'
export const VALIDATING_MACAROON = 'VALIDATING_MACAROON'
export const RESET_ONBOARDING = 'RESET_ONBOARDING'
// ------------------------------------
// Helpers
@ -65,6 +63,11 @@ function prettyPrint(json) {
// ------------------------------------
// Actions
// ------------------------------------
export const resetOnboarding = () => dispatch => {
dispatch({ type: SET_SEED, seed: [] })
}
export const setConnectionType = connectionType => async (dispatch, getState) => {
const previousType = connectionTypeSelector(getState())
@ -75,7 +78,7 @@ export const setConnectionType = connectionType => async (dispatch, getState) =>
dispatch(setConnectionHost(wallet.host || initialState.connectionHost))
dispatch(setConnectionCert(wallet.cert || initialState.connectionCert))
dispatch(setConnectionMacaroon(wallet.macaroon || initialState.connectionMacaroon))
dispatch(updateAlias(wallet.alias || initialState.alias))
dispatch(setAlias(wallet.alias || initialState.alias))
dispatch(setAutopilot(wallet.autopilot || initialState.autopilot))
dispatch(setStartLndError({}))
}
@ -103,12 +106,14 @@ export function setConnectionHost(connectionHost) {
connectionHost
}
}
export function setConnectionCert(connectionCert) {
return {
type: SET_CONNECTION_CERT,
connectionCert
}
}
export function setConnectionMacaroon(connectionMacaroon) {
return {
type: SET_CONNECTION_MACAROON,
@ -116,96 +121,112 @@ export function setConnectionMacaroon(connectionMacaroon) {
}
}
export function updateAlias(alias) {
export function setAlias(alias) {
return {
type: UPDATE_ALIAS,
type: SET_ALIAS,
alias
}
}
export function updatePassword(password) {
export function setAutopilot(autopilot) {
return {
type: UPDATE_PASSWORD,
password
type: SET_AUTOPILOT,
autopilot
}
}
export function updateCreateWalletPassword(createWalletPassword) {
export function setPassword(password) {
return {
type: UPDATE_CREATE_WALLET_PASSWORD,
createWalletPassword
type: SET_PASSWORD,
password
}
}
export function updateCreateWalletPasswordConfirmation(createWalletPasswordConfirmation) {
export function setLndWalletUnlockerStarted() {
return {
type: UPDATE_CREATE_WALLET_PASSWORD_CONFIRMATION,
createWalletPasswordConfirmation
type: SET_LND_WALLET_UNLOCKER_STARTED
}
}
export function updateReEnterSeedInput(inputSeedObj) {
export function setLndWalletStarted() {
return {
type: UPDATE_RE_ENTER_SEED_INPUT,
inputSeedObj
type: SET_LND_WALLET_STARTED
}
}
export function updateRecoverSeedInput(inputSeedObj) {
return {
type: UPDATE_RECOVER_SEED_INPUT,
inputSeedObj
export const validateHost = host => async dispatch => {
try {
dispatch({ type: VALIDATING_HOST, validatingHost: true })
const res = await doHostValidation(host)
dispatch({ type: VALIDATING_HOST, validatingHost: false })
return res
} catch (e) {
dispatch({ type: VALIDATING_HOST, validatingHost: false })
throw e.message
}
}
export function setAutopilot(autopilot) {
return {
type: SET_AUTOPILOT,
autopilot
export const validateCert = certPath => async dispatch => {
try {
dispatch({ type: VALIDATING_CERT, validatingCert: true })
const res = await fileExists(certPath)
dispatch({ type: VALIDATING_CERT, validatingCert: false })
return res
} catch (e) {
dispatch({ type: VALIDATING_CERT, validatingCert: false })
if (e.code === 'ENOENT') {
e.message = 'no such file or directory'
}
}
export function setSignupCreate() {
return {
type: SET_SIGNUP_CREATE
throw e.message
}
}
export function setSignupImport() {
return {
type: SET_SIGNUP_IMPORT
export const validateMacaroon = macaroonPath => async dispatch => {
try {
dispatch({ type: VALIDATING_MACAROON, validatingMacaroon: true })
const res = await fileExists(macaroonPath)
dispatch({ type: VALIDATING_MACAROON, validatingMacaroon: false })
return res
} catch (e) {
dispatch({ type: VALIDATING_MACAROON, validatingMacaroon: false })
if (e.code === 'ENOENT') {
e.message = 'no such file or directory'
}
}
export function changeStep(step) {
return {
type: CHANGE_STEP,
step
throw e.message
}
}
export const startLnd = options => async dispatch => {
// Attempt to load the wallet settings.
// TODO: Currently, this only support a single wallet config per type.
let wallet = await db.wallets.get({ type: options.type })
// If a wallet was found, merge in our user selected options and update in the db.
if (wallet) {
Object.assign(wallet, options)
await db.wallets.put(wallet)
}
return new Promise((resolve, reject) => {
// Tell the main process to start lnd using the supplied connection details.
dispatch({ type: STARTING_LND })
ipcRenderer.send('startLnd', options)
// Otherwise, save the new wallet config.
else {
const id = await db.wallets.put(options)
wallet = Object.assign(options, { id })
}
ipcRenderer.once('startLndError', error => {
ipcRenderer.removeListener('startLndSuccess', resolve)
reject(error)
})
// Tell the main process to start lnd using the supplied connection details.
ipcRenderer.send('startLnd', wallet)
ipcRenderer.once('startLndSuccess', res => {
ipcRenderer.removeListener('startLndError', reject)
resolve(res)
})
})
}
// Update the store.
dispatch({ type: STARTING_LND })
// Listener for errors connecting to LND gRPC
export const startLndError = (event, errors) => (dispatch, getState) => {
const connectionType = connectionTypeSelector(getState())
switch (connectionType) {
case 'custom':
dispatch(setStartLndError(errors))
break
case 'btcpayserver':
dispatch(setStartLndError(errors))
break
default:
dispatch(setError(errors))
}
}
export const lndStarted = () => async dispatch => {
@ -219,35 +240,130 @@ export function setStartLndError(errors) {
}
}
export function setReEnterSeedIndexes() {
// we only want the user to have to verify 3 random indexes from the seed they were just given
const INDEX_AMOUNT = 3
export const stopLnd = () => async dispatch => {
dispatch({ type: STOPPING_LND })
ipcRenderer.send('stopLnd')
}
export const lndStopped = () => async dispatch => {
dispatch({ type: LND_STOPPED })
}
const seedIndexesArr = []
while (seedIndexesArr.length < INDEX_AMOUNT) {
// add 1 because we dont want this to be 0 index based
const ranNum = Math.floor(Math.random() * 24) + 1
export const generateSeed = () => async dispatch => {
dispatch({ type: FETCH_SEED })
ipcRenderer.send('startLnd', {
id: `tmp`,
type: 'local',
chain: 'bitcoin',
network: 'testnet'
})
}
if (seedIndexesArr.indexOf(ranNum) > -1) {
continue
}
export const createNewWallet = () => (dispatch, getState) => {
crypto.randomBytes(16, async (err, buffer) => {
const state = getState().onboarding
seedIndexesArr[seedIndexesArr.length] = ranNum
// Define the wallet config.
const wallet = {
id: buffer.toString('hex'),
type: 'local',
chain: 'bitcoin',
network: 'testnet',
settings: {
autopilot: state.autopilot,
alias: state.alias
}
}
return {
type: SET_RE_ENTER_SEED_INDEXES,
seedIndexesArr
// Save the wallet config.
await db.wallets.put(wallet)
// Start Lnd and trigger the wallet to be initialised as soon as the wallet unlocker is available.
dispatch({ type: CREATING_NEW_WALLET })
ipcRenderer.send('startLnd', wallet)
})
}
export const recoverOldWallet = () => dispatch => {
crypto.randomBytes(16, function(err, buffer) {
const id = buffer.toString('hex')
dispatch({ type: RECOVERING_OLD_WALLET })
ipcRenderer.send('startLnd', {
id,
type: 'local',
chain: 'bitcoin',
network: 'testnet'
})
})
}
export const startActiveWallet = () => async dispatch => {
const activeWallet = await db.settings.get({ key: 'activeWallet' })
if (activeWallet) {
const wallet = await db.wallets.get({ id: activeWallet.value })
if (wallet) {
dispatch(startLnd(wallet))
}
}
}
export const unlockWallet = password => async dispatch => {
dispatch({ type: UNLOCKING_WALLET })
ipcRenderer.send('walletUnlocker', {
msg: 'unlockWallet',
data: { wallet_password: password }
})
}
/**
* As soon as we have an active connection to a WalletUnlocker service, attempt to generate a new seed which kicks off
* the process of creating or unlocking a wallet.
*/
export const lndWalletUnlockerStarted = () => dispatch => {
export const lndWalletUnlockerStarted = () => (dispatch, getState) => {
dispatch(setLndWalletUnlockerStarted('active'))
const state = getState().onboarding
// Handle generate seed.
if (state.fetchingSeed) {
ipcRenderer.send('walletUnlocker', { msg: 'genSeed' })
dispatch({ type: FETCH_SEED })
}
// Handle unlock wallet.
else if (state.unlockingWallet) {
ipcRenderer.send('walletUnlocker', {
msg: 'unlockWallet',
data: { wallet_password: state.password }
})
}
// Handle create wallet.
else if (state.creatingNewWallet) {
ipcRenderer.send('walletUnlocker', {
msg: 'initWallet',
data: { wallet_password: state.password, cipher_seed_mnemonic: state.seed }
})
}
// Handle recover wallet.
else if (state.recoveringOldWallet) {
ipcRenderer.send('walletUnlocker', {
msg: 'initWallet',
data: {
wallet_password: state.password,
cipher_seed_mnemonic: state.seed,
recovery_window: 250
}
})
}
// // Handle remote connect.
// else if (state.startingLnd) {
// ipcRenderer.send('walletUnlocker', {
// msg: 'unlockWallet',
// data: { wallet_password: state.password }
// })
// }
}
/**
@ -255,6 +371,8 @@ export const lndWalletUnlockerStarted = () => dispatch => {
* early as possible.
*/
export const lndWalletStarted = lndConfig => async dispatch => {
dispatch(setLndWalletStarted())
// Save the wallet settings.
const walletId = await db.wallets.put(lndConfig)
@ -266,113 +384,32 @@ export const lndWalletStarted = lndConfig => async dispatch => {
dispatch(fetchInfo())
dispatch(lndStarted(lndConfig))
}
export const submitNewWallet = (
wallet_password,
cipher_seed_mnemonic,
aezeed_passphrase
) => dispatch => {
// once the user submits the data needed to start LND we will alert the app that it should start LND
ipcRenderer.send('walletUnlocker', {
msg: 'initWallet',
data: { wallet_password, cipher_seed_mnemonic, aezeed_passphrase }
})
dispatch({ type: CREATING_NEW_WALLET })
}
export const recoverOldWallet = (
wallet_password,
cipher_seed_mnemonic,
aezeed_passphrase
) => dispatch => {
// once the user submits the data needed to start LND we will alert the app that it should start LND
ipcRenderer.send('walletUnlocker', {
msg: 'initWallet',
data: { wallet_password, cipher_seed_mnemonic, aezeed_passphrase, recovery_window: 250 }
})
dispatch({ type: RECOVERING_OLD_WALLET })
dispatch({ type: ONBOARDING_FINISHED })
}
// Listener for errors connecting to LND gRPC
export const startOnboarding = () => async dispatch => {
// If we have an active wallet saved, load it's settings.
const activeWallet = await db.settings.get({ key: 'activeWallet' })
if (activeWallet) {
const wallet = await db.wallets.get({ id: activeWallet.value })
if (wallet) {
dispatch(setConnectionType(wallet.type))
switch (wallet.type) {
case 'local':
dispatch(updateAlias(wallet.alias))
dispatch(setAutopilot(wallet.autopilot))
break
case 'custom':
dispatch(setConnectionHost(wallet.host))
dispatch(setConnectionCert(wallet.cert))
dispatch(setConnectionMacaroon(wallet.macaroon))
break
case 'btcpayserver':
dispatch(setConnectionString(wallet.string))
break
}
}
export const startOnboarding = () => async (dispatch, getState) => {
const state = getState().onboarding
if (state.stoppingLnd) {
dispatch(lndStopped())
}
dispatch({ type: ONBOARDING_STARTED })
}
// Listener for errors connecting to LND gRPC
export const startLndError = (event, errors) => (dispatch, getState) => {
const connectionType = connectionTypeSelector(getState())
switch (connectionType) {
case 'local':
dispatch(setError(errors))
dispatch({ type: CHANGE_STEP, step: 0.1 })
break
case 'custom':
dispatch(setStartLndError(errors))
dispatch({ type: CHANGE_STEP, step: 0.2 })
break
case 'btcpayserver':
dispatch(setStartLndError(errors))
dispatch({ type: CHANGE_STEP, step: 0.3 })
break
}
}
export const createWallet = () => dispatch => {
ipcRenderer.send('walletUnlocker', { msg: 'genSeed' })
dispatch({ type: CHANGE_STEP, step: 4 })
}
export const finishOnboarding = () => dispatch => dispatch({ type: ONBOARDING_FINISHED })
// Listener for when LND creates and sends us a generated seed
export const receiveSeed = (event, { cipher_seed_mnemonic }) => dispatch => {
dispatch({ type: CHANGE_STEP, step: 4 })
// there was no seed and we just generated a new one, send user to the login component
dispatch({ type: SET_SEED, seed: cipher_seed_mnemonic })
dispatch(stopLnd())
}
// Listener for when LND throws an error on seed creation
export const receiveSeedError = (event, error) => dispatch => {
dispatch({ type: SET_HAS_SEED, hasSeed: true })
// there is already a seed, send user to the login component
dispatch({ type: CHANGE_STEP, step: 3 })
dispatch({
type: LOADING_EXISTING_WALLET,
existingWalletDir: get(error, 'context.lndDataDir')
})
}
// Unlock an existing wallet with a wallet password
export const unlockWallet = wallet_password => dispatch => {
ipcRenderer.send('walletUnlocker', { msg: 'unlockWallet', data: { wallet_password } })
dispatch({ type: UNLOCKING_WALLET })
}
export const walletCreated = () => dispatch => {
dispatch({ type: WALLET_UNLOCKED })
dispatch({ type: ONBOARDING_FINISHED })
@ -385,13 +422,8 @@ export const walletUnlocked = () => dispatch => {
ipcRenderer.send('startLightningWallet')
}
export const walletConnected = () => dispatch => {
dispatch({ type: WALLET_UNLOCKED })
dispatch({ type: ONBOARDING_FINISHED })
}
export const unlockWalletError = () => dispatch => {
dispatch({ type: SET_UNLOCK_WALLET_ERROR })
export const setUnlockWalletError = (event, unlockWalletError) => dispatch => {
dispatch({ type: SET_UNLOCK_WALLET_ERROR, unlockWalletError })
}
// ------------------------------------
@ -403,41 +435,37 @@ const ACTION_HANDLERS = {
[SET_CONNECTION_HOST]: (state, { connectionHost }) => ({ ...state, connectionHost }),
[SET_CONNECTION_CERT]: (state, { connectionCert }) => ({ ...state, connectionCert }),
[SET_CONNECTION_MACAROON]: (state, { connectionMacaroon }) => ({ ...state, connectionMacaroon }),
[UPDATE_ALIAS]: (state, { alias }) => ({ ...state, alias }),
[UPDATE_PASSWORD]: (state, { password }) => ({ ...state, password }),
[UPDATE_CREATE_WALLET_PASSWORD]: (state, { createWalletPassword }) => ({
[SET_ALIAS]: (state, { alias }) => ({ ...state, alias }),
[SET_AUTOPILOT]: (state, { autopilot }) => ({ ...state, autopilot }),
[FETCH_SEED]: state => ({ ...state, fetchingSeed: true }),
[SET_SEED]: (state, { seed }) => ({ ...state, seed, fetchingSeed: false }),
[SET_PASSWORD]: (state, { password }) => ({ ...state, password }),
[SET_LND_WALLET_UNLOCKER_STARTED]: state => ({
...state,
createWalletPassword
lndWalletUnlockerStarted: true,
lndWalletStarted: false
}),
[UPDATE_CREATE_WALLET_PASSWORD_CONFIRMATION]: (state, { createWalletPasswordConfirmation }) => ({
[SET_LND_WALLET_STARTED]: state => ({
...state,
createWalletPasswordConfirmation
lndWalletStarted: true,
lndWalletUnlockerStarted: false
}),
[UPDATE_RE_ENTER_SEED_INPUT]: (state, { inputSeedObj }) => ({
[ONBOARDING_STARTED]: state => ({ ...state, onboarding: true, onboarded: false }),
[ONBOARDING_FINISHED]: state => ({ ...state, onboarding: false, onboarded: true }),
[STARTING_LND]: state => ({
...state,
reEnterSeedInput: { ...state.reEnterSeedInput, [inputSeedObj.index]: inputSeedObj.word }
startingLnd: true,
startLndHostError: '',
startLndCertError: '',
startLndMacaroonError: ''
}),
[UPDATE_RECOVER_SEED_INPUT]: (state, { inputSeedObj }) => ({
[LND_STARTED]: state => ({
...state,
recoverSeedInput: Object.assign([], state.recoverSeedInput, {
[inputSeedObj.index]: inputSeedObj
})
startingLnd: false,
startLndHostError: '',
startLndCertError: '',
startLndMacaroonError: ''
}),
[SET_AUTOPILOT]: (state, { autopilot }) => ({ ...state, autopilot }),
[FETCH_SEED]: state => ({ ...state, fetchingSeed: true }),
[SET_HAS_SEED]: (state, { hasSeed }) => ({ ...state, hasSeed, fetchingSeed: false }),
[SET_SEED]: (state, { seed }) => ({ ...state, seed, fetchingSeed: false }),
[SET_RE_ENTER_SEED_INDEXES]: (state, { seedIndexesArr }) => ({ ...state, seedIndexesArr }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step, previousStep: state.step }),
[ONBOARDING_STARTED]: state => ({ ...state, onboarding: true, onboarded: false }),
[ONBOARDING_FINISHED]: state => ({ ...state, onboarding: false, onboarded: true }),
[STARTING_LND]: state => ({ ...state, startingLnd: true }),
[LND_STARTED]: state => ({ ...state, startingLnd: false }),
[SET_START_LND_ERROR]: (state, { errors }) => ({
...state,
startingLnd: false,
@ -445,84 +473,44 @@ const ACTION_HANDLERS = {
startLndCertError: errors.cert,
startLndMacaroonError: errors.macaroon
}),
[STOPPING_LND]: state => ({
...state,
stoppingLnd: true,
lndWalletStarted: false,
lndWalletUnlockerStarted: false
}),
[LND_STOPPED]: state => ({ ...state, stoppingLnd: false }),
[LOADING_EXISTING_WALLET]: (state, { existingWalletDir }) => ({ ...state, existingWalletDir }),
[CREATING_NEW_WALLET]: state => ({ ...state, creatingNewWallet: true }),
[RECOVERING_OLD_WALLET]: state => ({ ...state, recoveringOldWallet: true }),
[UNLOCKING_WALLET]: state => ({ ...state, unlockingWallet: true }),
[WALLET_UNLOCKED]: state => ({
...state,
unlockingWallet: false,
unlockWalletError: { isError: false, message: '' }
unlockWalletError: ''
}),
[SET_UNLOCK_WALLET_ERROR]: state => ({
[SET_UNLOCK_WALLET_ERROR]: (state, { unlockWalletError }) => ({
...state,
unlockingWallet: false,
unlockWalletError: { isError: true, message: 'Incorrect password' }
unlockWalletError
}),
[SET_SIGNUP_CREATE]: state => ({ ...state, signupForm: { create: true, import: false } }),
[SET_SIGNUP_IMPORT]: state => ({ ...state, signupForm: { create: false, import: true } })
[VALIDATING_HOST]: (state, { validatingHost }) => ({ ...state, validatingHost }),
[VALIDATING_CERT]: (state, { validatingCert }) => ({ ...state, validatingCert }),
[VALIDATING_MACAROON]: (state, { validatingMacaroon }) => ({ ...state, validatingMacaroon }),
[RESET_ONBOARDING]: state => ({ ...state, ...initialState })
}
// ------------------------------------
// Selector
// ------------------------------------
const onboardingSelectors = {}
const passwordSelector = state => state.onboarding.password
const createWalletPasswordSelector = state => state.onboarding.createWalletPassword
const createWalletPasswordConfirmationSelector = state =>
state.onboarding.createWalletPasswordConfirmation
const seedSelector = state => state.onboarding.seed
const seedIndexesArrSelector = state => state.onboarding.seedIndexesArr
const reEnterSeedInputSelector = state => state.onboarding.reEnterSeedInput
const connectionStringSelector = state => state.onboarding.connectionString
const connectionTypeSelector = state => state.onboarding.connectionType
const connectionHostSelector = state => state.onboarding.connectionHost
onboardingSelectors.startingLnd = state => state.onboarding.startingLnd
onboardingSelectors.passwordIsValid = createSelector(
passwordSelector,
password => password.length >= 8
)
onboardingSelectors.passwordMinCharsError = createSelector(
createWalletPasswordSelector,
createWalletPasswordConfirmationSelector,
(pass1, pass2) => pass1 === pass2 && pass1.length < 8 && pass1.length > 0
)
onboardingSelectors.showCreateWalletPasswordConfirmationError = createSelector(
createWalletPasswordSelector,
createWalletPasswordConfirmationSelector,
(pass1, pass2) => pass1 !== pass2 && pass2.length > 0
)
onboardingSelectors.reEnterSeedChecker = createSelector(
seedSelector,
seedIndexesArrSelector,
reEnterSeedInputSelector,
(seed, seedIndexArr, reEnterSeedInput) =>
Object.keys(reEnterSeedInput).length >= seedIndexArr.length &&
seedIndexArr.every(
index => reEnterSeedInput[index] && reEnterSeedInput[index] === seed[index - 1]
)
)
onboardingSelectors.connectionHostIsValid = createSelector(
connectionHostSelector,
connectionHost => {
return connectionHost.length > 0
}
)
onboardingSelectors.connectionStringParamsSelector = createSelector(
connectionStringSelector,
connectionString => {
@ -538,14 +526,6 @@ onboardingSelectors.connectionStringParamsSelector = createSelector(
}
)
onboardingSelectors.connectionStringIsValid = createSelector(
onboardingSelectors.connectionStringParamsSelector,
connectionStringParams => {
const { host, port, macaroon } = connectionStringParams
return Boolean(host && port && macaroon)
}
)
export { onboardingSelectors }
// ------------------------------------
@ -555,8 +535,7 @@ export { onboardingSelectors }
const initialState = {
onboarding: false,
onboarded: false,
step: 0.1,
connectionType: 'default',
connectionType: 'create',
connectionString: '',
connectionHost: '',
connectionCert: '',
@ -564,43 +543,22 @@ const initialState = {
alias: '',
autopilot: true,
password: '',
startingLnd: false,
startLndHostError: '',
startLndCertError: '',
startLndMacaroonError: '',
fetchingSeed: false,
hasSeed: false,
seed: [],
// wallet password. password used to encrypt the wallet and is required to unlock the daemon after set
createWalletPassword: '',
createWalletPasswordConfirmation: '',
creatingNewWallet: false,
recoveringOldWallet: false,
existingWalletDir: null,
unlockingWallet: false,
unlockWalletError: {
isError: false,
message: ''
},
seedIndexesArr: [],
// object of inputs for when the user re-enters their seed
// {
// index: word,
// index: word,
// index: word
// }
reEnterSeedInput: {},
recoverSeedInput: [],
// step where the user decides whether they want a newly created seed or to import an existing one
signupForm: {
create: true,
import: false
}
unlockWalletError: '',
validatingHost: false,
validatingCert: false,
validatingMacaroon: false,
lndWalletUnlockerStarted: false,
lndWalletStarted: false
}
// ------------------------------------

1
package.json

@ -306,6 +306,7 @@
"@grpc/proto-loader": "0.3.0",
"@rebass/components": "4.0.0-1",
"axios": "0.18.0",
"bip39-en": "1.1.1",
"bitcoinjs-lib": "4.0.2",
"bolt11": "https://github.com/bitcoinjs/bolt11.git",
"connected-react-router": "5.0.1",

155
stories/containers/onboarding.stories.js

@ -0,0 +1,155 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { linkTo } from '@storybook/addon-links'
import { State, Store } from '@sambego/storybook-state'
import { Modal, Page } from 'components/UI'
import { Onboarding } from 'components/Onboarding'
const delay = time => new Promise(resolve => setTimeout(() => resolve(), time))
const initialValues = {
alias: '',
connectionType: 'local',
connectionHost: '',
connectionCert: '',
connectionMacaroon: '',
connectionString: `{
"configurations": [
{
"type": "grpc",
"cryptoCode": "BTC",
"host": "host",
"port": "19000",
"macaroon": "macaroon"
}
]
}`,
startLndHostError: '',
startLndCertError: '',
startLndMacaroonError: '',
password: '',
seed: [],
onboarded: false,
onboarding: true
}
const store = new Store(initialValues)
// State
const setConnectionType = connectionType => store.set({ connectionType })
const setConnectionHost = connectionHost => store.set({ connectionHost })
const setConnectionCert = connectionCert => store.set({ connectionCert })
const setConnectionMacaroon = connectionMacaroon => store.set({ connectionMacaroon })
const setConnectionString = connectionString => store.set({ connectionString })
const setAlias = alias => store.set({ alias })
const setAutopilot = autopilot => store.set({ autopilot })
const setPassword = password => store.set({ password })
const resetOnboarding = () => {
store.set(initialValues)
}
const generateSeed = async () => {
await delay(1000)
store.set({
seed: [
'idle',
'fork',
'derive',
'idea',
'pony',
'exercise',
'balance',
'squirrel',
'around',
'sustain',
'outdoor',
'beach',
'thrive',
'fringe',
'broom',
'sea',
'sick',
'bacon',
'card',
'palace',
'slender',
'blue',
'day',
'fix'
]
})
}
const startLnd = async () => {
action('startLnd')
await delay(500)
}
const stopLnd = async () => {
action('stopLnd')
await delay(500)
}
const validateHost = async value => {
action('validateHost')
await delay(300)
return value === 'valid'
? Promise.resolve()
: Promise.reject(new Error('invalid hostname (enter "valid")'))
}
const validateCert = async value => {
action('validateCert')
await delay(300)
return value === 'valid'
? Promise.resolve()
: Promise.reject(new Error('invalid cert (enter "valid")'))
}
const validateMacaroon = async value => {
action('validateMacaroon')
await delay(300)
return value === 'valid'
? Promise.resolve()
: Promise.reject(new Error('invalid macaroon (enter "valid")'))
}
const recoverOldWallet = async () => action('recoverOldWallet')
const createNewWallet = async () => action('recoverOldWallet')
storiesOf('Containers.Onboarding', module)
.addParameters({
info: {
disable: true
}
})
.addDecorator(story => (
<Page css={{ height: 'calc(100vh - 40px)' }}>
<Modal onClose={linkTo('Containers.Home', 'Home')}>{story()}</Modal>
</Page>
))
.add('Onboarding', () => {
return (
<State store={store}>
<Onboarding
// DISPATCH
resetOnboarding={resetOnboarding}
setAlias={setAlias}
setAutopilot={setAutopilot}
setConnectionType={setConnectionType}
setConnectionHost={setConnectionHost}
setConnectionCert={setConnectionCert}
setConnectionMacaroon={setConnectionMacaroon}
setConnectionString={setConnectionString}
setPassword={setPassword}
createNewWallet={createNewWallet}
recoverOldWallet={recoverOldWallet}
startLnd={startLnd}
stopLnd={stopLnd}
validateHost={validateHost}
validateCert={validateCert}
validateMacaroon={validateMacaroon}
generateSeed={generateSeed}
/>
</State>
)
})

96
stories/containers/onboarding/components.stories.js

@ -0,0 +1,96 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import {
Alias,
Autopilot,
BtcPayServer,
ConnectionType,
ConnectionDetails,
ConnectionConfirm,
Login,
Password,
Recover,
SeedConfirm,
SeedView
} from 'components/Onboarding/Steps'
const setConnectionHost = () => ({})
const setConnectionCert = () => ({})
const setConnectionMacaroon = () => ({})
storiesOf('Containers.Onboarding.Forms', module)
.add('ConnectionType', () => <ConnectionType />)
.add('ConnectionDetails', () => (
<ConnectionDetails
setConnectionHost={setConnectionHost}
setConnectionCert={setConnectionCert}
setConnectionMacaroon={setConnectionMacaroon}
/>
))
.add('ConnectionConfirm', () => <ConnectionConfirm connectionHost="example.com:10009" />)
.add('BtcPayServer', () => <BtcPayServer />)
.add('Login', () => <Login />)
.add('Password', () => <Password />)
.add('Recover', () => <Recover />)
.add('Alias', () => <Alias />)
.add('Autopilot', () => <Autopilot />)
.add('SeedConfirm', () => (
<SeedConfirm
seed={[
'idle',
'fork',
'derive',
'idea',
'pony',
'exercise',
'balance',
'squirrel',
'around',
'sustain',
'outdoor',
'beach',
'thrive',
'fringe',
'broom',
'sea',
'sick',
'bacon',
'card',
'palace',
'slender',
'blue',
'day',
'fix'
]}
/>
))
.add('SeedView', () => (
<SeedView
seed={[
'idle',
'fork',
'derive',
'idea',
'pony',
'exercise',
'balance',
'squirrel',
'around',
'sustain',
'outdoor',
'beach',
'thrive',
'fringe',
'broom',
'sea',
'sick',
'bacon',
'card',
'palace',
'slender',
'blue',
'day',
'fix'
]}
/>
))

5
yarn.lock

@ -4164,6 +4164,11 @@ bip32@^1.0.0:
typeforce "^1.11.5"
wif "^2.0.6"
bip39-en@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/bip39-en/-/bip39-en-1.1.1.tgz#0046f3081fdf2f4c87b277008d712ec6fc397721"
integrity sha512-ZlwJCe+4LdWlNQoNeaeEdpW5NoBGnSXr0pGCHCKzkwuyndFV5kr+C671VxCTQT74fwNzYh7c0oCUrdcaQKhXkQ==
bip66@^1.1.0, bip66@^1.1.3:
version "1.1.5"
resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"

Loading…
Cancel
Save