Add subscription tab

This commit is contained in:
Ben Gotow 2016-05-25 15:29:41 -07:00
parent bd4c25405a
commit c4f9dfb4e4
11 changed files with 229 additions and 43 deletions

View file

@ -1,8 +1,8 @@
import OnboardingActions from './onboarding-actions';
import {AccountStore, Actions, NylasAPI} from 'nylas-exports';
import {AccountStore, Actions, IdentityStore} from 'nylas-exports';
import {shell, ipcRenderer} from 'electron';
import NylasStore from 'nylas-store';
import AccountTypes from './account-types';
import {accountTypeForProvider} from './account-types';
import {buildWelcomeURL} from './onboarding-helpers';
class OnboardingStore extends NylasStore {
@ -16,23 +16,22 @@ class OnboardingStore extends NylasStore {
this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo);
this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType);
const {page, existingAccount} = NylasEnv.getWindowProps();
const {existingAccount} = NylasEnv.getWindowProps();
if (existingAccount) {
const accountType = accountTypeForProvider(existingAccount.provider);
this._pageStack = ['account-choose']
this._accountInfo = {
name: existingAccount.name,
email: existingAccount.email,
email: existingAccount.emailAddress,
};
const accountType = AccountTypes.accountTypeForProvider(existingAccount.provider);
this._onSetAccountType(accountType);
} else {
this._pageStack = [page || 'welcome'];
const N1Account = NylasAPI.N1UserAccount();
if (N1Account) {
const identity = IdentityStore.identity();
this._pageStack = ['welcome'];
if (identity) {
this._accountInfo = {
name: `${N1Account.firstname || ""} ${N1Account.lastname || ""}`,
name: `${identity.firstname || ""} ${identity.lastname || ""}`,
};
} else {
this._accountInfo = {};
@ -81,7 +80,7 @@ class OnboardingStore extends NylasStore {
_onAuthenticationJSONReceived = (json) => {
const isFirstAccount = AccountStore.accounts().length === 0;
NylasAPI.setN1UserAccount(json);
Actions.setNylasIdentity(json);
setTimeout(() => {
if (isFirstAccount) {

View file

@ -5,7 +5,7 @@ import {RetinaImg} from 'nylas-component-kit';
import OnboardingActions from './onboarding-actions';
import networkErrors from 'chromium-net-errors';
class AuthenticateLoadingCover extends React.Component {
class InitialLoadingCover extends React.Component {
static propTypes = {
ready: React.PropTypes.bool,
error: React.PropTypes.string,
@ -40,7 +40,7 @@ class AuthenticateLoadingCover extends React.Component {
if (this.props.error) {
message = this.props.error;
} else if (this.state.slow) {
message = "Still trying to reach Nylas.com...";
message = "Still trying to reach Nylas";
} else {
message = ' '
}
@ -79,7 +79,7 @@ export default class AuthenticatePage extends React.Component {
componentDidMount() {
const webview = ReactDOM.findDOMNode(this.refs.webview);
webview.src = "https://billing-staging.nylas.com/onboarding";
webview.src = `${IdentityStore.URLRoot}/onboarding`;
webview.addEventListener('did-start-loading', this.webviewDidStartLoading);
webview.addEventListener('did-fail-load', this.webviewDidFailLoad);
webview.addEventListener('did-finish-load', this.webviewDidFinishLoad);
@ -94,7 +94,7 @@ export default class AuthenticatePage extends React.Component {
}
webviewDidStartLoading = () => {
this.setState({error: null});
this.setState({error: null, webviewLoading: true});
}
webviewDidFailLoad = ({errorCode, errorDescription, validatedURL}) => {
@ -108,7 +108,7 @@ export default class AuthenticatePage extends React.Component {
const e = networkErrors.createByCode(errorCode);
error = `Could not reach ${validatedURL}. ${e ? e.message : errorCode}`;
}
this.setState({ready: false, error: error});
this.setState({ready: false, error: error, webviewLoading: false});
}
webviewDidFinishLoad = () => {
@ -122,7 +122,7 @@ export default class AuthenticatePage extends React.Component {
const webview = ReactDOM.findDOMNode(this.refs.webview);
webview.executeJavaScript(js, false, (result) => {
this.setState({ready: true});
this.setState({ready: true, webviewLoading: false});
if (result !== null) {
OnboardingActions.authenticationJSONReceived(JSON.parse(result));
}
@ -133,7 +133,14 @@ export default class AuthenticatePage extends React.Component {
return (
<div className="page authenticate">
<webview ref="webview"></webview>
<AuthenticateLoadingCover
<div className={`webview-loading-spinner loading-${this.state.webviewLoading}`}>
<RetinaImg
style={{width: 20, height: 20}}
name="inline-loading-spinner.gif"
mode={RetinaImg.Mode.ContentPreserve}
/>
</div>
<InitialLoadingCover
ready={this.state.ready}
error={this.state.error}
onTryAgain={this.onTryAgain}

View file

@ -155,6 +155,7 @@
label[for=subscribe-check] {
color: black;
white-space: inherit;
}
label.checkbox {
@ -174,7 +175,6 @@
}
}
.page.authenticate {
flex: 1;
display: flex;
@ -184,6 +184,18 @@
flex: 1;
}
.webview-loading-spinner {
position: absolute;
right: 17px;
top: 17px;
opacity: 0;
transition: opacity 200ms ease-in-out;
transition-delay: 200ms;
&.loading-true {
opacity: 1;
}
}
.webview-cover {
position: absolute;
top: 0;

View file

@ -8,7 +8,7 @@ import PreferencesAccounts from './tabs/preferences-accounts';
import PreferencesAppearance from './tabs/preferences-appearance';
import PreferencesKeymaps from './tabs/preferences-keymaps';
import PreferencesMailRules from './tabs/preferences-mail-rules';
import PreferencesIdentity from './tabs/preferences-identity';
export function activate() {
PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
@ -23,23 +23,29 @@ export function activate() {
component: PreferencesAccounts,
order: 2,
}))
PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
tabId: 'Subscription',
displayName: 'Subscription',
component: PreferencesIdentity,
order: 3,
}))
PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
tabId: 'Appearance',
displayName: 'Appearance',
component: PreferencesAppearance,
order: 3,
order: 4,
}))
PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
tabId: 'Shortcuts',
displayName: 'Shortcuts',
component: PreferencesKeymaps,
order: 4,
order: 5,
}))
PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
tabId: 'Mail Rules',
displayName: 'Mail Rules',
component: PreferencesMailRules,
order: 5,
order: 6,
}))
WorkspaceStore.defineSheet('Preferences', {}, {

View file

@ -0,0 +1,105 @@
import React from 'react';
import {shell} from 'electron';
import {RetinaImg} from 'nylas-component-kit';
import {IdentityStore} from 'nylas-exports';
import request from 'request';
class OpenIdentityPageButton extends React.Component {
static propTypes = {
destination: React.PropTypes.string,
label: React.PropTypes.string,
}
constructor(props) {
super(props);
this.state = {
loading: false,
};
}
_onClick = () => {
const identity = IdentityStore.identity();
if (!identity) {
return;
}
if (!this.props.destination.startsWith('/')) {
throw new Error("destination must start with a leading slash.");
}
this.setState({loading: true});
request({
method: 'POST',
url: `${IdentityStore.URLRoot}/n1/login-link`,
json: true,
body: {
destination: this.props.destination,
account_token: identity.token,
},
}, (error, response = {}, body) => {
this.setState({loading: false});
if (error || !body.startsWith('http')) {
// Single-sign on attempt failed. Rather than churn the user right here,
// at least try to open the page directly in the browser.
shell.openExternal(`${IdentityStore.URLRoot}${this.props.destination}`);
} else {
shell.openExternal(body);
}
});
}
render() {
if (this.state.loading) {
return (
<div className="btn btn-disabled">
<RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} />
&nbsp;{this.props.label}&hellip;
</div>
);
}
return (
<div className="btn" onClick={this._onClick}>{this.props.label}</div>
);
}
}
class PreferencesIdentity extends React.Component {
static displayName = 'PreferencesIdentity';
constructor() {
super();
this.state = this.getStateFromStores();
}
componentDidMount() {
this.unsubscribe = IdentityStore.listen(() => {
this.setState(this.getStateFromStores());
});
}
componentWillUnmount() {
this.unsubscribe();
}
getStateFromStores() {
return {
identity: IdentityStore.identity(),
};
}
render() {
return (
<div className="container-identity">
<div className="identity-content">
{JSON.stringify(this.state.identity)}
<OpenIdentityPageButton label="Go to web" destination="/billing" />
</div>
</div>
);
}
}
export default PreferencesIdentity;

View file

@ -198,26 +198,31 @@ export default class Application extends EventEmitter {
} else {
this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
title: "Welcome to N1",
windowProps: {
page: "welcome",
},
});
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);
}
}
_resetConfigAndRelaunch = () => {
_relaunchToInitialWindows = ({resetConfig, resetDatabase} = {}) => {
this.setDatabasePhase('close');
this.windowManager.destroyAllWindows();
this._deleteDatabase(() => {
this.config.set('nylas', null);
this.config.set('edgehill', null);
let fn = (callback) => callback()
if (resetDatabase) {
fn = this._deleteDatabase;
}
fn(() => {
if (resetConfig) {
this.config.set('nylas', null);
this.config.set('edgehill', null);
}
this.setDatabasePhase('setup');
this.openWindowsForTokenState();
});
}
_deleteDatabase(callback) {
_deleteDatabase = (callback) => {
this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db'), callback);
this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db-wal'));
this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db-shm'));
@ -293,7 +298,7 @@ export default class Application extends EventEmitter {
});
});
this.on('application:reset-config-and-relaunch', this._resetConfigAndRelaunch);
this.on('application:relaunch-to-initial-windows', this._relaunchToInitialWindows);
this.on('application:quit', () => {
app.quit()
@ -310,12 +315,10 @@ export default class Application extends EventEmitter {
this.on('application:add-account', ({existingAccount} = {}) => {
this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
title: "Add an Account",
windowProps: {
page: "account-choose",
pageData: {existingAccount},
},
windowProps: { existingAccount },
})
});
this.on('application:new-message', () => {
this.windowManager.sendToWindow(WindowManager.MAIN_WINDOW, 'new-message');
});

View file

@ -152,6 +152,12 @@ class Actions
###
@clearDeveloperConsole: ActionScopeWindow
###
Public: Manage the Nylas identity
###
@setNylasIdentity: ActionScopeWindow
@logoutNylasIdentity: ActionScopeWindow
###
Public: Remove the selected account

View file

@ -59,12 +59,6 @@ class NylasAPI
NylasEnv.config.onDidChange('env', @_onConfigChanged)
@_onConfigChanged()
N1UserAccount: =>
NylasEnv.config.get('nylas.identity')
setN1UserAccount: (n1Account) =>
NylasEnv.config.set('nylas.identity', n1Account)
_onConfigChanged: =>
prev = {@AppID, @APIRoot, @APITokens}

View file

@ -147,7 +147,9 @@ class AccountStore extends NylasStore
if remainingAccounts.length is 0
ipc = require('electron').ipcRenderer
ipc.send('command', 'application:reset-config-and-relaunch')
ipc.send('command', 'application:relaunch-to-initial-windows', {
resetDatabase: true,
})
_onReorderAccount: (id, newIdx) =>
existingIdx = _.findIndex @_accounts, (a) -> a.id is id

View file

@ -0,0 +1,51 @@
import NylasStore from 'nylas-store';
import Actions from '../actions';
import keytar from 'keytar';
import {ipcRenderer} from 'electron';
const configIdentityKey = "nylas.identity";
const keytarServiceName = 'Nylas';
const keytarIdentityKey = 'Nylas Account';
class IdentityStore extends NylasStore {
constructor() {
super();
this.URLRoot = "https://billing-staging.nylas.com";
this.listenTo(Actions.setNylasIdentity, this._onSetNylasIdentity);
this.listenTo(Actions.logoutNylasIdentity, this._onLogoutNylasIdentity);
NylasEnv.config.onDidChange(configIdentityKey, () => {
this._loadIdentity();
this.trigger();
});
this._loadIdentity();
}
_loadIdentity() {
this._identity = NylasEnv.config.get(configIdentityKey);
if (this._identity) {
this._identity.token = keytar.getPassword(keytarServiceName, keytarIdentityKey);
}
}
identity() {
return this._identity;
}
_onLogoutNylasIdentity = () => {
keytar.deletePassword(keytarServiceName, keytarIdentityKey);
NylasEnv.config.unset(configIdentityKey);
ipcRenderer.send('command', 'application:relaunch-to-initial-windows');
}
_onSetNylasIdentity = (identity) => {
keytar.replacePassword(keytarServiceName, keytarIdentityKey, identity.token);
delete identity.token;
NylasEnv.config.set(configIdentityKey, identity);
}
}
export default new IdentityStore()

View file

@ -122,6 +122,7 @@ class NylasExports
@lazyLoadAndRegisterStore "AccountStore", 'account-store'
@lazyLoadAndRegisterStore "MessageStore", 'message-store'
@lazyLoadAndRegisterStore "ContactStore", 'contact-store'
@lazyLoadAndRegisterStore "IdentityStore", 'identity-store'
@lazyLoadAndRegisterStore "MetadataStore", 'metadata-store'
@lazyLoadAndRegisterStore "CategoryStore", 'category-store'
@lazyLoadAndRegisterStore "UndoRedoStore", 'undo-redo-store'