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 = []; }