From 467955dc2c627fa3ed916c6bca9f7beeffdb3ba5 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Sun, 4 Oct 2015 16:49:41 -0700 Subject: [PATCH] fix(onboarding): Invitation code system, just in case commit 64938016f6ffbf366a220e7abd9af6f7a4cb478b Author: Ben Gotow 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 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 Date: Sun Oct 4 16:38:52 2015 -0700 Improve a few error states, adding "checking" state when checking token commit e4b49334cbf59145d9bdd955d35636f16a7c4924 Author: EthanBlackburn Date: Sun Oct 4 16:21:39 2015 -0700 Correct retry behavior commit 11cd9a75b2a1ca0f4347160df93815743909ccea Author: EthanBlackburn Date: Sat Oct 3 18:06:55 2015 -0700 Removed old auth token variable commit afe451cd70de528def3443d8b373fd24f4aa5cde Author: EthanBlackburn Date: Sat Oct 3 16:08:12 2015 -0700 Added token auth page --- dot-nylas/config.cson | 1 + .../onboarding/lib/account-choose-page.cjsx | 20 --- .../onboarding/lib/account-settings-page.cjsx | 17 ++ .../onboarding/lib/onboarding-actions.coffee | 1 + .../onboarding/lib/page-router-store.coffee | 28 +++- .../onboarding/lib/page-router.cjsx | 5 +- .../onboarding/lib/token-auth-api.coffee | 58 +++++++ .../onboarding/lib/token-auth-page.cjsx | 150 ++++++++++++++++++ .../onboarding/lib/welcome-page.cjsx | 6 +- .../onboarding/stylesheets/onboarding.less | 48 +++++- 10 files changed, 305 insertions(+), 29 deletions(-) create mode 100644 internal_packages/onboarding/lib/token-auth-api.coffee create mode 100644 internal_packages/onboarding/lib/token-auth-page.cjsx diff --git a/dot-nylas/config.cson b/dot-nylas/config.cson index 34c243b57..81bab3e22 100644 --- a/dot-nylas/config.cson +++ b/dot-nylas/config.cson @@ -7,3 +7,4 @@ "calendar-bar" ] 'updateLevel': 'patch' + 'env': 'production' diff --git a/internal_packages/onboarding/lib/account-choose-page.cjsx b/internal_packages/onboarding/lib/account-choose-page.cjsx index 0e438c4fa..69cd707da 100644 --- a/internal_packages/onboarding/lib/account-choose-page.cjsx +++ b/internal_packages/onboarding/lib/account-choose-page.cjsx @@ -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
unless atom.inDevMode() -
- -
- - _onEnvChange: (event) => - OnboardingActions.changeAPIEnvironment(event.target.value) - module.exports = AccountChoosePage diff --git a/internal_packages/onboarding/lib/account-settings-page.cjsx b/internal_packages/onboarding/lib/account-settings-page.cjsx index 86aee0868..f2c1b28e1 100644 --- a/internal_packages/onboarding/lib/account-settings-page.cjsx +++ b/internal_packages/onboarding/lib/account-settings-page.cjsx @@ -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 diff --git a/internal_packages/onboarding/lib/onboarding-actions.coffee b/internal_packages/onboarding/lib/onboarding-actions.coffee index 56d471f40..7b092caa3 100644 --- a/internal_packages/onboarding/lib/onboarding-actions.coffee +++ b/internal_packages/onboarding/lib/onboarding-actions.coffee @@ -7,6 +7,7 @@ OnboardingActions = Reflux.createActions [ "moveToPreviousPage" "moveToPage" "accountJSONReceived" + "retryCheckTokenAuthStatus" ] for key, action of OnboardingActions diff --git a/internal_packages/onboarding/lib/page-router-store.coffee b/internal_packages/onboarding/lib/page-router-store.coffee index 8714a9635..af2077638 100644 --- a/internal_packages/onboarding/lib/page-router-store.coffee +++ b/internal_packages/onboarding/lib/page-router-store.coffee @@ -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() diff --git a/internal_packages/onboarding/lib/page-router.cjsx b/internal_packages/onboarding/lib/page-router.cjsx index d28b6c24a..2ad178700 100644 --- a/internal_packages/onboarding/lib/page-router.cjsx +++ b/internal_packages/onboarding/lib/page-router.cjsx @@ -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
{@_renderDragRegion()} {@_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 diff --git a/internal_packages/onboarding/lib/token-auth-api.coffee b/internal_packages/onboarding/lib/token-auth-api.coffee new file mode 100644 index 000000000..3c2807a6c --- /dev/null +++ b/internal_packages/onboarding/lib/token-auth-api.coffee @@ -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 diff --git a/internal_packages/onboarding/lib/token-auth-page.cjsx b/internal_packages/onboarding/lib/token-auth-page.cjsx new file mode 100644 index 000000000..b730b845f --- /dev/null +++ b/internal_packages/onboarding/lib/token-auth-page.cjsx @@ -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" +
+ + {@_renderWaitingForTokenAuthAnswer()} + +
+ + else if @state.tokenAuthEnabled is "yes" +
+
OnboardingActions.closeWindow() }> + +
+ + +
+ Due to overwhelming interest, you need an invitation code to connect + an account to N1. Enter your invitation code below, or request one here. +
+ {@_renderContinueError()} + + {@_renderContinueButton()} +
+ else +
+
+ + _renderWaitingForTokenAuthAnswer: => + if @state.tokenAuthEnabledError +
+
{@state.tokenAuthEnabledError}
+ +
+ else +
+ +
+ + _renderInput: => + if @state.errorMessage + + else + + + _renderContinueButton: => + if @state.tokenAuthInflight + + else + + + _renderContinueError: => + if @state.tokenValidityError +
+ {@state.tokenValidityError} +
+ else +
+ + _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 diff --git a/internal_packages/onboarding/lib/welcome-page.cjsx b/internal_packages/onboarding/lib/welcome-page.cjsx index 3439bb4f8..76c8232ac 100644 --- a/internal_packages/onboarding/lib/welcome-page.cjsx +++ b/internal_packages/onboarding/lib/welcome-page.cjsx @@ -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 diff --git a/internal_packages/onboarding/stylesheets/onboarding.less b/internal_packages/onboarding/stylesheets/onboarding.less index eb6674171..ed1305a01 100644 --- a/internal_packages/onboarding/stylesheets/onboarding.less +++ b/internal_packages/onboarding/stylesheets/onboarding.less @@ -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;