diff --git a/internal_packages/preferences/lib/tabs/preferences-identity.jsx b/internal_packages/preferences/lib/tabs/preferences-identity.jsx index 35d090878..e29c83bb3 100644 --- a/internal_packages/preferences/lib/tabs/preferences-identity.jsx +++ b/internal_packages/preferences/lib/tabs/preferences-identity.jsx @@ -1,59 +1,8 @@ import React from 'react'; import {Actions, IdentityStore} from 'nylas-exports'; -import {RetinaImg} from 'nylas-component-kit'; +import {OpenIdentityPageButton, RetinaImg} from 'nylas-component-kit'; import {shell} from 'electron'; -class OpenIdentityPageButton extends React.Component { - static propTypes = { - path: React.PropTypes.string, - label: React.PropTypes.string, - source: React.PropTypes.string, - campaign: React.PropTypes.string, - img: React.PropTypes.string, - } - - constructor(props) { - super(props); - this.state = { - loading: false, - }; - } - - _onClick = () => { - this.setState({loading: true}); - IdentityStore.fetchSingleSignOnURL(this.props.path, { - source: this.props.source, - campaign: this.props.campaign, - content: this.props.label, - }).then((url) => { - this.setState({loading: false}); - shell.openExternal(url); - }); - } - - render() { - if (this.state.loading) { - return ( -
- -  {this.props.label}… -
- ); - } - if (this.props.img) { - return ( -
- -   {this.props.label} -
- ) - } - return ( -
{this.props.label}
- ); - } -} - class PreferencesIdentity extends React.Component { static displayName = 'PreferencesIdentity'; diff --git a/internal_packages/thread-snooze/assets/ic-snooze-modal@2x.png b/internal_packages/thread-snooze/assets/ic-snooze-modal@2x.png new file mode 100644 index 000000000..57e8a5e36 Binary files /dev/null and b/internal_packages/thread-snooze/assets/ic-snooze-modal@2x.png differ diff --git a/internal_packages/thread-snooze/lib/snooze-store.es6 b/internal_packages/thread-snooze/lib/snooze-store.jsx similarity index 73% rename from internal_packages/thread-snooze/lib/snooze-store.es6 rename to internal_packages/thread-snooze/lib/snooze-store.jsx index 5bcd2c1ab..278522c0b 100644 --- a/internal_packages/thread-snooze/lib/snooze-store.es6 +++ b/internal_packages/thread-snooze/lib/snooze-store.jsx @@ -1,7 +1,7 @@ -import {remote} from 'electron' import _ from 'underscore'; -import {FeatureUsageStore, Actions, NylasAPIHelpers, AccountStore, +import {React, FeatureUsageStore, Actions, NylasAPIHelpers, AccountStore, DatabaseStore, Message, CategoryStore} from 'nylas-exports'; +import {FeatureUsedUpModal} from 'nylas-component-kit' import SnoozeUtils from './snooze-utils' import {PLUGIN_ID, PLUGIN_NAME} from './snooze-constants'; import SnoozeActions from './snooze-actions'; @@ -70,12 +70,45 @@ class SnoozeStore { onSnoozeThreads = (threads, snoozeDate, label) => { if (!FeatureUsageStore.isUsable("snooze")) { - remote.dialog.showMessageBox({ - title: 'Out of snoozes', - detail: `You have used your monthly quota of Snoozes`, - buttons: ['OK'], - type: 'info', - }); + const featureData = FeatureUsageStore.featureData("snooze"); + + let headerText = ""; + let rechargeText = "" + if (!featureData.quota) { + headerText = "Snooze not yet enabled"; + rechargeText = "Upgrade to Pro to start Snoozing" + } else { + headerText = "All Snoozes used"; + let time = "later"; + if (featureData.period === "hourly") { + time = "next hour" + } else if (featureData.period === "daily") { + time = "tomorrow" + } else if (featureData.period === "weekly") { + time = "next week" + } else if (featureData.period === "monthly") { + time = "next month" + } else if (featureData.period === "yearly") { + time = "next year" + } else if (featureData.period === "unlimited") { + time = "if you upgrade to Pro" + } + rechargeText = `You’ll have ${featureData.quota} more snoozes ${time}` + } + + Actions.openModal({ + component: ( + + ), + height: 575, + width: 412, + }) return Promise.resolve() } this.recordSnoozeEvent(threads, snoozeDate, label) diff --git a/internal_packages/thread-snooze/stylesheets/snooze-feature-used-modal.less b/internal_packages/thread-snooze/stylesheets/snooze-feature-used-modal.less new file mode 100644 index 000000000..3b0381edf --- /dev/null +++ b/internal_packages/thread-snooze/stylesheets/snooze-feature-used-modal.less @@ -0,0 +1,20 @@ +@import "ui-variables"; + +.feature-usage-modal.snooze { + @snooze-color: #8e6ce3; + .feature-header { + @from: @snooze-color; + @to: lighten(@snooze-color, 10%); + background: linear-gradient(to top, @from, @to); + } + .feature-name { + color: @snooze-color; + } + .pro-description { + li { + &:before { + color: @snooze-color; + } + } + } +} diff --git a/src/K2 b/src/K2 index 13750c7ba..32c820d2e 160000 --- a/src/K2 +++ b/src/K2 @@ -1 +1 @@ -Subproject commit 13750c7ba3f5abb4f083ab4a8eaf0f5ab14fffce +Subproject commit 32c820d2e50607f9381392bf825b5076f5accd8f diff --git a/src/components/feature-used-up-modal.jsx b/src/components/feature-used-up-modal.jsx new file mode 100644 index 000000000..45cafe641 --- /dev/null +++ b/src/components/feature-used-up-modal.jsx @@ -0,0 +1,50 @@ +import React from 'react' +import {shell} from 'electron' +import RetinaImg from './retina-img' +import OpenIdentityPageButton from './open-identity-page-button' + +export default function FeatureUsedUpModal(props = {}) { + const gotoFeatures = () => shell.openExternal("https://nylas.com/nylas-pro"); + return ( +
+
+
+ +
+

{props.headerText}

+

{props.rechargeText}

+
+
+

Want to {props.featureName} more?

+
+

Nylas Pro includes:

+
    +
  • Unlimited Snoozing
  • +
  • Unlimited Reminders
  • +
  • Unlimited Mail Merge
  • +
+

… plus dozens of other features

+
+ + +
+
+ ) +} +FeatureUsedUpModal.propTypes = { + modalClass: React.PropTypes.string.isRequired, + featureName: React.PropTypes.string.isRequired, + headerText: React.PropTypes.string.isRequired, + rechargeText: React.PropTypes.string.isRequired, + iconUrl: React.PropTypes.string.isRequired, +} diff --git a/src/components/modal.jsx b/src/components/modal.jsx index 42969fbaf..7cb60ca94 100644 --- a/src/components/modal.jsx +++ b/src/components/modal.jsx @@ -45,22 +45,25 @@ class Modal extends React.Component { _computeModalStyles = (height, width) => { const modalStyle = { - top: "50%", - left: "50%", - margin: `-${height / 2}px 0 0 -${width / 2}px`, height: height, + maxHeight: "95%", width: width, + maxWidth: "95%", + overflow: "auto", position: "absolute", backgroundColor: "white", boxShadow: "0 10px 20px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5)", borderRadius: "5px", }; const containerStyle = { + display: "flex", + alignItems: "center", + justifyContent: "center", height: "100%", width: "100%", zIndex: 1000, position: "absolute", - backgroundColor: "transparent", + backgroundColor: "rgba(255,255,255,0.58)", }; return {containerStyle, modalStyle}; }; diff --git a/src/components/open-identity-page-button.jsx b/src/components/open-identity-page-button.jsx new file mode 100644 index 000000000..e97e1456e --- /dev/null +++ b/src/components/open-identity-page-button.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {shell} from 'electron'; +import classnames from 'classnames' +import RetinaImg from './retina-img' +import IdentityStore from '../flux/stores/identity-store'; + +export default class OpenIdentityPageButton extends React.Component { + static propTypes = { + path: React.PropTypes.string, + label: React.PropTypes.string, + source: React.PropTypes.string, + campaign: React.PropTypes.string, + img: React.PropTypes.string, + isCTA: React.PropTypes.bool, + } + + constructor(props) { + super(props); + this.state = { + loading: false, + }; + } + + _onClick = () => { + this.setState({loading: true}); + IdentityStore.fetchSingleSignOnURL(this.props.path, { + source: this.props.source, + campaign: this.props.campaign, + content: this.props.label, + }).then((url) => { + this.setState({loading: false}); + shell.openExternal(url); + }); + } + + render() { + if (this.state.loading) { + return ( +
+ +  {this.props.label}… +
+ ); + } + if (this.props.img) { + return ( +
+ +   {this.props.label} +
+ ) + } + const cls = classnames({ + "btn": true, + "btn-emphasis": this.props.isCTA, + }) + return ( +
{this.props.label}
+ ); + } +} + diff --git a/src/flux/stores/feature-usage-store.es6 b/src/flux/stores/feature-usage-store.es6 index c671ea084..56d2dd060 100644 --- a/src/flux/stores/feature-usage-store.es6 +++ b/src/flux/stores/feature-usage-store.es6 @@ -11,6 +11,45 @@ import SendFeatureUsageEventTask from '../tasks/send-feature-usage-event-task' * The billing site is responsible for returning with the Identity object * a usage hash that includes all supported features, their quotas for the * user, and the current usage of that user. We keep a cache locally + * + * The Identity object (aka Nylas ID or N1User) has a field called + * `feature_usage`. The schema for `feature_usage` is computed dynamically + * in `compute_feature_usage` here: + * https://github.com/nylas/cloud-core/blob/master/redwood/models/n1.py#L175-207 + * + * The schema of each feature is determined by the `FeatureUsage` model in + * redwood here: + * https://github.com/nylas/cloud-core/blob/master/redwood/models/feature_usage.py#L14-32 + * + * The final schema looks like (Feb 7, 2017): + * + * NylasID = { + * ... + * "feature_usage": { + * "snooze": { + * "quota": 15, + * "period": "monthly", + * "used_in_period": 10, + * "feature_limit_name": "snooze-experiment-A", + * }, + * "send-later": { + * "quota": 99999, + * "period": "unlimited", + * "used_in_period": 228, + * "feature_limit_name": "send-later-unlimited-A", + * }, + * "reminders": { + * "quota": 10, + * "period": "daily", + * "used_in_period": 10, + * "feature_limit_name": null, + * }, + * }, + * ... + * } + * + * Valid periods are: + * 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'unlimited' */ class FeatureUsageStore extends NylasStore { activate() { @@ -23,10 +62,19 @@ class FeatureUsageStore extends NylasStore { }) } + featureData(feature) { + const usage = this._featureUsage() + if (!usage[feature]) { + NylasEnv.reportError(new Error(`${feature} isn't supported`)); + return {} + } + return usage[feature] + } + isUsable(feature) { const usage = this._featureUsage() if (!usage[feature]) { - NylasEnv.reportError(`${feature} isn't supported`); + NylasEnv.reportError(new Error(`${feature} isn't supported`)); return false } return usage[feature].used_in_period < usage[feature].quota diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee index cde7a9ed8..7b377b336 100644 --- a/src/global/nylas-component-kit.coffee +++ b/src/global/nylas-component-kit.coffee @@ -46,6 +46,8 @@ class NylasComponentKit @load "FixedPopover", 'fixed-popover' @require "DatePickerPopover", 'date-picker-popover' @load "Modal", 'modal' + @load "FeatureUsedUpModal", 'feature-used-up-modal' + @load "OpenIdentityPageButton", 'open-identity-page-button' @load "Flexbox", 'flexbox' @load "RetinaImg", 'retina-img' @load "SwipeContainer", 'swipe-container' diff --git a/static/components/feature-used-up-modal.less b/static/components/feature-used-up-modal.less new file mode 100644 index 000000000..87378e0bb --- /dev/null +++ b/static/components/feature-used-up-modal.less @@ -0,0 +1,70 @@ +@import "ui-variables"; + +.feature-usage-modal { + position: absolute; + top: 0; + width: 100%; + height: 100%; + + .feature-header { + text-align: center; + padding-top: 32px; + padding-bottom: 30px; + color: @white; + border-radius: 5px 5px 0 0; + } + .header-text { + color: @white; + margin-top: 24px; + margin-bottom: 11px; + } + .recharge-text { + margin: 0; + opacity: 0.67; + } + .feature-cta { + text-align: center; + padding-bottom: 26px; + h2 { + margin-top: 26px; + margin-bottom: 26px; + } + } + .pro-description { + width: 275px; + margin: 0 auto; + padding-bottom: 28px; + border-top: 1px solid rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0,0,0,0.1); + + ul { + color: rgba(51, 51, 51, 0.67); + text-align: left; + list-style: none; + line-height: 24px; + margin-bottom: 0; + } + li { + &:before { + content: '✓'; + margin-right: 11px; + } + } + h3 { + margin-top: 23px; + margin-bottom: 18px; + } + p { + color: rgba(51, 51, 51, 0.67); + margin: 0; + text-align: left; + padding-left: 62px; + line-height: 24px; + } + } + + .btn { + margin-top: 21px; + padding: 0 33px 25px 33px; + } +} diff --git a/static/index.less b/static/index.less index bda6ff28d..c118bc3ad 100644 --- a/static/index.less +++ b/static/index.less @@ -47,3 +47,4 @@ @import "components/attachment-items"; @import "components/search-bar"; @import "components/code-snippet"; +@import "components/feature-used-up-modal";