import Component from '@glimmer/component'; import {action} from '@ember/object'; 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; export default class GhLaunchWizardConnectStripeComponent extends Component { @service ajax; @service config; @service ghostPaths; @service settings; @service store; @tracked hasActiveStripeSubscriptions = false; @tracked showDisconnectStripeConnectModal = false; @tracked stripeConnectTestMode = false; @tracked stripeConnectError = null; @tracked stripePublishableKeyError = null; @tracked stripeSecretKeyError = null; get stripeConnectAuthUrl() { const mode = this.stripeConnectTestMode ? 'test' : 'live'; return `${this.ghostPaths.url.api('members/stripe_connect')}?mode=${mode}`; } constructor() { super(...arguments); this.args.updatePreview(''); } willDestroy() { super.willDestroy?.(...arguments); // clear any unsaved settings changes when going back/forward/closing this.settings.rollbackAttributes(); } @action setStripeDirectPublicKey(event) { this.settings.set('stripeProductName', this.settings.get('title')); this.settings.set('stripePublishableKey', event.target.value); this.stripePublishableKeyError = null; } @action setStripeDirectSecretKey(event) { this.settings.set('stripeProductName', this.settings.get('title')); this.settings.set('stripeSecretKey', event.target.value); this.stripeSecretKeyError = null; } @action toggleStripeConnectTestMode() { this.stripeConnectTestMode = !this.stripeConnectTestMode; } @action setStripeConnectIntegrationToken(event) { this.settings.set('stripeProductName', this.settings.get('title')); this.settings.set('stripeConnectIntegrationToken', event.target.value); this.stripeConnectError = null; } 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() ); }); } @task({drop: true}) *saveProduct() { let pollTimeout = 0; while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) { yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH); try { const updatedProduct = yield this.product.save(); yield this.settings.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; } @task({drop: true}) *openDisconnectStripeConnectModalTask() { this.hasActiveStripeSubscriptions = false; 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; } @action closeDisconnectStripeModal() { this.showDisconnectStripeConnectModal = false; } @task *disconnectStripeConnectIntegrationTask() { this.disconnectStripeError = false; const url = this.ghostPaths.url.api('/settings/stripe/connect'); yield this.ajax.delete(url); yield this.settings.reload(); } @task *saveAndContinueTask() { if (this.config.get('stripeDirect')) { if (!this.settings.get('stripePublishableKey')) { this.stripePublishableKeyError = 'Enter your publishable key to continue'; } if (!this.settings.get('stripeSecretKey')) { this.stripeSecretKeyError = 'Enter your secret key to continue'; } if (this.stripePublishableKeyError || this.stripeSecretKeyError) { return false; } } else if (!this.settings.get('stripeConnectAccountId') && !this.settings.get('stripeConnectIntegrationToken')) { this.stripeConnectError = 'Paste your secure key to continue'; return false; } if (!this.config.get('stripeDirect') && this.settings.get('stripeConnectAccountId')) { this.args.nextStep(); return true; } try { yield this.settings.save(); const products = yield this.store.query('product', {filter: 'type:paid', 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' }); yield this.saveProduct.perform(); this.settings.set('portalPlans', ['free', 'monthly', 'yearly']); yield this.settings.save(); } this.pauseAndContinueTask.perform(); return true; } catch (error) { if (error.payload?.errors && error.payload.errors[0].type === 'ValidationError') { const [validationError] = error.payload.errors; if (this.config.get('stripeDirect')) { if (validationError.context.match(/stripe_publishable_key/)) { this.stripePublishableKeyError = 'Invalid publishable key'; } else { this.stripeSecretKeyError = 'Invalid secret key'; } } else { this.stripeConnectError = 'Invalid secure key'; } } throw error; } } @task *pauseAndContinueTask() { this.args.refreshPreview(); yield timeout(500); this.args.nextStep(); } }