0
Fork 0
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:
Rishabh 2021-05-20 17:00:52 +05:30
parent 1b66938327
commit 430ba2329d
4 changed files with 178 additions and 128 deletions

View file

@ -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);

View file

@ -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}}

View file

@ -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);

View file

@ -156,6 +156,7 @@ export default class LaunchController extends Controller {
@action
reset() {
this.data = null;
this.step = 'customise-design';
this.skippedSteps = [];
}