mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-08 07:21:18 +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 */
|
||||
import {AccountStore, Account, Actions, React} from 'nylas-exports'
|
||||
import {AccountStore, Account, Actions, React, IdentityStore} from 'nylas-exports'
|
||||
import {RetinaImg} from 'nylas-component-kit'
|
||||
import {shell} from 'electron';
|
||||
|
||||
export default class AccountErrorHeader extends React.Component {
|
||||
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 (
|
||||
<div className="account-error-header notifications-sticky">
|
||||
<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() {
|
||||
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) {
|
||||
const account = errorAccounts[0];
|
||||
|
||||
switch (account.syncState) {
|
||||
|
||||
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.`,
|
||||
"Reconnect",
|
||||
() => this._reconnect(account));
|
||||
|
||||
case Account.SYNC_STATE_STOPPED:
|
||||
return this.renderErrorHeader(
|
||||
return this._renderErrorHeader(
|
||||
`The cloud sync for ${account.emailAddress} has been disabled. You will
|
||||
not be able to send or receive mail. Please contact Nylas support.`,
|
||||
"Contact support",
|
||||
() => this._contactSupport());
|
||||
|
||||
default:
|
||||
return this.renderErrorHeader(
|
||||
return this._renderErrorHeader(
|
||||
`Nylas encountered an error while syncing mail for ${account.emailAddress} - we're
|
||||
looking into it. Contact Nylas support for details.`,
|
||||
"Contact support",
|
||||
|
|
@ -113,7 +153,7 @@ export default class AccountErrorHeader extends React.Component {
|
|||
}
|
||||
}
|
||||
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.",
|
||||
"Open preferences",
|
||||
() => this._openPreferences());
|
||||
|
|
|
|||
|
|
@ -135,14 +135,15 @@
|
|||
.notification-developer {
|
||||
background-color: #615396;
|
||||
}
|
||||
.notification-upgrade {
|
||||
background-image: -webkit-linear-gradient(bottom, #429E91, #40b1ac);
|
||||
img { background-color: @text-color-inverse; }
|
||||
}
|
||||
.notification-error {
|
||||
background: linear-gradient(to top, darken(@background-color-error, 4%) 0%, @background-color-error 100%);
|
||||
border-color: @background-color-error;
|
||||
color: @color-error;
|
||||
}
|
||||
.notification-success {
|
||||
border-color: @background-color-success;
|
||||
}
|
||||
.notification-offline {
|
||||
background: linear-gradient(to top, darken(#CC9900, 4%) 0%, #CC9900 100%);
|
||||
border-color: darken(#CC9900, 5%);
|
||||
|
|
@ -151,7 +152,7 @@
|
|||
.notifications-sticky-item {
|
||||
display:flex;
|
||||
font-size: @font-size-base;
|
||||
color:@text-color-inverse;
|
||||
color: @text-color-inverse;
|
||||
border-bottom:1px solid rgba(0,0,0,0.25);
|
||||
padding-left: @padding-base-horizontal;
|
||||
line-height: @line-height-base * 1.5;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import {Actions, NylasAPI, IdentityStore} from 'nylas-exports';
|
||||
import {Actions, IdentityStore} from 'nylas-exports';
|
||||
import {RetinaImg} from 'nylas-component-kit';
|
||||
import {shell} from 'electron';
|
||||
|
||||
class OpenIdentityPageButton extends React.Component {
|
||||
static propTypes = {
|
||||
destination: React.PropTypes.string,
|
||||
path: React.PropTypes.string,
|
||||
label: React.PropTypes.string,
|
||||
img: React.PropTypes.string,
|
||||
}
|
||||
|
|
@ -17,13 +18,10 @@ class OpenIdentityPageButton extends React.Component {
|
|||
}
|
||||
|
||||
_onClick = () => {
|
||||
this.setState({loading: true});
|
||||
const identity = IdentityStore.identity();
|
||||
if (!identity) { return }
|
||||
NylasAPI.navigateToBillingSite()
|
||||
.then(() => {
|
||||
this.setState({loading: false})
|
||||
})
|
||||
IdentityStore.fetchSingleSignOnURL(this.props.path).then((url) => {
|
||||
this.setState({loading: false});
|
||||
shell.openExternal(url);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
@ -37,7 +35,7 @@ class OpenIdentityPageButton extends React.Component {
|
|||
}
|
||||
if (this.props.img) {
|
||||
return (
|
||||
<div className="btn">
|
||||
<div className="btn" onClick={this._onClick}>
|
||||
<RetinaImg name={this.props.img} mode={RetinaImg.Mode.ContentPreserve} />
|
||||
{this.props.label}
|
||||
</div>
|
||||
|
|
@ -92,7 +90,7 @@ class PreferencesIdentity extends React.Component {
|
|||
<div className="name">{firstname} {lastname}</div>
|
||||
<div className="email">{email}</div>
|
||||
<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>
|
||||
</div>
|
||||
|
|
@ -102,7 +100,7 @@ class PreferencesIdentity extends React.Component {
|
|||
<OpenIdentityPageButton
|
||||
img="ic-upgrade.png"
|
||||
label="Upgrade to Nylas Pro"
|
||||
destination="/billing"
|
||||
path="/dashboard#subscription"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
React = require 'react'
|
||||
ReactDOM = require 'react-dom'
|
||||
{Utils,
|
||||
NylasAPI,
|
||||
RegExpUtils,
|
||||
IdentityStore,
|
||||
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
|
||||
# just following the link directly
|
||||
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
|
||||
|
||||
# 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({
|
||||
queryable: false,
|
||||
modelKey: 'label',
|
||||
}),
|
||||
|
||||
aliases: Attributes.Object({
|
||||
queryable: false,
|
||||
modelKey: 'aliases',
|
||||
}),
|
||||
|
||||
defaultAlias: Attributes.Object({
|
||||
queryable: false,
|
||||
modelKey: 'defaultAlias',
|
||||
jsonKey: 'default_alias',
|
||||
}),
|
||||
|
||||
syncState: Attributes.String({
|
||||
queryable: false,
|
||||
modelKey: 'syncState',
|
||||
jsonKey: 'sync_state',
|
||||
}),
|
||||
|
||||
trialExpirationDate: Attributes.DateTime({
|
||||
modelKey: 'trialExpirationDate',
|
||||
jsonKey: 'trial_expiration_date',
|
||||
}),
|
||||
});
|
||||
|
||||
constructor(args) {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ class NylasAPI
|
|||
if NylasEnv.getLoadSettings().isSpec
|
||||
return Promise.resolve()
|
||||
|
||||
NylasAPIRequest ?= require('./nylas-api-request').default
|
||||
req = new NylasAPIRequest(@, options)
|
||||
|
||||
success = (body) =>
|
||||
if options.beforeProcessing
|
||||
body = options.beforeProcessing(body)
|
||||
|
|
@ -124,19 +127,19 @@ class NylasAPI
|
|||
Promise.resolve(body)
|
||||
|
||||
error = (err) =>
|
||||
{url, auth, returnsModel} = req.options
|
||||
|
||||
handlePromise = Promise.resolve()
|
||||
if err.response
|
||||
if err.response.statusCode is 404 and options.returnsModel
|
||||
handlePromise = @_handleModel404(options.url)
|
||||
if err.response.statusCode is 404 and returnsModel
|
||||
handlePromise = @_handleModel404(url)
|
||||
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
|
||||
NylasEnv.reportError(err)
|
||||
handlePromise.finally ->
|
||||
Promise.reject(err)
|
||||
|
||||
NylasAPIRequest ?= require('./nylas-api-request').default
|
||||
req = new NylasAPIRequest(@, options)
|
||||
req.run().then(success, error)
|
||||
|
||||
longConnection: (opts) ->
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import NylasStore from 'nylas-store';
|
|||
import Actions from '../actions';
|
||||
import keytar from 'keytar';
|
||||
import {ipcRenderer} from 'electron';
|
||||
import request from 'request';
|
||||
|
||||
const configIdentityKey = "nylas.identity";
|
||||
const keytarServiceName = 'Nylas';
|
||||
|
|
@ -24,6 +25,11 @@ class IdentityStore extends NylasStore {
|
|||
this.trigger();
|
||||
});
|
||||
this._loadIdentity();
|
||||
|
||||
if (NylasEnv.isWorkWindow() && ['staging', 'production'].includes(NylasEnv.config.get('env'))) {
|
||||
setInterval(this.refreshStatus, 1000 * 60 * 60);
|
||||
this.refreshStatus();
|
||||
}
|
||||
}
|
||||
|
||||
_loadIdentity() {
|
||||
|
|
@ -42,7 +48,58 @@ class IdentityStore extends NylasStore {
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue