mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-29 07:09:48 +03:00
c3372f343a
closes https://github.com/TryGhost/Team/issues/2483 - the button is toggled depending on `stripeConnectIntegrationToken` which is a calculated value - however, this calculated value isn't reset when Stripe is disconnected right away without closing the modal - the reset action was already available and it's now passed to `StripeSettingsForm`, so that it can be called when Stripe is disconnected
336 lines
11 KiB
JavaScript
336 lines
11 KiB
JavaScript
import Component from '@glimmer/component';
|
|
import {action} from '@ember/object';
|
|
import {currencies} from 'ghost-admin/utils/currency';
|
|
import {inject} from 'ghost-admin/decorators/inject';
|
|
import {inject as service} from '@ember/service';
|
|
import {task, timeout} from 'ember-concurrency';
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
const RETRY_PRODUCT_SAVE_POLL_LENGTH = 1000;
|
|
const RETRY_PRODUCT_SAVE_MAX_POLL = 15 * RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
|
|
|
const NO_OF_TOP_CURRENCIES = 5;
|
|
|
|
export default class StripeSettingsForm extends Component {
|
|
@service ghostPaths;
|
|
@service ajax;
|
|
@service settings;
|
|
@service membersUtils;
|
|
@service store;
|
|
|
|
@inject config;
|
|
|
|
@tracked hasActiveStripeSubscriptions = false;
|
|
@tracked showDisconnectStripeConnectModal = false;
|
|
@tracked stripeConnectError = null;
|
|
@tracked stripeConnectTestMode = false;
|
|
@tracked stripeDirect = false;
|
|
@tracked stripePlanInvalidAmount = false;
|
|
|
|
@tracked _scratchStripeYearlyAmount = null;
|
|
@tracked _scratchStripeMonthlyAmount = null;
|
|
|
|
topCurrencies = currencies.slice(0, NO_OF_TOP_CURRENCIES).map((currency) => {
|
|
return {
|
|
value: currency.isoCode.toLowerCase(),
|
|
label: `${currency.isoCode} - ${currency.name}`,
|
|
isoCode: currency.isoCode
|
|
};
|
|
});
|
|
|
|
currencies = currencies.slice(NO_OF_TOP_CURRENCIES, currencies.length).map((currency) => {
|
|
return {
|
|
value: currency.isoCode.toLowerCase(),
|
|
label: `${currency.isoCode} - ${currency.name}`,
|
|
isoCode: currency.isoCode
|
|
};
|
|
});
|
|
|
|
allCurrencies = [
|
|
{
|
|
groupName: '—',
|
|
options: this.topCurrencies
|
|
},
|
|
{
|
|
groupName: '—',
|
|
options: this.currencies
|
|
}
|
|
];
|
|
|
|
/** OLD **/
|
|
get stripeDirectPublicKey() {
|
|
return this.settings.stripePublishableKey;
|
|
}
|
|
get stripeDirectSecretKey() {
|
|
return this.settings.stripeSecretKey;
|
|
}
|
|
|
|
get stripeConnectAccountId() {
|
|
return this.settings.stripeConnectAccountId;
|
|
}
|
|
get stripeConnectAccountName() {
|
|
return this.settings.stripeConnectDisplayName;
|
|
}
|
|
get stripeConnectLivemode() {
|
|
return this.settings.stripeConnectLivemode;
|
|
}
|
|
|
|
get selectedCurrency() {
|
|
return this.currencies.findBy('value', this.stripePlans.monthly.currency)
|
|
|| this.topCurrencies.findBy('value', this.stripePlans.monthly.currency);
|
|
}
|
|
|
|
get stripePlans() {
|
|
const plans = this.settings.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
|
|
}
|
|
};
|
|
}
|
|
|
|
get liveStripeConnectAuthUrl() {
|
|
return this.ghostPaths.url.api('members/stripe_connect') + '?mode=live';
|
|
}
|
|
|
|
get testStripeConnectAuthUrl() {
|
|
return this.ghostPaths.url.api('members/stripe_connect') + '?mode=test';
|
|
}
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
|
|
// Allow disabling stripe direct keys if stripe is still enabled, while the config is disabled
|
|
this.updateStripeDirect();
|
|
}
|
|
|
|
@action
|
|
setStripeDirectPublicKey(event) {
|
|
this.settings.stripeProductName = this.settings.title;
|
|
this.settings.stripePublishableKey = event.target.value;
|
|
}
|
|
|
|
@action
|
|
setStripeDirectSecretKey(event) {
|
|
this.settings.stripeProductName = this.settings.title;
|
|
this.settings.stripeSecretKey = event.target.value;
|
|
}
|
|
|
|
@action
|
|
setStripeConnectTestMode(event) {
|
|
this.stripeConnectTestMode = event.target.checked;
|
|
}
|
|
|
|
@action
|
|
setStripePlansCurrency(event) {
|
|
const newCurrency = event.value;
|
|
const updatedPlans = this.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.settings.stripePlans = updatedPlans;
|
|
this._scratchStripeYearlyAmount = null;
|
|
this._scratchStripeMonthlyAmount = null;
|
|
this.validateStripePlans();
|
|
}
|
|
|
|
@action
|
|
setStripeConnectIntegrationToken(event) {
|
|
this.settings.stripeProductName = this.settings.title;
|
|
this.args.setStripeConnectIntegrationTokenSetting(event.target.value);
|
|
}
|
|
|
|
@action
|
|
openDisconnectStripeModal() {
|
|
this.openDisconnectStripeConnectModalTask.perform();
|
|
}
|
|
|
|
@action
|
|
closeDisconnectStripeModal() {
|
|
this.showDisconnectStripeConnectModal = false;
|
|
}
|
|
|
|
@action
|
|
disconnectStripeConnectIntegration() {
|
|
this.disconnectStripeConnectIntegrationTask.perform();
|
|
}
|
|
|
|
@action
|
|
updateStripeDirect() {
|
|
// Allow disabling stripe direct keys if stripe is still enabled, while the config is disabled
|
|
this.stripeDirect = this.config.stripeDirect
|
|
|| (this.membersUtils.isStripeEnabled && !this.settings.stripeConnectAccountId);
|
|
}
|
|
|
|
@action
|
|
validateStripePlans() {
|
|
this.settings.errors.remove('stripePlans');
|
|
this.settings.hasValidated.removeObject('stripePlans');
|
|
|
|
if (this._scratchStripeYearlyAmount === null) {
|
|
this._scratchStripeYearlyAmount = this.stripePlans.yearly.amount;
|
|
}
|
|
if (this._scratchStripeMonthlyAmount === null) {
|
|
this._scratchStripeMonthlyAmount = this.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.locale, {
|
|
currency: selectedCurrency.isoCode,
|
|
style: 'currency'
|
|
}).format(1);
|
|
|
|
throw new TypeError(`Subscription amount must be at least ${minimum}`);
|
|
}
|
|
|
|
const updatedPlans = this.settings.stripePlans.map((plan) => {
|
|
if (plan.name !== 'Complimentary') {
|
|
let newAmount;
|
|
if (plan.interval === 'year') {
|
|
newAmount = Math.round(yearlyAmount * 100);
|
|
} else if (plan.interval === 'month') {
|
|
newAmount = Math.round(monthlyAmount * 100);
|
|
}
|
|
return Object.assign({}, plan, {
|
|
amount: newAmount
|
|
});
|
|
}
|
|
return plan;
|
|
});
|
|
|
|
this.settings.stripePlans = updatedPlans;
|
|
} catch (err) {
|
|
this.settings.errors.add('stripePlans', err.message);
|
|
} finally {
|
|
this.settings.hasValidated.pushObject('stripePlans');
|
|
}
|
|
}
|
|
|
|
@task({drop: true})
|
|
*openDisconnectStripeConnectModalTask() {
|
|
this.hasActiveStripeSubscriptions = false;
|
|
if (!this.stripeConnectAccountId) {
|
|
return;
|
|
}
|
|
const url = this.ghostPaths.url.api('/members/') + '?filter=status:paid&limit=0';
|
|
const response = yield this.ajax.request(url);
|
|
|
|
if (response?.meta?.pagination?.total !== 0) {
|
|
this.hasActiveStripeSubscriptions = true;
|
|
return;
|
|
}
|
|
this.showDisconnectStripeConnectModal = true;
|
|
}
|
|
|
|
@task
|
|
*disconnectStripeConnectIntegrationTask() {
|
|
const url = this.ghostPaths.url.api('/settings/stripe/connect');
|
|
|
|
yield this.ajax.delete(url);
|
|
yield this.settings.reload();
|
|
|
|
this.args.reset?.();
|
|
this.args.onDisconnected?.();
|
|
}
|
|
|
|
@task
|
|
*saveTier() {
|
|
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price, yearly_price'});
|
|
const tier = tiers.firstObject;
|
|
|
|
if (tier) {
|
|
tier.monthlyPrice = 500;
|
|
tier.yearlyPrice = 5000;
|
|
tier.currency = 'usd';
|
|
|
|
let pollTimeout = 0;
|
|
/** To allow Stripe config to be ready in backend, we poll the save tier request */
|
|
while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) {
|
|
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
|
|
|
|
try {
|
|
const updatedTier = yield tier.save();
|
|
|
|
// Reload in the background (no await here)
|
|
this.membersUtils.reload();
|
|
return updatedTier;
|
|
} 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 tier;
|
|
}
|
|
|
|
@task({drop: true})
|
|
*saveStripeSettingsTask() {
|
|
this.stripeConnectError = null;
|
|
|
|
if (this.settings.stripeConnectIntegrationToken) {
|
|
try {
|
|
let response = yield this.settings.save();
|
|
|
|
yield this.saveTier.perform();
|
|
this.settings.portalPlans = ['free', 'monthly', 'yearly'];
|
|
|
|
response = yield this.settings.save();
|
|
|
|
this.args.onConnected?.();
|
|
|
|
return response;
|
|
} catch (error) {
|
|
if (error.payload && error.payload.errors) {
|
|
this.stripeConnectError = 'Invalid secure key';
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
} else {
|
|
this.stripeConnectError = 'Please enter a secure key';
|
|
}
|
|
}
|
|
|
|
@task({drop: true})
|
|
*saveSettingsTask() {
|
|
yield this.settings.save();
|
|
this.updateStripeDirect();
|
|
return this.settings;
|
|
}
|
|
}
|