mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-10 00:11:34 +08:00
Periodically refresh identity, show expired notice in top bar
This commit is contained in:
parent
5dc39efe98
commit
80c3c7b956
7 changed files with 136 additions and 35 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint global-require: 0 */
|
/* eslint global-require: 0 */
|
||||||
import {AccountStore, Account, Actions, React} from 'nylas-exports'
|
import {AccountStore, Account, Actions, React, IdentityStore} from 'nylas-exports'
|
||||||
import {RetinaImg} from 'nylas-component-kit'
|
import {RetinaImg} from 'nylas-component-kit'
|
||||||
|
import {shell} from 'electron';
|
||||||
|
|
||||||
export default class AccountErrorHeader extends React.Component {
|
export default class AccountErrorHeader extends React.Component {
|
||||||
static displayName = 'AccountErrorHeader';
|
static displayName = 'AccountErrorHeader';
|
||||||
|
|
@ -58,7 +59,15 @@ export default class AccountErrorHeader extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderErrorHeader(message, buttonName, actionCallback) {
|
_onUpgrade = () => {
|
||||||
|
this.setState({buildingUpgradeURL: true});
|
||||||
|
IdentityStore.fetchSingleSignOnURL('/dashboard').then((url) => {
|
||||||
|
this.setState({buildingUpgradeURL: false});
|
||||||
|
shell.openExternal(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderErrorHeader(message, buttonName, actionCallback) {
|
||||||
return (
|
return (
|
||||||
<div className="account-error-header notifications-sticky">
|
<div className="account-error-header notifications-sticky">
|
||||||
<div
|
<div
|
||||||
|
|
@ -84,28 +93,59 @@ export default class AccountErrorHeader extends React.Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderUpgradeHeader() {
|
||||||
|
return (
|
||||||
|
<div className="account-error-header notifications-sticky">
|
||||||
|
<div
|
||||||
|
className={"notifications-sticky-item notification-upgrade has-default-action"}
|
||||||
|
onClick={this._onUpgrade}
|
||||||
|
>
|
||||||
|
<RetinaImg
|
||||||
|
className="icon"
|
||||||
|
name="ic-upgrade.png"
|
||||||
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
|
/>
|
||||||
|
<div className="message">
|
||||||
|
Your 30-day trial has expired and we've paused your mailboxes. Upgrade today to continue using N1!
|
||||||
|
</div>
|
||||||
|
<a className="action refresh" onClick={this._onCheckAgain}>
|
||||||
|
{this.state.refreshing ? "Checking..." : "Check Again"}
|
||||||
|
</a>
|
||||||
|
<a className="action default" onClick={this._onUpgrade}>
|
||||||
|
{this.state.buildingUpgradeURL ? "Please wait..." : "Upgrade to Nylas Pro..."}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const errorAccounts = this.state.accounts.filter(a => a.hasSyncStateError());
|
const errorAccounts = this.state.accounts.filter(a => a.hasSyncStateError());
|
||||||
|
const trialExpiredAccounts = errorAccounts.filter(a => a.trialExpirationDate && (a.trialExpirationDate < new Date()))
|
||||||
|
|
||||||
|
if (trialExpiredAccounts.length > 0 || true) {
|
||||||
|
return this._renderUpgradeHeader(trialExpiredAccounts)
|
||||||
|
}
|
||||||
|
|
||||||
if (errorAccounts.length === 1) {
|
if (errorAccounts.length === 1) {
|
||||||
const account = errorAccounts[0];
|
const account = errorAccounts[0];
|
||||||
|
|
||||||
switch (account.syncState) {
|
switch (account.syncState) {
|
||||||
|
|
||||||
case Account.SYNC_STATE_AUTH_FAILED:
|
case Account.SYNC_STATE_AUTH_FAILED:
|
||||||
return this.renderErrorHeader(
|
return this._renderErrorHeader(
|
||||||
`Nylas N1 can no longer authenticate with ${account.emailAddress}. Click here to reconnect.`,
|
`Nylas N1 can no longer authenticate with ${account.emailAddress}. Click here to reconnect.`,
|
||||||
"Reconnect",
|
"Reconnect",
|
||||||
() => this._reconnect(account));
|
() => this._reconnect(account));
|
||||||
|
|
||||||
case Account.SYNC_STATE_STOPPED:
|
case Account.SYNC_STATE_STOPPED:
|
||||||
return this.renderErrorHeader(
|
return this._renderErrorHeader(
|
||||||
`The cloud sync for ${account.emailAddress} has been disabled. You will
|
`The cloud sync for ${account.emailAddress} has been disabled. You will
|
||||||
not be able to send or receive mail. Please contact Nylas support.`,
|
not be able to send or receive mail. Please contact Nylas support.`,
|
||||||
"Contact support",
|
"Contact support",
|
||||||
() => this._contactSupport());
|
() => this._contactSupport());
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return this.renderErrorHeader(
|
return this._renderErrorHeader(
|
||||||
`Nylas encountered an error while syncing mail for ${account.emailAddress} - we're
|
`Nylas encountered an error while syncing mail for ${account.emailAddress} - we're
|
||||||
looking into it. Contact Nylas support for details.`,
|
looking into it. Contact Nylas support for details.`,
|
||||||
"Contact support",
|
"Contact support",
|
||||||
|
|
@ -113,7 +153,7 @@ export default class AccountErrorHeader extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errorAccounts.length > 1) {
|
if (errorAccounts.length > 1) {
|
||||||
return this.renderErrorHeader("Several of your accounts are having issues. " +
|
return this._renderErrorHeader("Several of your accounts are having issues. " +
|
||||||
"You will not be able to send or receive mail. Click here to manage your accounts.",
|
"You will not be able to send or receive mail. Click here to manage your accounts.",
|
||||||
"Open preferences",
|
"Open preferences",
|
||||||
() => this._openPreferences());
|
() => this._openPreferences());
|
||||||
|
|
|
||||||
|
|
@ -135,14 +135,15 @@
|
||||||
.notification-developer {
|
.notification-developer {
|
||||||
background-color: #615396;
|
background-color: #615396;
|
||||||
}
|
}
|
||||||
|
.notification-upgrade {
|
||||||
|
background-image: -webkit-linear-gradient(bottom, #429E91, #40b1ac);
|
||||||
|
img { background-color: @text-color-inverse; }
|
||||||
|
}
|
||||||
.notification-error {
|
.notification-error {
|
||||||
background: linear-gradient(to top, darken(@background-color-error, 4%) 0%, @background-color-error 100%);
|
background: linear-gradient(to top, darken(@background-color-error, 4%) 0%, @background-color-error 100%);
|
||||||
border-color: @background-color-error;
|
border-color: @background-color-error;
|
||||||
color: @color-error;
|
color: @color-error;
|
||||||
}
|
}
|
||||||
.notification-success {
|
|
||||||
border-color: @background-color-success;
|
|
||||||
}
|
|
||||||
.notification-offline {
|
.notification-offline {
|
||||||
background: linear-gradient(to top, darken(#CC9900, 4%) 0%, #CC9900 100%);
|
background: linear-gradient(to top, darken(#CC9900, 4%) 0%, #CC9900 100%);
|
||||||
border-color: darken(#CC9900, 5%);
|
border-color: darken(#CC9900, 5%);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Actions, NylasAPI, IdentityStore} from 'nylas-exports';
|
import {Actions, IdentityStore} from 'nylas-exports';
|
||||||
import {RetinaImg} from 'nylas-component-kit';
|
import {RetinaImg} from 'nylas-component-kit';
|
||||||
|
import {shell} from 'electron';
|
||||||
|
|
||||||
class OpenIdentityPageButton extends React.Component {
|
class OpenIdentityPageButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
destination: React.PropTypes.string,
|
path: React.PropTypes.string,
|
||||||
label: React.PropTypes.string,
|
label: React.PropTypes.string,
|
||||||
img: React.PropTypes.string,
|
img: React.PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
@ -17,13 +18,10 @@ class OpenIdentityPageButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClick = () => {
|
_onClick = () => {
|
||||||
this.setState({loading: true});
|
IdentityStore.fetchSingleSignOnURL(this.props.path).then((url) => {
|
||||||
const identity = IdentityStore.identity();
|
this.setState({loading: false});
|
||||||
if (!identity) { return }
|
shell.openExternal(url);
|
||||||
NylasAPI.navigateToBillingSite()
|
});
|
||||||
.then(() => {
|
|
||||||
this.setState({loading: false})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -37,7 +35,7 @@ class OpenIdentityPageButton extends React.Component {
|
||||||
}
|
}
|
||||||
if (this.props.img) {
|
if (this.props.img) {
|
||||||
return (
|
return (
|
||||||
<div className="btn">
|
<div className="btn" onClick={this._onClick}>
|
||||||
<RetinaImg name={this.props.img} mode={RetinaImg.Mode.ContentPreserve} />
|
<RetinaImg name={this.props.img} mode={RetinaImg.Mode.ContentPreserve} />
|
||||||
{this.props.label}
|
{this.props.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,7 +90,7 @@ class PreferencesIdentity extends React.Component {
|
||||||
<div className="name">{firstname} {lastname}</div>
|
<div className="name">{firstname} {lastname}</div>
|
||||||
<div className="email">{email}</div>
|
<div className="email">{email}</div>
|
||||||
<div className="identity-actions">
|
<div className="identity-actions">
|
||||||
<OpenIdentityPageButton label="Account Details" destination="/billing" />
|
<OpenIdentityPageButton label="Account Details" path="/dashboard" />
|
||||||
<div className="btn" onClick={() => Actions.logoutNylasIdentity()}>Sign Out</div>
|
<div className="btn" onClick={() => Actions.logoutNylasIdentity()}>Sign Out</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,7 +100,7 @@ class PreferencesIdentity extends React.Component {
|
||||||
<OpenIdentityPageButton
|
<OpenIdentityPageButton
|
||||||
img="ic-upgrade.png"
|
img="ic-upgrade.png"
|
||||||
label="Upgrade to Nylas Pro"
|
label="Upgrade to Nylas Pro"
|
||||||
destination="/billing"
|
path="/dashboard#subscription"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
React = require 'react'
|
React = require 'react'
|
||||||
ReactDOM = require 'react-dom'
|
ReactDOM = require 'react-dom'
|
||||||
{Utils,
|
{Utils,
|
||||||
NylasAPI,
|
|
||||||
RegExpUtils,
|
RegExpUtils,
|
||||||
IdentityStore,
|
IdentityStore,
|
||||||
SearchableComponentMaker,
|
SearchableComponentMaker,
|
||||||
|
|
@ -163,7 +162,9 @@ class EventedIFrame extends React.Component
|
||||||
# If this is a link to our billing site, attempt single sign on instead of
|
# If this is a link to our billing site, attempt single sign on instead of
|
||||||
# just following the link directly
|
# just following the link directly
|
||||||
if rawHref.startsWith(IdentityStore.URLRoot)
|
if rawHref.startsWith(IdentityStore.URLRoot)
|
||||||
NylasAPI.navigateToBillingSite(IdentityStore.identity(), '/billing')
|
path = rawHref.split(IdentityStore.URLRoot).pop()
|
||||||
|
IdentityStore.fetchSingleSignOnURL(IdentityStore.identity(), path).then (href) =>
|
||||||
|
NylasEnv.windowEventHandler.openLink(href: href, metaKey: e.metaKey)
|
||||||
return
|
return
|
||||||
|
|
||||||
# It's important to send the raw `href` here instead of the target.
|
# It's important to send the raw `href` here instead of the target.
|
||||||
|
|
|
||||||
|
|
@ -58,26 +58,27 @@ export default class Account extends ModelWithMetadata {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
label: Attributes.String({
|
label: Attributes.String({
|
||||||
queryable: false,
|
|
||||||
modelKey: 'label',
|
modelKey: 'label',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
aliases: Attributes.Object({
|
aliases: Attributes.Object({
|
||||||
queryable: false,
|
|
||||||
modelKey: 'aliases',
|
modelKey: 'aliases',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
defaultAlias: Attributes.Object({
|
defaultAlias: Attributes.Object({
|
||||||
queryable: false,
|
|
||||||
modelKey: 'defaultAlias',
|
modelKey: 'defaultAlias',
|
||||||
jsonKey: 'default_alias',
|
jsonKey: 'default_alias',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
syncState: Attributes.String({
|
syncState: Attributes.String({
|
||||||
queryable: false,
|
|
||||||
modelKey: 'syncState',
|
modelKey: 'syncState',
|
||||||
jsonKey: 'sync_state',
|
jsonKey: 'sync_state',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
trialExpirationDate: Attributes.DateTime({
|
||||||
|
modelKey: 'trialExpirationDate',
|
||||||
|
jsonKey: 'trial_expiration_date',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(args) {
|
constructor(args) {
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,9 @@ class NylasAPI
|
||||||
if NylasEnv.getLoadSettings().isSpec
|
if NylasEnv.getLoadSettings().isSpec
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|
||||||
|
NylasAPIRequest ?= require('./nylas-api-request').default
|
||||||
|
req = new NylasAPIRequest(@, options)
|
||||||
|
|
||||||
success = (body) =>
|
success = (body) =>
|
||||||
if options.beforeProcessing
|
if options.beforeProcessing
|
||||||
body = options.beforeProcessing(body)
|
body = options.beforeProcessing(body)
|
||||||
|
|
@ -124,19 +127,19 @@ class NylasAPI
|
||||||
Promise.resolve(body)
|
Promise.resolve(body)
|
||||||
|
|
||||||
error = (err) =>
|
error = (err) =>
|
||||||
|
{url, auth, returnsModel} = req.options
|
||||||
|
|
||||||
handlePromise = Promise.resolve()
|
handlePromise = Promise.resolve()
|
||||||
if err.response
|
if err.response
|
||||||
if err.response.statusCode is 404 and options.returnsModel
|
if err.response.statusCode is 404 and returnsModel
|
||||||
handlePromise = @_handleModel404(options.url)
|
handlePromise = @_handleModel404(url)
|
||||||
if err.response.statusCode in [401, 403]
|
if err.response.statusCode in [401, 403]
|
||||||
handlePromise = @_handleAuthenticationFailure(options.url, options.auth?.user)
|
handlePromise = @_handleAuthenticationFailure(url, auth?.user)
|
||||||
if err.response.statusCode is 400
|
if err.response.statusCode is 400
|
||||||
NylasEnv.reportError(err)
|
NylasEnv.reportError(err)
|
||||||
handlePromise.finally ->
|
handlePromise.finally ->
|
||||||
Promise.reject(err)
|
Promise.reject(err)
|
||||||
|
|
||||||
NylasAPIRequest ?= require('./nylas-api-request').default
|
|
||||||
req = new NylasAPIRequest(@, options)
|
|
||||||
req.run().then(success, error)
|
req.run().then(success, error)
|
||||||
|
|
||||||
longConnection: (opts) ->
|
longConnection: (opts) ->
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import NylasStore from 'nylas-store';
|
||||||
import Actions from '../actions';
|
import Actions from '../actions';
|
||||||
import keytar from 'keytar';
|
import keytar from 'keytar';
|
||||||
import {ipcRenderer} from 'electron';
|
import {ipcRenderer} from 'electron';
|
||||||
|
import request from 'request';
|
||||||
|
|
||||||
const configIdentityKey = "nylas.identity";
|
const configIdentityKey = "nylas.identity";
|
||||||
const keytarServiceName = 'Nylas';
|
const keytarServiceName = 'Nylas';
|
||||||
|
|
@ -24,6 +25,11 @@ class IdentityStore extends NylasStore {
|
||||||
this.trigger();
|
this.trigger();
|
||||||
});
|
});
|
||||||
this._loadIdentity();
|
this._loadIdentity();
|
||||||
|
|
||||||
|
if (NylasEnv.isWorkWindow() && ['staging', 'production'].includes(NylasEnv.config.get('env'))) {
|
||||||
|
setInterval(this.refreshStatus, 1000 * 60 * 60);
|
||||||
|
this.refreshStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadIdentity() {
|
_loadIdentity() {
|
||||||
|
|
@ -42,7 +48,58 @@ class IdentityStore extends NylasStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
trialDaysRemaining() {
|
trialDaysRemaining() {
|
||||||
return this._trialDaysRemaining;
|
return 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshStatus = () => {
|
||||||
|
request({
|
||||||
|
method: 'GET',
|
||||||
|
url: `${this.URLRoot}/n1/user`,
|
||||||
|
auth: {
|
||||||
|
username: this._identity.token,
|
||||||
|
password: '',
|
||||||
|
sendImmediately: true,
|
||||||
|
},
|
||||||
|
}, (error, response = {}, body) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
try {
|
||||||
|
const nextIdentity = Object.assign({}, this._identity, JSON.parse(body));
|
||||||
|
this._onSetNylasIdentity(nextIdentity)
|
||||||
|
} catch (err) {
|
||||||
|
NylasEnv.reportError("IdentityStore.refreshStatus: invalid JSON in response body.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSingleSignOnURL(path) {
|
||||||
|
if (!this._identity) {
|
||||||
|
return Promise.reject(new Error("fetchSingleSignOnURL: no identity set."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
return Promise.reject(new Error("fetchSingleSignOnURL: path must start with a leading slash."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${this.URLRoot}/n1/login-link`,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
next_path: path,
|
||||||
|
account_token: this._identity.token,
|
||||||
|
},
|
||||||
|
}, (error, response = {}, body) => {
|
||||||
|
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.
|
||||||
|
resolve(`${this.URLRoot}${path}`);
|
||||||
|
} else {
|
||||||
|
resolve(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLogoutNylasIdentity = () => {
|
_onLogoutNylasIdentity = () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue