mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-28 15:35:37 +08:00
Cleanup onboarding, enforce metadata.expiration as timestamp
This commit is contained in:
parent
44b00cbbf5
commit
bfaae56b59
32 changed files with 331 additions and 357 deletions
|
@ -3,7 +3,7 @@
|
|||
"main": "./lib/main",
|
||||
"version": "0.1.0",
|
||||
"serverUrl": {
|
||||
"development": "http://localhost:5100",
|
||||
"development": "http://localhost:5101",
|
||||
"staging": "https://link-staging.getmailspring.com",
|
||||
"production": "https://link.getmailspring.com"
|
||||
},
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
const AccountTypes = [
|
||||
const AccountProviders = [
|
||||
{
|
||||
type: 'gmail',
|
||||
provider: 'gmail',
|
||||
displayName: 'Gmail or G Suite',
|
||||
icon: 'ic-settings-account-gmail.png',
|
||||
headerIcon: 'setup-icon-provider-gmail.png',
|
||||
color: '#e99999',
|
||||
},
|
||||
{
|
||||
type: 'office365',
|
||||
provider: 'office365',
|
||||
displayName: 'Office 365',
|
||||
icon: 'ic-settings-account-outlook.png',
|
||||
headerIcon: 'setup-icon-provider-outlook.png',
|
||||
color: '#0078d7',
|
||||
},
|
||||
{
|
||||
type: 'yahoo',
|
||||
provider: 'yahoo',
|
||||
displayName: 'Yahoo',
|
||||
icon: 'ic-settings-account-yahoo.png',
|
||||
headerIcon: 'setup-icon-provider-yahoo.png',
|
||||
color: '#a76ead',
|
||||
},
|
||||
{
|
||||
type: 'icloud',
|
||||
provider: 'icloud',
|
||||
displayName: 'iCloud',
|
||||
icon: 'ic-settings-account-icloud.png',
|
||||
headerIcon: 'setup-icon-provider-icloud.png',
|
||||
color: '#61bfe9',
|
||||
},
|
||||
{
|
||||
type: 'fastmail',
|
||||
provider: 'fastmail',
|
||||
displayName: 'FastMail',
|
||||
title: 'Set up your account',
|
||||
icon: 'ic-settings-account-fastmail.png',
|
||||
|
@ -36,7 +36,7 @@ const AccountTypes = [
|
|||
color: '#24345a',
|
||||
},
|
||||
{
|
||||
type: 'imap',
|
||||
provider: 'imap',
|
||||
displayName: 'IMAP / SMTP',
|
||||
title: 'Set up your IMAP account',
|
||||
icon: 'ic-settings-account-imap.png',
|
||||
|
@ -45,4 +45,4 @@ const AccountTypes = [
|
|||
},
|
||||
]
|
||||
|
||||
export default AccountTypes;
|
||||
export default AccountProviders;
|
|
@ -5,26 +5,26 @@ import {RetinaImg} from 'nylas-component-kit';
|
|||
import {NylasAPIRequest, Actions} from 'nylas-exports';
|
||||
|
||||
import OnboardingActions from '../onboarding-actions';
|
||||
import {buildAndValidateAccount} from '../onboarding-helpers';
|
||||
import {finalizeAndValidateAccount} from '../onboarding-helpers';
|
||||
import FormErrorMessage from '../form-error-message';
|
||||
import AccountTypes from '../account-types'
|
||||
import AccountProviders from '../account-providers'
|
||||
|
||||
const CreatePageForForm = (FormComponent) => {
|
||||
return class Composed extends React.Component {
|
||||
static displayName = FormComponent.displayName;
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = Object.assign({
|
||||
accountInfo: JSON.parse(JSON.stringify(this.props.accountInfo)),
|
||||
account: this.props.account.clone(),
|
||||
errorFieldNames: [],
|
||||
errorMessage: null,
|
||||
}, FormComponent.validateAccountInfo(this.props.accountInfo));
|
||||
}, FormComponent.validateAccount(this.props.account));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -61,24 +61,36 @@ const CreatePageForForm = (FormComponent) => {
|
|||
}
|
||||
|
||||
onFieldChange = (event) => {
|
||||
const changes = {};
|
||||
const next = this.state.account.clone();
|
||||
|
||||
let val = event.target.value;
|
||||
if (event.target.type === 'checkbox') {
|
||||
changes[event.target.id] = event.target.checked;
|
||||
} else {
|
||||
changes[event.target.id] = event.target.value;
|
||||
if (event.target.id === 'email') {
|
||||
changes[event.target.id] = event.target.value.trim();
|
||||
}
|
||||
val = event.target.checked;
|
||||
}
|
||||
if (event.target.id === 'emailAddress') {
|
||||
val = val.trim();
|
||||
}
|
||||
|
||||
const accountInfo = Object.assign({}, this.state.accountInfo, changes);
|
||||
const {errorFieldNames, errorMessage, populated} = FormComponent.validateAccountInfo(accountInfo);
|
||||
if (event.target.id.includes('.')) {
|
||||
const [parent, key] = event.target.id.split('.');
|
||||
next[parent][key] = val;
|
||||
} else {
|
||||
next[event.target.id] = val;
|
||||
}
|
||||
|
||||
this.setState({accountInfo, errorFieldNames, errorMessage, populated, errorStatusCode: null});
|
||||
const {errorFieldNames, errorMessage, populated} = FormComponent.validateAccount(next);
|
||||
|
||||
this.setState({
|
||||
account: next,
|
||||
errorFieldNames,
|
||||
errorMessage,
|
||||
populated,
|
||||
errorStatusCode: null,
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
OnboardingActions.setAccountInfo(this.state.accountInfo);
|
||||
OnboardingActions.setAccount(this.state.account);
|
||||
this._formEl.submit();
|
||||
}
|
||||
|
||||
|
@ -90,24 +102,24 @@ const CreatePageForForm = (FormComponent) => {
|
|||
}
|
||||
|
||||
onBack = () => {
|
||||
OnboardingActions.setAccountInfo(this.state.accountInfo);
|
||||
OnboardingActions.setAccount(this.state.account);
|
||||
OnboardingActions.moveToPreviousPage();
|
||||
}
|
||||
|
||||
onConnect = (updatedAccountInfo) => {
|
||||
const accountInfo = updatedAccountInfo || this.state.accountInfo;
|
||||
onConnect = (updatedAccount) => {
|
||||
const account = updatedAccount || this.state.account;
|
||||
|
||||
this.setState({submitting: true});
|
||||
|
||||
buildAndValidateAccount(accountInfo)
|
||||
.then(({account}) => {
|
||||
finalizeAndValidateAccount(account)
|
||||
.then((validated) => {
|
||||
OnboardingActions.moveToPage('account-onboarding-success')
|
||||
OnboardingActions.accountJSONReceived(account)
|
||||
OnboardingActions.finishAndAddAccount(validated)
|
||||
})
|
||||
.catch((err) => {
|
||||
Actions.recordUserEvent('Email Account Auth Failed', {
|
||||
errorMessage: err.message,
|
||||
provider: accountInfo.type,
|
||||
provider: account.provider,
|
||||
})
|
||||
|
||||
const errorFieldNames = err.body ? (err.body.missing_fields || err.body.missing_settings || []) : []
|
||||
|
@ -117,7 +129,7 @@ const CreatePageForForm = (FormComponent) => {
|
|||
if (err.errorType === "setting_update_error") {
|
||||
errorMessage = 'The IMAP/SMTP servers for this account do not match our records. Please verify that any server names you entered are correct. If your IMAP/SMTP server has changed, first remove this account from Mailspring, then try logging in again.';
|
||||
}
|
||||
if (err.errorType && err.errorType.includes("autodiscover") && (accountInfo.type === 'exchange')) {
|
||||
if (err.errorType && err.errorType.includes("autodiscover") && (account.provider === 'exchange')) {
|
||||
errorFieldNames.push('eas_server_host')
|
||||
errorFieldNames.push('username');
|
||||
}
|
||||
|
@ -144,8 +156,8 @@ const CreatePageForForm = (FormComponent) => {
|
|||
}
|
||||
|
||||
_renderButton() {
|
||||
const {accountInfo, submitting} = this.state;
|
||||
const buttonLabel = FormComponent.submitLabel(accountInfo);
|
||||
const {account, submitting} = this.state;
|
||||
const buttonLabel = FormComponent.submitLabel(account);
|
||||
|
||||
// We're not on the last page.
|
||||
if (submitting) {
|
||||
|
@ -172,11 +184,11 @@ const CreatePageForForm = (FormComponent) => {
|
|||
// help with common problems. For instance, they may need an app password,
|
||||
// or to enable specific settings with their provider.
|
||||
_renderCredentialsNote() {
|
||||
const {errorStatusCode, accountInfo} = this.state;
|
||||
const {errorStatusCode, account} = this.state;
|
||||
if (errorStatusCode !== 401) { return false; }
|
||||
let message;
|
||||
let articleURL;
|
||||
if (accountInfo.email.includes("@yahoo.com")) {
|
||||
if (account.emailAddress.includes("@yahoo.com")) {
|
||||
message = "Have you enabled access through Yahoo?";
|
||||
articleURL = "https://support.getmailspring.com/hc/en-us/articles/115001076128";
|
||||
} else {
|
||||
|
@ -200,11 +212,11 @@ const CreatePageForForm = (FormComponent) => {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {accountInfo, errorMessage, errorFieldNames, submitting} = this.state;
|
||||
const AccountType = AccountTypes.find(a => a.type === accountInfo.type);
|
||||
const {account, errorMessage, errorFieldNames, submitting} = this.state;
|
||||
const providerConfig = AccountProviders.find(({provider}) => provider === account.provider);
|
||||
|
||||
if (!AccountType) {
|
||||
throw new Error(`Cannot find account type ${accountInfo.type}`);
|
||||
if (!providerConfig) {
|
||||
throw new Error(`Cannot find account provider ${account.provider}`);
|
||||
}
|
||||
|
||||
const hideTitle = errorMessage && errorMessage.length > 120;
|
||||
|
@ -213,21 +225,21 @@ const CreatePageForForm = (FormComponent) => {
|
|||
<div className={`page account-setup ${FormComponent.displayName}`}>
|
||||
<div className="logo-container">
|
||||
<RetinaImg
|
||||
style={{backgroundColor: AccountType.color, borderRadius: 44}}
|
||||
name={AccountType.headerIcon}
|
||||
style={{backgroundColor: providerConfig.color, borderRadius: 44}}
|
||||
name={providerConfig.headerIcon}
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
className="logo"
|
||||
/>
|
||||
</div>
|
||||
{hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(AccountType)}</h2>}
|
||||
{hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(providerConfig)}</h2>}
|
||||
<FormErrorMessage
|
||||
message={errorMessage}
|
||||
empty={FormComponent.subtitleLabel(AccountType)}
|
||||
empty={FormComponent.subtitleLabel(providerConfig)}
|
||||
/>
|
||||
{ this._renderCredentialsNote() }
|
||||
<FormComponent
|
||||
ref={(el) => { this._formEl = el; }}
|
||||
accountInfo={accountInfo}
|
||||
account={account}
|
||||
errorFieldNames={errorFieldNames}
|
||||
submitting={submitting}
|
||||
onFieldChange={this.onFieldChange}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
const FormField = (props) => {
|
||||
const field = props.field;
|
||||
let val = props.account[field];
|
||||
if (props.field.includes('.')) {
|
||||
const [parent, key] = props.field.split('.');
|
||||
val = props.account[parent][key];
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<label htmlFor={props.field}>{props.title}:</label>
|
||||
|
@ -8,9 +14,10 @@ const FormField = (props) => {
|
|||
type={props.type || "text"}
|
||||
id={props.field}
|
||||
style={props.style}
|
||||
className={(props.accountInfo[props.field] && props.errorFieldNames.includes(props.field)) ? 'error' : ''}
|
||||
className={(val && props.errorFieldNames.includes(props.field)) ? 'error' : ''}
|
||||
disabled={props.submitting}
|
||||
value={props.accountInfo[props.field] || ''}
|
||||
spellCheck="false"
|
||||
value={val || ''}
|
||||
onKeyPress={props.onFieldKeyPress}
|
||||
onChange={props.onFieldChange}
|
||||
/>
|
||||
|
@ -27,7 +34,7 @@ FormField.propTypes = {
|
|||
onFieldKeyPress: React.PropTypes.func,
|
||||
onFieldChange: React.PropTypes.func,
|
||||
errorFieldNames: React.PropTypes.array,
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
}
|
||||
|
||||
export default FormField;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Reflux from 'reflux';
|
||||
|
||||
const OnboardingActions = Reflux.createActions([
|
||||
"setAccountInfo",
|
||||
"setAccountType",
|
||||
"moveToPreviousPage",
|
||||
"moveToPage",
|
||||
"setAccount",
|
||||
"chooseAccountProvider",
|
||||
"identityJSONReceived",
|
||||
"accountJSONReceived",
|
||||
"finishAndAddAccount",
|
||||
]);
|
||||
|
||||
for (const key of Object.keys(OnboardingActions)) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import crypto from 'crypto';
|
||||
import {CommonProviderSettings} from 'imap-provider-settings';
|
||||
import {
|
||||
Account,
|
||||
NylasAPIRequest,
|
||||
IdentityStore,
|
||||
RegExpUtils,
|
||||
|
@ -11,22 +12,7 @@ import {
|
|||
|
||||
const {makeRequest, rootURLForServer} = NylasAPIRequest;
|
||||
|
||||
const IMAP_FIELDS = new Set([
|
||||
"imap_host",
|
||||
"imap_port",
|
||||
"imap_username",
|
||||
"imap_password",
|
||||
"imap_security",
|
||||
"imap_allow_insecure_ssl",
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_username",
|
||||
"smtp_password",
|
||||
"smtp_security",
|
||||
"smtp_allow_insecure_ssl",
|
||||
]);
|
||||
|
||||
function base64url(inBuffer) {
|
||||
function base64URL(inBuffer) {
|
||||
let buffer;
|
||||
if (typeof inBuffer === "string") {
|
||||
buffer = new Buffer(inBuffer);
|
||||
|
@ -55,114 +41,92 @@ function idForAccount(emailAddress, connectionSettings) {
|
|||
return crypto.createHash('sha256').update(idString, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
export function makeGmailOAuthRequest(sessionKey) {
|
||||
return makeRequest({
|
||||
server: 'accounts',
|
||||
path: `/auth/gmail/token?key=${sessionKey}`,
|
||||
method: 'GET',
|
||||
auth: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function authIMAPForGmail(serverTokenResponse) {
|
||||
// At this point, the Mailspring server has retrieved the Gmail token,
|
||||
// created an account object in the database and tested it. All we
|
||||
// need to do is save it locally, since we're confident Gmail will be
|
||||
// accessible from the local sync worker.
|
||||
const {emailAddress, refreshToken} = serverTokenResponse;
|
||||
const settings = expandAccountInfoWithCommonSettings({email: emailAddress, refreshToken, type: 'gmail'});
|
||||
|
||||
return {
|
||||
id: idForAccount(emailAddress, settings),
|
||||
provider: 'gmail',
|
||||
name,
|
||||
settings,
|
||||
emailAddress,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildGmailSessionKey() {
|
||||
return base64url(crypto.randomBytes(40));
|
||||
}
|
||||
|
||||
export function buildGmailAuthURL(sessionKey) {
|
||||
return `${rootURLForServer('accounts')}/auth/gmail?state=${sessionKey}`;
|
||||
}
|
||||
|
||||
export async function buildAndValidateAccount(accountInfo) {
|
||||
const {username, type, email, name} = accountInfo;
|
||||
|
||||
const data = {
|
||||
id: idForAccount(email, accountInfo),
|
||||
provider: type,
|
||||
name: name,
|
||||
emailAddress: email,
|
||||
settings: Object.assign({}, accountInfo),
|
||||
};
|
||||
|
||||
// handle special case for exchange/outlook/hotmail username field
|
||||
data.settings.username = username || email;
|
||||
|
||||
if (data.settings.imap_port) {
|
||||
data.settings.imap_port /= 1;
|
||||
}
|
||||
if (data.settings.smtp_port) {
|
||||
data.settings.smtp_port /= 1;
|
||||
}
|
||||
|
||||
// Only include the required IMAP fields. Auth validation does not allow extra fields
|
||||
if (type !== "gmail") {
|
||||
for (const key of Object.keys(data.settings)) {
|
||||
if (!IMAP_FIELDS.has(key)) {
|
||||
delete data.settings[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test the account locally - if it succeeds, send it to the server and test it there
|
||||
|
||||
const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), IdentityStore.identity(), data);
|
||||
const {account} = await proc.test();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
export function isValidHost(value) {
|
||||
return RegExpUtils.domainRegex().test(value) || RegExpUtils.ipAddressRegex().test(value);
|
||||
}
|
||||
|
||||
export function expandAccountInfoWithCommonSettings(accountInfo) {
|
||||
const {email, type} = accountInfo;
|
||||
const domain = email.split('@').pop().toLowerCase();
|
||||
let template = CommonProviderSettings[domain] || CommonProviderSettings[type] || {};
|
||||
export function expandAccountWithCommonSettings(account) {
|
||||
const domain = account.emailAddress.split('@').pop().toLowerCase();
|
||||
let template = CommonProviderSettings[domain] || CommonProviderSettings[account.provider] || {};
|
||||
if (template.alias) {
|
||||
template = CommonProviderSettings[template.alias];
|
||||
}
|
||||
|
||||
const usernameWithFormat = (format) => {
|
||||
if (format === 'email') {
|
||||
return email
|
||||
return account.emailAddress
|
||||
}
|
||||
if (format === 'email-without-domain') {
|
||||
return email.split('@').shift();
|
||||
return account.emailAddress.split('@').shift();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
const populated = account.clone();
|
||||
|
||||
populated.settings = Object.assign({
|
||||
imap_host: template.imap_host,
|
||||
imap_port: template.imap_port || 993,
|
||||
imap_username: usernameWithFormat(template.imap_user_format),
|
||||
imap_password: accountInfo.password,
|
||||
imap_password: populated.settings.imap_password,
|
||||
imap_security: template.imap_security || "SSL / TLS",
|
||||
imap_allow_insecure_ssl: template.imap_allow_insecure_ssl || false,
|
||||
smtp_host: template.smtp_host,
|
||||
smtp_port: template.smtp_port || 587,
|
||||
smtp_username: usernameWithFormat(template.smtp_user_format),
|
||||
smtp_password: accountInfo.password,
|
||||
smtp_password: populated.settings.smtp_password || populated.settings.imap_password,
|
||||
smtp_security: template.smtp_security || "STARTTLS",
|
||||
smtp_allow_insecure_ssl: template.smtp_allow_insecure_ssl || false,
|
||||
}, populated.settings);
|
||||
|
||||
return populated;
|
||||
}
|
||||
|
||||
export function makeGmailOAuthRequest(sessionKey) {
|
||||
return makeRequest({
|
||||
server: 'identity',
|
||||
path: `/auth/gmail/token?key=${sessionKey}`,
|
||||
method: 'GET',
|
||||
auth: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildGmailAccountFromToken(serverTokenResponse) {
|
||||
// At this point, the Mailspring server has retrieved the Gmail token,
|
||||
// created an account object in the database and tested it. All we
|
||||
// need to do is save it locally, since we're confident Gmail will be
|
||||
// accessible from the local sync worker.
|
||||
const {emailAddress, refreshToken} = serverTokenResponse;
|
||||
const account = new Account();
|
||||
account.emailAddress = emailAddress;
|
||||
account.provider = 'gmail';
|
||||
account.settings.refresh_token = refreshToken;
|
||||
return expandAccountWithCommonSettings(account);
|
||||
}
|
||||
|
||||
export function buildGmailSessionKey() {
|
||||
return base64URL(crypto.randomBytes(40));
|
||||
}
|
||||
|
||||
export function buildGmailAuthURL(sessionKey) {
|
||||
return `${rootURLForServer('identity')}/auth/gmail?state=${sessionKey}`;
|
||||
}
|
||||
|
||||
export async function finalizeAndValidateAccount(account) {
|
||||
account.id = idForAccount(account.emailAddress, account.settings);
|
||||
|
||||
// handle special case for exchange/outlook/hotmail username field
|
||||
account.settings.username = account.settings.username || account.settings.email;
|
||||
|
||||
if (account.settings.imap_port) {
|
||||
account.settings.imap_port /= 1;
|
||||
}
|
||||
if (account.settings.smtp_port) {
|
||||
account.settings.smtp_port /= 1;
|
||||
}
|
||||
|
||||
return Object.assign({}, accountInfo, defaults);
|
||||
// Test connections to IMAP and SMTP
|
||||
const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), IdentityStore.identity(), account);
|
||||
const response = await proc.test();
|
||||
return new Account(response.account);
|
||||
}
|
||||
|
||||
export function isValidHost(value) {
|
||||
return RegExpUtils.domainRegex().test(value) || RegExpUtils.ipAddressRegex().test(value);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class OnboardingRoot extends React.Component {
|
|||
return {
|
||||
page: OnboardingStore.page(),
|
||||
pageDepth: OnboardingStore.pageDepth(),
|
||||
accountInfo: OnboardingStore.accountInfo(),
|
||||
account: OnboardingStore.account(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ export default class OnboardingRoot extends React.Component {
|
|||
transitionEnterTimeout={150}
|
||||
>
|
||||
<div key={this.state.page} className="page-container">
|
||||
<Component accountInfo={this.state.accountInfo} />
|
||||
<Component account={this.state.account} />
|
||||
</div>
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
|
|
|
@ -1,65 +1,51 @@
|
|||
import {AccountStore, Actions, IdentityStore} from 'nylas-exports';
|
||||
import {AccountStore, Account, Actions, IdentityStore} from 'nylas-exports';
|
||||
import {ipcRenderer} from 'electron';
|
||||
import NylasStore from 'nylas-store';
|
||||
|
||||
import OnboardingActions from './onboarding-actions';
|
||||
|
||||
function accountTypeForProvider(provider) {
|
||||
if (provider === 'eas') {
|
||||
return 'exchange';
|
||||
}
|
||||
if (provider === 'custom') {
|
||||
return 'imap';
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
class OnboardingStore extends NylasStore {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage)
|
||||
this.listenTo(OnboardingActions.moveToPage, this._onMoveToPage)
|
||||
this.listenTo(OnboardingActions.accountJSONReceived, this._onAccountJSONReceived)
|
||||
this.listenTo(OnboardingActions.setAccount, this._onSetAccount);
|
||||
this.listenTo(OnboardingActions.chooseAccountProvider, this._onChooseAccountProvider);
|
||||
this.listenTo(OnboardingActions.finishAndAddAccount, this._onFinishAndAddAccount)
|
||||
this.listenTo(OnboardingActions.identityJSONReceived, this._onIdentityJSONReceived)
|
||||
this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo);
|
||||
this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType);
|
||||
ipcRenderer.on('set-account-type', (e, type) => {
|
||||
if (type) {
|
||||
this._onSetAccountType(type)
|
||||
|
||||
ipcRenderer.on('set-account-provider', (e, provider) => {
|
||||
if (provider) {
|
||||
this._onChooseAccountProvider(provider)
|
||||
} else {
|
||||
this._pageStack = ['account-choose']
|
||||
this.trigger()
|
||||
}
|
||||
})
|
||||
|
||||
const {existingAccount, addingAccount, accountType} = NylasEnv.getWindowProps();
|
||||
const {existingAccount, addingAccount, accountProvider} = NylasEnv.getWindowProps();
|
||||
|
||||
const hasAccounts = (AccountStore.accounts().length > 0)
|
||||
const identity = IdentityStore.identity();
|
||||
|
||||
if (identity) {
|
||||
this._accountInfo = {
|
||||
name: `${identity.firstName || ""} ${identity.lastName || ""}`,
|
||||
};
|
||||
} else {
|
||||
this._accountInfo = {};
|
||||
}
|
||||
this._account = new Account({
|
||||
name: identity ? `${identity.firstName || ""} ${identity.lastName || ""}` : '',
|
||||
emailAddress: identity ? identity.emailAddress : '',
|
||||
settings: {},
|
||||
});
|
||||
|
||||
if (existingAccount) {
|
||||
// Used when re-adding an account after re-connecting
|
||||
const existingAccountType = accountTypeForProvider(existingAccount.provider);
|
||||
this._pageStack = ['account-choose']
|
||||
this._accountInfo = {
|
||||
name: existingAccount.name,
|
||||
email: existingAccount.emailAddress,
|
||||
};
|
||||
this._onSetAccountType(existingAccountType);
|
||||
this._pageStack = ['account-choose'];
|
||||
this._account.name = existingAccount.name;
|
||||
this._account.emailAddress = existingAccount.emailAddress;
|
||||
this._onChooseAccountProvider(existingAccount.provider);
|
||||
} else if (addingAccount) {
|
||||
// Adding a new, unknown account
|
||||
this._pageStack = ['account-choose'];
|
||||
if (accountType) {
|
||||
this._onSetAccountType(accountType);
|
||||
if (accountProvider) {
|
||||
this._onChooseAccountProvider(accountProvider);
|
||||
}
|
||||
} else if (identity) {
|
||||
// Should only happen if config was edited to remove all accounts,
|
||||
|
@ -88,26 +74,34 @@ class OnboardingStore extends NylasStore {
|
|||
}, 100);
|
||||
}
|
||||
|
||||
_onSetAccountType = (type) => {
|
||||
_onChooseAccountProvider = (provider) => {
|
||||
let nextPage = "account-settings";
|
||||
if (type === 'gmail') {
|
||||
if (provider === 'gmail') {
|
||||
nextPage = "account-settings-gmail";
|
||||
} else if (type === 'exchange') {
|
||||
} else if (provider === 'exchange') {
|
||||
nextPage = "account-settings-exchange";
|
||||
}
|
||||
|
||||
Actions.recordUserEvent('Selected Account Type', {
|
||||
provider: type,
|
||||
Actions.recordUserEvent('Selected Account Provider', {
|
||||
provider,
|
||||
});
|
||||
|
||||
// Don't carry over any type-specific account information
|
||||
const {email, name, password} = this._accountInfo;
|
||||
this._onSetAccountInfo({email, name, password, type});
|
||||
this._onSetAccount(new Account({
|
||||
emailAddress: this._account.emailAddress,
|
||||
name: this._account.name,
|
||||
settings: {},
|
||||
provider,
|
||||
}));
|
||||
|
||||
this._onMoveToPage(nextPage);
|
||||
}
|
||||
|
||||
_onSetAccountInfo = (info) => {
|
||||
this._accountInfo = info;
|
||||
_onSetAccount = (acct) => {
|
||||
if (!(acct instanceof Account)) {
|
||||
throw new Error("OnboardingActions.setAccount expects an Account instance.");
|
||||
}
|
||||
this._account = acct;
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
|
@ -128,10 +122,10 @@ class OnboardingStore extends NylasStore {
|
|||
|
||||
setTimeout(() => {
|
||||
if (isFirstAccount) {
|
||||
this._onSetAccountInfo(Object.assign({}, this._accountInfo, {
|
||||
name: `${json.firstName || ""} ${json.lastName || ""}`,
|
||||
email: json.emailAddress,
|
||||
}));
|
||||
const next = this._account.clone();
|
||||
next.name = `${json.firstName || ""} ${json.lastName || ""}`;
|
||||
next.emailAddress = json.emailAddress;
|
||||
this._onSetAccount(next);
|
||||
OnboardingActions.moveToPage('account-choose');
|
||||
} else {
|
||||
this._onOnboardingComplete();
|
||||
|
@ -139,22 +133,21 @@ class OnboardingStore extends NylasStore {
|
|||
}, 1000);
|
||||
}
|
||||
|
||||
_onAccountJSONReceived = async (json) => {
|
||||
_onFinishAndAddAccount = async (account) => {
|
||||
try {
|
||||
const isFirstAccount = AccountStore.accounts().length === 0;
|
||||
AccountStore.addAccountFromJSON(json);
|
||||
|
||||
AccountStore.addAccount(account);
|
||||
NylasEnv.displayWindow();
|
||||
|
||||
Actions.recordUserEvent('Email Account Auth Succeeded', {
|
||||
provider: json.provider,
|
||||
provider: account.provider,
|
||||
});
|
||||
|
||||
ipcRenderer.send('new-account-added');
|
||||
NylasEnv.displayWindow();
|
||||
|
||||
if (isFirstAccount) {
|
||||
this._onMoveToPage('initial-preferences');
|
||||
Actions.recordUserEvent('First Account Linked', {
|
||||
provider: json.provider,
|
||||
provider: account.provider,
|
||||
});
|
||||
} else {
|
||||
// let them see the "success" screen for a moment
|
||||
|
@ -177,8 +170,8 @@ class OnboardingStore extends NylasStore {
|
|||
return this._pageStack.length;
|
||||
}
|
||||
|
||||
accountInfo() {
|
||||
return this._accountInfo;
|
||||
account() {
|
||||
return this._account;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
import React from 'react';
|
||||
import {RetinaImg} from 'nylas-component-kit';
|
||||
import OnboardingActions from './onboarding-actions';
|
||||
import AccountTypes from './account-types';
|
||||
import AccountProviders from './account-providers';
|
||||
|
||||
export default class AccountChoosePage extends React.Component {
|
||||
static displayName = "AccountChoosePage";
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
}
|
||||
|
||||
_renderAccountTypes() {
|
||||
return AccountTypes.map((accountType) =>
|
||||
_renderProviders() {
|
||||
return AccountProviders.map(({icon, displayName, provider}) =>
|
||||
<div
|
||||
key={accountType.type}
|
||||
className={`provider ${accountType.type}`}
|
||||
onClick={() => OnboardingActions.setAccountType(accountType.type)}
|
||||
key={provider}
|
||||
className={`provider ${provider}`}
|
||||
onClick={() => OnboardingActions.chooseAccountProvider(provider)}
|
||||
>
|
||||
<div className="icon-container">
|
||||
<RetinaImg
|
||||
name={accountType.icon}
|
||||
name={icon}
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
className="icon"
|
||||
/>
|
||||
</div>
|
||||
<span className="provider-name">{accountType.displayName}</span>
|
||||
<span className="provider-name">{displayName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export default class AccountChoosePage extends React.Component {
|
|||
Connect an email account
|
||||
</h2>
|
||||
<div className="provider-list">
|
||||
{this._renderAccountTypes()}
|
||||
{this._renderProviders()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types'
|
||||
import {RetinaImg} from 'nylas-component-kit';
|
||||
import AccountTypes from './account-types'
|
||||
import AccountProviders from './account-providers'
|
||||
|
||||
|
||||
class AccountOnboardingSuccess extends Component { // eslint-disable-line
|
||||
static displayName = 'AccountOnboardingSuccess'
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: PropTypes.object,
|
||||
account: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {accountInfo} = this.props
|
||||
const accountType = AccountTypes.find(a => a.type === accountInfo.type);
|
||||
const {account} = this.props;
|
||||
const providerConfig = AccountProviders.find(({provider}) => provider === account.provider);
|
||||
|
||||
return (
|
||||
<div className={`page account-setup AccountOnboardingSuccess`}>
|
||||
<div className="logo-container">
|
||||
<RetinaImg
|
||||
style={{backgroundColor: accountType.color, borderRadius: 44}}
|
||||
name={accountType.headerIcon}
|
||||
style={{backgroundColor: providerConfig.color, borderRadius: 44}}
|
||||
name={providerConfig.headerIcon}
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
className="logo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Successfully connected to {accountType.displayName}!</h2>
|
||||
<h2>Successfully connected to {providerConfig.displayName}!</h2>
|
||||
<h3>Adding your account to Mailspring…</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@ class AccountExchangeSettingsForm extends React.Component {
|
|||
static displayName = 'AccountExchangeSettingsForm';
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
errorFieldNames: React.PropTypes.array,
|
||||
submitting: React.PropTypes.bool,
|
||||
onConnect: React.PropTypes.func,
|
||||
|
@ -28,28 +28,28 @@ class AccountExchangeSettingsForm extends React.Component {
|
|||
return 'Enter your Exchange credentials to get started.';
|
||||
}
|
||||
|
||||
static validateAccountInfo = (accountInfo) => {
|
||||
const {email, password, name} = accountInfo;
|
||||
static validateAccount = (account) => {
|
||||
const {emailAddress, password, name} = account;
|
||||
const errorFieldNames = [];
|
||||
let errorMessage = null;
|
||||
|
||||
if (!email || !password || !name) {
|
||||
if (!emailAddress || !password || !name) {
|
||||
return {errorMessage, errorFieldNames, populated: false};
|
||||
}
|
||||
|
||||
if (!RegExpUtils.emailRegex().test(accountInfo.email)) {
|
||||
if (!RegExpUtils.emailRegex().test(emailAddress)) {
|
||||
errorFieldNames.push('email')
|
||||
errorMessage = "Please provide a valid email address."
|
||||
}
|
||||
if (!accountInfo.password) {
|
||||
if (!account.settings.password) {
|
||||
errorFieldNames.push('password')
|
||||
errorMessage = "Please provide a password for your account."
|
||||
}
|
||||
if (!accountInfo.name) {
|
||||
if (!account.name) {
|
||||
errorFieldNames.push('name')
|
||||
errorMessage = "Please provide your name."
|
||||
}
|
||||
if (accountInfo.eas_server_host && !isValidHost(accountInfo.eas_server_host)) {
|
||||
if (account.settings.eas_server_host && !isValidHost(account.settings.eas_server_host)) {
|
||||
errorFieldNames.push('eas_server_host')
|
||||
errorMessage = "Please provide a valid host name."
|
||||
}
|
||||
|
@ -67,13 +67,13 @@ class AccountExchangeSettingsForm extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {errorFieldNames, accountInfo} = this.props;
|
||||
const {errorFieldNames, account} = this.props;
|
||||
const showAdvanced = (
|
||||
this.state.showAdvanced ||
|
||||
errorFieldNames.includes('eas_server_host') ||
|
||||
errorFieldNames.includes('username') ||
|
||||
accountInfo.eas_server_host ||
|
||||
accountInfo.username
|
||||
account.eas_server_host ||
|
||||
account.username
|
||||
);
|
||||
|
||||
let classnames = "twocol";
|
||||
|
|
|
@ -3,20 +3,20 @@ import {OAuthSignInPage} from 'nylas-component-kit';
|
|||
|
||||
import {
|
||||
makeGmailOAuthRequest,
|
||||
authIMAPForGmail,
|
||||
buildGmailAccountFromToken,
|
||||
buildGmailSessionKey,
|
||||
buildGmailAuthURL,
|
||||
} from './onboarding-helpers';
|
||||
|
||||
import OnboardingActions from './onboarding-actions';
|
||||
import AccountTypes from './account-types';
|
||||
import AccountProviders from './account-providers';
|
||||
|
||||
|
||||
export default class AccountSettingsPageGmail extends React.Component {
|
||||
static displayName = "AccountSettingsPageGmail";
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -30,9 +30,10 @@ export default class AccountSettingsPageGmail extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {accountInfo} = this.props;
|
||||
const accountType = AccountTypes.find(a => a.type === accountInfo.type)
|
||||
const {headerIcon} = accountType;
|
||||
const providerConfig = AccountProviders.find(a =>
|
||||
a.provider === this.props.account.provider
|
||||
)
|
||||
const {headerIcon} = providerConfig;
|
||||
const goBack = () => OnboardingActions.moveToPreviousPage()
|
||||
|
||||
return (
|
||||
|
@ -41,7 +42,7 @@ export default class AccountSettingsPageGmail extends React.Component {
|
|||
providerAuthPageUrl={this._gmailAuthUrl}
|
||||
iconName={headerIcon}
|
||||
tokenRequestPollFn={makeGmailOAuthRequest}
|
||||
accountFromTokenFn={authIMAPForGmail}
|
||||
accountFromTokenFn={buildGmailAccountFromToken}
|
||||
onSuccess={this.onSuccess}
|
||||
onTryAgain={goBack}
|
||||
sessionKey={this._sessionKey}
|
||||
|
|
|
@ -7,7 +7,7 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
static displayName = 'AccountIMAPSettingsForm';
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
errorFieldNames: React.PropTypes.array,
|
||||
submitting: React.PropTypes.bool,
|
||||
onConnect: React.PropTypes.func,
|
||||
|
@ -27,24 +27,19 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
return 'Complete the IMAP and SMTP settings below to connect your account.';
|
||||
}
|
||||
|
||||
static validateAccountInfo = (accountInfo) => {
|
||||
static validateAccount = (account) => {
|
||||
let errorMessage = null;
|
||||
const errorFieldNames = [];
|
||||
|
||||
for (const type of ['imap', 'smtp']) {
|
||||
if (!accountInfo[`${type}_host`] || !accountInfo[`${type}_username`] || !accountInfo[`${type}_password`]) {
|
||||
if (!account.settings[`${type}_host`] || !account.settings[`${type}_username`] || !account.settings[`${type}_password`]) {
|
||||
return {errorMessage, errorFieldNames, populated: false};
|
||||
}
|
||||
if (!isValidHost(accountInfo[`${type}_host`])) {
|
||||
if (!isValidHost(account.settings[`${type}_host`])) {
|
||||
errorMessage = "Please provide a valid hostname or IP adddress.";
|
||||
errorFieldNames.push(`${type}_host`);
|
||||
}
|
||||
// todo bg
|
||||
// if (accountInfo[`${type}_host`] === 'imap.gmail.com') {
|
||||
// errorMessage = "Please link Gmail accounts by choosing 'Google' on the account type screen.";
|
||||
// errorFieldNames.push(`${type}_host`);
|
||||
// }
|
||||
if (!Number.isInteger(accountInfo[`${type}_port`] / 1)) {
|
||||
if (!Number.isInteger(account.settings[`${type}_port`] / 1)) {
|
||||
errorMessage = "Please provide a valid port number.";
|
||||
errorFieldNames.push(`${type}_port`);
|
||||
}
|
||||
|
@ -61,16 +56,16 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
if (!["imap", "smtp"].includes(protocol)) {
|
||||
throw new Error(`Can't render port dropdown for protocol '${protocol}'`);
|
||||
}
|
||||
const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props;
|
||||
const {account: {settings}, submitting, onFieldKeyPress, onFieldChange} = this.props;
|
||||
|
||||
if (protocol === "imap") {
|
||||
return (
|
||||
<span>
|
||||
<label htmlFor="imap_port">Port:</label>
|
||||
<label htmlFor="settings.imap_port">Port:</label>
|
||||
<select
|
||||
id="imap_port"
|
||||
id="settings.imap_port"
|
||||
tabIndex={0}
|
||||
value={accountInfo.imap_port}
|
||||
value={settings.imap_port}
|
||||
disabled={submitting}
|
||||
onKeyPress={onFieldKeyPress}
|
||||
onChange={onFieldChange}
|
||||
|
@ -84,11 +79,11 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
if (protocol === "smtp") {
|
||||
return (
|
||||
<span>
|
||||
<label htmlFor="smtp_port">Port:</label>
|
||||
<label htmlFor="settings.smtp_port">Port:</label>
|
||||
<select
|
||||
id="smtp_port"
|
||||
id="settings.smtp_port"
|
||||
tabIndex={0}
|
||||
value={accountInfo.smtp_port}
|
||||
value={settings.smtp_port}
|
||||
disabled={submitting}
|
||||
onKeyPress={onFieldKeyPress}
|
||||
onChange={onFieldChange}
|
||||
|
@ -104,16 +99,18 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
}
|
||||
|
||||
renderSecurityDropdown(protocol) {
|
||||
const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props;
|
||||
const {account: {settings}, submitting, onFieldKeyPress, onFieldChange} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
<label htmlFor={`${protocol}_security`}>Security:</label>
|
||||
<label htmlFor={`settings.${protocol}_security`}>
|
||||
Security:
|
||||
</label>
|
||||
<select
|
||||
id={`${protocol}_security`}
|
||||
id={`settings.${protocol}_security`}
|
||||
tabIndex={0}
|
||||
value={accountInfo[`${protocol}_security`]}
|
||||
value={settings[`${protocol}_security`]}
|
||||
disabled={submitting}
|
||||
onKeyPress={onFieldKeyPress}
|
||||
onChange={onFieldChange}
|
||||
|
@ -126,9 +123,9 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
<span style={{paddingLeft: '20px', paddingTop: '10px'}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${protocol}_allow_insecure_ssl`}
|
||||
id={`settings.${protocol}_allow_insecure_ssl`}
|
||||
disabled={submitting}
|
||||
checked={accountInfo[`${protocol}_allow_insecure_ssl`] || false}
|
||||
checked={settings[`${protocol}_allow_insecure_ssl`] || false}
|
||||
onKeyPress={onFieldKeyPress}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
|
@ -141,13 +138,13 @@ class AccountIMAPSettingsForm extends React.Component {
|
|||
renderFieldsForType(type) {
|
||||
return (
|
||||
<div>
|
||||
<FormField field={`${type}_host`} title={"Server"} {...this.props} />
|
||||
<FormField field={`settings.${type}_host`} title={"Server"} {...this.props} />
|
||||
<div style={{textAlign: 'left'}}>
|
||||
{this.renderPortDropdown(type)}
|
||||
{this.renderSecurityDropdown(type)}
|
||||
</div>
|
||||
<FormField field={`${type}_username`} title={"Username"} {...this.props} />
|
||||
<FormField field={`${type}_password`} title={"Password"} type="password" {...this.props} />
|
||||
<FormField field={`settings.${type}_username`} title={"Username"} {...this.props} />
|
||||
<FormField field={`settings.${type}_password`} title={"Password"} type="password" {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,14 +3,14 @@ import {RegExpUtils} from 'nylas-exports';
|
|||
|
||||
import OnboardingActions from './onboarding-actions';
|
||||
import CreatePageForForm from './decorators/create-page-for-form';
|
||||
import {expandAccountInfoWithCommonSettings} from './onboarding-helpers';
|
||||
import {expandAccountWithCommonSettings} from './onboarding-helpers';
|
||||
import FormField from './form-field';
|
||||
|
||||
class AccountBasicSettingsForm extends React.Component {
|
||||
static displayName = 'AccountBasicSettingsForm';
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
errorFieldNames: React.PropTypes.array,
|
||||
submitting: React.PropTypes.bool,
|
||||
onConnect: React.PropTypes.func,
|
||||
|
@ -18,53 +18,52 @@ class AccountBasicSettingsForm extends React.Component {
|
|||
onFieldKeyPress: React.PropTypes.func,
|
||||
};
|
||||
|
||||
static submitLabel = (accountInfo) => {
|
||||
return (accountInfo.type === 'imap') ? 'Continue' : 'Connect Account';
|
||||
static submitLabel = (account) => {
|
||||
return (account.provider === 'imap') ? 'Continue' : 'Connect Account';
|
||||
}
|
||||
|
||||
static titleLabel = (AccountType) => {
|
||||
return AccountType.title || `Add your ${AccountType.displayName} account`;
|
||||
static titleLabel = (providerConfig) => {
|
||||
return providerConfig.title || `Add your ${providerConfig.displayName} account`;
|
||||
}
|
||||
|
||||
static subtitleLabel = () => {
|
||||
return 'Enter your email account credentials to get started.';
|
||||
return `Enter your email account credentials to get started. Mailspring\nstores your email password securely and it is never sent to our servers.`;
|
||||
}
|
||||
|
||||
static validateAccountInfo = (accountInfo) => {
|
||||
const {email, password, name} = accountInfo;
|
||||
static validateAccount = (account) => {
|
||||
const errorFieldNames = [];
|
||||
let errorMessage = null;
|
||||
|
||||
if (!email || !password || !name) {
|
||||
if (!account.emailAddress || !account.settings.imap_password || !account.name) {
|
||||
return {errorMessage, errorFieldNames, populated: false};
|
||||
}
|
||||
|
||||
if (!RegExpUtils.emailRegex().test(accountInfo.email)) {
|
||||
if (!RegExpUtils.emailRegex().test(account.emailAddress)) {
|
||||
errorFieldNames.push('email')
|
||||
errorMessage = "Please provide a valid email address."
|
||||
}
|
||||
if (!accountInfo.password) {
|
||||
errorFieldNames.push('password')
|
||||
errorMessage = "Please provide a password for your account."
|
||||
}
|
||||
if (!accountInfo.name) {
|
||||
if (!account.name) {
|
||||
errorFieldNames.push('name')
|
||||
errorMessage = "Please provide your name."
|
||||
}
|
||||
if (!account.settings.imap_password) {
|
||||
errorFieldNames.push('password')
|
||||
errorMessage = "Please provide a password for your account."
|
||||
}
|
||||
|
||||
return {errorMessage, errorFieldNames, populated: true};
|
||||
}
|
||||
|
||||
submit() {
|
||||
const accountInfo = expandAccountInfoWithCommonSettings(this.props.accountInfo);
|
||||
OnboardingActions.setAccountInfo(accountInfo);
|
||||
if (this.props.accountInfo.type === 'imap') {
|
||||
const account = expandAccountWithCommonSettings(this.props.account);
|
||||
OnboardingActions.setAccount(account);
|
||||
if (this.props.account.provider === 'imap') {
|
||||
OnboardingActions.moveToPage('account-settings-imap');
|
||||
} else {
|
||||
// We have to pass in the updated accountInfo, because the onConnect()
|
||||
// We have to pass in the updated account, because the onConnect()
|
||||
// we're calling exists on a component that won't have had it's state
|
||||
// updated from the OnboardingStore change yet.
|
||||
this.props.onConnect(accountInfo);
|
||||
this.props.onConnect(account);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,8 +71,8 @@ class AccountBasicSettingsForm extends React.Component {
|
|||
return (
|
||||
<form className="settings">
|
||||
<FormField field="name" title="Name" {...this.props} />
|
||||
<FormField field="email" title="Email" {...this.props} />
|
||||
<FormField field="password" title="Password" type="password" {...this.props} />
|
||||
<FormField field="emailAddress" title="Email" {...this.props} />
|
||||
<FormField field="settings.imap_password" title="Password" type="password" {...this.props} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ export default class AuthenticatePage extends React.Component {
|
|||
static displayName = "AuthenticatePage";
|
||||
|
||||
static propTypes = {
|
||||
accountInfo: React.PropTypes.object,
|
||||
account: React.PropTypes.object,
|
||||
};
|
||||
|
||||
_src() {
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
margin-bottom:15px;
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.error {
|
||||
color: #A33;
|
||||
|
@ -116,6 +117,9 @@
|
|||
form.settings {
|
||||
padding: 0 20px;
|
||||
padding-bottom: 20px;
|
||||
span:last-child input {
|
||||
margin-bottom:0;
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: inline-block;
|
||||
|
@ -169,10 +173,6 @@
|
|||
text-align: right;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top:8px;
|
||||
}
|
||||
}
|
||||
|
||||
.page.authenticate {
|
||||
|
@ -339,7 +339,7 @@
|
|||
}
|
||||
.twocol {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.page.account-setup.google, .page.account-setup.AccountOnboardingSuccess {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"main": "./lib/main",
|
||||
"version": "0.1.0",
|
||||
"serverUrl": {
|
||||
"development": "http://localhost:5100",
|
||||
"development": "http://localhost:5101",
|
||||
"staging": "https://link-staging.getmailspring.com",
|
||||
"production": "https://link.getmailspring.com"
|
||||
},
|
||||
|
|
|
@ -194,12 +194,12 @@ export function getLatestMessageWithReminder(thread, messages) {
|
|||
}
|
||||
|
||||
export function getReminderLabel(reminderDate, {fromNow = false, shortFormat = false} = {}) {
|
||||
const momentDate = DateUtils.futureDateFromString(reminderDate);
|
||||
const momentDate = moment(reminderDate);
|
||||
if (shortFormat) {
|
||||
return momentDate ? `in ${momentDate.fromNow(true)}` : 'now'
|
||||
}
|
||||
if (fromNow) {
|
||||
return momentDate ? `Reminder set for ${momentDate.fromNow(true)} from now` : `Reminder set`;
|
||||
}
|
||||
return moment(reminderDate).format(DATE_FORMAT_LONG_NO_YEAR)
|
||||
return momentDate.format(DATE_FORMAT_LONG_NO_YEAR)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import _ from 'underscore';
|
||||
import NylasStore from 'nylas-store';
|
||||
|
||||
import {
|
||||
|
@ -7,7 +6,6 @@ import {
|
|||
Actions,
|
||||
DatabaseStore,
|
||||
Message,
|
||||
CategoryStore,
|
||||
} from 'nylas-exports';
|
||||
|
||||
import SnoozeUtils from './snooze-utils'
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "thread-sharing",
|
||||
"version": "0.1.0",
|
||||
"serverUrl": {
|
||||
"development": "http://localhost:5100",
|
||||
"development": "http://localhost:5101",
|
||||
"staging": "https://share-staging.getmailspring.com",
|
||||
"production": "https://share.getmailspring.com"
|
||||
},
|
||||
|
|
|
@ -288,18 +288,18 @@ export default class Application extends EventEmitter {
|
|||
win.browserWindow.inspectElement(x, y);
|
||||
});
|
||||
|
||||
this.on('application:add-account', ({existingAccount, accountType} = {}) => {
|
||||
this.on('application:add-account', ({existingAccount, accountProvider} = {}) => {
|
||||
const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW);
|
||||
if (onboarding) {
|
||||
if (onboarding.browserWindow.webContents) {
|
||||
onboarding.browserWindow.webContents.send('set-account-type', accountType)
|
||||
onboarding.browserWindow.webContents.send('set-account-provider', accountProvider)
|
||||
}
|
||||
onboarding.show();
|
||||
onboarding.focus();
|
||||
} else {
|
||||
this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
|
||||
title: "Add an Account",
|
||||
windowProps: { addingAccount: true, existingAccount, accountType },
|
||||
windowProps: { addingAccount: true, existingAccount, accountProvider },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -526,11 +526,6 @@ export default class Application extends EventEmitter {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.on('new-account-added', () => {
|
||||
// TODO BEN
|
||||
// this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
|
||||
});
|
||||
|
||||
ipcMain.on('run-in-window', (event, params) => {
|
||||
const sourceWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
this._sourceWindows = this._sourceWindows || {};
|
||||
|
|
|
@ -30,10 +30,11 @@ The value of this attribute is always an array of other model objects.
|
|||
Section: Database
|
||||
*/
|
||||
export default class AttributeCollection extends Attribute {
|
||||
constructor({modelKey, jsonKey, itemClass, joinOnField, joinQueryableBy, queryable}) {
|
||||
constructor({modelKey, jsonKey, itemClass, joinOnField, joinQueryableBy, joinTableName, queryable}) {
|
||||
super({modelKey, jsonKey, queryable});
|
||||
this.itemClass = itemClass;
|
||||
this.joinOnField = joinOnField;
|
||||
this.joinTableName = joinTableName;
|
||||
this.joinQueryableBy = joinQueryableBy || [];
|
||||
}
|
||||
|
||||
|
@ -68,6 +69,12 @@ export default class AttributeCollection extends Attribute {
|
|||
});
|
||||
}
|
||||
|
||||
// Private: The Matcher interface uses this method to determine how to
|
||||
// constuct a SQL join:
|
||||
tableNameForJoinAgainst(primaryKlass) {
|
||||
return this.joinTableName || `${primaryKlass.name}${this.itemClass.name}`;
|
||||
}
|
||||
|
||||
// Public: Returns a {Matcher} for objects containing the provided value.
|
||||
contains(val) {
|
||||
this._assertPresentAndQueryable('contains', val);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {tableNameForJoin} from '../models/utils';
|
||||
import LocalSearchQueryBackend from '../../services/search/search-query-backend-local'
|
||||
|
||||
// https://www.sqlite.org/faq.html#q14
|
||||
|
@ -115,7 +114,7 @@ class Matcher {
|
|||
switch (this.comparator) {
|
||||
case 'contains':
|
||||
case 'containsAny': {
|
||||
const joinTable = tableNameForJoin(klass, this.attr.itemClass);
|
||||
const joinTable = this.attr.tableNameForJoinAgainst(klass);
|
||||
const joinTableRef = this.joinTableRef();
|
||||
return `INNER JOIN \`${joinTable}\` AS \`${joinTableRef}\` ON \`${joinTableRef}\`.\`id\` = \`${klass.name}\`.\`id\``;
|
||||
}
|
||||
|
|
|
@ -191,7 +191,7 @@ export default class MailsyncBridge {
|
|||
// Private
|
||||
|
||||
_launchClient(account, {force} = {}) {
|
||||
const fullAccountJSON = KeyManager.insertAccountSecrets(account.toJSON());
|
||||
const fullAccountJSON = KeyManager.insertAccountSecrets(account).toJSON();
|
||||
const identity = IdentityStore.identity();
|
||||
const id = account.id;
|
||||
|
||||
|
|
|
@ -47,8 +47,9 @@ export default class ModelWithMetadata extends Model {
|
|||
static attributes = Object.assign({}, Model.attributes, {
|
||||
pluginMetadata: Attributes.Collection({
|
||||
queryable: true,
|
||||
joinOnField: 'pluginId',
|
||||
itemClass: PluginMetadata,
|
||||
joinOnField: 'pluginId',
|
||||
joinTableName: 'ModelPluginMetadata',
|
||||
modelKey: 'pluginMetadata',
|
||||
jsonKey: 'metadata',
|
||||
}),
|
||||
|
@ -70,7 +71,11 @@ export default class ModelWithMetadata extends Model {
|
|||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(metadata.value));
|
||||
const m = JSON.parse(JSON.stringify(metadata.value));
|
||||
if (m.expiration) {
|
||||
m.expiration = new Date(m.expiration * 1000);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// Private helpers
|
||||
|
@ -88,7 +93,10 @@ export default class ModelWithMetadata extends Model {
|
|||
metadata = new PluginMetadata({pluginId});
|
||||
this.pluginMetadata.push(metadata);
|
||||
}
|
||||
metadata.value = metadataValue;
|
||||
metadata.value = Object.assign({}, metadataValue);
|
||||
if (metadata.value.expiration) {
|
||||
metadata.value.expiration = Math.round(new Date(metadata.value.expiration).getTime() / 1000);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -409,7 +409,7 @@ export default class ModelQuery {
|
|||
_subselectSQL(returningMatcher, subselectMatchers, order, limit) {
|
||||
const returningAttribute = returningMatcher.attribute()
|
||||
|
||||
const table = Utils.tableNameForJoin(this._klass, returningAttribute.itemClass);
|
||||
const table = returningAttribute.tableNameForJoinAgainst(this._klass);
|
||||
const wheres = subselectMatchers.map(c => c.whereSQL(this._klass)).filter(c => !!c);
|
||||
|
||||
let innerSQL = `SELECT \`id\` FROM \`${table}\` WHERE ${wheres.join(' AND ')} ${order} ${limit}`;
|
||||
|
|
|
@ -159,9 +159,6 @@ Utils =
|
|||
return false unless id and _.isString(id)
|
||||
id[0..5] is 'local-'
|
||||
|
||||
tableNameForJoin: (primaryKlass, secondaryKlass) ->
|
||||
"#{primaryKlass.name}#{secondaryKlass.name}"
|
||||
|
||||
imageNamed: (fullname, resourcePath) ->
|
||||
[name, ext] = fullname.split('.')
|
||||
|
||||
|
|
|
@ -25,14 +25,6 @@ export function rootURLForServer(server) {
|
|||
production: "https://id.getmailspring.com",
|
||||
}[env];
|
||||
}
|
||||
if (server === 'accounts') {
|
||||
return {
|
||||
development: "http://localhost:5100",
|
||||
staging: "https://accounts-staging.getmailspring.com",
|
||||
production: "https://accounts.getmailspring.com",
|
||||
}[env];
|
||||
}
|
||||
|
||||
throw new Error("rootURLForServer: You must provide a valid `server` value");
|
||||
}
|
||||
|
||||
|
|
|
@ -203,28 +203,29 @@ class AccountStore extends NylasStore {
|
|||
this._save()
|
||||
}
|
||||
|
||||
addAccountFromJSON = (json) => {
|
||||
if (!json.emailAddress || !json.provider) {
|
||||
throw new Error(`Returned account data is invalid: ${JSON.stringify(json)}`)
|
||||
addAccount = (account) => {
|
||||
if (!account.emailAddress || !account.provider || !(account instanceof Account)) {
|
||||
throw new Error(`Returned account data is invalid: ${JSON.stringify(account)}`)
|
||||
}
|
||||
|
||||
// send the account JSON and cloud token to the KeyManager,
|
||||
// which gives us back a version with no secrets.
|
||||
const cleanJSON = KeyManager.extractAccountSecrets(json);
|
||||
const cleanAccount = KeyManager.extractAccountSecrets(account);
|
||||
|
||||
this._loadAccounts();
|
||||
|
||||
const existingIdx = this._accounts.findIndex((a) =>
|
||||
a.id === cleanJSON.id || a.emailAddress === cleanJSON.emailAddress
|
||||
a.id === cleanAccount.id || a.emailAddress === cleanAccount.emailAddress
|
||||
);
|
||||
|
||||
if (existingIdx === -1) {
|
||||
const account = (new Account()).fromJSON(cleanJSON);
|
||||
this._accounts.push(account);
|
||||
this._accounts.push(cleanAccount);
|
||||
} else {
|
||||
const account = this._accounts[existingIdx];
|
||||
account.syncState = Account.SYNC_STATE_OK;
|
||||
account.fromJSON(cleanJSON);
|
||||
const existing = this._accounts[existingIdx];
|
||||
existing.syncState = Account.SYNC_STATE_OK;
|
||||
existing.name = cleanAccount.name;
|
||||
existing.emailAddress = cleanAccount.emailAddress;
|
||||
existing.settings = cleanAccount.settings;
|
||||
}
|
||||
|
||||
this._save();
|
||||
|
|
|
@ -23,6 +23,11 @@ export default class SyncbackMetadataTask extends Task {
|
|||
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
|
||||
if (data.value && data.value.expiration) {
|
||||
data.value.expiration = Math.round(new Date(data.value.expiration).getTime() / 1000);
|
||||
}
|
||||
|
||||
if (data.model) {
|
||||
this.modelId = data.model.id;
|
||||
this.modelClassName = data.model.constructor.name.toLowerCase();
|
||||
|
|
|
@ -33,27 +33,27 @@ class KeyManager {
|
|||
});
|
||||
}
|
||||
|
||||
extractAccountSecrets(accountJSON) {
|
||||
const next = Object.assign({}, accountJSON);
|
||||
extractAccountSecrets(account) {
|
||||
const next = account.clone();
|
||||
this._try(() => {
|
||||
const keys = this._getKeyHash();
|
||||
keys[`${accountJSON.emailAddress}-imap`] = next.settings.imap_password;
|
||||
keys[`${account.emailAddress}-imap`] = next.settings.imap_password;
|
||||
delete next.settings.imap_password;
|
||||
keys[`${accountJSON.emailAddress}-smtp`] = next.settings.smtp_password;
|
||||
keys[`${account.emailAddress}-smtp`] = next.settings.smtp_password;
|
||||
delete next.settings.smtp_password;
|
||||
keys[`${accountJSON.emailAddress}-refresh-token`] = next.settings.refresh_token;
|
||||
keys[`${account.emailAddress}-refresh-token`] = next.settings.refresh_token;
|
||||
delete next.settings.refresh_token;
|
||||
return this._writeKeyHash(keys);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
insertAccountSecrets(accountJSON) {
|
||||
const next = Object.assign({}, accountJSON);
|
||||
insertAccountSecrets(account) {
|
||||
const next = account.clone();
|
||||
const keys = this._getKeyHash();
|
||||
next.settings.imap_password = keys[`${accountJSON.emailAddress}-imap`];
|
||||
next.settings.smtp_password = keys[`${accountJSON.emailAddress}-smtp`];
|
||||
next.settings.refresh_token = keys[`${accountJSON.emailAddress}-refresh-token`];
|
||||
next.settings.imap_password = keys[`${account.emailAddress}-imap`];
|
||||
next.settings.smtp_password = keys[`${account.emailAddress}-smtp`];
|
||||
next.settings.refresh_token = keys[`${account.emailAddress}-refresh-token`];
|
||||
return next;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,12 +53,10 @@ export default class MailsyncProcess extends EventEmitter {
|
|||
const env = {
|
||||
CONFIG_DIR_PATH: this.configDirPath,
|
||||
IDENTITY_SERVER: 'unknown',
|
||||
ACCOUNTS_SERVER: 'unknown',
|
||||
};
|
||||
if (process.type === 'renderer') {
|
||||
const rootURLForServer = require('./flux/nylas-api-request').rootURLForServer;
|
||||
env.IDENTITY_SERVER = rootURLForServer('identity');
|
||||
env.ACCOUNTS_SERVER = rootURLForServer('accounts');
|
||||
}
|
||||
|
||||
this._proc = spawn(this.binaryPath, [`--mode`, mode], {env});
|
||||
|
|
Loading…
Add table
Reference in a new issue