Browse Source

Merge pull request #303 from LN-Zap/feature/new-onboarding

Feature/new onboarding
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
8a28f3fd96
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 83
      app/components/LndSyncing/LndSyncing.js
  2. 242
      app/components/LndSyncing/LndSyncing.scss
  3. 3
      app/components/LndSyncing/index.js
  4. 23
      app/components/Onboarding/Alias.js
  5. 15
      app/components/Onboarding/Alias.scss
  6. 55
      app/components/Onboarding/FormContainer.js
  7. 67
      app/components/Onboarding/FormContainer.scss
  8. 49
      app/components/Onboarding/Onboarding.js
  9. 0
      app/components/Onboarding/Onboarding.scss
  10. 45
      app/components/Onboarding/Syncing.js
  11. 50
      app/components/Onboarding/Syncing.scss
  12. 3
      app/components/Onboarding/index.js
  13. 3
      app/components/Wallet/Wallet.js
  14. 74
      app/containers/Root.js
  15. 34
      app/main.dev.js
  16. 3
      app/reducers/index.js
  17. 6
      app/reducers/ipc.js
  18. 69
      app/reducers/onboarding.js
  19. 2
      app/variables.scss

83
app/components/LndSyncing/LndSyncing.js

@ -1,83 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import styles from './LndSyncing.scss'
class LndSyncing extends Component {
constructor(props) {
super(props)
this.state = {
facts: [
{
title: 'The Lightning Network',
description: 'The Lightning Network is a second layer solution built on top of the Bitcoin block chain that attempts to increase Bitcoin\'s scalability and privacy' // eslint-disable-line max-len
},
{
title: 'Payment Channel',
description: 'A payment channel is a class of techniques designed to allow users to make multiple Bitcoin transactions without commiting all of the transactions to the Bitcoin block chain. You can think of payment channels like tubes of money' // eslint-disable-line max-len
},
{
title: 'HTLC',
description: 'Hashed TimeLock Contracts is a class of payments that use hashlocks and timelocks to require the receiver of a payment either acknowledge receiving the payment before a deadline or forfeit the ability to claim the payment. HTLCs are useful within the Lightning Network for routing payments across two or more payment channels' // eslint-disable-line max-len
},
{
title: 'Onion Routing',
description: 'Onion routing is a technique for anonymous communication over a computer network. In an onion network, messages are encapsulated in layers of encryption, analogous to layers of an onion.' // eslint-disable-line max-len
}
],
currentFact: 0
}
}
componentWillMount() {
this.props.fetchBlockHeight()
}
render() {
const { syncPercentage } = this.props
const { facts, currentFact } = this.state
const renderCurrentFact = facts[currentFact]
return (
<div className={styles.container}>
<header>
<section>
<h3>zap</h3>
</section>
<section className={`${styles.loading} hint--left`} data-hint='Syncing your Lightning Network node to the blockchain'>
<h4>{syncPercentage.toString().length > 0 && `${syncPercentage}%`}</h4>
<div className={styles.spinner} />
</section>
</header>
<div className={styles.facts}>
<div className={styles.fact}>
<h2>{renderCurrentFact.title}</h2>
<p>{renderCurrentFact.description}</p>
</div>
<ul className={styles.factButtons}>
{
facts.map((fact, index) => (
<li
className={`${styles.factButton} ${currentFact === index && styles.active}`}
key={index}
onClick={() => this.setState({ currentFact: index })}
/>
))
}
</ul>
</div>
</div>
)
}
}
LndSyncing.propTypes = {
fetchBlockHeight: PropTypes.func.isRequired,
syncPercentage: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]).isRequired
}
export default LndSyncing

242
app/components/LndSyncing/LndSyncing.scss

@ -1,242 +0,0 @@
@import '../../variables.scss';
.container {
height: 100vh;
background: $secondary;
header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: top;
padding: 100px;
}
h3 {
font-size: 50px;
color: $white;
}
.loading {
text-align: center;
position: relative;
.spinner, h1 {
display: inline-block;
vertical-align: top;
}
h4 {
position: absolute;
min-width: 100px;
top: calc(50% - 5px);
left: calc(50% - 48px);
color: $main;
font-size: 10px;
}
h1 {
color: $main;
line-height: 50px;
margin-left: 20px;
}
}
.facts {
color: $white;
.fact {
transition: all 0.25s;
width: 50%;
margin: 0 auto;
text-align: center;
line-height: 1.5;
letter-spacing: 1.1px;
height: 250px;
h2 {
font-size: 50px;
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
}
}
}
.factButtons {
text-align: center;
}
.factButton {
cursor: pointer;
display: inline-block;
width: 15px;
height: 15px;
background: $white;
opacity: 0.5;
border-radius: 50%;
margin: 0 5px;
&:hover {
opacity: 0.75;
}
&.active {
opacity: 1;
}
}
}
.aliasForm {
width: 50%;
margin: 0 auto;
h1 {
text-align: center;
font-size: 32px;
color: $white;
}
p {
color: $darkgrey;
text-align: center;
margin-top: 20px;
font-weight: 100;
}
.inputContainer {
text-align: center;
margin-top: 50px;
}
.input {
padding: 20px;
font-size: 18px;
color: $darkestgrey;
background: lighten($black, 15%);
border: none;
outline: 0;
-webkit-appearance: none;
font-weight: 200;
width: calc(100% - 20px);
}
.submit {
background: $main;
color: $white;
font-size: 18px;
cursor: pointer;
width: 10%;
margin: 50px auto 0 auto;
padding: 20px 60px;
opacity: 0.5;
}
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
background: darken($secondary, 5%);
white-space: nowrap;
section {
display: inline-block;
vertical-align: top;
width: 50%;
white-space: normal;
padding: 30px 0;
h2 {
color: $white;
font-size: 22px;
letter-spacing: 1.2px;
font-weight: bold;
margin-bottom: 20px;
padding: 0 30px;
}
p {
color: $white;
padding: 0 30px;
line-height: 1.5;
}
}
.address {
display: flex;
flex-direction: row;
font-family: 'Roboto';
font-size: 14px;
font-weight: 200;
background: lighten($black, 15%);
color: $darkestgrey;
width: 75%;
margin: 0 auto;
span {
padding: 20px;
}
span:nth-child(1) {
flex: 9;
overflow-x: scroll;
font-size: 14px;
}
span:nth-child(2) {
background: $darkestgrey;
color: $white;
cursor: pointer;
transition: all 0.25s;
&:hover {
background: $darkestgrey;
}
}
}
}
.spinner {
border: 1px solid rgba(235, 184, 100, 0.1);
border-left-color: rgba(235, 184, 100, 0.4);
-webkit-border-radius: 999px;
-moz-border-radius: 999px;
border-radius: 999px;
}
.spinner {
margin: 0 auto;
height: 50px;
width: 50px;
-webkit-animation: animation-rotate 1000ms linear infinite;
-moz-animation: animation-rotate 1000ms linear infinite;
-o-animation: animation-rotate 1000ms linear infinite;
animation: animation-rotate 1000ms linear infinite;
}
@-webkit-keyframes animation-rotate {
100% {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes animation-rotate {
100% {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes animation-rotate {
100% {
-o-transform: rotate(360deg);
}
}
@keyframes animation-rotate {
100% {
transform: rotate(360deg);
}
}

3
app/components/LndSyncing/index.js

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

23
app/components/Onboarding/Alias.js

@ -0,0 +1,23 @@
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

15
app/components/Onboarding/Alias.scss

@ -0,0 +1,15 @@
@import '../../variables.scss';
.alias {
background: transparent;
outline: none;
border: 0;
color: $gold;
-webkit-text-fill-color: $white;
font-size: 22px;
}
.alias::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}

55
app/components/Onboarding/FormContainer.js

@ -0,0 +1,55 @@
import React from 'react'
import PropTypes from 'prop-types'
import Isvg from 'react-inlinesvg'
import zapLogo from 'icons/zap_logo.svg'
import styles from './FormContainer.scss'
const FormContainer = ({ title, description, back, next, children }) => (
<div className={styles.container}>
<div className={styles.titleBar} />
<header className={styles.header}>
<section>
<Isvg src={zapLogo} />
</section>
<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 && <div onClick={back}>Back</div>
}
</section>
<section>
{
next && <div onClick={next}>Next</div>
}
</section>
</div>
</footer>
</div>
)
FormContainer.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
back: PropTypes.func,
next: PropTypes.func,
children: PropTypes.object.isRequired
}
export default FormContainer

67
app/components/Onboarding/FormContainer.scss

@ -0,0 +1,67 @@
@import '../../variables.scss';
.container {
position: relative;
height: 100vh;
background: $darkspaceblue;
}
.titleBar {
background: $spacegrey;
height: 20px;
-webkit-user-select: none;
-webkit-app-region: drag;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 20px 40px;
}
.info {
color: $white;
margin: 20px 0 20px 0;
padding: 20px 40px;
h1 {
font-size: 22px;
margin-bottom: 10px;
}
p {
font-size: 12px;
}
}
.content {
position: relative;
background: $spaceblue;
height: 100vh;
padding: 60px 40px;
}
.footer {
position: absolute;
bottom: 0;
padding: 20px 40px;
color: $white;
width: calc(100% - 80px);
.buttonsContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
div {
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
}

49
app/components/Onboarding/Onboarding.js

@ -0,0 +1,49 @@
import React from 'react'
import PropTypes from 'prop-types'
import LoadingBolt from 'components/LoadingBolt'
import FormContainer from './FormContainer'
import Alias from './Alias'
import styles from './Onboarding.scss'
const Onboarding = ({
onboarding: {
step,
alias
},
submit,
aliasProps
}) => {
const renderStep = () => {
switch (step) {
case 1:
return (
<FormContainer
title={'1. What should we call you?'}
description={'Set your nickname to help others connect with you on the Lightning Network'}
back={null}
next={() => submit(alias)}
>
<Alias {...aliasProps} />
</FormContainer>
)
default:
return <LoadingBolt />
}
}
return (
<div className={styles.container}>
{renderStep()}
</div>
)
}
Onboarding.propTypes = {
onboarding: PropTypes.object.isRequired,
aliasProps: PropTypes.object.isRequired,
submit: PropTypes.func.isRequired
}
export default Onboarding

0
app/components/Onboarding/Onboarding.scss

45
app/components/Onboarding/Syncing.js

@ -0,0 +1,45 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Isvg from 'react-inlinesvg'
import zapLogo from 'icons/zap_logo.svg'
import styles from './Syncing.scss'
class Syncing extends Component {
componentWillMount() {
this.props.fetchBlockHeight()
}
render() {
const { syncPercentage } = this.props
return (
<div className={styles.container}>
<div className={styles.titleBar} />
<div className={styles.content}>
<header>
<Isvg className={styles.bitcoinLogo} src={zapLogo} />
</header>
<section className={styles.progressContainer}>
<h1>Syncing to the blockchain...</h1>
<div className={styles.progressBar}>
<div className={styles.progress} style={{ width: isNaN(syncPercentage) ? 0 : `${syncPercentage}%` }} />
</div>
<h4>{isNaN(parseInt(syncPercentage, 10)) || syncPercentage.toString().length === 0 ? '' : `${syncPercentage}%`}</h4>
</section>
</div>
</div>
)
}
}
Syncing.propTypes = {
fetchBlockHeight: PropTypes.func.isRequired,
syncPercentage: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]).isRequired
}
export default Syncing

50
app/components/Onboarding/Syncing.scss

@ -0,0 +1,50 @@
@import '../../variables.scss';
.container {
position: relative;
height: 100vh;
background: $spaceblue;
}
.titleBar {
background: $spacegrey;
height: 20px;
-webkit-user-select: none;
-webkit-app-region: drag;
}
.content {
padding: 20px 40px;
}
.progressContainer {
color: $white;
text-align: center;
margin-top: 20%;
h1 {
margin-bottom: 20px;
}
.progressBar {
width: 75%;
max-width: 700px;
margin: 0 auto;
height: 10px;
border-radius: 5px;
background: $spaceborder;
}
.progress {
background: $gold;
background: #DEA326;
height: 10px;
border-radius: 5px;
transition: all 0.25s;
}
h4 {
color: $gold;
margin-top: 10px;
}
}

3
app/components/Onboarding/index.js

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

3
app/components/Wallet/Wallet.js

@ -32,7 +32,8 @@ class Wallet extends Component {
} = this.props
const { modalOpen, qrCodeType } = this.state
const usdAmount = parseFloat(btc.satoshisToUsd(balance.walletBalance, currentTicker.price_usd))
const usdAmount = btc.satoshisToUsd(balance.channelBalance, currentTicker.price_usd)
console.log('usdAmount: ', usdAmount)
const changeQrCode = () => {
const qrCodeNum = this.state.qrCodeType === 1 ? 2 : 1

74
app/containers/Root.js

@ -5,43 +5,70 @@ import { ConnectedRouter } from 'react-router-redux'
import PropTypes from 'prop-types'
import LoadingBolt from '../components/LoadingBolt'
import LndSyncing from '../components/LndSyncing'
import Onboarding from '../components/Onboarding'
import Syncing from '../components/Onboarding/Syncing'
import { updateAlias, changeStep, submit } from '../reducers/onboarding'
import { fetchBlockHeight, lndSelectors } from '../reducers/lnd'
import { newAddress } from '../reducers/address'
import Routes from '../routes'
const mapDispatchToProps = {
fetchBlockHeight,
newAddress
updateAlias,
changeStep,
submit,
fetchBlockHeight
}
const mapStateToProps = state => ({
lnd: state.lnd,
address: state.address,
onboarding: state.onboarding,
syncPercentage: lndSelectors.syncPercentage(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const syncingProps = {
fetchBlockHeight: dispatchProps.fetchBlockHeight,
syncPercentage: stateProps.syncPercentage
}
const aliasProps = {
updateAlias: dispatchProps.updateAlias,
alias: stateProps.onboarding.alias
}
const onboardingProps = {
onboarding: stateProps.onboarding,
submit: dispatchProps.submit,
aliasProps
}
return {
...stateProps,
...dispatchProps,
...ownProps,
onboardingProps,
syncingProps
}
}
const Root = ({
store,
history,
lnd,
newAddress, // eslint-disable-line no-shadow
fetchBlockHeight, // eslint-disable-line no-shadow
syncPercentage,
address
onboardingProps,
syncingProps
}) => {
// If we are syncing show the syncing screen
if (!onboardingProps.onboarding.onboarded) {
return <Onboarding {...onboardingProps} />
}
// If we are syncing show the syncing screen
if (lnd.syncing) {
return (
<LndSyncing
newAddress={newAddress}
fetchBlockHeight={fetchBlockHeight}
syncPercentage={syncPercentage}
address={address}
grpcStarted={lnd.grpcStarted}
/>
)
return <Syncing {...syncingProps} />
}
// Don't launch the app without gRPC connection
@ -60,13 +87,8 @@ Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
lnd: PropTypes.object.isRequired,
fetchBlockHeight: PropTypes.func.isRequired,
newAddress: PropTypes.func.isRequired,
syncPercentage: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]).isRequired,
address: PropTypes.object.isRequired
onboardingProps: PropTypes.object.isRequired,
syncingProps: PropTypes.object.isRequired
}
export default connect(mapStateToProps, mapDispatchToProps)(Root)
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Root)

34
app/main.dev.js

@ -8,7 +8,7 @@
* When running `npm run build` or `npm run build-main`, this file is compiled to
* `./app/main.prod.js` using webpack. This gives us some performance wins.
*
* @flow
*
*/
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
@ -83,6 +83,19 @@ const sendLndSyncing = () => {
}, 1000)
}
const sendStartOnboarding = () => {
const sendStartOnboardingInterval = setInterval(() => {
if (didFinishLoad) {
clearInterval(sendStartOnboardingInterval)
if (mainWindow) {
console.log('STARTING ONBOARDING')
mainWindow.webContents.send('startOnboarding')
}
}
}, 1000)
}
// Send the front end event letting them know the gRPC connection has started
const sendGrpcConnected = () => {
const sendGrpcConnectedInterval = setInterval(() => {
@ -126,7 +139,7 @@ const sendLndSynced = () => {
}
// Starts the LND node
const startLnd = () => {
const startLnd = (alias) => {
let lndPath
if (process.env.NODE_ENV === 'development') {
@ -146,7 +159,8 @@ const startLnd = () => {
'--neutrino.connect=127.0.0.1:18333',
'--autopilot.active',
'--debuglevel=debug',
'--noencryptwallet'
'--noencryptwallet',
`--alias=${alias}`
]
)
.on('error', error => console.log(`lnd error: ${error}`))
@ -242,16 +256,17 @@ app.on('ready', async () => {
menuBuilder.buildMenu()
sendGrpcDisconnected()
// Check to see if and LND process is running
// Check to see if an LND process is running
lookup({ command: 'lnd' }, (err, results) => {
// There was an error checking for the LND process
if (err) { throw new Error(err) }
// No LND process was found
if (!results.length) {
// Assign path to certs to certPath
sendLndSyncing()
// let the application know onboarding has started
sendStartOnboarding()
// Assign path to certs to certPath
switch (os.platform()) {
case 'darwin':
certPath = path.join(homedir, 'Library/Application Support/Lnd/tls.cert')
@ -267,7 +282,12 @@ app.on('ready', async () => {
}
// Start LND
startLnd()
// startLnd()
// once the onboarding has finished we wanna let the application we have started syncing and start LND
ipcMain.on('onboardingFinished', (event, { alias }) => {
sendLndSyncing()
startLnd(alias)
})
} else {
// An LND process was found, no need to start our own
console.log('LND ALREADY RUNNING')

3
app/reducers/index.js

@ -1,6 +1,6 @@
// @flow
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import onboarding from './onboarding'
import lnd from './lnd'
import ticker from './ticker'
import info from './info'
@ -26,6 +26,7 @@ import error from './error'
const rootReducer = combineReducers({
router,
onboarding,
lnd,
ticker,
info,

6
app/reducers/ipc.js

@ -35,6 +35,8 @@ import {
import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoutes } from './network'
import { startOnboarding } from './onboarding'
// Import all receiving IPC event handlers and pass them into createIpc
const ipc = createIpc({
lndSyncing,
@ -91,7 +93,9 @@ const ipc = createIpc({
receiveDescribeNetwork,
receiveQueryRoutes,
receiveInvoiceAndQueryRoutes
receiveInvoiceAndQueryRoutes,
startOnboarding
})
export default ipc

69
app/reducers/onboarding.js

@ -0,0 +1,69 @@
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
export const UPDATE_ALIAS = 'UPDATE_ALIAS'
export const CHANGE_STEP = 'CHANGE_STEP'
export const ONBOARDING_STARTED = 'ONBOARDING_STARTED'
export const ONBOARDING_FINISHED = 'ONBOARDING_FINISHED'
// ------------------------------------
// Actions
// ------------------------------------
export function updateAlias(alias) {
return {
type: UPDATE_ALIAS,
alias
}
}
export function changeStep(step) {
return {
type: CHANGE_STEP,
step
}
}
export function submit(alias) {
// alert the app we're done onboarding and it's cool to start LND
ipcRenderer.send('onboardingFinished', { alias })
return {
type: ONBOARDING_FINISHED
}
}
export const startOnboarding = () => (dispatch) => {
dispatch({ type: ONBOARDING_STARTED })
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[UPDATE_ALIAS]: (state, { alias }) => ({ ...state, alias }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step }),
[ONBOARDING_STARTED]: state => ({ ...state, onboarded: false }),
[ONBOARDING_FINISHED]: state => ({ ...state, onboarded: true })
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
onboarded: true,
step: 1,
alias: ''
}
// ------------------------------------
// Reducer
// ------------------------------------
export default function lndReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

2
app/variables.scss

@ -11,8 +11,10 @@ $darkestgrey: #999999;
$bluegrey: #2A2D38;
$spacegrey: #222E2B;
$spaceblue: #252832;
$darkspaceblue: #1c1e26;
$spaceborder: #404040;
$gold: #DEA326;
$green: #0bb634;
$terminalgreen: #00FF00;
$red: #FF556A;

Loading…
Cancel
Save