diff --git a/packages/host/app/components/operator-mode/profile/profile-subscription.gts b/packages/host/app/components/operator-mode/profile/profile-subscription.gts index 95cc66df9a..8d31b0be1b 100644 --- a/packages/host/app/components/operator-mode/profile/profile-subscription.gts +++ b/packages/host/app/components/operator-mode/profile/profile-subscription.gts @@ -1,10 +1,13 @@ import { fn } from '@ember/helper'; import { on } from '@ember/modifier'; +import Owner from '@ember/owner'; import { service } from '@ember/service'; import Component from '@glimmer/component'; -import { trackedFunction } from 'ember-resources/util/function'; +import { task } from 'ember-concurrency'; + +import window from 'ember-window-mock'; import { BoxelButton, @@ -60,22 +63,25 @@ export default class ProfileSubscription extends Component {
Buy more credits
@@ -103,6 +109,7 @@ export default class ProfileSubscription extends Component { gap: var(--boxel-sp-xs); padding-left: var(--boxel-sp-sm); border-left: 5px solid #c6c6c6; + min-height: 40px; } .credit-info__label { font: var(--boxel-font-xs); @@ -148,16 +155,21 @@ export default class ProfileSubscription extends Component { --icon-color: var(--boxel-teal); --boxel-loading-indicator-size: var(--boxel-icon-xs); } + :deep(.boxel-loading-indicator) { + width: 100%; + text-align: center; + } @service private declare billingService: BillingService; - private get paymentLinks() { - return this.fetchPaymentLinks.value ?? []; + constructor(owner: Owner, args: any) { + super(owner, args); + this.fetchPaymentLinks.perform(); } - private fetchPaymentLinks = trackedFunction(this, async () => { + private fetchPaymentLinks = task(async () => { return await this.billingService.fetchPaymentLinks(); }); diff --git a/packages/host/app/services/billing-service.ts b/packages/host/app/services/billing-service.ts index 247d515051..e22eedf77c 100644 --- a/packages/host/app/services/billing-service.ts +++ b/packages/host/app/services/billing-service.ts @@ -5,6 +5,7 @@ import { tracked } from '@glimmer/tracking'; import { dropTask } from 'ember-concurrency'; +import window from 'ember-window-mock'; import Stripe from 'stripe'; import { SupportedMimeType } from '@cardstack/runtime-common'; @@ -31,7 +32,7 @@ interface PaymentLink { export default class BillingService extends Service { @tracked private _subscriptionData: SubscriptionData | null = null; - private _paymentLinks: PaymentLink[] | null = null; + @tracked private _paymentLinks: PaymentLink[] | null = null; @service private declare realmServer: RealmServerService; @service private declare network: NetworkService; @@ -57,22 +58,30 @@ export default class BillingService extends Service { async fetchPaymentLinks() { if (!this._paymentLinks) { - let response = await stripe.paymentLinks.list(); - this._paymentLinks = response.data - .filter((data) => data.metadata.credit_reload_amount) - .map((data) => ({ - url: data.url, - creditReloadAmount: Number(data.metadata.credit_reload_amount), - })) - .sort( - (paymentLinkA, paymentLinkB) => - paymentLinkA.creditReloadAmount - paymentLinkB.creditReloadAmount, - ); + await this.fetchPaymentLinksTask.perform(); } return this._paymentLinks; } + get paymentLinks() { + return this._paymentLinks; + } + + private fetchPaymentLinksTask = dropTask(async () => { + let response = await stripe.paymentLinks.list(); + this._paymentLinks = response.data + .filter((data) => data.metadata.credit_reload_amount) + .map((data) => ({ + url: data.url, + creditReloadAmount: Number(data.metadata.credit_reload_amount), + })) + .sort( + (paymentLinkA, paymentLinkB) => + paymentLinkA.creditReloadAmount - paymentLinkB.creditReloadAmount, + ); + }); + get subscriptionData() { return this._subscriptionData; } diff --git a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts index 1444c04098..2722199f7f 100644 --- a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts +++ b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts @@ -867,6 +867,25 @@ module('Acceptance | operator mode tests', function (hooks) { }); test(`displays credit info in account popover`, async function (assert) { + let countWindowOpenCalled = 0; + let mockWindow = { + closeCalled: false, + close() { + this.closeCalled = true; + }, + }; + + const originalWindowOpen = window.open; + window.open = (url, _target, _feature) => { + assert.true( + typeof url === 'string' + ? url.includes('stripe') + : url?.href.includes('stripe'), + ); + countWindowOpenCalled++; + return mockWindow as unknown as Window; + }; + await visitOperatorMode({ submode: 'interact', codePath: `${testRealmURL}employee.gts`, @@ -901,19 +920,35 @@ module('Acceptance | operator mode tests', function (hooks) { assert.dom('[data-test-buy-more-credits]').hasNoClass('out-of-credit'); await click('[data-test-upgrade-plan-button]'); - assert.dom('[data-test-profile-popover]').doesNotExist(); - assert - .dom('[data-test-boxel-card-container]') - .hasClass('profile-settings'); - await click('[aria-label="close modal"]'); - await click('[data-test-profile-icon-button]'); assert.dom('[data-test-profile-popover]').exists(); await click('[data-test-buy-more-credits] button'); assert.dom('[data-test-profile-popover]').doesNotExist(); assert .dom('[data-test-boxel-card-container]') .hasClass('profile-settings'); + assert.dom('[data-test-subscription-data="plan"]').hasText('Free'); + assert + .dom('[data-test-subscription-data="monthly-credit"]') + .hasText('1000 of 1000 left'); + assert + .dom('[data-test-subscription-data="monthly-credit"]') + .hasNoClass('out-of-credit'); + assert + .dom('[data-test-subscription-data="additional-credit"]') + .hasText('100'); + assert + .dom('[data-test-subscription-data="additional-credit"]') + .hasNoClass('out-of-credit'); + await waitFor('[data-test-payment-link]'); + assert.dom('[data-test-payment-link]').exists({ count: 3 }); + await click('[data-test-manage-plan-button]'); + await click('[data-test-pay-button]'); + assert.strictEqual( + countWindowOpenCalled, + 3, + 'Correct number of times window.open was called', + ); // out of credit await click('[aria-label="close modal"]'); @@ -986,6 +1021,7 @@ module('Acceptance | operator mode tests', function (hooks) { .dom('[data-test-subscription-data="additional-credit"]') .hasNoClass('out-of-credit'); assert.dom('[data-test-buy-more-credits]').hasNoClass('out-of-credit'); + window.open = originalWindowOpen; }); }); });