mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-21 20:15:52 +08:00
Add subscription tab
This commit is contained in:
parent
bd4c25405a
commit
c4f9dfb4e4
11 changed files with 229 additions and 43 deletions
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', {}, {
|
||||
|
|
105
internal_packages/preferences/lib/tabs/preferences-identity.jsx
Normal file
105
internal_packages/preferences/lib/tabs/preferences-identity.jsx
Normal 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} />
|
||||
{this.props.label}…
|
||||
</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;
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -152,6 +152,12 @@ class Actions
|
|||
###
|
||||
@clearDeveloperConsole: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Manage the Nylas identity
|
||||
###
|
||||
@setNylasIdentity: ActionScopeWindow
|
||||
@logoutNylasIdentity: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Remove the selected account
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
51
src/flux/stores/identity-store.es6
Normal file
51
src/flux/stores/identity-store.es6
Normal 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()
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Reference in a new issue