From 430ba2329deecd8c58106402c7cf64176b08a05f Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 20 May 2021 17:00:52 +0530 Subject: [PATCH] Updated pricing setup on launch wizard no refs Reworks the set pricing page on the launch wizard to work similar to membership settings, where instead of being disabled when prices exist, it shows existing monthly/yearly prices as well as handles creation of new prices and updating portal settings. --- .../components/gh-launch-wizard/finalise.js | 102 +++++++-- .../gh-launch-wizard/set-pricing.hbs | 9 +- .../gh-launch-wizard/set-pricing.js | 194 ++++++++---------- ghost/admin/app/controllers/launch.js | 1 + 4 files changed, 178 insertions(+), 128 deletions(-) diff --git a/ghost/admin/app/components/gh-launch-wizard/finalise.js b/ghost/admin/app/components/gh-launch-wizard/finalise.js index 2898713bef..54c845c245 100644 --- a/ghost/admin/app/components/gh-launch-wizard/finalise.js +++ b/ghost/admin/app/components/gh-launch-wizard/finalise.js @@ -14,23 +14,97 @@ export default class GhLaunchWizardFinaliseComponent extends Component { this.settings.rollbackAttributes(); } + updatePortalPlans(monthlyPriceId, yearlyPriceId, data) { + let portalPlans = this.settings.get('portalPlans') || []; + const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId'); + const currentYearlyPriceId = this.settings.get('membersYearlyPriceId'); + if (portalPlans.includes(currentMontlyPriceId)) { + portalPlans = portalPlans.filter(priceId => priceId !== currentMontlyPriceId); + } + if (data.isMonthlyChecked) { + portalPlans.pushObject(monthlyPriceId); + } + + if (portalPlans.includes(currentYearlyPriceId)) { + portalPlans = portalPlans.filter(priceId => priceId !== currentYearlyPriceId); + } + if (data.isYearlyChecked) { + portalPlans.pushObject(yearlyPriceId); + } + portalPlans = portalPlans.filter(priceId => priceId !== 'free'); + if (data.isFreeChecked) { + portalPlans.pushObject('free'); + } + this.settings.set('portalPlans', portalPlans); + } + + async saveProduct() { + const data = this.args.getData(); + this.product = data?.product; + if (this.product) { + const stripePrices = this.product.stripePrices || []; + const monthlyAmount = data.monthlyAmount * 100; + const yearlyAmount = data.yearlyAmount * 100; + const currency = data.currency; + const getActivePrice = (prices, type, amount) => { + return prices.find((price) => { + return ( + price.active && price.amount === amount && price.type === 'recurring' && + price.interval === type && price.currency.toLowerCase() === currency.toLowerCase() + ); + }); + }; + const monthlyPrice = getActivePrice(stripePrices, 'month', monthlyAmount); + const yearlyPrice = getActivePrice(stripePrices, 'year', yearlyAmount); + + if (!monthlyPrice) { + stripePrices.push( + { + nickname: 'Monthly', + amount: monthlyAmount, + active: 1, + currency: currency, + interval: 'month', + type: 'recurring' + } + ); + } + if (!yearlyPrice) { + stripePrices.push( + { + nickname: 'Yearly', + amount: yearlyAmount, + active: 1, + currency: currency, + interval: 'year', + type: 'recurring' + } + ); + } + if (monthlyPrice && yearlyPrice) { + this.updatePortalPlans(monthlyPrice.id, yearlyPrice.id, data); + this.settings.set('membersMonthlyPriceId', monthlyPrice.id); + this.settings.set('membersYearlyPriceId', yearlyPrice.id); + return this.product; + } else { + this.product.set('stripePrices', stripePrices); + const savedProduct = await this.product.save(); + const updatedStripePrices = savedProduct.stripePrices || []; + const updatedMonthlyPrice = getActivePrice(updatedStripePrices, 'month', monthlyAmount); + const updatedYearlyPrice = getActivePrice(updatedStripePrices, 'year', yearlyAmount); + this.updatePortalPlans(updatedMonthlyPrice.id, updatedYearlyPrice.id, data); + this.settings.set('membersMonthlyPriceId', updatedMonthlyPrice.id); + this.settings.set('membersYearlyPriceId', updatedYearlyPrice.id); + return savedProduct; + } + } + } + @task *finaliseTask() { const data = this.args.getData(); - if (data && data.product) { - const updatedProduct = yield data.product.save(); - const monthlyPrice = updatedProduct.get('stripePrices').find(d => d.nickname === 'Monthly'); - const yearlyPrice = updatedProduct.get('stripePrices').find(d => d.nickname === 'Yearly'); - const portalPlans = this.settings.get('portalPlans') || []; - let allowedPlans = [...portalPlans]; - if (data.isMonthlyChecked && monthlyPrice && !allowedPlans.includes(monthlyPrice.id)) { - allowedPlans.push(monthlyPrice.id); - } - - if (data.isYearlyChecked && yearlyPrice && !allowedPlans.includes(yearlyPrice.id)) { - allowedPlans.push(yearlyPrice.id); - } - this.settings.set('portalPlans', allowedPlans); + if (data?.product) { + yield this.saveProduct(); yield this.settings.save(); } yield this.feature.set('launchComplete', true); diff --git a/ghost/admin/app/components/gh-launch-wizard/set-pricing.hbs b/ghost/admin/app/components/gh-launch-wizard/set-pricing.hbs index 49ac8392fd..e8cdd71652 100644 --- a/ghost/admin/app/components/gh-launch-wizard/set-pricing.hbs +++ b/ghost/admin/app/components/gh-launch-wizard/set-pricing.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.isConnectDisallowed}}
@@ -29,11 +29,6 @@
- {{else if this.isHidden}} -
-

Subscriptions already set

-

You can change product subscription pricing and signup options in Membership settings.

-
{{else}}
@@ -46,7 +41,7 @@ @value={{this.selectedCurrency}} id="currency" name="currency" - @options={{this.currencies}} + @options={{readonly this.allCurrencies}} @optionValuePath="value" @optionLabelPath="label" @update={{this.setStripePlansCurrency}} diff --git a/ghost/admin/app/components/gh-launch-wizard/set-pricing.js b/ghost/admin/app/components/gh-launch-wizard/set-pricing.js index 299f7d4acb..bafba8401b 100644 --- a/ghost/admin/app/components/gh-launch-wizard/set-pricing.js +++ b/ghost/admin/app/components/gh-launch-wizard/set-pricing.js @@ -1,7 +1,7 @@ import Component from '@glimmer/component'; import envConfig from 'ghost-admin/config/environment'; import {action} from '@ember/object'; -import {currencies, getSymbol} from 'ghost-admin/utils/currency'; +import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency'; import {inject as service} from '@ember/service'; import {task} from 'ember-concurrency-decorators'; import {tracked} from '@glimmer/tracking'; @@ -36,6 +36,10 @@ export default class GhLaunchWizardSetPricingComponent extends Component { return this.currencies.findBy('value', this.currency); } + get allCurrencies() { + return getCurrencyOptions(); + } + get isConnectDisallowed() { const siteUrl = this.config.get('blogUrl'); @@ -43,21 +47,7 @@ export default class GhLaunchWizardSetPricingComponent extends Component { } get disabled() { - if (this.product) { - return this.product.get('stripePrices') && this.product.get('stripePrices').length > 0; - } - return true; - } - - get isHidden() { - if (this.loadingProduct) { - return false; - } - - if (this.product) { - return this.product.get('stripePrices') && this.product.get('stripePrices').length > 0; - } - return true; + return false; } get isPaidPriceDisabled() { @@ -68,44 +58,32 @@ export default class GhLaunchWizardSetPricingComponent extends Component { return this.disabled || this.settings.get('membersSignupAccess') !== 'all'; } - constructor() { - super(...arguments); - const storedData = this.args.getData(); - if (storedData) { - if (storedData.product) { - this.updatePricesFromProduct(storedData.product); - } else { - this.stripeMonthlyAmount = 5; - this.stripeYearlyAmount = 50; - } - if (storedData.isMonthlyChecked !== undefined) { - this.isMonthlyChecked = storedData.isMonthlyChecked; - } - if (storedData.isYearlyChecked !== undefined) { - this.isYearlyChecked = storedData.isYearlyChecked; - } - if (storedData.isFreeChecked !== undefined) { - this.isFreeChecked = storedData.isFreeChecked; - } + getPrice(prices, type) { + const monthlyPriceId = this.settings.get('membersMonthlyPriceId'); + const yearlyPriceId = this.settings.get('membersYearlyPriceId'); + + if (type === 'monthly') { + return ( + prices.find(price => price.id === monthlyPriceId) || + prices.find(price => price.nickname === 'Monthly') || + prices.find(price => price.interval === 'month') + ); } - this.updatePreviewUrl(); - this.loadingProduct = true; - this.fetchDefaultProduct(); + + if (type === 'yearly') { + return ( + prices.find(price => price.id === yearlyPriceId) || + prices.find(price => price.nickname === 'Yearly') || + prices.find(price => price.interval === 'year') + ); + } + return null; } - updatePricesFromProduct(product) { - if (product) { - const prices = product.get('stripePrices') || []; - const monthlyPrice = prices.find(d => d.nickname === 'Monthly'); - const yearlyPrice = prices.find(d => d.nickname === 'Yearly'); - if (monthlyPrice && monthlyPrice.amount) { - this.stripeMonthlyAmount = (monthlyPrice.amount / 100); - this.currency = monthlyPrice.currency; - } - if (yearlyPrice && yearlyPrice.amount) { - this.stripeYearlyAmount = (yearlyPrice.amount / 100); - } - } + @action + setup() { + this.fetchDefaultProduct.perform(); + this.updatePreviewUrl(); } willDestroy() { @@ -115,7 +93,7 @@ export default class GhLaunchWizardSetPricingComponent extends Component { @action backStep() { - const product = this.getProduct(); + const product = this.product; const data = this.args.getData() || {}; this.args.storeData({ ...data, @@ -124,7 +102,8 @@ export default class GhLaunchWizardSetPricingComponent extends Component { isMonthlyChecked: this.isMonthlyChecked, isYearlyChecked: this.isYearlyChecked, monthlyAmount: this.stripeMonthlyAmount, - yearlyAmount: this.stripeYearlyAmount + yearlyAmount: this.stripeYearlyAmount, + currency: this.currency }); this.args.backStep(); } @@ -174,7 +153,7 @@ export default class GhLaunchWizardSetPricingComponent extends Component { @task *saveAndContinue() { - if (this.isHidden || !this.isConnectDisallowed) { + if (this.isConnectDisallowed) { this.args.nextStep(); } else { yield this.validateStripePlans(); @@ -182,7 +161,7 @@ export default class GhLaunchWizardSetPricingComponent extends Component { if (this.stripePlanError) { return false; } - const product = this.getProduct(); + const product = this.product; const data = this.args.getData() || {}; this.args.storeData({ ...data, @@ -191,65 +170,65 @@ export default class GhLaunchWizardSetPricingComponent extends Component { isMonthlyChecked: this.isMonthlyChecked, isYearlyChecked: this.isYearlyChecked, monthlyAmount: this.stripeMonthlyAmount, - yearlyAmount: this.stripeYearlyAmount + yearlyAmount: this.stripeYearlyAmount, + currency: this.currency }); this.args.nextStep(); } } - calculateDiscount(monthly, yearly) { - if (isNaN(monthly) || isNaN(yearly)) { - return 0; - } + @task({drop: true}) + *fetchDefaultProduct() { + const storedData = this.args.getData(); + if (storedData?.product) { + this.product = storedData.product; + this.stripePrices = this.product.get('stripePrices') || []; - return monthly ? 100 - Math.floor((yearly / 12 * 100) / monthly) : 0; - } - - getProduct() { - if (this.product) { - const stripePrices = this.product.stripePrices || []; - if (stripePrices.length === 0 && this.stripeMonthlyAmount && this.stripeYearlyAmount) { - const yearlyDiscount = this.calculateDiscount(this.stripeMonthlyAmount, this.stripeYearlyAmount); - stripePrices.push( - { - nickname: 'Monthly', - amount: this.stripeMonthlyAmount * 100, - active: 1, - description: 'Full access', - currency: this.currency, - interval: 'month', - type: 'recurring' - }, - { - nickname: 'Yearly', - amount: this.stripeYearlyAmount * 100, - active: 1, - currency: this.currency, - description: yearlyDiscount > 0 ? `${yearlyDiscount}% discount` : 'Full access', - interval: 'year', - type: 'recurring' - } - ); - this.product.set('stripePrices', stripePrices); - return this.product; - } else { - return this.product; + if (storedData.isMonthlyChecked !== undefined) { + this.isMonthlyChecked = storedData.isMonthlyChecked; + } + if (storedData.isYearlyChecked !== undefined) { + this.isYearlyChecked = storedData.isYearlyChecked; + } + if (storedData.isFreeChecked !== undefined) { + this.isFreeChecked = storedData.isFreeChecked; + } + if (storedData.currency !== undefined) { + this.currency = storedData.currency; + } + this.stripeMonthlyAmount = storedData.monthlyAmount; + this.stripeYearlyAmount = storedData.yearlyAmount; + } else { + const products = yield this.store.query('product', {include: 'stripe_prices'}); + this.product = products.firstObject; + let portalPlans = this.settings.get('portalPlans') || []; + const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId'); + const currentYearlyPriceId = this.settings.get('membersYearlyPriceId'); + this.isMonthlyChecked = false; + this.isYearlyChecked = false; + this.isFreeChecked = false; + if (portalPlans.includes(currentMontlyPriceId)) { + this.isMonthlyChecked = true; + } + if (portalPlans.includes(currentYearlyPriceId)) { + this.isYearlyChecked = true; + } + if (portalPlans.includes('free')) { + this.isFreeChecked = true; + } + this.stripePrices = this.product.get('stripePrices') || []; + const activePrices = this.stripePrices.filter(price => !!price.active); + const monthlyPrice = this.getPrice(activePrices, 'monthly'); + const yearlyPrice = this.getPrice(activePrices, 'yearly'); + if (monthlyPrice && monthlyPrice.amount) { + this.stripeMonthlyAmount = (monthlyPrice.amount / 100); + this.currency = monthlyPrice.currency; + } + if (yearlyPrice && yearlyPrice.amount) { + this.stripeYearlyAmount = (yearlyPrice.amount / 100); } } - return null; - } - - async fetchDefaultProduct() { - const products = await this.store.query('product', {include: 'stripe_prices'}); - this.product = products.firstObject; - this.loadingProduct = false; - if (this.product.get('stripePrices').length > 0) { - const data = this.args.getData() || {}; - this.args.storeData({ - ...data, - product: null - }); - } + this.updatePreviewUrl(); } updatePreviewUrl() { @@ -260,7 +239,8 @@ export default class GhLaunchWizardSetPricingComponent extends Component { yearlyPrice: this.stripeYearlyAmount * 100, isMonthlyChecked: this.isMonthlyChecked, isYearlyChecked: this.isYearlyChecked, - isFreeChecked: this.isFreeChecked + isFreeChecked: this.isFreeChecked, + portalPlans: null }; const url = this.membersUtils.getPortalPreviewUrl(options); diff --git a/ghost/admin/app/controllers/launch.js b/ghost/admin/app/controllers/launch.js index e40db2649f..3c9bb9d247 100644 --- a/ghost/admin/app/controllers/launch.js +++ b/ghost/admin/app/controllers/launch.js @@ -156,6 +156,7 @@ export default class LaunchController extends Controller { @action reset() { + this.data = null; this.step = 'customise-design'; this.skippedSteps = []; }