0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Wired new membership tiers UI to API

refs https://github.com/TryGhost/Team/issues/712
closes https://github.com/TryGhost/Team/issues/717

The product API is updated to support `monthly/yearly_price` on each product instead of using list of stripe prices. This change updates the handling of membership settings to use the updated API instead of `stripe_prices` property.
This commit is contained in:
Rishabh 2021-06-04 13:12:52 +05:30 committed by Rishabh Garg
parent 6165441c30
commit 46e281241e
5 changed files with 130 additions and 200 deletions

View file

@ -12,14 +12,10 @@ export default ApplicationAdapter.extend({
return this._super(...arguments); return this._super(...arguments);
}, },
urlForDeleteRecord(id, modelName, snapshot) { urlForDeleteRecord() {
let url = this._super(...arguments); let url = this._super(...arguments);
let parsedUrl = new URL(url); let parsedUrl = new URL(url);
if (snapshot && snapshot.adapterOptions && snapshot.adapterOptions.cancel) {
parsedUrl.searchParams.set('cancel', 'true');
}
return parsedUrl.toString(); return parsedUrl.toString();
} }
}); });

View file

@ -250,19 +250,32 @@ export default Component.extend({
}); });
}, },
updatePortalPlans(monthlyPriceId, yearlyPriceId) {
let portalPlans = ['free'];
if (monthlyPriceId) {
portalPlans.push(monthlyPriceId);
}
if (yearlyPriceId) {
portalPlans.push(yearlyPriceId);
}
this.settings.set('portalPlans', portalPlans);
},
saveProduct: task(function* () { saveProduct: task(function* () {
const products = yield this.store.query('product', {include: 'monthly_price, yearly_price'});
this.product = products.firstObject;
if (this.product) {
const yearlyDiscount = this.calculateDiscount(5, 50);
this.product.set('monthlyPrice', {
nickname: 'Monthly',
amount: 500,
active: 1,
description: 'Full access',
currency: 'usd',
interval: 'month',
type: 'recurring'
});
this.product.set('yearlyPrice', {
nickname: 'Yearly',
amount: 5000,
active: 1,
currency: 'usd',
description: yearlyDiscount > 0 ? `${yearlyDiscount}% discount` : 'Full access',
interval: 'year',
type: 'recurring'
});
let pollTimeout = 0; let pollTimeout = 0;
/** To allow Stripe config to be ready in backend, we poll the save product request */
while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) { while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) {
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH); yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
@ -279,6 +292,7 @@ export default Component.extend({
} }
} }
} }
}
return this.product; return this.product;
}), }),
@ -289,41 +303,9 @@ export default Component.extend({
try { try {
let response = yield this.settings.save(); let response = yield this.settings.save();
const products = yield this.store.query('product', {include: 'stripe_prices'}); yield this.saveProduct.perform();
this.product = products.firstObject; this.settings.set('portalPlans', ['free', 'monthly', 'yearly']);
if (this.product) {
const stripePrices = this.product.stripePrices || [];
const yearlyDiscount = this.calculateDiscount(5, 50);
stripePrices.push(
{
nickname: 'Monthly',
amount: 500,
active: 1,
description: 'Full access',
currency: 'usd',
interval: 'month',
type: 'recurring'
},
{
nickname: 'Yearly',
amount: 5000,
active: 1,
currency: 'usd',
description: yearlyDiscount > 0 ? `${yearlyDiscount}% discount` : 'Full access',
interval: 'year',
type: 'recurring'
}
);
this.product.set('stripePrices', stripePrices);
const updatedProduct = yield this.saveProduct.perform();
const monthlyPrice = this.getActivePrice(updatedProduct.stripePrices, 'month', 500, 'usd');
const yearlyPrice = this.getActivePrice(updatedProduct.stripePrices, 'year', 5000, 'usd');
this.updatePortalPlans(monthlyPrice.id, yearlyPrice.id);
this.settings.set('membersMonthlyPriceId', monthlyPrice.id);
this.settings.set('membersYearlyPriceId', yearlyPrice.id);
response = yield this.settings.save(); response = yield this.settings.save();
}
this.set('membersStripeOpen', false); this.set('membersStripeOpen', false);
this.set('stripeConnectSuccess', true); this.set('stripeConnectSuccess', true);

View file

@ -24,9 +24,11 @@ export default class MembersAccessController extends Controller {
@tracked showLeaveRouteModal = false; @tracked showLeaveRouteModal = false;
@tracked showPortalSettings = false; @tracked showPortalSettings = false;
@tracked showStripeConnect = false; @tracked showStripeConnect = false;
@tracked showProductModal = false;
@tracked product = null; @tracked product = null;
@tracked stripePrices = []; @tracked products = null;
@tracked productModel = null;
@tracked paidSignupRedirect; @tracked paidSignupRedirect;
@tracked freeSignupRedirect; @tracked freeSignupRedirect;
@tracked stripeMonthlyAmount = 5; @tracked stripeMonthlyAmount = 5;
@ -59,10 +61,8 @@ export default class MembersAccessController extends Controller {
get hasChangedPrices() { get hasChangedPrices() {
if (this.product) { if (this.product) {
this.stripePrices = this.product.get('stripePrices') || []; const monthlyPrice = this.product.get('monthlyPrice');
const activePrices = this.stripePrices.filter(price => !!price.active); const yearlyPrice = this.product.get('yearlyPrice');
const monthlyPrice = this.getPrice(activePrices, 'monthly');
const yearlyPrice = this.getPrice(activePrices, 'yearly');
if (monthlyPrice?.amount && parseInt(this.stripeMonthlyAmount, 10) !== (monthlyPrice.amount / 100)) { if (monthlyPrice?.amount && parseInt(this.stripeMonthlyAmount, 10) !== (monthlyPrice.amount / 100)) {
return true; return true;
@ -77,7 +77,7 @@ export default class MembersAccessController extends Controller {
@action @action
setup() { setup() {
this.fetchDefaultProduct.perform(); this.fetchProducts.perform();
this.updatePortalPreview(); this.updatePortalPreview();
} }
@ -176,6 +176,23 @@ export default class MembersAccessController extends Controller {
this.showStripeConnect = false; this.showStripeConnect = false;
} }
@action
async openEditProduct(product) {
this.productModel = product;
this.showProductModal = true;
}
@action
async openNewProduct() {
this.productModel = this.store.createRecord('product');
this.showProductModal = true;
}
@action
closeProductModal() {
this.showProductModal = false;
}
@action @action
openPortalSettings() { openPortalSettings() {
this.saveSettingsTask.perform(); this.saveSettingsTask.perform();
@ -207,22 +224,14 @@ export default class MembersAccessController extends Controller {
} }
@action @action
updatePortalPreview({forceRefresh} = {}) { updatePortalPreview({forceRefresh} = {forceRefresh: false}) {
// TODO: can these be worked out from settings in membersUtils? // TODO: can these be worked out from settings in membersUtils?
const monthlyPrice = this.stripeMonthlyAmount * 100; const monthlyPrice = this.stripeMonthlyAmount * 100;
const yearlyPrice = this.stripeYearlyAmount * 100; const yearlyPrice = this.stripeYearlyAmount * 100;
let portalPlans = this.settings.get('portalPlans') || []; let portalPlans = this.settings.get('portalPlans') || [];
const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId');
const currentYearlyPriceId = this.settings.get('membersYearlyPriceId');
let isMonthlyChecked = false;
let isYearlyChecked = false;
if (portalPlans.includes(currentMontlyPriceId)) {
isMonthlyChecked = true;
}
if (portalPlans.includes(currentYearlyPriceId)) { let isMonthlyChecked = portalPlans.includes('monthly');
isYearlyChecked = true; let isYearlyChecked = portalPlans.includes('yearly');
}
const newUrl = new URL(this.membersUtils.getPortalPreviewUrl({ const newUrl = new URL(this.membersUtils.getPortalPreviewUrl({
button: false, button: false,
@ -272,119 +281,20 @@ export default class MembersAccessController extends Controller {
} }
} }
@action
confirmProductSave() {
return this.fetchProducts.perform();
}
@task @task
*switchFromNoneTask() { *switchFromNoneTask() {
return yield this.saveSettingsTask.perform({forceRefresh: true}); return yield this.saveSettingsTask.perform({forceRefresh: true});
} }
async saveProduct() { setupPortalProduct(product) {
const isStripeConnected = this.settings.get('stripeConnectAccountId'); if (product) {
if (this.product && isStripeConnected) { const monthlyPrice = product.get('monthlyPrice');
const stripePrices = this.product.stripePrices || []; const yearlyPrice = product.get('yearlyPrice');
const monthlyAmount = this.stripeMonthlyAmount * 100;
const yearlyAmount = this.stripeYearlyAmount * 100;
const getActivePrice = (prices, type, amount) => {
return prices.find((price) => {
return (
price.active && price.amount === amount && price.type === 'recurring' &&
price.interval === type && price.currency.toLowerCase() === this.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: this.currency,
interval: 'month',
type: 'recurring'
}
);
}
if (!yearlyPrice) {
stripePrices.push(
{
nickname: 'Yearly',
amount: this.stripeYearlyAmount * 100,
active: 1,
currency: this.currency,
interval: 'year',
type: 'recurring'
}
);
}
if (monthlyPrice && yearlyPrice) {
this.updatePortalPlans(monthlyPrice.id, yearlyPrice.id);
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);
this.settings.set('membersMonthlyPriceId', updatedMonthlyPrice.id);
this.settings.set('membersYearlyPriceId', updatedYearlyPrice.id);
return savedProduct;
}
}
}
updatePortalPlans(monthlyPriceId, yearlyPriceId) {
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);
portalPlans.pushObject(monthlyPriceId);
}
if (portalPlans.includes(currentYearlyPriceId)) {
portalPlans = portalPlans.filter(priceId => priceId !== currentYearlyPriceId);
portalPlans.pushObject(yearlyPriceId);
}
this.settings.set('portalPlans', portalPlans);
}
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')
);
}
if (type === 'yearly') {
return (
prices.find(price => price.id === yearlyPriceId) ||
prices.find(price => price.nickname === 'Yearly') ||
prices.find(price => price.interval === 'year')
);
}
return null;
}
@task({drop: true})
*fetchDefaultProduct() {
const products = yield this.store.query('product', {include: 'stripe_prices'});
this.product = products.firstObject;
this.stripePrices = [];
if (this.product) {
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) { if (monthlyPrice && monthlyPrice.amount) {
this.stripeMonthlyAmount = (monthlyPrice.amount / 100); this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
this.currency = monthlyPrice.currency; this.currency = monthlyPrice.currency;
@ -396,19 +306,27 @@ export default class MembersAccessController extends Controller {
} }
} }
@task({drop: true})
*fetchProducts() {
this.products = yield this.store.query('product', {include: 'monthly_price,yearly_price'});
this.product = this.products.firstObject;
this.setupPortalProduct(this.product);
}
@task({drop: true}) @task({drop: true})
*saveSettingsTask(options) { *saveSettingsTask(options) {
yield this.validateStripePlans({updatePortalPreview: false}); yield this.validateStripePlans({updatePortalPreview: false});
if (this.stripePlanError) { if (this.stripePlanError && !this.config.get('enableDeveloperExperiments')) {
return; return;
} }
if (this.settings.get('errors').length !== 0) { if (this.settings.get('errors').length !== 0) {
return; return;
} }
if (!this.config.get('enableDeveloperExperiments')) {
yield this.saveProduct(); yield this.saveProduct();
}
const result = yield this.settings.save(); const result = yield this.settings.save();
this.updatePortalPreview(options); this.updatePortalPreview(options);
@ -416,10 +334,37 @@ export default class MembersAccessController extends Controller {
return result; return result;
} }
async saveProduct() {
const isStripeConnected = this.settings.get('stripeConnectAccountId');
if (this.product && isStripeConnected) {
const monthlyAmount = this.stripeMonthlyAmount * 100;
const yearlyAmount = this.stripeYearlyAmount * 100;
this.product.set('monthlyPrice', {
nickname: 'Monthly',
amount: monthlyAmount,
active: true,
currency: this.currency,
interval: 'month',
type: 'recurring'
});
this.product.set('yearlyPrice', {
nickname: 'Yearly',
amount: yearlyAmount,
active: true,
currency: this.currency,
interval: 'year',
type: 'recurring'
});
const savedProduct = await this.product.save();
return savedProduct;
}
}
resetPrices() { resetPrices() {
const activePrices = this.stripePrices.filter(price => !!price.active); const monthlyPrice = this.product.get('monthlyPrice');
const monthlyPrice = this.getPrice(activePrices, 'monthly'); const yearlyPrice = this.product.get('yearlyPrice');
const yearlyPrice = this.getPrice(activePrices, 'yearly');
this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5; this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5;
this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50; this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50;

View file

@ -7,5 +7,6 @@ export default Model.extend(ValidationEngine, {
name: attr('string'), name: attr('string'),
description: attr('string'), description: attr('string'),
slug: attr('string'), slug: attr('string'),
stripePrices: attr('stripe-price') monthlyPrice: attr('stripe-price'),
yearlyPrice: attr('stripe-price')
}); });

View file

@ -3,10 +3,16 @@ import Transform from '@ember-data/serializer/transform';
import {A as emberA, isArray as isEmberArray} from '@ember/array'; import {A as emberA, isArray as isEmberArray} from '@ember/array';
export default Transform.extend({ export default Transform.extend({
deserialize(serialized = []) { deserialize(serialized) {
if (serialized === null || serialized === undefined) {
return null;
} else if (Array.isArray(serialized)) {
const stripePrices = serialized.map(itemDetails => StripePrice.create(itemDetails)); const stripePrices = serialized.map(itemDetails => StripePrice.create(itemDetails));
return emberA(stripePrices); return emberA(stripePrices);
} else {
return StripePrice.create(serialized);
}
}, },
serialize(deserialized) { serialize(deserialized) {
@ -15,7 +21,7 @@ export default Transform.extend({
return item; return item;
}).compact(); }).compact();
} else { } else {
return []; return deserialized || null;
} }
} }
}); });