fix(onboarding): Tweaks, styles, etc. for new onboarding experience

Summary:
Remove logout menu item and buttons, turn Link External Account to Add Account

Onboarding window starts hidden, is shown when react component is mounted and sized

Use get/setBounds to animate position and size at the same time smoothly

Fix specs, change 401 notice

Delay bouncing to Gmail to show users the Gmail screen momentarily

Make the animated resizing code defer so it doesn't run in a hard loop, and other animations can run at the same time

Bring back crossfade between screens, remove left/right shift on welcome screens

Test Plan: Run tests

Reviewers: drew, evan

Reviewed By: evan

Maniphest Tasks: T3529

Differential Revision: https://phab.nylas.com/D2054
This commit is contained in:
Ben Gotow 2015-09-23 09:59:34 -07:00
parent a7815b098b
commit 213bbde692
14 changed files with 105 additions and 118 deletions

View file

@ -1,5 +1,5 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg} = require 'nylas-component-kit'
{EdgehillAPI, Utils} = require 'nylas-exports'
@ -61,26 +61,32 @@ class AccountChoosePage extends Page
_onChooseProvider: (provider) =>
if provider.name is 'gmail'
provider.clientKey = Utils.generateTempId()[6..]+'-'+Utils.generateTempId()[6..]
shell = require 'shell'
googleUrl = url.format({
protocol: 'https'
host: 'accounts.google.com/o/oauth2/auth'
query:
response_type: 'code'
state: provider.clientKey
client_id: '372024217839-cdsnrrqfr4d6b4gmlqepd7v0n0l0ip9q.apps.googleusercontent.com'
redirect_uri: 'http://localhost:5009/oauth/google/callback'
access_type: 'offline'
scope: 'https://www.googleapis.com/auth/userinfo.email \
https://www.googleapis.com/auth/userinfo.profile \
https://mail.google.com/ \
https://www.google.com/m8/feeds \
https://www.googleapis.com/auth/calendar'
})
shell.openExternal(googleUrl)
# Show the "Sign in to Gmail" prompt for a moment before actually bouncing
# to Gmail. (400msec animation + 200msec to read)
_.delay =>
@_onBounceToGmail(provider)
, 600
OnboardingActions.moveToPage("account-settings", {provider})
_onBounceToGmail: (provider) =>
provider.clientKey = Utils.generateTempId()[6..]+'-'+Utils.generateTempId()[6..]
shell = require 'shell'
googleUrl = url.format({
protocol: 'https'
host: 'accounts.google.com/o/oauth2/auth'
query:
response_type: 'code'
state: provider.clientKey
client_id: '372024217839-cdsnrrqfr4d6b4gmlqepd7v0n0l0ip9q.apps.googleusercontent.com'
redirect_uri: "#{EdgehillAPI.APIRoot}/oauth/google/callback"
access_type: 'offline'
scope: 'https://www.googleapis.com/auth/userinfo.email \
https://www.googleapis.com/auth/userinfo.profile \
https://mail.google.com/ \
https://www.google.com/m8/feeds \
https://www.googleapis.com/auth/calendar'
})
shell.openExternal(googleUrl)
_onSubmit: (e) =>
valid = React.findDOMNode(@refs.form).reportValidity()

View file

@ -22,17 +22,18 @@ class PageRouter extends React.Component
pageData: PageRouterStore.pageData()
componentDidMount: =>
atom.setSize(667,482)
@unsubscribe = PageRouterStore.listen(@_onStateChanged, @)
{width, height} = React.findDOMNode(@refs.activePage).getBoundingClientRect()
atom.center()
atom.setSizeAnimated(width, height, 0)
atom.show()
componentDidUpdate: =>
setTimeout( =>
@_resizePage()
,10)
setTimeout(@_resizePage, 10)
_resizePage: =>
{width,height} = React.findDOMNode(@refs.container).getBoundingClientRect()
atom.setSizeAnimated(width,height)
{width, height} = React.findDOMNode(@refs.activePage).getBoundingClientRect()
atom.setSizeAnimated(width, height)
_onStateChanged: => @setState(@_getStateFromStore())
@ -41,20 +42,17 @@ class PageRouter extends React.Component
render: =>
<div className="page-frame">
{@_renderDragRegion()}
<div
className="page-container"
ref="container"
<ReactCSSTransitionGroup
transitionName="page"
leaveTimeout={150}
enterTimeout={150}>
{@_renderCurrentPage()}
</div>
{@_renderGradients()}
<div className="page-background" style={background: "#f6f7f8"}/>
{@_renderCurrentPageGradient()}
</ReactCSSTransitionGroup>
<div className="page-background" style={background: "#f6f7f8"}/>
</div>
_renderGradients: =>
_renderCurrentPageGradient: =>
gradient = @state.pageData?.provider?.color
if gradient
background = "linear-gradient(to top, #f6f7f8, #{gradient})"
@ -62,22 +60,20 @@ class PageRouter extends React.Component
background = "linear-gradient(to top, #f6f7f8 0%, rgba(255,255,255,0) 100%),
linear-gradient(to right, #e1e58f 0%, #a8d29e 50%, #8bc9c9 100%)"
<div className="page-gradient" style={background: background}/>
<div className="page-gradient" key={"#{@state.page}-gradient"} style={background: background}/>
_renderCurrentPage: =>
switch @state.page
when "welcome"
<WelcomePage key="welcome" pageData={@state.pageData} />
when "account-choose"
<AccountChoosePage key="account-choose" pageData={@state.pageData} />
when "account-settings"
<AccountSettingsPage key="account-settings" pageData={@state.pageData} onResize={@_resizePage} />
when "initial-preferences"
<InitialPreferencesPage key="initial-preferences" pageData={@state.pageData} />
when "initial-packages"
<InitialPackagesPage key="initial-packages" pageData={@state.pageData} />
else
<div></div>
Component = {
"welcome": WelcomePage
"account-choose": AccountChoosePage
"account-settings": AccountSettingsPage
"initial-preferences": InitialPreferencesPage
"initial-packages": InitialPackagesPage
}[@state.page]
<div key={@state.page} className="page-container">
<Component pageData={@state.pageData} ref="activePage" onResize={@_resizePage}/>
</div>
_renderDragRegion: ->
styles =

View file

@ -86,17 +86,15 @@
.page-container {
z-index: 10;
display: inline-block;
position: relative;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.page {
//position: absolute;
//top: 0;
//width: 100%;
//height: 100%;
margin:auto;
padding-top: 10%;
z-index: 10;
&.no-top {
padding-top: 0;
}
@ -169,24 +167,20 @@
position:absolute;
}
.welcome-image-enter {
transform: translate(10%, 0);
opacity: 0;
transition: all .3s linear;
transition: opacity .3s linear;
}
.welcome-image-enter.welcome-image-enter-active {
opacity: 1;
transform: translate(0%, 0);
}
.welcome-image-leave {
opacity: 1;
transform: translate(0%, 0);
transition: all .3s linear;
transition: opacity .3s linear;
}
.welcome-image-leave.welcome-image-leave-active {
opacity: 0;
transform: translate(-10%, 0);
}
.check {

View file

@ -26,10 +26,6 @@ class PreferencesAccounts extends React.Component
</div>
{@_renderLinkedAccounts()}
<div style={textAlign:"left", marginTop: '20'}>
<button className="btn btn-large" onClick={@_onLogout}>Log out</button>
</div>
</div>
_renderAccounts: =>
@ -123,8 +119,4 @@ class PreferencesAccounts extends React.Component
EdgehillAPI.unlinkToken(token)
return
_onLogout: =>
atom.logout()
module.exports = PreferencesAccounts

View file

@ -6,8 +6,7 @@
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ type: 'separator' }
{ label: 'Link External Account', command: 'atom-workspace:add-account' }
{ label: 'Log Out', command: 'application:logout' }
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
{ label: 'VERSION', enabled: false }
{ label: 'Restart and Install Update', command: 'application:install-update', visible: false}
{ label: 'Check for Update', command: 'application:check-for-update', visible: false}

View file

@ -5,8 +5,7 @@
{ label: '&New Message', command: 'application:new-message' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ label: 'Link External Account', command: 'atom-workspace:add-account' }
{ label: 'Log Out', command: 'application:logout' }
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
{ label: 'Clos&e Window', command: 'window:close' }
{ label: 'Quit', command: 'application:quit' }
]

View file

@ -1,5 +1,5 @@
'menu': [
{ label: 'Link External Account', command: 'atom-workspace:add-account' }
{ label: 'Add Account...', command: 'atom-workspace:add-account' }
{
label: '&Edit'
submenu: [
@ -55,7 +55,6 @@
}
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ label: 'Log Out', command: 'application:logout' }
{ type: 'separator' }
{ label: 'E&xit', command: 'application:quit' }
]

View file

@ -83,19 +83,19 @@ describe "ActionBridge", ->
describe "when called with TargetWindows.ALL", ->
it "should broadcast the action over IPC to all windows", ->
spyOn(ipc, 'send')
Actions.logout.firing = false
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'logout', [{oldModel: '1', newModel: 2}])
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'logout', '[{"oldModel":"1","newModel":2}]')
Actions.didPassivelyReceiveNewModels.firing = false
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'didPassivelyReceiveNewModels', [{oldModel: '1', newModel: 2}])
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'didPassivelyReceiveNewModels', '[{"oldModel":"1","newModel":2}]')
describe "when called with TargetWindows.WORK", ->
it "should broadcast the action over IPC to the main window only", ->
spyOn(ipc, 'send')
Actions.logout.firing = false
@bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'logout', [{oldModel: '1', newModel: 2}])
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'logout', '[{"oldModel":"1","newModel":2}]')
Actions.didPassivelyReceiveNewModels.firing = false
@bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'didPassivelyReceiveNewModels', [{oldModel: '1', newModel: 2}])
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'didPassivelyReceiveNewModels', '[{"oldModel":"1","newModel":2}]')
it "should not do anything if the current invocation of the Action was triggered by itself", ->
spyOn(ipc, 'send')
Actions.logout.firing = true
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'logout', [{oldModel: '1', newModel: 2}])
Actions.didPassivelyReceiveNewModels.firing = true
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'didPassivelyReceiveNewModels', [{oldModel: '1', newModel: 2}])
expect(ipc.send).not.toHaveBeenCalled()

View file

@ -52,7 +52,7 @@ describe "NylasAPI", ->
spyOn(Actions, 'postNotification')
NylasAPI._handle401('/threads/1234')
expect(Actions.postNotification).toHaveBeenCalled()
expect(Actions.postNotification.mostRecentCall.args[0].message).toEqual("Nylas can no longer authenticate with your mail provider. You will not be able to send or receive mail. Please log out and sign in again.")
expect(Actions.postNotification.mostRecentCall.args[0].message).toEqual("Nylas can no longer authenticate with your mail provider. You will not be able to send or receive mail. Please unlink your account and sign in again.")
describe "handleModelResponse", ->

View file

@ -389,9 +389,6 @@ class Atom extends Model
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
logout: ->
ipc.send('command', 'application:logout')
# Public: Get the directory path to Atom's configuration area.
#
# Returns the absolute path to `~/.atom`.
@ -443,16 +440,23 @@ class Atom extends Model
# * `duration` The {Number} of pixels.
setSizeAnimated: (width, height, duration=400) ->
cubicInOut = (t) -> if t<.5 then 4*t**3 else (t-1)*(2*t-2)**2+1
{width:startWidth,height:startHeight} = @getSize()
win = @getCurrentWindow()
startBounds = win.getBounds()
startTime = Date.now()
while (t = (Date.now() - startTime) / (duration)) < 1
boundsForI = (i) ->
x: Math.round(startBounds.x + (width-startBounds.width) * -0.5 * i)
y: Math.round(startBounds.y + (height-startBounds.height) * -0.5 * i)
width: Math.round(startBounds.width + (width-startBounds.width) * i)
height: Math.round(startBounds.height + (height-startBounds.height) * i)
tick = ->
t = Math.min(1, (Date.now() - startTime) / (duration))
i = cubicInOut(t)
@setSize(
Math.round(startWidth + (width-startWidth) * i),
Math.round(startHeight + (height-startHeight) * i))
@setSize(width, height)
win.setBounds(boundsForI(i))
unless t is 1
_.defer(tick)
tick()
setMinimumWidth: (minWidth) ->
win = @getCurrentWindow()

View file

@ -159,9 +159,10 @@ class Application
@windowManager.showMainWindow()
@windowManager.ensureWorkWindow()
else
@windowManager.newOnboardingWindow().showWhenLoaded()
_logout: =>
@windowManager.newOnboardingWindow()
# The onboarding window automatically shows when it's ready
_resetConfigAndRelaunch: =>
@setDatabasePhase('close')
@windowManager.closeAllWindows()
@_deleteDatabase =>
@ -247,7 +248,7 @@ class Application
@on 'application:run-benchmarks', ->
@runBenchmarks()
@on 'application:logout', @_logout
@on 'application:reset-config-and-relaunch', @_resetConfigAndRelaunch
@on 'application:quit', => app.quit()
@on 'application:inspect', ({x,y, atomWindow}) ->

View file

@ -128,9 +128,8 @@ class WindowManager
@newWindow
title: 'Welcome to Nylas'
toolbar: false
width: 340
height: 550
resizable: false
hidden: true
windowType: 'onboarding'
windowProps:
page: "welcome"

View file

@ -74,14 +74,6 @@ class Actions
###
@didPassivelyReceiveNewModels: ActionScopeGlobal
###
Public: Log out the current user. Closes the main application window and takes
the user back to the sign-in window.
*Scope: Global*
###
@logout: ActionScopeGlobal
@uploadStateChanged: ActionScopeGlobal
@fileAborted: ActionScopeGlobal
@downloadStateChanged: ActionScopeGlobal
@ -371,7 +363,7 @@ class Actions
Public: Fire to display an in-window notification to the user in the app's standard
notification interface.
*Scope: Window*
*Scope: Global*
```
# A simple notification
@ -396,13 +388,13 @@ class Actions
```
###
@postNotification: ActionScopeWindow
@postNotification: ActionScopeGlobal
###
Public: Listen to this action to handle user interaction with notifications you
published via `postNotification`.
*Scope: Window*
*Scope: Global*
```
@_unlisten = Actions.notificationActionTaken.listen(@_onActionTaken, @)
@ -412,7 +404,7 @@ class Actions
# perform action
```
###
@notificationActionTaken: ActionScopeWindow
@notificationActionTaken: ActionScopeGlobal
# FullContact Sidebar
@getFullContactDetails: ActionScopeWindow

View file

@ -134,8 +134,13 @@ class NylasAPI
@APITokens = tokens.map (t) -> t.access_token
env = atom.config.get('env')
if not env
env = 'production'
console.error("NylasAPI: config.cson does not contain an environment \
value. Defaulting to `production`.")
if env in ['production']
@AppID = 'c96gge1jo29pl2rebcb7utsbp'
@AppID = 'eco3rpsghu81xdc48t5qugwq7'
@APIRoot = 'https://api.nylas.com'
else if env in ['staging', 'development']
@AppID = '54miogmnotxuo5st254trcmb9'
@ -229,17 +234,18 @@ class NylasAPI
type: 'error'
tag: '401'
sticky: true
message: "Nylas can no longer authenticate with your mail provider. You will not be able to send or receive mail. Please log out and sign in again.",
message: "Nylas can no longer authenticate with your mail provider. You will not be able to send or receive mail. Please unlink your account and sign in again.",
icon: 'fa-sign-out'
actions: [{
label: 'Log Out'
id: '401:logout'
label: 'Unlink'
id: '401:unlink'
}]
unless @_notificationUnlisten
handler = ({notification, action}) ->
if action.id is '401:logout'
atom.logout()
if action.id is '401:unlink'
ipc = require 'ipc'
ipc.send('command', 'application:reset-config-and-relaunch')
@_notificationUnlisten = Actions.notificationActionTaken.listen(handler, @)
return Promise.resolve()