mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +03: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:
parent
6165441c30
commit
46e281241e
@ -12,14 +12,10 @@ export default ApplicationAdapter.extend({
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
urlForDeleteRecord(id, modelName, snapshot) {
|
||||
urlForDeleteRecord() {
|
||||
let url = this._super(...arguments);
|
||||
let parsedUrl = new URL(url);
|
||||
|
||||
if (snapshot && snapshot.adapterOptions && snapshot.adapterOptions.cancel) {
|
||||
parsedUrl.searchParams.set('cancel', 'true');
|
||||
}
|
||||
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
});
|
||||
|
@ -250,32 +250,46 @@ 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* () {
|
||||
let pollTimeout = 0;
|
||||
while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) {
|
||||
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
|
||||
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'
|
||||
});
|
||||
|
||||
try {
|
||||
const updatedProduct = yield this.product.save();
|
||||
return updatedProduct;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
|
||||
pollTimeout += RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
||||
// no-op: will try saving again as stripe is not ready
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
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) {
|
||||
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
|
||||
|
||||
try {
|
||||
const updatedProduct = yield this.product.save();
|
||||
return updatedProduct;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
|
||||
pollTimeout += RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
||||
// no-op: will try saving again as stripe is not ready
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -289,41 +303,9 @@ export default Component.extend({
|
||||
try {
|
||||
let response = yield this.settings.save();
|
||||
|
||||
const products = yield this.store.query('product', {include: 'stripe_prices'});
|
||||
this.product = products.firstObject;
|
||||
|
||||
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();
|
||||
}
|
||||
yield this.saveProduct.perform();
|
||||
this.settings.set('portalPlans', ['free', 'monthly', 'yearly']);
|
||||
response = yield this.settings.save();
|
||||
|
||||
this.set('membersStripeOpen', false);
|
||||
this.set('stripeConnectSuccess', true);
|
||||
|
@ -24,9 +24,11 @@ export default class MembersAccessController extends Controller {
|
||||
@tracked showLeaveRouteModal = false;
|
||||
@tracked showPortalSettings = false;
|
||||
@tracked showStripeConnect = false;
|
||||
@tracked showProductModal = false;
|
||||
|
||||
@tracked product = null;
|
||||
@tracked stripePrices = [];
|
||||
@tracked products = null;
|
||||
@tracked productModel = null;
|
||||
@tracked paidSignupRedirect;
|
||||
@tracked freeSignupRedirect;
|
||||
@tracked stripeMonthlyAmount = 5;
|
||||
@ -59,10 +61,8 @@ export default class MembersAccessController extends Controller {
|
||||
|
||||
get hasChangedPrices() {
|
||||
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');
|
||||
const monthlyPrice = this.product.get('monthlyPrice');
|
||||
const yearlyPrice = this.product.get('yearlyPrice');
|
||||
|
||||
if (monthlyPrice?.amount && parseInt(this.stripeMonthlyAmount, 10) !== (monthlyPrice.amount / 100)) {
|
||||
return true;
|
||||
@ -77,7 +77,7 @@ export default class MembersAccessController extends Controller {
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.fetchDefaultProduct.perform();
|
||||
this.fetchProducts.perform();
|
||||
this.updatePortalPreview();
|
||||
}
|
||||
|
||||
@ -176,6 +176,23 @@ export default class MembersAccessController extends Controller {
|
||||
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
|
||||
openPortalSettings() {
|
||||
this.saveSettingsTask.perform();
|
||||
@ -207,22 +224,14 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
|
||||
@action
|
||||
updatePortalPreview({forceRefresh} = {}) {
|
||||
updatePortalPreview({forceRefresh} = {forceRefresh: false}) {
|
||||
// TODO: can these be worked out from settings in membersUtils?
|
||||
const monthlyPrice = this.stripeMonthlyAmount * 100;
|
||||
const yearlyPrice = this.stripeYearlyAmount * 100;
|
||||
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)) {
|
||||
isYearlyChecked = true;
|
||||
}
|
||||
let isMonthlyChecked = portalPlans.includes('monthly');
|
||||
let isYearlyChecked = portalPlans.includes('yearly');
|
||||
|
||||
const newUrl = new URL(this.membersUtils.getPortalPreviewUrl({
|
||||
button: false,
|
||||
@ -272,119 +281,20 @@ export default class MembersAccessController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
confirmProductSave() {
|
||||
return this.fetchProducts.perform();
|
||||
}
|
||||
|
||||
@task
|
||||
*switchFromNoneTask() {
|
||||
return yield this.saveSettingsTask.perform({forceRefresh: true});
|
||||
}
|
||||
|
||||
async saveProduct() {
|
||||
const isStripeConnected = this.settings.get('stripeConnectAccountId');
|
||||
if (this.product && isStripeConnected) {
|
||||
const stripePrices = this.product.stripePrices || [];
|
||||
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');
|
||||
setupPortalProduct(product) {
|
||||
if (product) {
|
||||
const monthlyPrice = product.get('monthlyPrice');
|
||||
const yearlyPrice = product.get('yearlyPrice');
|
||||
if (monthlyPrice && monthlyPrice.amount) {
|
||||
this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
|
||||
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})
|
||||
*saveSettingsTask(options) {
|
||||
yield this.validateStripePlans({updatePortalPreview: false});
|
||||
|
||||
if (this.stripePlanError) {
|
||||
if (this.stripePlanError && !this.config.get('enableDeveloperExperiments')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.get('errors').length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield this.saveProduct();
|
||||
if (!this.config.get('enableDeveloperExperiments')) {
|
||||
yield this.saveProduct();
|
||||
}
|
||||
const result = yield this.settings.save();
|
||||
|
||||
this.updatePortalPreview(options);
|
||||
@ -416,10 +334,37 @@ export default class MembersAccessController extends Controller {
|
||||
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() {
|
||||
const activePrices = this.stripePrices.filter(price => !!price.active);
|
||||
const monthlyPrice = this.getPrice(activePrices, 'monthly');
|
||||
const yearlyPrice = this.getPrice(activePrices, 'yearly');
|
||||
const monthlyPrice = this.product.get('monthlyPrice');
|
||||
const yearlyPrice = this.product.get('yearlyPrice');
|
||||
|
||||
this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5;
|
||||
this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50;
|
||||
|
@ -7,5 +7,6 @@ export default Model.extend(ValidationEngine, {
|
||||
name: attr('string'),
|
||||
description: attr('string'),
|
||||
slug: attr('string'),
|
||||
stripePrices: attr('stripe-price')
|
||||
monthlyPrice: attr('stripe-price'),
|
||||
yearlyPrice: attr('stripe-price')
|
||||
});
|
||||
|
@ -3,10 +3,16 @@ import Transform from '@ember-data/serializer/transform';
|
||||
import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
||||
|
||||
export default Transform.extend({
|
||||
deserialize(serialized = []) {
|
||||
const stripePrices = serialized.map(itemDetails => StripePrice.create(itemDetails));
|
||||
deserialize(serialized) {
|
||||
if (serialized === null || serialized === undefined) {
|
||||
return null;
|
||||
} else if (Array.isArray(serialized)) {
|
||||
const stripePrices = serialized.map(itemDetails => StripePrice.create(itemDetails));
|
||||
|
||||
return emberA(stripePrices);
|
||||
return emberA(stripePrices);
|
||||
} else {
|
||||
return StripePrice.create(serialized);
|
||||
}
|
||||
},
|
||||
|
||||
serialize(deserialized) {
|
||||
@ -15,7 +21,7 @@ export default Transform.extend({
|
||||
return item;
|
||||
}).compact();
|
||||
} else {
|
||||
return [];
|
||||
return deserialized || null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user