diff --git a/src/components/ContentWrap.jsx b/src/components/ContentWrap.jsx
index 63902aa1..33f95c0c 100644
--- a/src/components/ContentWrap.jsx
+++ b/src/components/ContentWrap.jsx
@@ -804,7 +804,7 @@ export default class ContentWrap extends Component {
onCSSActiviation() {
if (!window.user) {
this.props.onLogin();
- } else if (userService.isPlusOrAdvanced()) {
+ } else if (userService.getPlan().canCustomizeCSS()) {
return true;
} else {
this.props.onProFeature();
diff --git a/src/components/MainHeader.jsx b/src/components/MainHeader.jsx
index 0c187b69..75e3c20b 100644
--- a/src/components/MainHeader.jsx
+++ b/src/components/MainHeader.jsx
@@ -57,7 +57,7 @@ export function MainHeader(props) {
mixpanel.track({ event: 'toLanguageGuide', category: 'ui' });
};
- const isSubscribed = userService.isSubscribed();
+ const isSubscribed = userService.getPlan().isSubscribed();
return (
diff --git a/src/components/app.jsx b/src/components/app.jsx
index dccacf88..674be7b1 100644
--- a/src/components/app.jsx
+++ b/src/components/app.jsx
@@ -143,30 +143,34 @@ export default class App extends Component {
alertsService.add('You are now logged in!');
await this.setState({ user });
window.user = user;
- if (!window.localStorage[LocalStorageKeys.ASKED_TO_IMPORT_CREATIONS]) {
- this.fetchItems(false, true).then(async (items) => {
- if (!items.length) {
- return;
- }
- this.oldSavedItems = items;
- this.oldSavedCreationsCount = items.length;
- await this.setState({
- isAskToImportModalOpen: true,
- });
- mixpanel.track({ event: 'askToImportModalSeen', category: 'ui' });
- });
- }
window.db.getUser(user.uid).then(async (customUser) => {
if (customUser) {
const prefs = { ...this.state.prefs };
Object.assign(prefs, user.settings);
await this.setState({ prefs: prefs });
await this.updateSetting();
+ await this.fetchSavedItems();
}
if (this.onUserItemsResolved) {
this.onUserItemsResolved(user.items);
}
+
+ if (
+ !window.localStorage[LocalStorageKeys.ASKED_TO_IMPORT_CREATIONS]
+ ) {
+ this.fetchItems(false, true).then(async (items) => {
+ if (!items.length) {
+ return;
+ }
+ this.oldSavedItems = items;
+ this.oldSavedCreationsCount = items.length;
+ await this.setState({
+ isAskToImportModalOpen: true,
+ });
+ mixpanel.track({ event: 'askToImportModalSeen', category: 'ui' });
+ });
+ }
});
//load subscription from firestore
@@ -455,23 +459,55 @@ BookLibService.Borrow(id) {
return d.promise;
}
- checkItemsLimit() {
- if (
- !this.state.user ||
- !this.state.user.items ||
- Object.keys(this.state.user.items).length <= 3 ||
- userService.isPlusOrAdvanced() ||
- (Object.keys(this.state.user.items).length <= 20 && userService.isBasic())
- ) {
- return true;
+ alertAndTrackIfExceedItemsLimit(userActionName, addingItemsCount = 0) {
+ const exceed = !this.checkItemsLimit(addingItemsCount);
+ if (exceed) {
+ this.alertItemsLimit();
+ var plan = userService.getPlan();
+ mixpanel.track({
+ event: `${plan.getPlanType()} Limit`,
+ category: `${plan.getMaxItemsCount()} diagrams limit`,
+ label: userActionName,
+ });
}
+ return exceed;
+ }
+ alertItemsLimit() {
alert(
- `You have ${Object.keys(this.state.user.items).length} diagrams, the limit is ${userService.isBasic() ? 20 : 3}. Upgrade now for more storage.`,
+ `You have ${this.getUserItemsCount()} diagrams, the limit is ${userService.getPlan().getMaxItemsCount()}. Upgrade now for more storage.`,
);
this.proBtnClickHandler();
}
+ getUserItemsCount() {
+ return Object.keys(this.getUserItems()).length;
+ }
+
+ getUserItems() {
+ if (!this.state.savedItems) return [];
+ return this.state.savedItems;
+ }
+
+ checkItemsLimit(addingItemsCount = 0) {
+ return (
+ userService.getPlan().getMaxItemsCount() >=
+ this.getUserItemsCount() + addingItemsCount
+ );
+ }
+
+ isNewItem(itemId) {
+ if (!itemId) return true;
+ const { user } = this.state;
+ if (user && user.items) {
+ const found = Object.keys(this.getUserItems()).some(
+ (key) => itemId === key,
+ );
+ return !found;
+ }
+ return true;
+ }
+
saveBtnClickHandler() {
trackEvent(
'ui',
@@ -482,15 +518,7 @@ BookLibService.Borrow(id) {
? 'saved'
: 'new',
);
-
- if (!this.checkItemsLimit()) {
- mixpanel.track({
- event: 'Free Limit',
- category: '3 diagrams limit',
- label: 'Save',
- });
- return;
- }
+ if (this.alertAndTrackIfExceedItemsLimit('Save')) return;
if (this.state.user || window.zenumlDesktop) {
this.saveItem();
@@ -510,7 +538,6 @@ BookLibService.Borrow(id) {
await this.setState({
savedItems: { ...this.state.savedItems },
});
-
await this.toggleSavedItemsPane();
// HACK: Set overflow after sometime so that the items can animate without getting cropped.
// setTimeout(() => $('#js-saved-items-wrap').style.overflowY = 'auto', 1000);
@@ -582,6 +609,12 @@ BookLibService.Borrow(id) {
}
async openSavedItemsPane() {
+ await this.fetchSavedItems(async (items) => {
+ await this.populateItemsInSavedPane(items);
+ });
+ }
+
+ async fetchSavedItems(callbackFunc) {
await this.setState({
isFetchingItems: true,
});
@@ -589,7 +622,9 @@ BookLibService.Borrow(id) {
await this.setState({
isFetchingItems: false,
});
- await this.populateItemsInSavedPane(items);
+ if (callbackFunc) {
+ callbackFunc(items);
+ }
});
}
@@ -624,7 +659,7 @@ BookLibService.Borrow(id) {
// Ctrl/⌘ + S
if ((event.ctrlKey || event.metaKey) && event.keyCode === 83) {
event.preventDefault();
- this.saveItem();
+ this.saveItem(true);
trackEvent('ui', 'saveItemKeyboardShortcut');
}
// Ctrl/⌘ + Shift + 5
@@ -840,7 +875,7 @@ BookLibService.Borrow(id) {
}
// Save current item to storage
- async saveItem() {
+ async saveItem(isManual = false) {
if (
!window.user &&
!window.localStorage[LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN] &&
@@ -858,7 +893,16 @@ BookLibService.Borrow(id) {
}
trackEvent('ui', LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN, 'local');
}
- var isNewItem = !this.state.currentItem.id;
+ var isNewItem = this.isNewItem(this.state.currentItem.id);
+ const check = this.checkItemsLimit(isNewItem ? 1 : 0);
+ var preventedSaving = isNewItem && !check;
+ console.debug(
+ `saveItem preventedSaving:${preventedSaving} user:${window.user} isManual:${isManual} checkItemsLimit:${check} isNewItem:${isNewItem}`,
+ );
+ if (preventedSaving) {
+ if (isManual) this.alertItemsLimit();
+ return;
+ }
this.state.currentItem.id =
this.state.currentItem.id || 'item-' + generateRandomId();
await this.setState({
@@ -881,6 +925,7 @@ BookLibService.Borrow(id) {
// Push into the items hash if its a new item being saved
if (isNewItem) {
await itemService.setItemForUser(this.state.currentItem.id);
+ await this.fetchSavedItems();
}
}
@@ -1058,14 +1103,7 @@ BookLibService.Borrow(id) {
}
async itemForkBtnClickHandler(item) {
- if (!this.checkItemsLimit()) {
- mixpanel.track({
- event: 'Free Limit',
- category: '3 diagrams limit',
- label: 'Fork',
- });
- return;
- }
+ if (this.alertAndTrackIfExceedItemsLimit('Fork', 1)) return;
await this.toggleSavedItemsPane();
setTimeout(() => {
@@ -1075,15 +1113,7 @@ BookLibService.Borrow(id) {
async newBtnClickHandler() {
mixpanel.track({ event: 'newBtnClick', category: 'ui' });
-
- if (!this.checkItemsLimit()) {
- mixpanel.track({
- event: 'Free Limit',
- category: '3 diagrams limit',
- label: 'New',
- });
- return;
- }
+ if (this.alertAndTrackIfExceedItemsLimit('New', 1)) return;
if (this.state.unsavedEditCount) {
var shouldDiscard = confirm(
@@ -1204,14 +1234,7 @@ BookLibService.Borrow(id) {
}
exportBtnClickHandler(e) {
- if (!this.checkItemsLimit()) {
- mixpanel.track({
- event: 'Free Limit',
- category: '3 diagrams limit',
- label: 'Fork',
- });
- return;
- }
+ if (this.alertAndTrackIfExceedItemsLimit('Export')) return;
this.exportItems();
e.preventDefault();
@@ -1248,6 +1271,7 @@ BookLibService.Borrow(id) {
}
mergeImportedItems(items) {
+ if (this.alertAndTrackIfExceedItemsLimit('Merge', items.length)) return;
var existingItemIds = [];
var toMergeItems = {};
const d = deferred();
@@ -1300,6 +1324,10 @@ BookLibService.Borrow(id) {
* Called from inside ask-to-import-modal
*/
importCreationsAndSettingsIntoApp() {
+ if (
+ this.alertAndTrackIfExceedItemsLimit('Import', this.oldSavedItems.length)
+ )
+ return;
this.mergeImportedItems(this.oldSavedItems).then(() => {
trackEvent('fn', 'oldItemsImported');
this.dontAskToImportAnymore();
diff --git a/src/components/subscription/SubscriptionAction.jsx b/src/components/subscription/SubscriptionAction.jsx
index 2034b323..03fd4dca 100644
--- a/src/components/subscription/SubscriptionAction.jsx
+++ b/src/components/subscription/SubscriptionAction.jsx
@@ -10,9 +10,10 @@ const SubscriptionAction = (props) => {
return null;
}
- if (userService.isSubscribed()) {
+ const plan = userService.getPlan();
+ if (plan.isSubscribed()) {
const subscription = userService.subscription();
- if (props.planType == userService.getPlanType()) {
+ if (props.planType == plan.getPlanType()) {
return
;
}
return
;
diff --git a/src/components/subscription/UpgradeLink.jsx b/src/components/subscription/UpgradeLink.jsx
index dc23be53..b8524a44 100644
--- a/src/components/subscription/UpgradeLink.jsx
+++ b/src/components/subscription/UpgradeLink.jsx
@@ -1,5 +1,4 @@
import planService from '../../services/planService';
-import userService from '../../services/user_service';
const UpgradeLink = (props) => {
const checkout = (e) => {
@@ -7,7 +6,7 @@ const UpgradeLink = (props) => {
props.preActionCallback();
Paddle.Checkout.open({
- product: planService.getProductByPlanType(props.planType),
+ product: planService.getPlanByType(props.planType).getProductId(),
email: props.userEmail,
passthrough: JSON.stringify({
userId: props.userId,
diff --git a/src/db.js b/src/db.js
index 32da1147..9482a50f 100644
--- a/src/db.js
+++ b/src/db.js
@@ -135,6 +135,7 @@ import { log } from './utils';
merge: true,
},
);
+ //user.items value source
const user = doc.data();
Object.assign(window.user, user);
return user;
diff --git a/src/services/planService.js b/src/services/planService.js
index 6a0859b9..28950c9b 100644
--- a/src/services/planService.js
+++ b/src/services/planService.js
@@ -1,20 +1,63 @@
import config from './configuration';
-//TODO(refactor): It is necessary to integrate more plan-related logic into this module.
-
-const getProductByPlanType = (planType) => {
- const productMap = {
- 'basic-monthly': config.paddleProductBasicMonthly,
- 'plus-monthly': config.paddleProductPlusMonthly,
- 'basic-yearly': config.paddleProductBasicYearly,
- 'plus-yearly': config.paddleProductPlusYearly,
+const getPlanByType = (planType) => {
+ const planMap = {
+ free: {
+ getMaxItemsCount: () => 3,
+ canCustomizeCSS: () => false,
+ getProductId: () => '',
+ getPlanType: () => 'free',
+ isSubscribed: () => false,
+ },
+ 'basic-monthly': {
+ getMaxItemsCount: () => 20,
+ canCustomizeCSS: () => false,
+ getProductId: () => config.paddleProductBasicMonthly,
+ getPlanType: () => 'basic-monthly',
+ isSubscribed: () => true,
+ },
+ 'plus-monthly': {
+ getMaxItemsCount: () => 999999,
+ canCustomizeCSS: () => true,
+ getProductId: () => config.paddleProductPlusMonthly,
+ getPlanType: () => 'plus-monthly',
+ isSubscribed: () => true,
+ },
+ 'basic-yearly': {
+ getMaxItemsCount: () => 20,
+ canCustomizeCSS: () => false,
+ getProductId: () => config.paddleProductBasicYearly,
+ getPlanType: () => 'basic-yearly',
+ isSubscribed: () => true,
+ },
+ 'plus-yearly': {
+ getMaxItemsCount: () => 999999,
+ canCustomizeCSS: () => true,
+ getProductId: () => config.paddleProductPlusYearly,
+ getPlanType: () => 'plus-yearly',
+ isSubscribed: () => true,
+ },
};
- const product = productMap[planType] || '';
- console.debug('getProductByPlanType', planType, product);
- return product;
+ return planMap[planType] || planMap['free'];
+};
+
+const checkPlanTypeFromUserSubscription = (
+ isSubscribed,
+ getSubscriptionPassthroughFunc,
+) => {
+ if (!isSubscribed) return 'free';
+
+ try {
+ return (
+ JSON.parse(getSubscriptionPassthroughFunc())?.planType || 'plus-monthly'
+ );
+ } catch {
+ return 'plus-monthly';
+ }
};
export default {
- getProductByPlanType: getProductByPlanType,
+ checkPlanTypeFromUserSubscription: checkPlanTypeFromUserSubscription,
+ getPlanByType: getPlanByType,
};
diff --git a/src/services/user_service.js b/src/services/user_service.js
index 2f0fe4b8..2a69137b 100644
--- a/src/services/user_service.js
+++ b/src/services/user_service.js
@@ -1,45 +1,19 @@
+import planService from './planService';
const user = () => window.user;
const subscription = () => user() && user().subscription;
export default {
user: user,
subscription: subscription,
- isSubscribed: function () {
- //console.debug('subscription', subscription());
- return (
- subscription() &&
- (subscription().status === 'active' ||
- subscription().status === 'trialing')
+ getPlanType: function () {
+ const status = subscription()?.status;
+ const isSubscribed = status === 'active' || status === 'trialing';
+ return planService.checkPlanTypeFromUserSubscription(
+ isSubscribed,
+ () => subscription().passthrough,
);
},
- isBasic: function () {
- return this.getPlanType().includes('basic');
- },
- isPlus: function () {
- return this.getPlanType().includes('plus');
- },
- isPlusOrAdvanced: function () {
- return this.isPlus();
- },
- getPlanType: function () {
- if (!this.isSubscribed()) return 'free';
- const currentSubscription = subscription();
- return getPlanTypeFromPassthrough(currentSubscription.passthrough);
+ getPlan: function () {
+ return planService.getPlanByType(this.getPlanType());
},
};
-
-// Compatible with previous pro users, before subscription.passthrough only stored userId
-function getPlanTypeFromPassthrough(passthrough) {
- return isJSONString(passthrough)
- ? JSON.parse(passthrough).planType
- : 'basic-monthly';
-}
-
-function isJSONString(str) {
- try {
- JSON.parse(str);
- return true;
- } catch (e) {
- return false;
- }
-}
diff --git a/src/templates/template-blue.json b/src/templates/template-blue.json
index 33742d77..908611fa 100644
--- a/src/templates/template-blue.json
+++ b/src/templates/template-blue.json
@@ -6,6 +6,6 @@
"jsMode": "js",
"layoutMode": 1,
"js": "Client->SGW.\"Get order by id\" {\n svc.Get(id) {\n new X()\n rep.\"load order\" {\n ==\"Start Here\"==\n MF.\"load order from mainframe\"\n ==\"End Here\"==\n if(order == null) {\n @return \n SGW->Client:404\n } else {\n return order\n }\n \n while(true) {\n svc.refresh(data)\n }\n processOrder()\n }\n return order\n }\n return response\n}",
- "css": "@fragmentBorderColor: rgba(4, 46, 110, 0.30);\n@nameBackgroundColor: rgba(4, 46, 110, 0.10);\n@labelTextColor: #032C72;\n@messageLineColor: #032C72;\n@occuranceBorderColor: #032C72;\n@occuranceBackgroundColor: #fff;\n@participantLineColor: #032C72;\n@participantBorderColor: #032C72;\n@participantTextColor: #032C72;\n@participantBackgroundColor: rgba(146, 192, 240, 0.30);\n@dividerBackgroundColor: #E28553;\n@dividerBorderColor: #E28553;\n@dividerTextColor: #E28553;\n\n#diagram {\n .sequence-diagram {\n .divider {\n .name {\n padding: 2px 6px 2px 6px;\n border-radius: 4px;\n margin: 0px;\n border-color: @dividerBorderColor;\n color: @dividerTextColor;\n }\n .left {\n background: @dividerBackgroundColor;\n }\n .right {\n background: @dividerBackgroundColor;\n }\n }\n .lifeline {\n .participant {\n font-weight: 400;\n border: 2px solid @participantBorderColor;\n background: @participantBackgroundColor; \n label {\n text-decoration: underline;\n color: @participantTextColor;\n }\n }\n .line {\n border-left-color: @participantLineColor;\n }\n }\n .message {\n .name {\n padding-bottom: 1px;\n color: @messageLineColor;\n }\n border-bottom-color: @messageLineColor;\n svg {\n polyline {\n fill: @messageLineColor;\n stroke: @messageLineColor;\n }\n }\n }\n .message.self {\n svg>polyline:not(.head) {\n fill: none;\n }\n }\n .occurrence {\n background-color: white;\n border: 2px solid @occuranceBorderColor;\n background-color: @occuranceBackgroundColor;\n }\n .fragment {\n margin-top: 8px;\n margin-bottom: 8px;\n border-radius: 4px;\n border: 1px solid @fragmentBorderColor;\n .fragment.par>.block>.statement-container:not(:first-child), .segment:not(:first-child) {\n border-top: 1px solid @fragmentBorderColor;\n }\n .header {\n .name {\n padding: 4px 4px 4px 6px;\n background: @nameBackgroundColor;\n label { /* name label */\n background: transparent;\n padding: 0px;\n color: @labelTextColor;\n }\n }\n }\n }\n .statement-container {\n margin-bottom: 8px;\n }\n }\n}",
+ "css": "@fragmentBorderColor: rgba(4, 46, 110, 0.30);\n@nameBackgroundColor: rgba(4, 46, 110, 0.10);\n@labelTextColor: #032C72;\n@messageLineColor: #032C72;\n@occuranceBorderColor: #032C72;\n@occuranceBackgroundColor: #fff;\n@participantLineColor: #032C72;\n@participantBorderColor: #032C72;\n@participantTextColor: #032C72;\n@participantBackgroundColor: #dfecfa;\n@dividerBackgroundColor: #E28553;\n@dividerBorderColor: #E28553;\n@dividerTextColor: #E28553;\n\n#diagram {\n .sequence-diagram {\n .divider {\n .name {\n padding: 2px 6px 2px 6px;\n border-radius: 4px;\n margin: 0px;\n border-color: @dividerBorderColor;\n color: @dividerTextColor;\n }\n .left {\n background: @dividerBackgroundColor;\n }\n .right {\n background: @dividerBackgroundColor;\n }\n }\n .lifeline {\n .participant {\n font-weight: 400;\n border: 2px solid @participantBorderColor;\n background: @participantBackgroundColor; \n label {\n text-decoration: underline;\n color: @participantTextColor;\n }\n }\n .line {\n border-left-color: @participantLineColor;\n }\n }\n .message {\n .name {\n padding-bottom: 1px;\n color: @messageLineColor;\n }\n border-bottom-color: @messageLineColor;\n svg {\n polyline {\n fill: @messageLineColor;\n stroke: @messageLineColor;\n }\n }\n }\n .message.self {\n svg>polyline:not(.head) {\n fill: none;\n }\n }\n .occurrence {\n background-color: white;\n border: 2px solid @occuranceBorderColor;\n background-color: @occuranceBackgroundColor;\n }\n .fragment {\n margin-top: 8px;\n margin-bottom: 8px;\n border-radius: 4px;\n border: 1px solid @fragmentBorderColor;\n .fragment.par>.block>.statement-container:not(:first-child), .segment:not(:first-child) {\n border-top: 1px solid @fragmentBorderColor;\n }\n .header {\n .name {\n padding: 4px 4px 4px 6px;\n background: @nameBackgroundColor;\n label { /* name label */\n background: transparent;\n padding: 0px;\n color: @labelTextColor;\n }\n }\n }\n }\n .statement-container {\n margin-bottom: 8px;\n }\n }\n}",
"html": ""
}
diff --git a/src/zenuml/components/MainHeader/ProductVersionLabel/ProductVersionLabel.js b/src/zenuml/components/MainHeader/ProductVersionLabel/ProductVersionLabel.js
index acf10ead..43298ec4 100644
--- a/src/zenuml/components/MainHeader/ProductVersionLabel/ProductVersionLabel.js
+++ b/src/zenuml/components/MainHeader/ProductVersionLabel/ProductVersionLabel.js
@@ -7,7 +7,7 @@ export function ProductVersionLabel(props) {
? 'Please login to upgrade to Pro'
: 'Get more out of ZenUML — Go Pro';
- if (!window.user || userService.isSubscribed()) return null;
+ if (!window.user || userService.getPlan().isSubscribed()) return null;
return (