Ghost/ghost/admin/app/components/settings/members/stripe-settings-form.js
Sodbileg Gansukh c3372f343a
Fixed Stripe connection modal button visibility (#16446)
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
2023-03-22 18:04:30 +08:00

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;
}
}