mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
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:
parent
5d2e456f61
commit
2a6d7226d2
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -18,6 +18,8 @@
|
||||
refreshPreview=this.refreshPreview
|
||||
updatePreview=this.updatePreview
|
||||
replacePreviewContents=this.replacePreviewContents
|
||||
storeData=this.storeData
|
||||
getData=this.getData
|
||||
}}
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user