Updated launch wizard to create custom price

refs https://github.com/TryGhost/Team/issues/644

Updates site setup to create custom Monthly/Yearly prices in default product as part of launch wizard. Also updates available portal plans based on user selection.
This commit is contained in:
Rishabh 2021-05-04 21:17:50 +05:30 committed by Rishabh Garg
parent 5d2e456f61
commit 2a6d7226d2
5 changed files with 158 additions and 115 deletions

View File

@ -7,9 +7,32 @@ export default class GhLaunchWizardFinaliseComponent extends Component {
@service feature;
@service notifications;
@service router;
@service settings;
willDestroy() {
// clear any unsaved settings changes when going back/forward/closing
this.settings.rollbackAttributes();
}
@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.push(monthlyPrice.id);
}
if (data.isYearlyChecked && yearlyPrice) {
allowedPlans.push(yearlyPrice.id);
}
this.settings.set('portalPlans', allowedPlans);
yield this.settings.save();
}
yield this.feature.set('launchComplete', true);
this.router.transitionTo('dashboard');
this.notifications.showNotification(

View File

@ -6,6 +6,7 @@
<div class="gh-setting-title" for="currency">Plan currency</div>
<span class="gh-select mt2">
<OneWaySelect
@disabled={{this.disabled}}
@value={{this.selectedCurrency}}
id="currency"
name="currency"
@ -20,38 +21,42 @@
</div>
<div class="w-100 flex flex-column flex-row-ns">
<div class="w-100 w-50-ns mr3-ns">
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="stripePlans">
<GhFormGroup>
<div class="gh-setting-title">Monthly price</div>
<div class="flex items-center justify-center mt2 gh-input-group gh-labs-price-label">
<GhTextInput
@value={{readonly this.stripePlans.monthly.amount}}
@disabled={{this.disabled}}
@value={{readonly this.stripeMonthlyAmount}}
@type="number"
@input={{action (mut this.stripeMonthlyAmount) value="target.value"}}
{{on "blur" this.validateStripePlans}}
/>
<span class="gh-input-append"><span class="ttu">{{this.stripePlans.monthly.currency}}</span>/month</span>
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/month</span>
</div>
</GhFormGroup>
</div>
<div class="w-100 w-50-ns ml2-ns">
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="stripePlans">
<GhFormGroup>
<div class="gh-setting-title">Yearly price</div>
<div class="flex items-center justify-center mt2 gh-input-group gh-labs-price-label">
<GhTextInput
@value={{readonly this.stripePlans.yearly.amount}}
@disabled={{this.disabled}}
@value={{readonly this.stripeYearlyAmount}}
@type="number"
@input={{action (mut this.stripeYearlyAmount) value="target.value"}}
{{on "blur" this.validateStripePlans}}
/>
<span class="gh-input-append"><span class="ttu">{{this.stripePlans.yearly.currency}}</span>/year</span>
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/year</span>
</div>
</GhFormGroup>
</div>
</div>
</div>
<div class="w-100 w-50-l flex flex-column flex-row-ns">
<GhErrorMessage @errors={{this.settings.errors}} @property="stripePlans" class="w-100 red"/>
{{#if this.stripePlanError}}
<p class="response w-100 red"> {{this.stripePlanError}} </p>
{{/if}}
</div>
<div class="gh-stack-item gh-setting flex-column">
@ -66,7 +71,7 @@
checked={{this.isFreeChecked}}
id="free-plan"
name="free-plan"
disabled={{not (eq this.settings.membersSignupAccess "all")}}
disabled={{this.isFreeDisabled}}
class="gh-input post-settings-featured"
{{on "click" this.toggleFreePlan}}
data-test-checkbox="featured"
@ -85,7 +90,7 @@
id="monthly-plan"
name="monthly-plan"
checked={{this.isMonthlyChecked}}
disabled={{not this.membersUtils.isStripeEnabled}}
disabled={{this.isPaidPriceDisabled}}
class="gh-input post-settings-featured"
{{on "click" this.toggleMonthlyPlan}}
data-test-checkbox="featured"
@ -104,7 +109,7 @@
id="yearly-plan"
name="yearly-plan"
checked={{this.isYearlyChecked}}
disabled={{not this.membersUtils.isStripeEnabled}}
disabled={{this.isPaidPriceDisabled}}
class="gh-input post-settings-featured"
{{on "click" this.toggleYearlyPlan}}
data-test-checkbox="featured"

View File

@ -1,6 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {currencies} from 'ghost-admin/utils/currency';
import {currencies, getSymbol} from 'ghost-admin/utils/currency';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';
@ -17,145 +17,111 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
@service config;
@service membersUtils;
@service settings;
@service store;
currencies = CURRENCIES;
@tracked stripeMonthlyAmount = null;
@tracked stripeYearlyAmount = null;
get stripePlans() {
const plans = this.settings.get('stripePlans') || [];
const monthly = plans.find(plan => plan.interval === 'month');
const yearly = plans.find(plan => plan.interval === 'year' && plan.name !== 'Complimentary');
return {
monthly: {
amount: (parseInt(monthly?.amount) || 0) / 100 || 5,
currency: monthly?.currency || this.currencies[0].value
},
yearly: {
amount: (parseInt(yearly?.amount) || 0) / 100 || 50,
currency: yearly?.currency || this.currencies[0].value
}
};
}
@tracked stripeMonthlyAmount = 5;
@tracked stripeYearlyAmount = 50;
@tracked currency = 'usd';
@tracked isFreeChecked = true;
@tracked isMonthlyChecked = true;
@tracked isYearlyChecked = true;
@tracked stripePlanError = '';
@tracked product;
get selectedCurrency() {
return this.currencies.findBy('value', this.stripePlans.monthly.currency);
return this.currencies.findBy('value', this.currency);
}
get isFreeChecked() {
const allowedPlans = this.settings.get('portalPlans') || [];
return (this.settings.get('membersSignupAccess') === 'all' && allowedPlans.includes('free'));
get disabled() {
if (this.product) {
return this.product.get('stripePrices') && this.product.get('stripePrices').length > 0;
}
return true;
}
get isMonthlyChecked() {
const allowedPlans = this.settings.get('portalPlans') || [];
return (this.membersUtils.isStripeEnabled && allowedPlans.includes('monthly'));
get isPaidPriceDisabled() {
return this.disabled || !this.membersUtils.isStripeEnabled;
}
get isYearlyChecked() {
const allowedPlans = this.settings.get('portalPlans') || [];
return (this.membersUtils.isStripeEnabled && allowedPlans.includes('yearly'));
get isFreeDisabled() {
return this.disabled || this.settings.get('membersSignupAccess') !== 'all';
}
constructor() {
super(...arguments);
const storedData = this.args.getData();
if (storedData && storedData.product) {
this.updatePricesFromProduct(storedData.product);
} else {
this.stripeMonthlyAmount = 5;
this.stripeYearlyAmount = 50;
}
this.updatePreviewUrl();
this.fetchDefaultProduct();
}
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);
}
}
}
willDestroy() {
// clear any unsaved settings changes when going back/forward/closing
this.settings.rollbackAttributes();
this.args.updatePreview('');
}
@action
setStripePlansCurrency(event) {
const newCurrency = event.value;
const updatedPlans = this.settings.get('stripePlans').map((plan) => {
if (plan.name !== 'Complimentary') {
return Object.assign({}, plan, {
currency: newCurrency
});
}
return plan;
});
const currentComplimentaryPlan = updatedPlans.find((plan) => {
return plan.name === 'Complimentary' && plan.currency === event.value;
});
if (!currentComplimentaryPlan) {
updatedPlans.push({
name: 'Complimentary',
currency: event.value,
interval: 'year',
amount: 0
});
}
this.settings.set('stripePlans', updatedPlans);
this.currency = newCurrency;
this.updatePreviewUrl();
}
@action
toggleFreePlan(event) {
this.updateAllowedPlan('free', event.target.checked);
this.isFreeChecked = event.target.checked;
this.updatePreviewUrl();
}
@action
toggleMonthlyPlan(event) {
this.updateAllowedPlan('monthly', event.target.checked);
this.isMonthlyChecked = event.target.checked;
this.updatePreviewUrl();
}
@action
toggleYearlyPlan(event) {
this.updateAllowedPlan('yearly', event.target.checked);
this.isYearlyChecked = event.target.checked;
this.updatePreviewUrl();
}
@action
validateStripePlans() {
this.settings.errors.remove('stripePlans');
this.settings.hasValidated.removeObject('stripePlans');
if (this.stripeYearlyAmount === null) {
this.stripeYearlyAmount = this.stripePlans.yearly.amount;
}
if (this.stripeMonthlyAmount === null) {
this.stripeMonthlyAmount = this.stripePlans.monthly.amount;
}
this.stripePlanError = undefined;
try {
const selectedCurrency = this.selectedCurrency;
const yearlyAmount = parseInt(this.stripeYearlyAmount);
const monthlyAmount = parseInt(this.stripeMonthlyAmount);
const yearlyAmount = this.stripeYearlyAmount;
const monthlyAmount = this.stripeMonthlyAmount;
const symbol = getSymbol(this.currency);
if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
throw new TypeError(`Subscription amount must be at least ${selectedCurrency.symbol}1.00`);
throw new TypeError(`Subscription amount must be at least ${symbol}1.00`);
}
const updatedPlans = this.settings.get('stripePlans').map((plan) => {
if (plan.name !== 'Complimentary') {
let newAmount;
if (plan.interval === 'year') {
newAmount = yearlyAmount * 100;
} else if (plan.interval === 'month') {
newAmount = monthlyAmount * 100;
}
return Object.assign({}, plan, {
amount: newAmount
});
}
return plan;
});
this.settings.set('stripePlans', updatedPlans);
this.updatePreviewUrl();
} catch (err) {
this.settings.errors.add('stripePlans', err.message);
} finally {
this.settings.hasValidated.pushObject('stripePlans');
this.stripePlanError = err.message;
}
}
@ -163,34 +129,70 @@ export default class GhLaunchWizardSetPricingComponent extends Component {
*saveAndContinue() {
yield this.validateStripePlans();
if (this.settings.errors.length > 0) {
if (this.stripePlanError) {
return false;
}
yield this.settings.save();
const product = this.getProduct();
const data = this.args.getData() || {};
this.args.storeData({
...data,
product,
isMonthlyChecked: this.isMonthlyChecked,
isYearlyChecked: this.isYearlyChecked
});
this.args.nextStep();
}
updateAllowedPlan(plan, isChecked) {
const portalPlans = this.settings.get('portalPlans') || [];
const allowedPlans = [...portalPlans];
if (!isChecked) {
this.settings.set('portalPlans', allowedPlans.filter(p => p !== plan));
} else {
allowedPlans.push(plan);
this.settings.set('portalPlans', allowedPlans);
getProduct() {
if (this.product) {
const stripePrices = this.product.stripePrices || [];
if (stripePrices.length === 0 && this.stripeMonthlyAmount && this.stripeYearlyAmount) {
stripePrices.push(
{
nickname: 'Monthly',
amount: this.stripeMonthlyAmount * 100,
active: 1,
currency: this.currency,
interval: 'month',
type: 'recurring'
},
{
nickname: 'Yearly',
amount: this.stripeYearlyAmount * 100,
active: 1,
currency: this.currency,
interval: 'month',
type: 'recurring'
}
);
this.product.set('stripePrices', stripePrices);
return this.product;
} else {
return this.product;
}
}
return null;
}
this.updatePreviewUrl();
async fetchDefaultProduct() {
const products = await this.store.query('product', {include: 'stripe_prices'});
this.product = products.firstObject;
if (this.product.get('stripePrices').length > 0) {
const data = this.args.getData() || {};
this.args.storeData({
...data,
product: null
});
this.args.nextStep();
}
}
updatePreviewUrl() {
const options = {
disableBackground: true,
currency: this.selectedCurrency.value,
monthlyPrice: this.stripePlans.monthly.amount,
yearlyPrice: this.stripePlans.yearly.amount,
monthlyPrice: this.stripeMonthlyAmount * 100,
yearlyPrice: this.stripeYearlyAmount * 100,
isMonthlyChecked: this.isMonthlyChecked,
isYearlyChecked: this.isYearlyChecked,
isFreeChecked: this.isFreeChecked

View File

@ -13,6 +13,7 @@ export default class LaunchController extends Controller {
@tracked previewGuid = (new Date()).valueOf();
@tracked previewSrc = '';
@tracked step = 'customise-design';
@tracked data = null;
steps = {
'customise-design': {
@ -46,6 +47,16 @@ export default class LaunchController extends Controller {
return this.steps[this.step];
}
@action
storeData(data) {
this.data = data;
}
@action
getData() {
return this.data;
}
@action
goToStep(step) {
if (step) {

View File

@ -18,6 +18,8 @@
refreshPreview=this.refreshPreview
updatePreview=this.updatePreview
replacePreviewContents=this.replacePreviewContents
storeData=this.storeData
getData=this.getData
}}
</div>