mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-02 08:13:34 +03:00
740a304bf0
refs https://github.com/TryGhost/Team/issues/704 We try and create new default prices soon after Stripe Connect is completed, but it might take couple of seconds for backend to have Stripe config ready and in the meanwhile saving a product with new prices will fail with 409 Conflict error as it will be unable to create prices on Stripe. This change allows re-attempting saving a product with new prices soon after connect in case of a STRIPE_NOT_CONFIGURED error so that the default prices can be created properly.
357 lines
13 KiB
JavaScript
357 lines
13 KiB
JavaScript
import Component from '@ember/component';
|
|
import {computed} from '@ember/object';
|
|
import {currencies} from 'ghost-admin/utils/currency';
|
|
import {reads} from '@ember/object/computed';
|
|
import {inject as service} from '@ember/service';
|
|
import {task, timeout} from 'ember-concurrency';
|
|
|
|
const RETRY_PRODUCT_SAVE_POLL_LENGTH = 1000;
|
|
const RETRY_PRODUCT_SAVE_MAX_POLL = 15 * RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
|
|
|
export default Component.extend({
|
|
config: service(),
|
|
ghostPaths: service(),
|
|
ajax: service(),
|
|
settings: service(),
|
|
store: service(),
|
|
|
|
topCurrencies: null,
|
|
currencies: null,
|
|
allCurrencies: null,
|
|
stripePlanInvalidAmount: false,
|
|
_scratchStripeYearlyAmount: null,
|
|
_scratchStripeMonthlyAmount: null,
|
|
|
|
// passed in actions
|
|
setStripeConnectIntegrationTokenSetting() {},
|
|
|
|
stripeDirect: reads('config.stripeDirect'),
|
|
|
|
/** OLD **/
|
|
stripeDirectPublicKey: reads('settings.stripePublishableKey'),
|
|
stripeDirectSecretKey: reads('settings.stripeSecretKey'),
|
|
|
|
stripeConnectAccountId: reads('settings.stripeConnectAccountId'),
|
|
stripeConnectAccountName: reads('settings.stripeConnectDisplayName'),
|
|
stripeConnectLivemode: reads('settings.stripeConnectLivemode'),
|
|
|
|
selectedCurrency: computed('stripePlans.monthly.currency', function () {
|
|
return this.get('currencies').findBy('value', this.get('stripePlans.monthly.currency')) || this.get('topCurrencies').findBy('value', this.get('stripePlans.monthly.currency'));
|
|
}),
|
|
|
|
stripePlans: computed('settings.stripePlans', function () {
|
|
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) / 100 || 5,
|
|
currency: monthly.currency
|
|
},
|
|
yearly: {
|
|
amount: parseInt(yearly.amount) / 100 || 50,
|
|
currency: yearly.currency
|
|
}
|
|
};
|
|
}),
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
|
|
const noOfTopCurrencies = 5;
|
|
this.set('topCurrencies', currencies.slice(0, noOfTopCurrencies).map((currency) => {
|
|
return {
|
|
value: currency.isoCode.toLowerCase(),
|
|
label: `${currency.isoCode} - ${currency.name}`,
|
|
isoCode: currency.isoCode
|
|
};
|
|
}));
|
|
|
|
this.set('currencies', currencies.slice(noOfTopCurrencies, currencies.length).map((currency) => {
|
|
return {
|
|
value: currency.isoCode.toLowerCase(),
|
|
label: `${currency.isoCode} - ${currency.name}`,
|
|
isoCode: currency.isoCode
|
|
};
|
|
}));
|
|
|
|
this.set('allCurrencies', [
|
|
{
|
|
groupName: '—',
|
|
options: this.get('topCurrencies')
|
|
},
|
|
{
|
|
groupName: '—',
|
|
options: this.get('currencies')
|
|
}
|
|
]);
|
|
|
|
if (this.get('stripeConnectAccountId')) {
|
|
this.set('membersStripeOpen', false);
|
|
} else {
|
|
this.set('membersStripeOpen', true);
|
|
}
|
|
},
|
|
|
|
actions: {
|
|
setStripeDirectPublicKey(event) {
|
|
this.set('settings.stripeProductName', this.get('settings.title'));
|
|
this.set('settings.stripePublishableKey', event.target.value);
|
|
},
|
|
|
|
setStripeDirectSecretKey(event) {
|
|
this.set('settings.stripeProductName', this.get('settings.title'));
|
|
this.set('settings.stripeSecretKey', event.target.value);
|
|
},
|
|
|
|
validateStripePlans() {
|
|
this.validateStripePlans();
|
|
},
|
|
|
|
setStripePlansCurrency(event) {
|
|
const newCurrency = event.value;
|
|
const updatedPlans = this.get('settings.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.set('settings.stripePlans', updatedPlans);
|
|
this.set('_scratchStripeYearlyAmount', null);
|
|
this.set('_scratchStripeMonthlyAmount', null);
|
|
this.validateStripePlans();
|
|
},
|
|
|
|
setStripeConnectIntegrationToken(event) {
|
|
this.set('settings.stripeProductName', this.get('settings.title'));
|
|
this.setStripeConnectIntegrationTokenSetting(event.target.value);
|
|
},
|
|
|
|
openDisconnectStripeModal() {
|
|
this.openDisconnectStripeConnectModal.perform();
|
|
},
|
|
|
|
closeDisconnectStripeModal() {
|
|
this.set('showDisconnectStripeConnectModal', false);
|
|
},
|
|
|
|
disconnectStripeConnectIntegration() {
|
|
this.disconnectStripeConnectIntegration.perform();
|
|
},
|
|
|
|
openStripeSettings() {
|
|
this.set('membersStripeOpen', true);
|
|
}
|
|
},
|
|
|
|
validateStripePlans() {
|
|
this.get('settings.errors').remove('stripePlans');
|
|
this.get('settings.hasValidated').removeObject('stripePlans');
|
|
|
|
if (this._scratchStripeYearlyAmount === null) {
|
|
this._scratchStripeYearlyAmount = this.get('stripePlans').yearly.amount;
|
|
}
|
|
if (this._scratchStripeMonthlyAmount === null) {
|
|
this._scratchStripeMonthlyAmount = this.get('stripePlans').monthly.amount;
|
|
}
|
|
|
|
try {
|
|
const selectedCurrency = this.selectedCurrency;
|
|
const yearlyAmount = parseInt(this._scratchStripeYearlyAmount);
|
|
const monthlyAmount = parseInt(this._scratchStripeMonthlyAmount);
|
|
if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
|
|
const minimum = Intl.NumberFormat(this.settings.get('lang'), {
|
|
currency: selectedCurrency.isoCode,
|
|
style: 'currency'
|
|
}).format(1);
|
|
|
|
throw new TypeError(`Subscription amount must be at least ${minimum}`);
|
|
}
|
|
|
|
const updatedPlans = this.get('settings.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.set('settings.stripePlans', updatedPlans);
|
|
} catch (err) {
|
|
this.get('settings.errors').add('stripePlans', err.message);
|
|
} finally {
|
|
this.get('settings.hasValidated').pushObject('stripePlans');
|
|
}
|
|
},
|
|
|
|
openDisconnectStripeConnectModal: task(function* () {
|
|
this.set('hasActiveStripeSubscriptions', false);
|
|
if (!this.get('stripeConnectAccountId')) {
|
|
return;
|
|
}
|
|
const url = this.get('ghostPaths.url').api('/members/hasActiveStripeSubscriptions');
|
|
const response = yield this.ajax.request(url);
|
|
|
|
if (response.hasActiveStripeSubscriptions) {
|
|
this.set('hasActiveStripeSubscriptions', true);
|
|
return;
|
|
}
|
|
this.set('showDisconnectStripeConnectModal', true);
|
|
}).drop(),
|
|
|
|
disconnectStripeConnectIntegration: task(function* () {
|
|
this.set('disconnectStripeError', false);
|
|
const url = this.get('ghostPaths.url').api('/settings/stripe/connect');
|
|
|
|
yield this.ajax.delete(url);
|
|
yield this.settings.reload();
|
|
|
|
this.onDisconnected?.();
|
|
}),
|
|
|
|
calculateDiscount(monthly, yearly) {
|
|
if (isNaN(monthly) || isNaN(yearly)) {
|
|
return 0;
|
|
}
|
|
|
|
return monthly ? 100 - Math.floor((yearly / 12 * 100) / monthly) : 0;
|
|
},
|
|
|
|
getActivePrice(prices, interval, amount, currency) {
|
|
return prices.find((price) => {
|
|
return (
|
|
price.active && price.amount === amount && price.type === 'recurring' &&
|
|
price.interval === interval && price.currency.toLowerCase() === currency.toLowerCase()
|
|
);
|
|
});
|
|
},
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
return this.product;
|
|
}),
|
|
|
|
saveStripeSettings: task(function* () {
|
|
this.set('stripeConnectError', null);
|
|
this.set('stripeConnectSuccess', null);
|
|
if (this.get('settings.stripeConnectIntegrationToken')) {
|
|
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();
|
|
}
|
|
|
|
this.set('membersStripeOpen', false);
|
|
this.set('stripeConnectSuccess', true);
|
|
this.onConnected?.();
|
|
|
|
return response;
|
|
} catch (error) {
|
|
if (error.payload && error.payload.errors) {
|
|
this.set('stripeConnectError', 'Invalid secure key');
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
} else {
|
|
this.set('stripeConnectError', 'Please enter a secure key');
|
|
}
|
|
}).drop(),
|
|
|
|
saveSettings: task(function* () {
|
|
return yield this.settings.save();
|
|
}).drop(),
|
|
|
|
get liveStripeConnectAuthUrl() {
|
|
return this.ghostPaths.url.api('members/stripe_connect') + '?mode=live';
|
|
},
|
|
|
|
get testStripeConnectAuthUrl() {
|
|
return this.ghostPaths.url.api('members/stripe_connect') + '?mode=test';
|
|
}
|
|
});
|