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