Periodically refresh identity, show expired notice in top bar

This commit is contained in:
Ben Gotow 2016-05-27 12:03:53 -07:00
parent 5dc39efe98
commit 80c3c7b956
7 changed files with 136 additions and 35 deletions

View file

@ -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());

View file

@ -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;

View file

@ -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} />
&nbsp;&nbsp;{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>

View file

@ -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.

View file

@ -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) {

View file

@ -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) ->

View file

@ -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 = () => {