Refactored stripe settings form component with Octane patterns

refs https://github.com/TryGhost/Ghost/issues/14101

- migrated to native class syntax and Glimmer component patterns
This commit is contained in:
Kevin Ansfield 2022-10-06 14:16:24 +01:00
parent c2d8950bd5
commit fc5f0f7c79
3 changed files with 237 additions and 197 deletions

View File

@ -942,3 +942,14 @@ add|ember-template-lint|no-action|132|15|132|15|c7db9d737e3f06cc754d7a49a3e35897
add|ember-template-lint|no-passed-in-event-handlers|11|28|11|28|02c81dfb804e41f5b3c10730e431d1e4b0958e6f|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
add|ember-template-lint|no-passed-in-event-handlers|20|28|20|28|1591adfb7d0dbab4321126ada2e2c5a4a8c66516|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
add|ember-template-lint|no-passed-in-event-handlers|93|28|93|28|55dadf0e7dc5e2ed57771f46ca3cb82607d1799c|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|11|35|11|35|6f79b46dcaccf8061b341b369ec20f6e0f0805bd|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|20|35|20|35|c84840bfbe762e604dc56cf4df06ab19d38a4777|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|71|78|71|78|09238ca0047aba9705befa8bb94c6d577d7b78cf|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|82|85|82|85|8d227d609618ed70d583fd1c1ecec3638f03138d|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|83|123|83|123|d5b4c9ae3e5c4e36db552abdbc83280f9e6f8d0b|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|93|35|93|35|1808b6250616df5c80cd814986955242d49e70c0|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|131|17|131|17|79c12d2474c1fd8c0ff5ab8cf1fdb3952ab1825b|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-action|132|15|132|15|c7db9d737e3f06cc754d7a49a3e35897117e5777|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-passed-in-event-handlers|11|28|11|28|02c81dfb804e41f5b3c10730e431d1e4b0958e6f|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-passed-in-event-handlers|20|28|20|28|1591adfb7d0dbab4321126ada2e2c5a4a8c66516|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs
remove|ember-template-lint|no-passed-in-event-handlers|93|28|93|28|55dadf0e7dc5e2ed57771f46ca3cb82607d1799c|1665014400000|1675386000000|1680566400000|app/components/settings/members/stripe-settings-form.hbs

View File

@ -4,21 +4,23 @@
<div class="flex flex-column flex-row-l items-start justify-between">
<div class="w-100 w-50-l">
<div class="mb4">
<label class="fw6 f8">Stripe Publishable key</label>
<GhTextInput
@type="password"
@value={{readonly this.stripeDirectPublicKey}}
@input={{action "setStripeDirectPublicKey"}}
@class="mt1 password"
<label class="fw6 f8" for="stripe-publishable-key">Stripe Publishable key</label>
<input
type="text"
id="stripe-publishable-key"
class="gh-input mt1 password"
value={{this.stripeDirectPublicKey}}
{{on "input" this.setStripeDirectPublicKey}}
/>
</div>
<div class="nudge-top--3">
<label class="fw6 f8 mt4">Stripe Secret key</label>
<GhTextInput
@type="password"
@value={{readonly this.stripeDirectSecretKey}}
@input={{action "setStripeDirectSecretKey"}}
@class="mt1 password"
<label class="fw6 f8 mt4" for="stripe-secret-key">Stripe Secret key</label>
<input
type="text"
id="stripe-secret-key"
class="gh-input mt1 password"
value={{this.stripeDirectSecretKey}}
{{on "input" this.setStripeDirectSecretKey}}
/>
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8" rel="noopener noreferrer">
Find your Stripe API keys here &raquo;
@ -41,8 +43,9 @@
</section>
<div class="mb4 mt4 flex justify-end">
<GhTaskButton @buttonText="Save settings"
@task={{this.saveSettings}}
<GhTaskButton
@buttonText="Save settings"
@task={{this.saveSettingsTask}}
@successText="Saved"
@runningText="Saving"
@class="gh-btn gh-btn-primary gh-btn-icon"
@ -68,7 +71,7 @@
</span>
{{/if}}
</div>
<button type="button" class="gh-btn gh-btn-stripe-disconnect" {{action "openDisconnectStripeModal"}}><span>Disconnect</span></button>
<button type="button" class="gh-btn gh-btn-stripe-disconnect" {{on "click" this.openDisconnectStripeModal}}><span>Disconnect</span></button>
</div>
{{else}}
<div class="flex flex-column flex-row-l items-start justify-between">
@ -79,19 +82,20 @@
<div class="ml2 flex items-center flex-nowrap">
<span class="mr2 f8 midgrey nowrap {{if this.stripeConnectTestMode "gh-members-connect-testmodeon"}}">{{if this.stripeConnectTestMode "Using" "Use"}} test mode</span>
<div class="for-switch small">
<label class="switch" for="stripe-connect-test-mode" {{action (toggle "stripeConnectTestMode" this) bubbles="false"}}>
<input type="checkbox" class="gh-input" checked={{this.stripeConnectTestMode}} onclick={{action (toggle "stripeConnectTestMode" this)}} data-test-checkbox="stripe-connect-test-mode">
<label class="switch" for="stripe-connect-test-mode">
<input type="checkbox" id="stripe-connect-test-mode" class="gh-input" checked={{this.stripeConnectTestMode}} {{on "input" this.setStripeConnectTestMode}} aria-label="Use Stripe test mode" data-test-checkbox="stripe-connect-test-mode">
<span class="input-toggle-component mt1"></span>
</label>
</div>
</div>
</div>
<div class="nudge-top--3">
<GhTextarea
@class="gh-members-stripe-connect-token"
@placeholder="Paste your secure key here"
@input={{action "setStripeConnectIntegrationToken"}}
/>
<textarea
class="gh-input gh-members-stripe-connect-token"
placeholder="Paste your secure key here"
aria-label="Stripe connect secure key"
{{on "input" this.setStripeConnectIntegrationToken}}
></textarea>
{{#if this.stripeConnectError}}<p class="mb0 mt2 f8 red">{{this.stripeConnectError}}</p>{{/if}}
</div>
</div>
@ -110,14 +114,15 @@
</div>
<div class="gh-members-connect-savecontainer {{if this.settings.stripeConnectIntegrationToken "expanded"}}">
<GhTaskButton @buttonText="Save Stripe settings"
@task={{this.saveStripeSettings}}
@unlinkedTask={{true}}
@successText="Saved"
@disabled={{is-empty this.settings.stripeConnectIntegrationToken}}
@runningText="Saving"
@class="gh-btn gh-btn-green gh-btn-icon"
/>
<GhTaskButton
@buttonText="Save Stripe settings"
@task={{this.saveStripeSettingsTask}}
@unlinkedTask={{true}}
@successText="Saved"
@disabled={{is-empty this.settings.stripeConnectIntegrationToken}}
@runningText="Saving"
@class="gh-btn gh-btn-green gh-btn-icon"
/>
</div>
{{/if}}
{{/if}}
@ -128,7 +133,7 @@
@model={{hash
stripeConnectAccountName=this.stripeConnectAccountName
}}
@confirm={{action "disconnectStripeConnectIntegration"}}
@close={{action "closeDisconnectStripeModal"}}
@confirm={{this.disconnectStripeConnectIntegration}}
@close={{this.closeDisconnectStripeModal}}
@modifier="action wide" />
{{/if}}

View File

@ -1,46 +1,84 @@
import Component from '@ember/component';
import {computed} from '@ember/object';
import Component from '@glimmer/component';
import {action} 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';
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 Component.extend({
config: service(),
ghostPaths: service(),
ajax: service(),
settings: service(),
membersUtils: service(),
store: service(),
const NO_OF_TOP_CURRENCIES = 5;
topCurrencies: null,
currencies: null,
allCurrencies: null,
stripePlanInvalidAmount: false,
_scratchStripeYearlyAmount: null,
_scratchStripeMonthlyAmount: null,
export default class StripeSettingsForm extends Component {
@service config;
@service ghostPaths;
@service ajax;
@service settings;
@service membersUtils;
@service store;
stripeDirect: false,
@tracked hasActiveStripeSubscriptions = false;
@tracked showDisconnectStripeConnectModal = false;
@tracked stripeConnectError = null;
@tracked stripeConnectTestMode = false;
@tracked stripeDirect = false;
@tracked stripePlanInvalidAmount = false;
// passed in actions
setStripeConnectIntegrationTokenSetting() {},
@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 **/
stripeDirectPublicKey: reads('settings.stripePublishableKey'),
stripeDirectSecretKey: reads('settings.stripeSecretKey'),
get stripeDirectPublicKey() {
return this.settings.get('stripePublishableKey');
}
get stripeDirectSecretKey() {
return this.settings.settings.get('stripeSecretKey');
}
stripeConnectAccountId: reads('settings.stripeConnectAccountId'),
stripeConnectAccountName: reads('settings.stripeConnectDisplayName'),
stripeConnectLivemode: reads('settings.stripeConnectLivemode'),
get stripeConnectAccountId() {
return this.settings.get('stripeConnectAccountId');
}
get stripeConnectAccountName() {
return this.settings.get('stripeConnectDisplayName');
}
get stripeConnectLivemode() {
return this.settings.get('stripeConnectLivemode');
}
selectedCurrency: computed('stripePlans.monthly.currency', function () {
return this.currencies.findBy('value', this.get('stripePlans.monthly.currency')) || this.topCurrencies.findBy('value', this.get('stripePlans.monthly.currency'));
}),
get selectedCurrency() {
return this.currencies.findBy('value', this.stripePlans.monthly.currency)
|| this.topCurrencies.findBy('value', this.stripePlans.monthly.currency);
}
stripePlans: computed('settings.stripePlans', function () {
get stripePlans() {
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');
@ -55,114 +93,103 @@ export default Component.extend({
currency: yearly.currency
}
};
}),
}
init() {
this._super(...arguments);
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();
}
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
};
}));
@action
setStripeDirectPublicKey(event) {
this.settings.set('stripeProductName', this.settings.get('title'));
this.settings.set('stripePublishableKey', event.target.value);
}
this.set('currencies', currencies.slice(noOfTopCurrencies, currencies.length).map((currency) => {
return {
value: currency.isoCode.toLowerCase(),
label: `${currency.isoCode} - ${currency.name}`,
isoCode: currency.isoCode
};
}));
@action
setStripeDirectSecretKey(event) {
this.settings.set('stripeProductName', this.settings.get('title'));
this.settings.set('stripeSecretKey', event.target.value);
}
this.set('allCurrencies', [
{
groupName: '—',
options: this.topCurrencies
},
{
groupName: '—',
options: this.currencies
}
]);
},
@action
setStripeConnectTestMode(event) {
this.stripeConnectTestMode = event.target.checked;
}
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
@action
setStripePlansCurrency(event) {
const newCurrency = event.value;
const updatedPlans = this.settings.get('stripePlans').map((plan) => {
if (plan.name !== 'Complimentary') {
return Object.assign({}, plan, {
currency: newCurrency
});
}
return plan;
});
this.set('settings.stripePlans', updatedPlans);
this.set('_scratchStripeYearlyAmount', null);
this.set('_scratchStripeMonthlyAmount', null);
this.validateStripePlans();
},
const currentComplimentaryPlan = updatedPlans.find((plan) => {
return plan.name === 'Complimentary' && plan.currency === event.value;
});
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();
if (!currentComplimentaryPlan) {
updatedPlans.push({
name: 'Complimentary',
currency: event.value,
interval: 'year',
amount: 0
});
}
},
this.settings.set('stripePlans', updatedPlans);
this._scratchStripeYearlyAmount = null;
this._scratchStripeMonthlyAmount = null;
this.validateStripePlans();
}
@action
setStripeConnectIntegrationToken(event) {
this.settings.set('stripeProductName', this.settings.get('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.set('stripeDirect', this.get('config.stripeDirect') || (this.get('membersUtils.isStripeEnabled') && !this.get('settings.stripeConnectAccountId')));
},
this.stripeDirect = this.config.get('stripeDirect')
|| (this.membersUtils.isStripeEnabled && !this.settings.get('stripeConnectAccountId'));
}
@action
validateStripePlans() {
this.get('settings.errors').remove('stripePlans');
this.get('settings.hasValidated').removeObject('stripePlans');
this.settings.get('errors').remove('stripePlans');
this.settings.get('hasValidated').removeObject('stripePlans');
if (this._scratchStripeYearlyAmount === null) {
this._scratchStripeYearlyAmount = this.stripePlans.yearly.amount;
@ -184,7 +211,7 @@ export default Component.extend({
throw new TypeError(`Subscription amount must be at least ${minimum}`);
}
const updatedPlans = this.get('settings.stripePlans').map((plan) => {
const updatedPlans = this.settings.get('stripePlans').map((plan) => {
if (plan.name !== 'Complimentary') {
let newAmount;
if (plan.interval === 'year') {
@ -199,16 +226,17 @@ export default Component.extend({
return plan;
});
this.set('settings.stripePlans', updatedPlans);
this.settings.set('stripePlans', updatedPlans);
} catch (err) {
this.get('settings.errors').add('stripePlans', err.message);
this.settings.get('errors').add('stripePlans', err.message);
} finally {
this.get('settings.hasValidated').pushObject('stripePlans');
this.settings.get('hasValidated').pushObject('stripePlans');
}
},
}
openDisconnectStripeConnectModal: task(function* () {
this.set('hasActiveStripeSubscriptions', false);
@task({drop: true})
*openDisconnectStripeConnectModalTask() {
this.hasActiveStripeSubscriptions = false;
if (!this.stripeConnectAccountId) {
return;
}
@ -216,36 +244,39 @@ export default Component.extend({
const response = yield this.ajax.request(url);
if (response?.meta?.pagination?.total !== 0) {
this.set('hasActiveStripeSubscriptions', true);
this.hasActiveStripeSubscriptions = true;
return;
}
this.set('showDisconnectStripeConnectModal', true);
}).drop(),
this.showDisconnectStripeConnectModal = true;
}
disconnectStripeConnectIntegration: task(function* () {
this.set('disconnectStripeError', false);
const url = this.get('ghostPaths.url').api('/settings/stripe/connect');
@task
*disconnectStripeConnectIntegrationTask() {
const url = this.ghostPaths.url.api('/settings/stripe/connect');
yield this.ajax.delete(url);
yield this.settings.reload();
this.onDisconnected?.();
}),
this.args.onDisconnected?.();
}
saveTier: task(function* () {
@task
*saveTier() {
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price, yearly_price'});
this.tier = tiers.firstObject;
if (this.tier) {
this.tier.set('monthlyPrice', 500);
this.tier.set('yearlyPrice', 5000);
this.tier.set('currency', 'usd');
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 this.tier.save();
const updatedTier = yield tier.save();
return updatedTier;
} catch (error) {
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
@ -258,13 +289,14 @@ export default Component.extend({
}
}
}
return this.tier;
}),
return tier;
}
saveStripeSettings: task(function* () {
this.set('stripeConnectError', null);
this.set('stripeConnectSuccess', null);
if (this.get('settings.stripeConnectIntegrationToken')) {
@task({drop: true})
*saveStripeSettingsTask() {
this.stripeConnectError = null;
if (this.settings.get('stripeConnectIntegrationToken')) {
try {
let response = yield this.settings.save();
@ -273,33 +305,25 @@ export default Component.extend({
response = yield this.settings.save();
this.set('stripeConnectSuccess', true);
this.onConnected?.();
this.args.onConnected?.();
return response;
} catch (error) {
if (error.payload && error.payload.errors) {
this.set('stripeConnectError', 'Invalid secure key');
this.stripeConnectError = 'Invalid secure key';
return false;
}
throw error;
}
} else {
this.set('stripeConnectError', 'Please enter a secure key');
this.stripeConnectError = 'Please enter a secure key';
}
}).drop(),
saveSettings: task(function* () {
const s = yield this.settings.save();
this.updateStripeDirect();
return s;
}).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';
}
});
@task({drop: true})
*saveSettingsTask() {
yield this.settings.save();
this.updateStripeDirect();
return this.settings;
}
}