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 (