mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-04 19:54:32 +08:00
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:
parent
11086761dd
commit
467955dc2c
10 changed files with 305 additions and 29 deletions
|
@ -7,3 +7,4 @@
|
|||
"calendar-bar"
|
||||
]
|
||||
'updateLevel': 'patch'
|
||||
'env': 'production'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, you’ll 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
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ OnboardingActions = Reflux.createActions [
|
|||
"moveToPreviousPage"
|
||||
"moveToPage"
|
||||
"accountJSONReceived"
|
||||
"retryCheckTokenAuthStatus"
|
||||
]
|
||||
|
||||
for key, action of OnboardingActions
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
58
internal_packages/onboarding/lib/token-auth-api.coffee
Normal file
58
internal_packages/onboarding/lib/token-auth-api.coffee
Normal 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
|
150
internal_packages/onboarding/lib/token-auth-page.cjsx
Normal file
150
internal_packages/onboarding/lib/token-auth-page.cjsx
Normal 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…
|
||||
</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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue