fix(onboarding): Invitation code system, just in case

commit 64938016f6ffbf366a220e7abd9af6f7a4cb478b
Author: Ben Gotow <bengotow@gmail.com>
Date:   Sun Oct 4 16:42:38 2015 -0700

    Don't allow people to double click the token check button, make requests take at least 400msec

commit e548f3ee449c676a813c5630f1624963872ed6e6
Merge: 3350b91 e4b4933
Author: Ben Gotow <bengotow@gmail.com>
Date:   Sun Oct 4 16:39:56 2015 -0700

    Merge branch 'token-auth' of github.com:nylas/N1 into token-auth

    # Conflicts:
    #	internal_packages/onboarding/lib/token-auth-page.cjsx

commit 3350b917449c29299fa078d59a4a5a9339fdf29b
Author: Ben Gotow <bengotow@gmail.com>
Date:   Sun Oct 4 16:38:52 2015 -0700

    Improve a few error states, adding "checking" state when checking token

commit e4b49334cbf59145d9bdd955d35636f16a7c4924
Author: EthanBlackburn <ethan@nylas.com>
Date:   Sun Oct 4 16:21:39 2015 -0700

    Correct retry behavior

commit 11cd9a75b2a1ca0f4347160df93815743909ccea
Author: EthanBlackburn <ethan@nylas.com>
Date:   Sat Oct 3 18:06:55 2015 -0700

    Removed old auth token variable

commit afe451cd70de528def3443d8b373fd24f4aa5cde
Author: EthanBlackburn <ethan@nylas.com>
Date:   Sat Oct 3 16:08:12 2015 -0700

    Added token auth page
This commit is contained in:
Ben Gotow 2015-10-04 16:49:41 -07:00
parent 11086761dd
commit 467955dc2c
10 changed files with 305 additions and 29 deletions

View file

@ -7,3 +7,4 @@
"calendar-bar"
]
'updateLevel': 'patch'
'env': 'production'

View file

@ -4,7 +4,6 @@ _ = require 'underscore'
{EdgehillAPI, Utils} = require 'nylas-exports'
OnboardingActions = require './onboarding-actions'
NylasApiEnvironmentStore = require './nylas-api-environment-store'
Providers = require './account-types'
url = require 'url'
@ -15,11 +14,6 @@ class AccountChoosePage extends React.Component
@state =
email: ""
provider: ""
environment: NylasApiEnvironmentStore.getEnvironment()
componentDidMount: ->
@_usub = NylasApiEnvironmentStore.listen =>
@setState environment: NylasApiEnvironmentStore.getEnvironment()
componentWillUnmount: ->
@_usub?()
@ -88,18 +82,4 @@ class AccountChoosePage extends React.Component
})
shell.openExternal(googleUrl)
_environmentComponent: =>
return <div></div> unless atom.inDevMode()
<div className="environment-selector">
<select value={@state.environment} onChange={@_onEnvChange}>
<option value="development">Development (edgehill-dev, api-staging)</option>
<option value="experimental">Experimental (edgehill-experimental, api-experimental)</option>
<option value="staging">Staging (edgehill-staging, api-staging)</option>
<option value="production">Production (edgehill, api)</option>
</select>
</div>
_onEnvChange: (event) =>
OnboardingActions.changeAPIEnvironment(event.target.value)
module.exports = AccountChoosePage

View file

@ -7,6 +7,8 @@ ipc = require 'ipc'
OnboardingActions = require './onboarding-actions'
NylasApiEnvironmentStore = require './nylas-api-environment-store'
Providers = require './account-types'
remote = require('remote')
dialog = remote.require('dialog')
class AccountSettingsPage extends React.Component
@displayName: "AccountSettingsPage"
@ -224,6 +226,8 @@ class AccountSettingsPage extends React.Component
pass: ''
sendImmediately: true
.then (json) =>
json.invite_code = atom.config.get('edgehill.token')
json.email = data.email
EdgehillAPI.request
path: "/connect/nylas"
method: "POST"
@ -236,6 +240,19 @@ class AccountSettingsPage extends React.Component
_onNetworkError: (err) =>
errorMessage = err.message
if errorMessage == "Invite code required"
choice = dialog.showMessageBox(
remote.getCurrentWindow(),
{
type: 'info',
buttons: ['Okay'],
title: 'Confirm',
message: 'Due to a large number of sign-ups this week, youll need an invitation code to add another account! Visit http://invite.nylas.com/ to grab one, or hold tight!'
});
OnboardingActions.moveToPage("token-auth")
if errorMessage == "Invalid invite code"
# delay?
OnboardingActions.moveToPage("token-auth")
pageNumber = @state.pageNumber
errorFieldNames = err.body?.missing_fields || err.body?.missing_settings

View file

@ -7,6 +7,7 @@ OnboardingActions = Reflux.createActions [
"moveToPreviousPage"
"moveToPage"
"accountJSONReceived"
"retryCheckTokenAuthStatus"
]
for key, action of OnboardingActions

View file

@ -1,5 +1,6 @@
Reflux = require 'reflux'
OnboardingActions = require './onboarding-actions'
TokenAuthAPI = require './token-auth-api'
{AccountStore} = require 'nylas-exports'
NylasStore = require 'nylas-store'
ipc = require 'ipc'
@ -13,13 +14,15 @@ class PageRouterStore extends NylasStore
@_page = atom.getWindowProps().page ? ''
@_pageData = atom.getWindowProps().pageData ? {}
@_pageStack = [{page: @_page, pageData: @_pageData}]
@_checkTokenAuthStatus()
@listenTo OnboardingActions.moveToPreviousPage, @_onMoveToPreviousPage
@listenTo OnboardingActions.moveToPage, @_onMoveToPage
@listenTo OnboardingActions.closeWindow, @_onCloseWindow
@listenTo OnboardingActions.accountJSONReceived, @_onAccountJSONReceived
@listenTo OnboardingActions.retryCheckTokenAuthStatus, @_checkTokenAuthStatus
_onAccountJSONReceived: (json) =>
isFirstAccount = AccountStore.items().length is 0
@ -38,6 +41,10 @@ class PageRouterStore extends NylasStore
pageData: -> @_pageData
tokenAuthEnabled: -> @_tokenAuthEnabled
tokenAuthEnabledError: -> @_tokenAuthEnabledError
connectType: ->
@_connectType
@ -59,4 +66,23 @@ class PageRouterStore extends NylasStore
else
atom.close()
_checkTokenAuthStatus: ->
@_tokenAuthEnabled = "unknown"
@_tokenAuthEnabledError = null
@trigger()
TokenAuthAPI.request
path: "/status"
returnsModel: false
timeout: 10000
success: (json) =>
if json.restricted
@_tokenAuthEnabled = "yes"
else
@_tokenAuthEnabled = "no"
@trigger()
error: (err) =>
@_tokenAuthEnabledError = err.message
@trigger()
module.exports = new PageRouterStore()

View file

@ -8,6 +8,8 @@ AccountChoosePage = require './account-choose-page'
AccountSettingsPage = require './account-settings-page'
InitialPreferencesPage = require './initial-preferences-page'
InitialPackagesPage = require './initial-packages-page'
TokenAuthPage = require './token-auth-page'
class PageRouter extends React.Component
@displayName: 'PageRouter'
@ -51,7 +53,7 @@ class PageRouter extends React.Component
<div className="page-frame">
{@_renderDragRegion()}
<ReactCSSTransitionGroup
transitionName="page"
transitionName="alpha-fade"
leaveTimeout={150}
enterTimeout={150}>
{@_renderCurrentPage()}
@ -73,6 +75,7 @@ class PageRouter extends React.Component
_renderCurrentPage: =>
Component = {
"welcome": WelcomePage
"token-auth": TokenAuthPage
"account-choose": AccountChoosePage
"account-settings": AccountSettingsPage
"initial-preferences": InitialPreferencesPage

View file

@ -0,0 +1,58 @@
nodeRequest = require 'request'
{Actions, Utils, APIError} = require 'nylas-exports'
class TokenAuthAPI
constructor: ->
atom.config.onDidChange('env', @_onConfigChanged)
@_onConfigChanged()
@
_onConfigChanged: =>
env = atom.config.get('env')
if env is 'development'
@APIRoot = "http://localhost:6001"
else if env in ['experimental', 'staging']
@APIRoot = "https://invite-staging.nylas.com"
else
@APIRoot = "https://invite.nylas.com"
request: (options={}) ->
return if atom.getLoadSettings().isSpec
options.method ?= 'GET'
options.url ?= "#{@APIRoot}#{options.path}" if options.path
options.body ?= {} unless options.formData
options.json = true
options.error ?= @_defaultErrorCallback
# This is to provide functional closure for the variable.
rid = Utils.generateTempId()
[rid].forEach (requestId) ->
options.startTime = Date.now()
Actions.willMakeAPIRequest({
request: options,
requestId: requestId
})
nodeRequest options, (error, response, body) ->
statusCode = response?.statusCode
Actions.didMakeAPIRequest({
request: options,
statusCode: statusCode,
error: error,
requestId: requestId
})
if error? or statusCode > 299
if not statusCode or statusCode in [-123, 500]
body = "Sorry, we could not reach the Nylas API. Please try
again, or email us if you continue to have trouble.
(#{statusCode})"
options.error(new APIError({error:error, response:response, body:body, requestOptions: options}))
else
options.success(body) if options.success
_defaultErrorCallback: (apiError) ->
console.error(apiError)
module.exports = new TokenAuthAPI

View file

@ -0,0 +1,150 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, TimeoutTransitionGroup} = require 'nylas-component-kit'
{Utils} = require 'nylas-exports'
TokenAuthAPI = require './token-auth-api'
OnboardingActions = require './onboarding-actions'
PageRouterStore = require './page-router-store'
Providers = require './account-types'
url = require 'url'
class TokenAuthPage extends React.Component
@displayName: "TokenAuthPage"
constructor: (@props) ->
@state =
token: ""
tokenValidityError: null
tokenAuthInflight: false
tokenAuthEnabled: PageRouterStore.tokenAuthEnabled()
tokenAuthEnabledError: PageRouterStore.tokenAuthEnabledError()
componentDidMount: ->
@_usub = PageRouterStore.listen(@_onTokenAuthChange)
_onTokenAuthChange: =>
@setState({
tokenAuthEnabled: PageRouterStore.tokenAuthEnabled()
tokenAuthEnabledError: PageRouterStore.tokenAuthEnabledError()
})
componentWillUnmount: ->
@_usub?()
render: =>
if @state.tokenAuthEnabled is "unknown"
<div className="page token-auth">
<TimeoutTransitionGroup leaveTimeout={125} enterTimeout={125} transitionName="alpha-fade">
{@_renderWaitingForTokenAuthAnswer()}
</TimeoutTransitionGroup>
</div>
else if @state.tokenAuthEnabled is "yes"
<div className="page token-auth token-auth-enabled">
<div className="quit" onClick={ -> OnboardingActions.closeWindow() }>
<RetinaImg name="onboarding-close.png" mode={RetinaImg.Mode.ContentPreserve}/>
</div>
<RetinaImg url="nylas://onboarding/assets/nylas-pictograph@2x.png" mode={RetinaImg.Mode.ContentIsMask} style={zoom: 0.29} className="logo"/>
<div className="caption" style={padding: 40}>
Due to overwhelming interest, you need an invitation code to connect
an account to N1. Enter your invitation code below, or <a href="https://invite.nylas.com">request one here</a>.
</div>
{@_renderContinueError()}
<label className="token-label">
{@_renderInput()}
</label>
{@_renderContinueButton()}
</div>
else
<div className="page token-auth">
</div>
_renderWaitingForTokenAuthAnswer: =>
if @state.tokenAuthEnabledError
<div style={position:'absolute', width:'100%', padding:60, paddingTop:100} key="error">
<div className="errormsg">{@state.tokenAuthEnabledError}</div>
<button key="retry"
style={marginTop: 15}
className="btn btn-large btn-retry"
onClick={OnboardingActions.retryCheckTokenAuthStatus}>
Try Again
</button>
</div>
else
<div style={position:'absolute', width:'100%'} key="spinner">
<RetinaImg url="nylas://onboarding/assets/installing-spinner.gif"
mode={RetinaImg.Mode.ContentPreserve}
style={marginTop: 150}/>
</div>
_renderInput: =>
if @state.errorMessage
<input type="text"
value={@state.token}
onChange={@_onTokenChange}
placeholder="Invitation Code"
className="token-input error" />
else
<input type="text"
value={@state.token}
onChange={@_onTokenChange}
placeholder="Invitation Code"
className="token-input" />
_renderContinueButton: =>
if @state.tokenAuthInflight
<button className="btn btn-large btn-disabled" type="button">
<RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} /> Checking&hellip;
</button>
else
<button className="btn btn-large btn-gradient" type="button" onClick={@_onContinue}>Continue</button>
_renderContinueError: =>
if @state.tokenValidityError
<div className="errormsg" role="alert">
{@state.tokenValidityError}
</div>
else
<div></div>
_onTokenChange: (event) =>
@setState(token: event.target.value)
_onContinue: =>
if @state.tokenAuthInflight
return
if not @state.token
@setState({
tokenAuthInflight: false,
tokenValidityError: "Please enter an invitation code."
})
@_resize()
return
@setState({tokenAuthInflight: true})
TokenAuthAPI.request
path: "/token/#{@state.token}"
returnsModel: false
timeout: 30000
success: (json) =>
atom.config.set("edgehill.token", @state.token)
OnboardingActions.moveToPage("account-choose")
error: (err) =>
_.delay =>
@setState
tokenValidityError: err.message
tokenAuthInflight: false
@_resize()
, 400
_resize: =>
setTimeout( =>
@props.onResize?()
,10)
module.exports = TokenAuthPage

View file

@ -2,6 +2,7 @@ React = require 'react'
shell = require 'shell'
classnames = require 'classnames'
{RetinaImg, TimeoutTransitionGroup} = require 'nylas-component-kit'
PageRouterStore = require './page-router-store'
OnboardingActions = require './onboarding-actions'
class WelcomePage extends React.Component
@ -119,6 +120,9 @@ class WelcomePage extends React.Component
if @state.step < 2
@setState(step: @state.step + 1)
else
OnboardingActions.moveToPage("account-choose")
if PageRouterStore.tokenAuthEnabled() is "no"
OnboardingActions.moveToPage("account-choose")
else
OnboardingActions.moveToPage("token-auth")
module.exports = WelcomePage

View file

@ -88,10 +88,9 @@
}
.environment-selector {
position: absolute;
opacity:0.1;
top: 244px;
width:292px;
margin: 0 auto 20px auto;
select {
width:100%;
}
@ -163,20 +162,20 @@
left:25px;
}
.page-enter {
.alpha-fade-enter {
opacity: 0.01;
transition: all .15s ease-out;
}
.page-enter.page-enter-active {
.alpha-fade-enter.alpha-fade-enter-active {
opacity: 1;
}
.page-leave {
.alpha-fade-leave {
opacity: 1;
transition: all .15s ease-in;
}
.page-leave.page-leave-active {
.alpha-fade-leave.alpha-fade-leave-active {
opacity: 0.01;
}
@ -348,6 +347,43 @@
}
}
.page.token-auth.token-auth-enabled {
height: auto;
padding-bottom:25px;
}
.page.token-auth{
width: 388px;
height: 405px;
position: relative;
padding-top: 50px;
img.logo.content-mask {
background-color: rgba(255,255,255,0.4);
}
.caption {
font-size: 17px;
color: rgba(0,0,0,0.56);
}
.token-label {
max-width: 75%;
}
input {
display:block;
padding:10px;
width:75%;
margin: 0 auto 20px auto;
background-color: #F5F5F5;
text-align: center;
}
input.error {
border: 1px solid #A33;
}
}
.initial-package {
display:block;
margin:auto;