mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
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.
This commit is contained in:
parent
1b66938327
commit
430ba2329d
4 changed files with 178 additions and 128 deletions
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="gh-launch-wizard-settings-container">
|
||||
<div class="gh-launch-wizard-settings-container" {{did-insert this.setup}}>
|
||||
{{#if this.isConnectDisallowed}}
|
||||
<div class="gh-stack overflow-y-auto flex-grow-1">
|
||||
<div class="gh-setting-nossl-container">
|
||||
|
@ -29,11 +29,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if this.isHidden}}
|
||||
<div class="gh-stack overflow-y-auto flex-grow-1">
|
||||
<h4 class="gh-setting-title">Subscriptions already set</h4>
|
||||
<p>You can change product subscription pricing and signup options in Membership settings.</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-stack overflow-y-auto flex-grow-1">
|
||||
<div class="gh-stack-item flex-column">
|
||||
|
@ -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}}
|
||||
|
|
|
@ -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,23 +47,9 @@ 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;
|
||||
}
|
||||
|
||||
get isPaidPriceDisabled() {
|
||||
return this.disabled || !this.membersUtils.isStripeEnabled;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
this.updatePreviewUrl();
|
||||
this.loadingProduct = true;
|
||||
this.fetchDefaultProduct();
|
||||
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')
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
if (type === 'yearly') {
|
||||
return (
|
||||
prices.find(price => price.id === yearlyPriceId) ||
|
||||
prices.find(price => price.nickname === 'Yearly') ||
|
||||
prices.find(price => price.interval === 'year')
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@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;
|
||||
if (storedData.isMonthlyChecked !== undefined) {
|
||||
this.isMonthlyChecked = storedData.isMonthlyChecked;
|
||||
}
|
||||
|
||||
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'
|
||||
if (storedData.isYearlyChecked !== undefined) {
|
||||
this.isYearlyChecked = storedData.isYearlyChecked;
|
||||
}
|
||||
);
|
||||
this.product.set('stripePrices', stripePrices);
|
||||
return this.product;
|
||||
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 {
|
||||
return this.product;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async fetchDefaultProduct() {
|
||||
const products = await this.store.query('product', {include: 'stripe_prices'});
|
||||
const products = yield 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
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
|
|
@ -156,6 +156,7 @@ export default class LaunchController extends Controller {
|
|||
|
||||
@action
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.step = 'customise-design';
|
||||
this.skippedSteps = [];
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue