From f0cd0b99171816981fe13f9b1571acaea4ca57b1 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Wed, 23 Apr 2025 19:23:56 +0100 Subject: [PATCH 01/31] Add a billing tab to the settings --- .../billing-page/membership-section.hbs | 68 +++++++++++++++++++ .../billing-page/membership-section.ts | 18 +++++ .../billing-page/payment-history-section.hbs | 38 +++++++++++ .../billing-page/payment-history-section.ts | 18 +++++ .../settings/billing-page/renewal-section.hbs | 10 +++ .../settings/billing-page/renewal-section.ts | 13 ++++ .../settings/billing-page/support-section.hbs | 12 ++++ .../settings/billing-page/support-section.ts | 17 +++++ app/controllers/settings.ts | 1 + app/controllers/settings/billing.ts | 6 ++ app/helpers/format-date.ts | 16 +++++ app/router.ts | 1 + app/templates/settings/billing.hbs | 21 ++++++ 13 files changed, 239 insertions(+) create mode 100644 app/components/settings/billing-page/membership-section.hbs create mode 100644 app/components/settings/billing-page/membership-section.ts create mode 100644 app/components/settings/billing-page/payment-history-section.hbs create mode 100644 app/components/settings/billing-page/payment-history-section.ts create mode 100644 app/components/settings/billing-page/renewal-section.hbs create mode 100644 app/components/settings/billing-page/renewal-section.ts create mode 100644 app/components/settings/billing-page/support-section.hbs create mode 100644 app/components/settings/billing-page/support-section.ts create mode 100644 app/controllers/settings/billing.ts create mode 100644 app/helpers/format-date.ts create mode 100644 app/templates/settings/billing.hbs diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs new file mode 100644 index 0000000000..46c118b9f3 --- /dev/null +++ b/app/components/settings/billing-page/membership-section.hbs @@ -0,0 +1,68 @@ +{{#if @user.isVip}} +
+ +
+ VIP Access +
+
+ +
+

+ {{#if @user.vipStatusExpiresAt}} + πŸŽ‰ You have VIP access to all CodeCrafters content, valid until + {{format-date @user.vipStatusExpiresAt}}. + {{else}} + πŸŽ‰ You have VIP access to all CodeCrafters content. + {{/if}} +

+
+{{else if @user.hasActiveSubscription}} +
+ +
+ Membership active +
+
+ +
+

+ Your membership is valid until + {{format-date @user.activeSubscription.endDate}}. +

+
+{{else if @user.activeSubscription}} +
+ +
+ Membership expired +
+
+ +
+

+ Your membership expired on + {{format-date @user.activeSubscription.endDate}}. Start a new membership to get access to + membership benefits. +

+
+ + Start membership β†’ + +{{else}} +
+ +
+ No membership found +
+
+ +
+

+ You don't have a CodeCrafters membership. Start one to get access to + membership benefits. +

+
+ + Start membership β†’ + +{{/if}} \ No newline at end of file diff --git a/app/components/settings/billing-page/membership-section.ts b/app/components/settings/billing-page/membership-section.ts new file mode 100644 index 0000000000..6ddadcd08e --- /dev/null +++ b/app/components/settings/billing-page/membership-section.ts @@ -0,0 +1,18 @@ +import Component from '@glimmer/component'; +import type UserModel from 'codecrafters-frontend/models/user'; + +interface Signature { + Element: HTMLDivElement; + + Args: { + user: UserModel; + }; +} + +export default class MembershipSectionComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Settings::BillingPage::MembershipSection': typeof MembershipSectionComponent; + } +} \ No newline at end of file diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs new file mode 100644 index 0000000000..f8abffa785 --- /dev/null +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -0,0 +1,38 @@ +
+
+ {{! Header }} +
Date
+
Amount
+
+ + {{#each @charges as |charge|}} +
{{format-date charge.createdAt}}
+
+ {{charge.displayString}} + {{#if (gt charge.amountRefunded 0)}} + {{#if charge.isFullyRefunded}} + (refunded) + {{else}} + ({{charge.refundedAmountDisplayString}} + refunded) + {{/if}} + {{/if}} +
+
+ {{#if (and charge.invoiceId charge.statusIsSucceeded)}} + + Download Invoice + + {{else if charge.statusIsFailed}} + Payment failed + {{/if}} +
+ {{/each}} +
+
\ No newline at end of file diff --git a/app/components/settings/billing-page/payment-history-section.ts b/app/components/settings/billing-page/payment-history-section.ts new file mode 100644 index 0000000000..d1b90a64d4 --- /dev/null +++ b/app/components/settings/billing-page/payment-history-section.ts @@ -0,0 +1,18 @@ +import Component from '@glimmer/component'; +import type ChargeModel from 'codecrafters-frontend/models/charge'; + +interface Signature { + Element: HTMLDivElement; + + Args: { + charges: ChargeModel[]; + }; +} + +export default class PaymentHistorySectionComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Settings::BillingPage::PaymentHistorySection': typeof PaymentHistorySectionComponent; + } +} \ No newline at end of file diff --git a/app/components/settings/billing-page/renewal-section.hbs b/app/components/settings/billing-page/renewal-section.hbs new file mode 100644 index 0000000000..afab5788b1 --- /dev/null +++ b/app/components/settings/billing-page/renewal-section.hbs @@ -0,0 +1,10 @@ +
+ +
+ Auto-renew disabled +
+
+ +
+ Your membership does not renew automatically. Once your membership expires, you'll be able to make a new one-time payment. +
\ No newline at end of file diff --git a/app/components/settings/billing-page/renewal-section.ts b/app/components/settings/billing-page/renewal-section.ts new file mode 100644 index 0000000000..d3f8582ca7 --- /dev/null +++ b/app/components/settings/billing-page/renewal-section.ts @@ -0,0 +1,13 @@ +import Component from '@glimmer/component'; + +interface Signature { + Element: HTMLDivElement; +} + +export default class RenewalSectionComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Settings::BillingPage::RenewalSection': typeof RenewalSectionComponent; + } +} \ No newline at end of file diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs new file mode 100644 index 0000000000..fcfbfb74e8 --- /dev/null +++ b/app/components/settings/billing-page/support-section.hbs @@ -0,0 +1,12 @@ + + + Get help + + +
+

+ Questions? Click the button below or write to us at + hello@codecrafters.io + and we'll help sort things out. +

+
diff --git a/app/components/settings/billing-page/support-section.ts b/app/components/settings/billing-page/support-section.ts new file mode 100644 index 0000000000..a67af564d6 --- /dev/null +++ b/app/components/settings/billing-page/support-section.ts @@ -0,0 +1,17 @@ +import Component from '@glimmer/component'; + +interface Signature { + Element: HTMLDivElement; + + Args: { + username: string; + }; +} + +export default class SupportSectionComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Settings::BillingPage::SupportSection': typeof SupportSectionComponent; + } +} \ No newline at end of file diff --git a/app/controllers/settings.ts b/app/controllers/settings.ts index f2ff73b2a2..05f7fdf3b6 100644 --- a/app/controllers/settings.ts +++ b/app/controllers/settings.ts @@ -15,6 +15,7 @@ export default class SettingsController extends Controller { get tabs() { return [ { route: 'settings.profile', label: 'Profile' }, + { route: 'settings.billing', label: 'Billing' }, { route: 'settings.account', label: 'Account' }, ]; } diff --git a/app/controllers/settings/billing.ts b/app/controllers/settings/billing.ts new file mode 100644 index 0000000000..a9c9747f3b --- /dev/null +++ b/app/controllers/settings/billing.ts @@ -0,0 +1,6 @@ +import Controller from '@ember/controller'; +import type { ModelType } from 'codecrafters-frontend/routes/settings'; + +export default class BillingController extends Controller { + declare model: ModelType; +} diff --git a/app/helpers/format-date.ts b/app/helpers/format-date.ts new file mode 100644 index 0000000000..3d5981f339 --- /dev/null +++ b/app/helpers/format-date.ts @@ -0,0 +1,16 @@ +import { helper } from '@ember/component/helper'; + +export function formatDate([date]: [Date]): string { + if (!date) return ''; + + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true + }).format(date); +} + +export default helper(formatDate); \ No newline at end of file diff --git a/app/router.ts b/app/router.ts index 88872f955e..a20d933352 100644 --- a/app/router.ts +++ b/app/router.ts @@ -81,6 +81,7 @@ Router.map(function () { this.route('settings', function () { this.route('profile'); + this.route('billing'); this.route('account'); }); diff --git a/app/templates/settings/billing.hbs b/app/templates/settings/billing.hbs new file mode 100644 index 0000000000..7491c8a056 --- /dev/null +++ b/app/templates/settings/billing.hbs @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 22a50d9754cc809cbe6a30a5478f7ad1d4d999b9 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Wed, 23 Apr 2025 19:39:45 +0100 Subject: [PATCH 02/31] ofc I forgot about the linter --- .../billing-page/membership-section.hbs | 8 +++---- .../billing-page/membership-section.ts | 2 +- .../billing-page/payment-history-section.hbs | 14 +++++++---- .../billing-page/payment-history-section.ts | 2 +- .../settings/billing-page/renewal-section.hbs | 2 +- .../settings/billing-page/renewal-section.ts | 2 +- .../settings/billing-page/support-section.hbs | 4 ++-- .../settings/billing-page/support-section.ts | 2 +- app/controllers/settings/billing.ts | 9 ++++++-- app/helpers/format-date.ts | 16 ------------- app/routes/settings/billing.ts | 23 +++++++++++++++++++ app/templates/settings/billing.hbs | 9 +++++--- 12 files changed, 56 insertions(+), 37 deletions(-) delete mode 100644 app/helpers/format-date.ts create mode 100644 app/routes/settings/billing.ts diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 46c118b9f3..69d3b50fd5 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -10,7 +10,7 @@

{{#if @user.vipStatusExpiresAt}} πŸŽ‰ You have VIP access to all CodeCrafters content, valid until - {{format-date @user.vipStatusExpiresAt}}. + {{date-format @user.vipStatusExpiresAt format="PPPp"}}. {{else}} πŸŽ‰ You have VIP access to all CodeCrafters content. {{/if}} @@ -27,7 +27,7 @@

Your membership is valid until - {{format-date @user.activeSubscription.endDate}}. + {{date-format @user.activeSubscription.endedAt format="PPPp"}}.

{{else if @user.activeSubscription}} @@ -41,7 +41,7 @@

Your membership expired on - {{format-date @user.activeSubscription.endDate}}. Start a new membership to get access to + {{date-format @user.activeSubscription.endedAt format="PPPp"}}. Start a new membership to get access to membership benefits.

@@ -65,4 +65,4 @@ Start membership β†’ -{{/if}} \ No newline at end of file +{{/if}} \ No newline at end of file diff --git a/app/components/settings/billing-page/membership-section.ts b/app/components/settings/billing-page/membership-section.ts index 6ddadcd08e..fb9de128a3 100644 --- a/app/components/settings/billing-page/membership-section.ts +++ b/app/components/settings/billing-page/membership-section.ts @@ -15,4 +15,4 @@ declare module '@glint/environment-ember-loose/registry' { export default interface Registry { 'Settings::BillingPage::MembershipSection': typeof MembershipSectionComponent; } -} \ No newline at end of file +} diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index f8abffa785..017f158dcb 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,4 +1,4 @@ -
+{{#if (gt @charges.length 0)}}
{{! Header }}
Date
@@ -6,8 +6,8 @@
{{#each @charges as |charge|}} -
{{format-date charge.createdAt}}
- \ No newline at end of file diff --git a/app/components/settings/billing-page/renewal-section.ts b/app/components/settings/billing-page/renewal-section.ts index d3f8582ca7..7022713939 100644 --- a/app/components/settings/billing-page/renewal-section.ts +++ b/app/components/settings/billing-page/renewal-section.ts @@ -10,4 +10,4 @@ declare module '@glint/environment-ember-loose/registry' { export default interface Registry { 'Settings::BillingPage::RenewalSection': typeof RenewalSectionComponent; } -} \ No newline at end of file +} diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs index fcfbfb74e8..d61f511743 100644 --- a/app/components/settings/billing-page/support-section.hbs +++ b/app/components/settings/billing-page/support-section.hbs @@ -2,11 +2,11 @@ Get help - +

Questions? Click the button below or write to us at hello@codecrafters.io and we'll help sort things out.

-
+
\ No newline at end of file diff --git a/app/components/settings/billing-page/support-section.ts b/app/components/settings/billing-page/support-section.ts index a67af564d6..8b3ed0db55 100644 --- a/app/components/settings/billing-page/support-section.ts +++ b/app/components/settings/billing-page/support-section.ts @@ -14,4 +14,4 @@ declare module '@glint/environment-ember-loose/registry' { export default interface Registry { 'Settings::BillingPage::SupportSection': typeof SupportSectionComponent; } -} \ No newline at end of file +} diff --git a/app/controllers/settings/billing.ts b/app/controllers/settings/billing.ts index a9c9747f3b..35966972cd 100644 --- a/app/controllers/settings/billing.ts +++ b/app/controllers/settings/billing.ts @@ -1,6 +1,11 @@ import Controller from '@ember/controller'; -import type { ModelType } from 'codecrafters-frontend/routes/settings'; +import type ChargeModel from 'codecrafters-frontend/models/charge'; +import type { ModelType as SettingsModelType } from 'codecrafters-frontend/routes/settings'; + +interface BillingModelType extends SettingsModelType { + charges: ChargeModel[]; +} export default class BillingController extends Controller { - declare model: ModelType; + declare model: BillingModelType; } diff --git a/app/helpers/format-date.ts b/app/helpers/format-date.ts deleted file mode 100644 index 3d5981f339..0000000000 --- a/app/helpers/format-date.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { helper } from '@ember/component/helper'; - -export function formatDate([date]: [Date]): string { - if (!date) return ''; - - return new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - hour12: true - }).format(date); -} - -export default helper(formatDate); \ No newline at end of file diff --git a/app/routes/settings/billing.ts b/app/routes/settings/billing.ts new file mode 100644 index 0000000000..4d12fa930a --- /dev/null +++ b/app/routes/settings/billing.ts @@ -0,0 +1,23 @@ +import Route from '@ember/routing/route'; +import type ChargeModel from 'codecrafters-frontend/models/charge'; +import { inject as service } from '@ember/service'; +import type Store from '@ember-data/store'; +import type { ModelType as SettingsModelType } from 'codecrafters-frontend/routes/settings'; + +interface BillingModelType extends SettingsModelType { + charges: ChargeModel[]; +} + +export default class BillingRoute extends Route { + @service declare store: Store; + + async model(): Promise { + const user = this.modelFor('settings') as SettingsModelType; + const charges = await this.store.findAll('charge'); + + return { + user: user.user, + charges: charges.toArray(), + }; + } +} diff --git a/app/templates/settings/billing.hbs b/app/templates/settings/billing.hbs index 7491c8a056..6dab89199a 100644 --- a/app/templates/settings/billing.hbs +++ b/app/templates/settings/billing.hbs @@ -1,5 +1,6 @@ +{{! template-lint-disable no-implicit-this }} - + @@ -11,11 +12,13 @@ - + - + {{#let this.model.charges as |charges|}} + + {{/let}} \ No newline at end of file From fdf94800e6ce6205e6fcc577f963f7fb2ebb2d9a Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 16:39:30 +0100 Subject: [PATCH 03/31] most tests are working --- .../billing-page/membership-section.hbs | 37 ++++--- .../billing-page/payment-history-section.hbs | 21 ++-- .../settings/billing-page/support-section.hbs | 6 +- mirage/models/charge.js | 13 +++ .../acceptance/settings-page/billing-test.js | 96 +++++++++++++++++++ tests/pages/settings/billing-page.ts | 25 +++++ tests/support/authentication-helpers.js | 8 ++ 7 files changed, 183 insertions(+), 23 deletions(-) create mode 100644 mirage/models/charge.js create mode 100644 tests/acceptance/settings-page/billing-test.js create mode 100644 tests/pages/settings/billing-page.ts diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 69d3b50fd5..5b497773c4 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,5 +1,8 @@ {{#if @user.isVip}} -
+
VIP Access @@ -17,7 +20,7 @@

{{else if @user.hasActiveSubscription}} -
+
Membership active @@ -25,23 +28,35 @@
-

- Your membership is valid until - {{date-format @user.activeSubscription.endedAt format="PPPp"}}. -

+ {{#if @user.activeSubscription.cancelAt}} +

+ Your CodeCrafters membership is valid until + {{date-format @user.activeSubscription.cancelAt format="PPPp"}}. +

+

Your membership doesn't renew automatically. To restart your membership, make a new one-time payment.

+ {{else}} +

+ You are currently subscribed to the + {{@user.activeSubscription.pricingPlanName}} + plan. +

+ {{/if}}
{{else if @user.activeSubscription}} -
+
- Membership expired + Membership inactive

- Your membership expired on - {{date-format @user.activeSubscription.endedAt format="PPPp"}}. Start a new membership to get access to + Your CodeCrafters membership is + currently inactive. +

+

+ Start a new membership to get access to membership benefits.

@@ -49,7 +64,7 @@ Start membership β†’ {{else}} -
+
No membership found diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index 017f158dcb..0b4288372f 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,20 +1,22 @@ {{#if (gt @charges.length 0)}} -
- {{! Header }} +
+
Date
Amount
{{#each @charges as |charge|}} -
{{date-format charge.createdAt format="PPP"}}
-
+
+ {{date-format charge.createdAt format="PPP"}} +
+
{{charge.displayString}} {{#if (gt charge.amountRefunded 0)}} {{#if charge.isFullyRefunded}} - (refunded) + (refunded) {{else}} - ({{charge.refundedAmountDisplayString}} - refunded) + ({{charge.refundedAmountDisplayString}} + refunded) {{/if}} {{/if}}
@@ -24,19 +26,20 @@ href={{charge.invoiceDownloadUrl}} target="_blank" class="text-teal-500 hover:text-teal-600 font-semibold text-sm" + data-test-status data-test-download-invoice-link rel="noopener noreferrer" > Download Invoice {{else if charge.statusIsFailed}} - Payment failed + Payment failed {{/if}}
{{/each}}
{{else}} -
+
No payment history found.
{{/if}} \ No newline at end of file diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs index d61f511743..6aeb0cc0f6 100644 --- a/app/components/settings/billing-page/support-section.hbs +++ b/app/components/settings/billing-page/support-section.hbs @@ -1,11 +1,11 @@ - - + + Get help

- Questions? Click the button below or write to us at + Questions? Click the button above or write to us at hello@codecrafters.io and we'll help sort things out.

diff --git a/mirage/models/charge.js b/mirage/models/charge.js new file mode 100644 index 0000000000..e9ccd43a25 --- /dev/null +++ b/mirage/models/charge.js @@ -0,0 +1,13 @@ +import { Model, belongsTo } from 'miragejs'; + +export default Model.extend({ + user: belongsTo(), + amount: 0, + amountRefunded: 0, + currency: 'usd', + createdAt() { + return new Date(); // Done this way due to linting error and shared objects + }, + invoiceId: null, + status: 'succeeded', +}); diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js new file mode 100644 index 0000000000..d1a1b620f5 --- /dev/null +++ b/tests/acceptance/settings-page/billing-test.js @@ -0,0 +1,96 @@ +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'codecrafters-frontend/tests/helpers'; +import { signIn, signInAsSubscriber, signInAsVipUser } from 'codecrafters-frontend/tests/support/authentication-helpers'; +import testScenario from 'codecrafters-frontend/mirage/scenarios/test'; +import billingPage from 'codecrafters-frontend/tests/pages/settings/billing-page'; +import { settled } from '@ember/test-helpers'; + +module('Acceptance | settings-page | billing-test', function (hooks) { + setupApplicationTest(hooks); + + test('membership section shows correct plan for subscriber with active subscription', async function (assert) { + testScenario(this.server); + signInAsSubscriber(this.owner, this.server); + const subscription = this.server.schema.subscriptions.first(); + subscription.update('pricingPlanName', 'Yearly Plan'); + + await billingPage.visit(); + await settled(); + + assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); + assert.ok(billingPage.membershipSection.hasActivePlan, 'shows active plan'); + }); + + test('membership section shows correct plan for subscriber with canceled subscription', async function (assert) { + testScenario(this.server); + signInAsSubscriber(this.owner, this.server); + const subscription = this.server.schema.subscriptions.first(); + subscription.update('cancelAt', new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 30)); // 30 days from now + + await billingPage.visit(); + await settled(); + + assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); + assert.ok(billingPage.membershipSection.hasActivePlan, 'shows active plan'); + }); + + test('membership section shows correct plan for non-subscriber', async function (assert) { + testScenario(this.server); + signIn(this.owner, this.server); + + await billingPage.visit(); + await settled(); + + assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); + assert.notOk(billingPage.membershipSection.hasActivePlan, 'does not show active plan'); + }); + + test('membership section shows VIP access for subscriber with VIP access', async function (assert) { + testScenario(this.server); + signInAsVipUser(this.owner, this.server); + + await billingPage.visit(); + await settled(); + + assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); + assert.ok(billingPage.membershipSection.hasVipAccess, 'shows VIP access'); + }); + + test('support section is visible', async function (assert) { + testScenario(this.server); + signInAsSubscriber(this.owner, this.server); + + await billingPage.visit(); + await settled(); + + assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); + // await billingPage.supportSection.clickContactButton(); + // Commented out because it opens the email client, if behavior changes, uncomment + }); + + test('payment history section shows correct information', async function (assert) { + testScenario(this.server); + const user = signInAsSubscriber(this.owner, this.server); + + await billingPage.visit(); + await settled(); + + assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); + assert.equal(billingPage.paymentHistorySection.isEmpty, '', 'shows empty state initially'); + + this.server.create('charge', { + user: user, + amount: 12000, + amountRefunded: 0, + currency: 'usd', + status: 'succeeded', + }); + + await settled(); + + await billingPage.visit(); + await settled(); + + assert.notEqual(billingPage.paymentHistorySection.isEmpty, '', 'shows payment history after charge is created'); + }); +}); diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts new file mode 100644 index 0000000000..5acd9a3d4b --- /dev/null +++ b/tests/pages/settings/billing-page.ts @@ -0,0 +1,25 @@ +import { clickable, hasClass, isVisible, visitable, attribute } from 'ember-cli-page-object'; +import createPage from 'codecrafters-frontend/tests/support/create-page'; + +export default createPage({ + membershipSection: { + scope: '[data-test-membership-section]', + hasActivePlan: hasClass('has-active-plan'), + isVisible: isVisible(), + hasVipAccess: hasClass('has-vip-access'), + }, + + paymentHistorySection: { + scope: '[data-test-payment-history-section]', + isVisible: isVisible(), + isEmpty: attribute('data-test-empty-state'), + }, + + supportSection: { + scope: '[data-test-support-section]', + isVisible: isVisible(), + clickContactButton: clickable('[data-test-support-contact-button]'), + }, + + visit: visitable('/settings/billing'), +}); diff --git a/tests/support/authentication-helpers.js b/tests/support/authentication-helpers.js index f95df44759..323e62ec54 100644 --- a/tests/support/authentication-helpers.js +++ b/tests/support/authentication-helpers.js @@ -60,6 +60,14 @@ export function signInAsSubscriber(owner, server, user) { return signIn(owner, server, user); } +export function signInAsVipUser(owner, server, user) { + user = user || server.schema.users.find('63c51e91-e448-4ea9-821b-a80415f266d3'); + user.update('isVip', true); + user.update('vipStatusExpiresAt', new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 30)); // 30 days from now + + return signIn(owner, server, user); +} + export function signInAsTeamAdmin(owner, server, user) { user = user || server.schema.users.find('63c51e91-e448-4ea9-821b-a80415f266d3'); const team = server.create('team', { id: 'dummy-team-id', name: 'Dummy Team' }); From 7646797f9e7ac30b092ff0b6d67f0e4035224678 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 16:59:00 +0100 Subject: [PATCH 04/31] tests are working and are extensive --- .../billing-page/payment-history-section.hbs | 6 +-- .../acceptance/settings-page/billing-test.js | 43 ++++++++++++++++--- tests/pages/settings/billing-page.ts | 6 ++- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index 0b4288372f..b04753a474 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -6,11 +6,11 @@
{{#each @charges as |charge|}} -
+
{{date-format charge.createdAt format="PPP"}}
-
- {{charge.displayString}} +
+ {{charge.displayString}} {{#if (gt charge.amountRefunded 0)}} {{#if charge.isFullyRefunded}} (refunded) diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index d1a1b620f5..98c3714a98 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -68,29 +68,58 @@ module('Acceptance | settings-page | billing-test', function (hooks) { // Commented out because it opens the email client, if behavior changes, uncomment }); - test('payment history section shows correct information', async function (assert) { + test('payment history section shows empty state initially', async function (assert) { testScenario(this.server); - const user = signInAsSubscriber(this.owner, this.server); + signInAsSubscriber(this.owner, this.server); await billingPage.visit(); await settled(); assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); - assert.equal(billingPage.paymentHistorySection.isEmpty, '', 'shows empty state initially'); + assert.equal(billingPage.paymentHistorySection.charges.length, 0, 'shows no charges initially'); + }); + + test('payment history section shows charges after creation', async function (assert) { + testScenario(this.server); + const user = signInAsSubscriber(this.owner, this.server); + user.update({ + id: '63c51e91-e448-4ea9-821b-a80415f266d3', + email: 'test@example.com', + name: 'Test User', + createdAt: new Date(), + updatedAt: new Date(), + }); this.server.create('charge', { + id: 'charge-1', user: user, amount: 12000, amountRefunded: 0, currency: 'usd', + createdAt: new Date(), + invoiceId: 'invoice-1', + status: 'failed', + }); + + this.server.create('charge', { + id: 'charge-2', + user: user, + amount: 12000, + amountRefunded: 0, + currency: 'usd', + createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 7), + invoiceId: 'invoice-2', status: 'succeeded', }); - await settled(); - await billingPage.visit(); await settled(); - - assert.notEqual(billingPage.paymentHistorySection.isEmpty, '', 'shows payment history after charge is created'); + + assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); + assert.equal(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges after creation'); + assert.equal(billingPage.paymentHistorySection.charges[0].amount, '$120', 'shows correct amount for first charge'); + assert.ok(billingPage.paymentHistorySection.charges[0].failed, 'shows failed status for first charge'); + assert.equal(billingPage.paymentHistorySection.charges[1].amount, '$120', 'shows correct amount for second charge'); + assert.notOk(billingPage.paymentHistorySection.charges[1].failed, 'shows succeeded status for second charge'); }); }); diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index 5acd9a3d4b..a7cb0e0bf3 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -1,4 +1,4 @@ -import { clickable, hasClass, isVisible, visitable, attribute } from 'ember-cli-page-object'; +import { clickable, hasClass, isVisible, visitable, attribute, collection, text } from 'ember-cli-page-object'; import createPage from 'codecrafters-frontend/tests/support/create-page'; export default createPage({ @@ -13,6 +13,10 @@ export default createPage({ scope: '[data-test-payment-history-section]', isVisible: isVisible(), isEmpty: attribute('data-test-empty-state'), + charges: collection('[data-test-payment-history-item]', { + amount: text('[data-test-amount]'), + failed: hasClass('text-red-600'), + }), }, supportSection: { From ef59d5abb2d8b32a056fac8a0322f37924a05af9 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 17:00:17 +0100 Subject: [PATCH 05/31] making the linter a little less upset --- tests/acceptance/settings-page/billing-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 98c3714a98..0153153d17 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -76,7 +76,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await settled(); assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); - assert.equal(billingPage.paymentHistorySection.charges.length, 0, 'shows no charges initially'); + assert.strictEqual(billingPage.paymentHistorySection.charges.length, 0, 'shows no charges initially'); }); test('payment history section shows charges after creation', async function (assert) { @@ -116,10 +116,10 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await settled(); assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); - assert.equal(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges after creation'); - assert.equal(billingPage.paymentHistorySection.charges[0].amount, '$120', 'shows correct amount for first charge'); + assert.strictEqual(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges after creation'); + assert.strictEqual(billingPage.paymentHistorySection.charges[0].amount, '$120', 'shows correct amount for first charge'); assert.ok(billingPage.paymentHistorySection.charges[0].failed, 'shows failed status for first charge'); - assert.equal(billingPage.paymentHistorySection.charges[1].amount, '$120', 'shows correct amount for second charge'); + assert.strictEqual(billingPage.paymentHistorySection.charges[1].amount, '$120', 'shows correct amount for second charge'); assert.notOk(billingPage.paymentHistorySection.charges[1].failed, 'shows succeeded status for second charge'); }); }); From d2a4e06446fcbf604267636a7938bd14aa919a68 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 17:04:21 +0100 Subject: [PATCH 06/31] [percy] Let's check what this means From a744efb45435694e47efb4855c362fc3043616aa Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 17:26:33 +0100 Subject: [PATCH 07/31] Remove /membership (test, templates etc), also redirect to billing [percy] --- .../billing-status-badge/member-badge.hbs | 2 +- app/components/header/account-dropdown.hbs | 10 -- app/routes/membership.js | 21 ---- app/routes/pay.ts | 3 +- app/templates/membership.hbs | 25 ----- .../course-page/view-course-stages-test.js | 8 +- tests/acceptance/header-test.js | 4 +- tests/acceptance/manage-membership-test.js | 106 ------------------ tests/acceptance/pay-test.js | 6 +- tests/pages/membership-page.js | 33 ------ vercel.json | 5 + 11 files changed, 15 insertions(+), 208 deletions(-) delete mode 100644 app/routes/membership.js delete mode 100644 app/templates/membership.hbs delete mode 100644 tests/acceptance/manage-membership-test.js delete mode 100644 tests/pages/membership-page.js diff --git a/app/components/billing-status-badge/member-badge.hbs b/app/components/billing-status-badge/member-badge.hbs index 8d6a709825..abf32b2657 100644 --- a/app/components/billing-status-badge/member-badge.hbs +++ b/app/components/billing-status-badge/member-badge.hbs @@ -1,4 +1,4 @@ - + Member diff --git a/app/components/header/account-dropdown.hbs b/app/components/header/account-dropdown.hbs index d02dc75f64..9ac987d5de 100644 --- a/app/components/header/account-dropdown.hbs +++ b/app/components/header/account-dropdown.hbs @@ -14,16 +14,6 @@
- {{#if this.currentUser.hasActiveSubscription}} - - {{else}} - - {{/if}} - {{#if this.currentUser.isTeamAdmin}} diff --git a/app/routes/membership.js b/app/routes/membership.js deleted file mode 100644 index 5c7dcd5491..0000000000 --- a/app/routes/membership.js +++ /dev/null @@ -1,21 +0,0 @@ -import BaseRoute from 'codecrafters-frontend/utils/base-route'; -import { inject as service } from '@ember/service'; - -export default class MembershipRoute extends BaseRoute { - @service store; - @service router; - @service authenticator; - - afterModel() { - // Force a sync of subscriptions - this.store.findAll('subscription'); - } - - async model() { - await this.authenticator.authenticate(); - - if (this.authenticator.currentUser && this.authenticator.currentUser.subscriptions.length === 0) { - this.router.transitionTo('pay'); - } - } -} diff --git a/app/routes/pay.ts b/app/routes/pay.ts index d772b999a1..2d24f67466 100644 --- a/app/routes/pay.ts +++ b/app/routes/pay.ts @@ -28,8 +28,7 @@ export default class PayRoute extends BaseRoute { await this.authenticator.authenticate(); if (this.authenticator.currentUser && this.authenticator.currentUser.hasActiveSubscription) { - this.router.transitionTo('membership'); - + this.router.transitionTo('settings.billing'); return; } diff --git a/app/templates/membership.hbs b/app/templates/membership.hbs deleted file mode 100644 index 116b4a595b..0000000000 --- a/app/templates/membership.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{page-title "Manage Membership"}} - -
-
Manage Membership
- -
-
- - - - - {{!-- - - - --}} - -
- - -
-
\ No newline at end of file diff --git a/tests/acceptance/course-page/view-course-stages-test.js b/tests/acceptance/course-page/view-course-stages-test.js index 78b101dadd..c1c6a28350 100644 --- a/tests/acceptance/course-page/view-course-stages-test.js +++ b/tests/acceptance/course-page/view-course-stages-test.js @@ -747,15 +747,13 @@ module('Acceptance | course-page | view-course-stages-test', function (hooks) { }); }); - test('member badge redirects to /membership', async function (assert) { + test('member badge redirects to /settings/billing', async function (assert) { testScenario(this.server); signInAsSubscriber(this.owner, this.server); - await catalogPage.visit(); - await catalogPage.clickOnCourse('Build your own Redis'); - await courseOverviewPage.clickOnStartCourse(); + await coursePage.visit(); await coursePage.header.memberBadge.click(); - assert.strictEqual(currentURL(), '/membership', 'expect to be redirected to membership page'); + assert.strictEqual(currentURL(), '/settings/billing', 'expect to be redirected to settings billing page'); }); }); diff --git a/tests/acceptance/header-test.js b/tests/acceptance/header-test.js index afc8044ac0..1c6f0a6f0b 100644 --- a/tests/acceptance/header-test.js +++ b/tests/acceptance/header-test.js @@ -50,13 +50,13 @@ module('Acceptance | header-test', function (hooks) { }); }); - test('member badge redirects to /membership', async function (assert) { + test('member badge redirects to /settings/billing', async function (assert) { testScenario(this.server); signInAsSubscriber(this.owner, this.server); await catalogPage.visit(); await catalogPage.header.memberBadge.click(); - assert.strictEqual(currentURL(), '/membership', 'expect to be redirected to membership page'); + assert.strictEqual(currentURL(), '/settings/billing', 'expect to be redirected to settings billing page'); }); }); diff --git a/tests/acceptance/manage-membership-test.js b/tests/acceptance/manage-membership-test.js deleted file mode 100644 index dfbd798e5f..0000000000 --- a/tests/acceptance/manage-membership-test.js +++ /dev/null @@ -1,106 +0,0 @@ -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'codecrafters-frontend/tests/helpers'; -import { setupWindowMock } from 'ember-window-mock/test-support'; -import { signInAsSubscriber } from 'codecrafters-frontend/tests/support/authentication-helpers'; -import catalogPage from 'codecrafters-frontend/tests/pages/catalog-page'; -import membershipPage from 'codecrafters-frontend/tests/pages/membership-page'; -import testScenario from 'codecrafters-frontend/mirage/scenarios/test'; -import { currentURL } from '@ember/test-helpers'; - -module('Acceptance | manage-membership-test', function (hooks) { - setupApplicationTest(hooks); - setupWindowMock(hooks); - - test('subscriber can manage membership', async function (assert) { - testScenario(this.server); - signInAsSubscriber(this.owner, this.server); - - await catalogPage.visit(); - await catalogPage.accountDropdown.toggle(); - await catalogPage.accountDropdown.clickOnLink('Manage Membership'); - - assert.strictEqual(currentURL(), '/membership'); - }); - - test('subscriber that is a partner has correct membership plan copy', async function (assert) { - testScenario(this.server); - - const user = this.server.schema.users.first(); - user.update('isVip', true); - - signInAsSubscriber(this.owner, this.server, user); - - await catalogPage.visit(); - await catalogPage.accountDropdown.toggle(); - await catalogPage.accountDropdown.clickOnLink('Manage Membership'); - - assert.dom('[data-test-membership-plan-section] div:nth-of-type(3)').includesText('πŸŽ‰ You have VIP access to all CodeCrafters content.'); - }); - - test('subscriber that is a partner with expiry has correct membership plan copy', async function (assert) { - testScenario(this.server); - - const expiryDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000); - const user = this.server.schema.users.first(); - user.update('isVip', true); - user.update('vipStatusExpiresAt', expiryDate); - - signInAsSubscriber(this.owner, this.server, user); - - await catalogPage.visit(); - await catalogPage.accountDropdown.toggle(); - await catalogPage.accountDropdown.clickOnLink('Manage Membership'); - - assert - .dom('[data-test-membership-plan-section] div:nth-of-type(3)') - .includesText('πŸŽ‰ You have VIP access to all CodeCrafters content, valid until'); - }); - - test('subscriber can view recent payments', async function (assert) { - testScenario(this.server); - signInAsSubscriber(this.owner, this.server); - - let subscription = this.server.schema.subscriptions.first(); - - this.server.schema.charges.create({ - user: subscription.user, - amount: 7900, - amountRefunded: 0, - currency: 'usd', - createdAt: new Date(), - invoiceId: 'invoice-id', - status: 'succeeded', - }); - - this.server.schema.charges.create({ - user: subscription.user, - amount: 3500, - amountRefunded: 0, - currency: 'usd', - createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 7), - invoiceId: 'invoice-id', - status: 'succeeded', - }); - - await membershipPage.visit(); - - assert.strictEqual(membershipPage.recentPaymentsSection.downloadInvoiceLinks.length, 2); - }); - - test('subscriber can view upcoming payments', async function (assert) { - testScenario(this.server); - signInAsSubscriber(this.owner, this.server); - - await membershipPage.visit(); - assert.strictEqual(1, 1); - }); - - test('subscriber can update payment method', async function (assert) { - testScenario(this.server); - signInAsSubscriber(this.owner, this.server); - - await membershipPage.visit(); - await membershipPage.clickOnUpdatePaymentMethodButton(); - assert.strictEqual(1, 1); // Dummy test - }); -}); diff --git a/tests/acceptance/pay-test.js b/tests/acceptance/pay-test.js index c28b7e0634..bcaf1bbcbb 100644 --- a/tests/acceptance/pay-test.js +++ b/tests/acceptance/pay-test.js @@ -222,12 +222,12 @@ module('Acceptance | pay-test', function (hooks) { assert.strictEqual(currentURL(), '/catalog'); }); - test('user should be redirected to /membership if user is authenticated and has an active subscription', async function (assert) { + test('user should be redirected to /settings/billing if user is authenticated and has an active subscription', async function (assert) { testScenario(this.server); signInAsSubscriber(this.owner, this.server); - await visit('/pay'); + await payPage.visit(); - assert.strictEqual(currentURL(), '/membership'); + assert.strictEqual(currentURL(), '/settings/billing'); }); }); diff --git a/tests/pages/membership-page.js b/tests/pages/membership-page.js deleted file mode 100644 index 4281486867..0000000000 --- a/tests/pages/membership-page.js +++ /dev/null @@ -1,33 +0,0 @@ -import { clickOnText, clickable, collection, create, fillable, hasClass, text, visitable } from 'ember-cli-page-object'; -import AccountDropdown from 'codecrafters-frontend/tests/pages/components/account-dropdown'; -import Header from 'codecrafters-frontend/tests/pages/components/header'; - -export default create({ - accountDropdown: AccountDropdown, - - cancelSubscriptionModal: { - selectReason: clickOnText('label'), - cancelButtonIsDisabled: hasClass('cursor-not-allowed', '[data-test-cancel-subscription-button]'), - cancelButtonText: text('[data-test-cancel-subscription-button]'), - clickOnCancelSubscriptionButton: clickable('[data-test-cancel-subscription-button]'), - fillInReasonDescription: fillable('[data-test-reason-description-input]'), - scope: '[data-test-cancel-subscription-modal]', - }, - - clickOnCancelSubscriptionButton: clickable('[data-test-cancel-subscription-button]'), - clickOnCancelTrialButton: clickable('[data-test-cancel-trial-button]'), - clickOnUpdatePaymentMethodButton: clickable('[data-test-update-payment-method-button]'), - - membershipPlanSection: { - descriptionText: text('[data-test-membership-plan-description]'), - scope: '[data-test-membership-plan-section]', - }, - - recentPaymentsSection: { - downloadInvoiceLinks: collection('[data-test-download-invoice-link]'), - scope: '[data-test-recent-payments-section]', - }, - - header: Header, - visit: visitable('/membership'), -}); diff --git a/vercel.json b/vercel.json index b9f2b52a5f..02a283a4c2 100644 --- a/vercel.json +++ b/vercel.json @@ -19,6 +19,11 @@ "source": "/vote/challenge-extension-ideas", "destination": "/vote/challenge-extensions", "permanent": true + }, + { + "source": "/membership", + "destination": "/settings/billing", + "permanent": true } ], "rewrites": [ From b8e5abe8c1ec14384e39888ca1fc7f6227545a31 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 17:37:30 +0100 Subject: [PATCH 08/31] Rabbit suggestion + Linter --- .../billing-page/membership-section.hbs | 2 +- .../billing-page/payment-history-section.hbs | 4 +-- app/routes/pay.ts | 1 + app/routes/settings/billing.ts | 11 ++++++- tests/acceptance/pay-test.js | 2 +- .../acceptance/settings-page/billing-test.js | 31 +++++++++++++++++++ 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 5b497773c4..42af84941d 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -42,7 +42,7 @@

{{/if}}
-{{else if @user.activeSubscription}} +{{else if @user.expiredSubscription}}
diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index b04753a474..16a22b9850 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -13,9 +13,9 @@ {{charge.displayString}} {{#if (gt charge.amountRefunded 0)}} {{#if charge.isFullyRefunded}} - (refunded) + (refunded) {{else}} - ({{charge.refundedAmountDisplayString}} + ({{charge.refundedAmountDisplayString}} refunded) {{/if}} {{/if}} diff --git a/app/routes/pay.ts b/app/routes/pay.ts index 2d24f67466..5ed052e798 100644 --- a/app/routes/pay.ts +++ b/app/routes/pay.ts @@ -29,6 +29,7 @@ export default class PayRoute extends BaseRoute { if (this.authenticator.currentUser && this.authenticator.currentUser.hasActiveSubscription) { this.router.transitionTo('settings.billing'); + return; } diff --git a/app/routes/settings/billing.ts b/app/routes/settings/billing.ts index 4d12fa930a..5fdbc865a3 100644 --- a/app/routes/settings/billing.ts +++ b/app/routes/settings/billing.ts @@ -13,7 +13,16 @@ export default class BillingRoute extends Route { async model(): Promise { const user = this.modelFor('settings') as SettingsModelType; - const charges = await this.store.findAll('charge'); + let charges; + + try { + charges = await this.store.query('charge', { + filter: { user_id: user.user.id }, + }); + } catch (error) { + console.error('Failed to fetch charges:', error); + charges = []; + } return { user: user.user, diff --git a/tests/acceptance/pay-test.js b/tests/acceptance/pay-test.js index bcaf1bbcbb..5cad7c7534 100644 --- a/tests/acceptance/pay-test.js +++ b/tests/acceptance/pay-test.js @@ -226,7 +226,7 @@ module('Acceptance | pay-test', function (hooks) { testScenario(this.server); signInAsSubscriber(this.owner, this.server); - await payPage.visit(); + await visit('/pay'); assert.strictEqual(currentURL(), '/settings/billing'); }); diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 0153153d17..c59b5be060 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -122,4 +122,35 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.strictEqual(billingPage.paymentHistorySection.charges[1].amount, '$120', 'shows correct amount for second charge'); assert.notOk(billingPage.paymentHistorySection.charges[1].failed, 'shows succeeded status for second charge'); }); + + test('payment history section shows refunded charges correctly', async function (assert) { + testScenario(this.server); + const user = signInAsSubscriber(this.owner, this.server); + + // Fully refunded charge + this.server.create('charge', { + user: user, + amount: 12000, + amountRefunded: 12000, + currency: 'usd', + createdAt: new Date(), + status: 'succeeded', + }); + + // Partially refunded charge + this.server.create('charge', { + user: user, + amount: 12000, + amountRefunded: 6000, + currency: 'usd', + createdAt: new Date(), + status: 'succeeded', + }); + + await billingPage.visit(); + await settled(); + + assert.strictEqual(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges'); + assert.dom('[data-test-refund-text]').exists({ count: 2 }, 'shows refund text for both charges'); + }); }); From 45866921bc4d406a480de6496cd19840c9990e15 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 17:42:30 +0100 Subject: [PATCH 09/31] Minor fix to the test --- tests/acceptance/course-page/view-course-stages-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/course-page/view-course-stages-test.js b/tests/acceptance/course-page/view-course-stages-test.js index c1c6a28350..c94d743351 100644 --- a/tests/acceptance/course-page/view-course-stages-test.js +++ b/tests/acceptance/course-page/view-course-stages-test.js @@ -751,7 +751,7 @@ module('Acceptance | course-page | view-course-stages-test', function (hooks) { testScenario(this.server); signInAsSubscriber(this.owner, this.server); - await coursePage.visit(); + await coursePage.visit({ course_slug: 'redis' }); await coursePage.header.memberBadge.click(); assert.strictEqual(currentURL(), '/settings/billing', 'expect to be redirected to settings billing page'); From 492ea65d9ebc84ce0ae89af6be2ebf514804e0e2 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 18:00:41 +0100 Subject: [PATCH 10/31] Bring back the pay thing --- app/components/header/account-dropdown.hbs | 9 +++++++++ app/components/header/account-dropdown.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/components/header/account-dropdown.hbs b/app/components/header/account-dropdown.hbs index 9ac987d5de..687cf0fac6 100644 --- a/app/components/header/account-dropdown.hbs +++ b/app/components/header/account-dropdown.hbs @@ -14,6 +14,15 @@
+ {{#if this.currentUser.hasActiveSubscription}} + + {{else}} + + {{/if}} {{#if this.currentUser.isTeamAdmin}} diff --git a/app/components/header/account-dropdown.ts b/app/components/header/account-dropdown.ts index 59d46772b3..6cdaff1dc9 100644 --- a/app/components/header/account-dropdown.ts +++ b/app/components/header/account-dropdown.ts @@ -52,7 +52,7 @@ export default class AccountDropdownComponent extends Component { @action async handleManageSubscriptionClick(dropdownActions: { close: () => void }) { dropdownActions.close(); - this.router.transitionTo('membership'); + this.router.transitionTo('settings.billing'); } @action From 28c49d4a8a6887d3bddf3dd8db606b53fd17b11d Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 19:47:22 +0100 Subject: [PATCH 11/31] maintain consistancy Co-authored-by: Paul Kuruvilla --- tests/pages/settings/billing-page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index a7cb0e0bf3..4e35ea3ba9 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -22,7 +22,7 @@ export default createPage({ supportSection: { scope: '[data-test-support-section]', isVisible: isVisible(), - clickContactButton: clickable('[data-test-support-contact-button]'), + clickOnContactButton: clickable('[data-test-support-contact-button]'), }, visit: visitable('/settings/billing'), From 80fd584243fcb1deba7d2fc92c49176acc038c1c Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 20:07:04 +0100 Subject: [PATCH 12/31] Move the Container inwards and input user as a whole --- .../billing-page/membership-section.hbs | 144 +++++++++--------- .../billing-page/payment-history-section.hbs | 86 ++++++----- .../settings/billing-page/renewal-section.hbs | 18 ++- .../settings/billing-page/support-section.hbs | 26 ++-- .../settings/billing-page/support-section.ts | 8 +- app/templates/settings/billing.hbs | 27 +--- .../acceptance/settings-page/billing-test.js | 1 + 7 files changed, 155 insertions(+), 155 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 42af84941d..99139b4f77 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,83 +1,85 @@ -{{#if @user.isVip}} -
- -
- VIP Access + + {{#if @user.isVip}} +
+ +
+ VIP Access +
-
-
-

- {{#if @user.vipStatusExpiresAt}} - πŸŽ‰ You have VIP access to all CodeCrafters content, valid until - {{date-format @user.vipStatusExpiresAt format="PPPp"}}. +

+

+ {{#if @user.vipStatusExpiresAt}} + πŸŽ‰ You have VIP access to all CodeCrafters content, valid until + {{date-format @user.vipStatusExpiresAt format="PPPp"}}. + {{else}} + πŸŽ‰ You have VIP access to all CodeCrafters content. + {{/if}} +

+
+ {{else if @user.hasActiveSubscription}} +
+ +
+ Membership active +
+
+ +
+ {{#if @user.activeSubscription.cancelAt}} +

+ Your CodeCrafters membership is valid until + {{date-format @user.activeSubscription.cancelAt format="PPPp"}}. +

+

Your membership doesn't renew automatically. To restart your membership, make a new one-time payment.

{{else}} - πŸŽ‰ You have VIP access to all CodeCrafters content. +

+ You are currently subscribed to the + {{@user.activeSubscription.pricingPlanName}} + plan. +

{{/if}} -

-
-{{else if @user.hasActiveSubscription}} -
- -
- Membership active
-
+ {{else if @user.expiredSubscription}} +
+ +
+ Membership inactive +
+
-
- {{#if @user.activeSubscription.cancelAt}} -

- Your CodeCrafters membership is valid until - {{date-format @user.activeSubscription.cancelAt format="PPPp"}}. +

+

+ Your CodeCrafters membership is + currently inactive.

-

Your membership doesn't renew automatically. To restart your membership, make a new one-time payment.

- {{else}}

- You are currently subscribed to the - {{@user.activeSubscription.pricingPlanName}} - plan. + Start a new membership to get access to + membership benefits.

- {{/if}} -
-{{else if @user.expiredSubscription}} -
- -
- Membership inactive
-
- -
-

- Your CodeCrafters membership is - currently inactive. -

-

- Start a new membership to get access to - membership benefits. -

-
- - Start membership β†’ - -{{else}} -
- -
- No membership found + + Start membership β†’ + + {{else}} +
+ +
+ No membership found +
-
-
-

- You don't have a CodeCrafters membership. Start one to get access to - membership benefits. -

-
- - Start membership β†’ - -{{/if}} \ No newline at end of file +
+

+ You don't have a CodeCrafters membership. Start one to get access to + membership benefits. +

+
+ + Start membership β†’ + + {{/if}} + \ No newline at end of file diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index 16a22b9850..a66061e6be 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,45 +1,47 @@ -{{#if (gt @charges.length 0)}} -
+ + {{#if (gt @charges.length 0)}} +
-
Date
-
Amount
-
+
Date
+
Amount
+
- {{#each @charges as |charge|}} -
- {{date-format charge.createdAt format="PPP"}} -
-
- {{charge.displayString}} - {{#if (gt charge.amountRefunded 0)}} - {{#if charge.isFullyRefunded}} - (refunded) - {{else}} - ({{charge.refundedAmountDisplayString}} - refunded) + {{#each @charges as |charge|}} +
+ {{date-format charge.createdAt format="PPP"}} +
+
+ {{charge.displayString}} + {{#if (gt charge.amountRefunded 0)}} + {{#if charge.isFullyRefunded}} + (refunded) + {{else}} + ({{charge.refundedAmountDisplayString}} + refunded) + {{/if}} {{/if}} - {{/if}} -
-
- {{#if (and charge.invoiceId charge.statusIsSucceeded)}} - - Download Invoice - - {{else if charge.statusIsFailed}} - Payment failed - {{/if}} -
- {{/each}} -
-{{else}} -
- No payment history found. -
-{{/if}} \ No newline at end of file +
+
+ {{#if (and charge.invoiceId charge.statusIsSucceeded)}} + + Download Invoice + + {{else if charge.statusIsFailed}} + Payment failed + {{/if}} +
+ {{/each}} +
+ {{else}} +
+ No payment history found. +
+ {{/if}} + \ No newline at end of file diff --git a/app/components/settings/billing-page/renewal-section.hbs b/app/components/settings/billing-page/renewal-section.hbs index 226fcdf9c7..fdf2eb0b15 100644 --- a/app/components/settings/billing-page/renewal-section.hbs +++ b/app/components/settings/billing-page/renewal-section.hbs @@ -1,10 +1,12 @@ -
- -
- Auto-renew disabled + +
+ +
+ Auto-renew disabled +
-
-
- Your membership does not renew automatically. Once your membership expires, you'll be able to make a new one-time payment. -
\ No newline at end of file +
+ Your membership does not renew automatically. Once your membership expires, you'll be able to make a new one-time payment. +
+ \ No newline at end of file diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs index 6aeb0cc0f6..61f1c6fba0 100644 --- a/app/components/settings/billing-page/support-section.hbs +++ b/app/components/settings/billing-page/support-section.hbs @@ -1,12 +1,14 @@ - - - Get help - - -
-

- Questions? Click the button above or write to us at - hello@codecrafters.io - and we'll help sort things out. -

-
\ No newline at end of file + + + + Get help + + +
+

+ Questions? Click the button above or write to us at + hello@codecrafters.io + and we'll help sort things out. +

+
+
\ No newline at end of file diff --git a/app/components/settings/billing-page/support-section.ts b/app/components/settings/billing-page/support-section.ts index 8b3ed0db55..0371e71090 100644 --- a/app/components/settings/billing-page/support-section.ts +++ b/app/components/settings/billing-page/support-section.ts @@ -1,10 +1,16 @@ import Component from '@glimmer/component'; +interface User { + username: string; + id?: string; + isVip?: boolean; +} + interface Signature { Element: HTMLDivElement; Args: { - username: string; + user: User; }; } diff --git a/app/templates/settings/billing.hbs b/app/templates/settings/billing.hbs index 6dab89199a..cac19a8ab2 100644 --- a/app/templates/settings/billing.hbs +++ b/app/templates/settings/billing.hbs @@ -1,24 +1,9 @@ -{{! template-lint-disable no-implicit-this }} - - - - + - - - - - + - - - - - + - - - {{#let this.model.charges as |charges|}} - - {{/let}} - \ No newline at end of file +{{#let @model.charges as |charges|}} + +{{/let}} \ No newline at end of file diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index c59b5be060..dd03289e28 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -77,6 +77,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); assert.strictEqual(billingPage.paymentHistorySection.charges.length, 0, 'shows no charges initially'); + assert.dom('[data-test-payment-history-section]').hasText('No payment history found.', 'shows empty state text'); }); test('payment history section shows charges after creation', async function (assert) { From 54847e9c3cebee5163efb4865a910daeb292a46d Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 21:01:22 +0100 Subject: [PATCH 13/31] Pauls' changes --- app/components/header/account-dropdown.hbs | 1 + .../billing-page/membership-section.hbs | 2 +- .../billing-page/payment-history-section.hbs | 25 +++++++++--- .../billing-page/payment-history-section.ts | 40 +++++++++++++++++-- .../settings/billing-page/support-section.hbs | 2 +- app/routes/settings/billing.ts | 27 ++++--------- app/templates/settings/billing.hbs | 4 +- mirage/models/charge.js | 13 ------ .../acceptance/settings-page/billing-test.js | 33 ++++++++------- tests/pages/settings/billing-page.ts | 13 ++---- 10 files changed, 89 insertions(+), 71 deletions(-) delete mode 100644 mirage/models/charge.js diff --git a/app/components/header/account-dropdown.hbs b/app/components/header/account-dropdown.hbs index 687cf0fac6..2394546489 100644 --- a/app/components/header/account-dropdown.hbs +++ b/app/components/header/account-dropdown.hbs @@ -23,6 +23,7 @@ {{else}} {{/if}} + {{#if this.currentUser.isTeamAdmin}} diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 99139b4f77..a2bf314b91 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,7 +1,7 @@ {{#if @user.isVip}}
diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index a66061e6be..f671bc5069 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,12 +1,28 @@ - {{#if (gt @charges.length 0)}} + {{#if this.isLoading}} +
+
+
+ {{else if this.errorMessage}} +
+
+
+ + + +
+
+

{{this.errorMessage}}

+
+
+
+ {{else if (gt this.charges.length 0)}}
-
Date
Amount
- {{#each @charges as |charge|}} + {{#each this.charges as |charge|}}
{{date-format charge.createdAt format="PPP"}}
@@ -27,14 +43,13 @@ href={{charge.invoiceDownloadUrl}} target="_blank" class="text-teal-500 hover:text-teal-600 font-semibold text-sm" - data-test-status data-test-download-invoice-link rel="noopener noreferrer" > Download Invoice {{else if charge.statusIsFailed}} - Payment failed + Payment failed {{/if}}
{{/each}} diff --git a/app/components/settings/billing-page/payment-history-section.ts b/app/components/settings/billing-page/payment-history-section.ts index 1bba77306d..7aca41dc89 100644 --- a/app/components/settings/billing-page/payment-history-section.ts +++ b/app/components/settings/billing-page/payment-history-section.ts @@ -1,15 +1,49 @@ import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import * as Sentry from '@sentry/ember'; +import type Store from '@ember-data/store'; +import type UserModel from 'codecrafters-frontend/models/user'; import type ChargeModel from 'codecrafters-frontend/models/charge'; interface Signature { Element: HTMLDivElement; - Args: { - charges: ChargeModel[]; + user: UserModel; }; } -export default class PaymentHistorySectionComponent extends Component {} +export default class PaymentHistorySectionComponent extends Component { + @service declare store: Store; + + @tracked charges: ChargeModel[] = []; + @tracked isLoading = true; + @tracked errorMessage: string | null = null; + + constructor(owner: unknown, args: Signature['Args']) { + super(owner, args); + this.loadCharges(); + } + + async loadCharges() { + this.isLoading = true; + this.errorMessage = null; + + try { + const result = await this.store.query('charge', { + filter: { user_id: this.args.user.id }, + }); + this.charges = result.toArray(); + } catch (error) { + console.error('Failed to fetch charges:', error); + this.errorMessage = 'Failed to load payment history. Please try again later.'; + Sentry.captureException(error); + this.charges = []; + } finally { + this.isLoading = false; + } + } +} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs index 61f1c6fba0..fe3391e025 100644 --- a/app/components/settings/billing-page/support-section.hbs +++ b/app/components/settings/billing-page/support-section.hbs @@ -1,5 +1,5 @@ - + Get help diff --git a/app/routes/settings/billing.ts b/app/routes/settings/billing.ts index 5fdbc865a3..d43949e5f5 100644 --- a/app/routes/settings/billing.ts +++ b/app/routes/settings/billing.ts @@ -1,32 +1,21 @@ import Route from '@ember/routing/route'; -import type ChargeModel from 'codecrafters-frontend/models/charge'; import { inject as service } from '@ember/service'; +import type AuthenticatorService from 'codecrafters-frontend/services/authenticator'; import type Store from '@ember-data/store'; -import type { ModelType as SettingsModelType } from 'codecrafters-frontend/routes/settings'; -interface BillingModelType extends SettingsModelType { - charges: ChargeModel[]; -} - -export default class BillingRoute extends Route { +export default class SettingsBillingRoute extends Route { + @service declare authenticator: AuthenticatorService; @service declare store: Store; - async model(): Promise { - const user = this.modelFor('settings') as SettingsModelType; - let charges; + async model() { + const user = this.authenticator.currentUser; - try { - charges = await this.store.query('charge', { - filter: { user_id: user.user.id }, - }); - } catch (error) { - console.error('Failed to fetch charges:', error); - charges = []; + if (!user) { + throw new Error('User must be authenticated to access billing settings'); } return { - user: user.user, - charges: charges.toArray(), + user, }; } } diff --git a/app/templates/settings/billing.hbs b/app/templates/settings/billing.hbs index cac19a8ab2..8ecbec419f 100644 --- a/app/templates/settings/billing.hbs +++ b/app/templates/settings/billing.hbs @@ -4,6 +4,4 @@ -{{#let @model.charges as |charges|}} - -{{/let}} \ No newline at end of file + diff --git a/mirage/models/charge.js b/mirage/models/charge.js deleted file mode 100644 index e9ccd43a25..0000000000 --- a/mirage/models/charge.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Model, belongsTo } from 'miragejs'; - -export default Model.extend({ - user: belongsTo(), - amount: 0, - amountRefunded: 0, - currency: 'usd', - createdAt() { - return new Date(); // Done this way due to linting error and shared objects - }, - invoiceId: null, - status: 'succeeded', -}); diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index dd03289e28..40beb6b0bd 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -4,9 +4,15 @@ import { signIn, signInAsSubscriber, signInAsVipUser } from 'codecrafters-fronte import testScenario from 'codecrafters-frontend/mirage/scenarios/test'; import billingPage from 'codecrafters-frontend/tests/pages/settings/billing-page'; import { settled } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupWindowMock } from 'ember-window-mock/test-support'; +import window from 'ember-window-mock'; +import percySnapshot from '@percy/ember'; module('Acceptance | settings-page | billing-test', function (hooks) { setupApplicationTest(hooks); + setupMirage(hooks); + setupWindowMock(hooks); test('membership section shows correct plan for subscriber with active subscription', async function (assert) { testScenario(this.server); @@ -18,20 +24,9 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await settled(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); - assert.ok(billingPage.membershipSection.hasActivePlan, 'shows active plan'); - }); - - test('membership section shows correct plan for subscriber with canceled subscription', async function (assert) { - testScenario(this.server); - signInAsSubscriber(this.owner, this.server); - const subscription = this.server.schema.subscriptions.first(); - subscription.update('cancelAt', new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 30)); // 30 days from now - - await billingPage.visit(); - await settled(); - - assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); - assert.ok(billingPage.membershipSection.hasActivePlan, 'shows active plan'); + assert.strictEqual(billingPage.membershipSection.text, 'Membership active', 'shows active plan'); + + await percySnapshot('Billing Page - Active Subscription'); }); test('membership section shows correct plan for non-subscriber', async function (assert) { @@ -53,7 +48,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await settled(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); - assert.ok(billingPage.membershipSection.hasVipAccess, 'shows VIP access'); + assert.strictEqual(billingPage.membershipSection.text, 'VIP Access', 'shows VIP access'); }); test('support section is visible', async function (assert) { @@ -64,8 +59,8 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await settled(); assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); - // await billingPage.supportSection.clickContactButton(); - // Commented out because it opens the email client, if behavior changes, uncomment + assert.ok(billingPage.supportSection.clickOnContactButton, 'click on contact button'); + }); test('payment history section shows empty state initially', async function (assert) { @@ -122,6 +117,8 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.paymentHistorySection.charges[0].failed, 'shows failed status for first charge'); assert.strictEqual(billingPage.paymentHistorySection.charges[1].amount, '$120', 'shows correct amount for second charge'); assert.notOk(billingPage.paymentHistorySection.charges[1].failed, 'shows succeeded status for second charge'); + + await percySnapshot('Billing Page - Payment History with Multiple Charges'); }); test('payment history section shows refunded charges correctly', async function (assert) { @@ -153,5 +150,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.strictEqual(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges'); assert.dom('[data-test-refund-text]').exists({ count: 2 }, 'shows refund text for both charges'); + + await percySnapshot('Billing Page - Payment History with Refunded Charges'); }); }); diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index 4e35ea3ba9..9258e14d02 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -1,28 +1,23 @@ -import { clickable, hasClass, isVisible, visitable, attribute, collection, text } from 'ember-cli-page-object'; +import { clickable, hasClass, visitable, collection, text } from 'ember-cli-page-object'; import createPage from 'codecrafters-frontend/tests/support/create-page'; export default createPage({ membershipSection: { scope: '[data-test-membership-section]', - hasActivePlan: hasClass('has-active-plan'), - isVisible: isVisible(), - hasVipAccess: hasClass('has-vip-access'), + text: text(), }, paymentHistorySection: { - scope: '[data-test-payment-history-section]', - isVisible: isVisible(), - isEmpty: attribute('data-test-empty-state'), charges: collection('[data-test-payment-history-item]', { amount: text('[data-test-amount]'), failed: hasClass('text-red-600'), }), + scope: '[data-test-payment-history-section]', }, supportSection: { - scope: '[data-test-support-section]', - isVisible: isVisible(), clickOnContactButton: clickable('[data-test-support-contact-button]'), + scope: '[data-test-support-section]', }, visit: visitable('/settings/billing'), From fc9bfe393af5d34e766fd76be36e34097b20f785 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 21:02:07 +0100 Subject: [PATCH 14/31] [percy] forgot this From ff65f5003a79762ceafb626ceff12fa62b89864d Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 21:03:06 +0100 Subject: [PATCH 15/31] [percy] linter? --- app/components/header/account-dropdown.hbs | 2 +- app/components/settings/billing-page/membership-section.hbs | 5 +---- .../settings/billing-page/payment-history-section.hbs | 6 +++++- app/templates/settings/billing.hbs | 2 +- tests/acceptance/settings-page/billing-test.js | 4 +--- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/components/header/account-dropdown.hbs b/app/components/header/account-dropdown.hbs index 2394546489..d02dc75f64 100644 --- a/app/components/header/account-dropdown.hbs +++ b/app/components/header/account-dropdown.hbs @@ -23,7 +23,7 @@ {{else}} {{/if}} - + {{#if this.currentUser.isTeamAdmin}} diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index a2bf314b91..7d062f7896 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,9 +1,6 @@ {{#if @user.isVip}} -
+
VIP Access diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index f671bc5069..c244cc74f8 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -8,7 +8,11 @@
- +
diff --git a/app/templates/settings/billing.hbs b/app/templates/settings/billing.hbs index 8ecbec419f..a8d5ff8b52 100644 --- a/app/templates/settings/billing.hbs +++ b/app/templates/settings/billing.hbs @@ -4,4 +4,4 @@ - + \ No newline at end of file diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 40beb6b0bd..e95d152945 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -6,7 +6,6 @@ import billingPage from 'codecrafters-frontend/tests/pages/settings/billing-page import { settled } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupWindowMock } from 'ember-window-mock/test-support'; -import window from 'ember-window-mock'; import percySnapshot from '@percy/ember'; module('Acceptance | settings-page | billing-test', function (hooks) { @@ -25,7 +24,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); assert.strictEqual(billingPage.membershipSection.text, 'Membership active', 'shows active plan'); - + await percySnapshot('Billing Page - Active Subscription'); }); @@ -60,7 +59,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); assert.ok(billingPage.supportSection.clickOnContactButton, 'click on contact button'); - }); test('payment history section shows empty state initially', async function (assert) { From a23e8130bde85cea1dfd7e3027974beaf291845f Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 21:16:07 +0100 Subject: [PATCH 16/31] Switch back to convention --- tests/acceptance/course-page/view-course-stages-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/acceptance/course-page/view-course-stages-test.js b/tests/acceptance/course-page/view-course-stages-test.js index c94d743351..928e1d643c 100644 --- a/tests/acceptance/course-page/view-course-stages-test.js +++ b/tests/acceptance/course-page/view-course-stages-test.js @@ -751,7 +751,9 @@ module('Acceptance | course-page | view-course-stages-test', function (hooks) { testScenario(this.server); signInAsSubscriber(this.owner, this.server); - await coursePage.visit({ course_slug: 'redis' }); + await catalogPage.visit(); + await catalogPage.clickOnCourse('Build your own Redis'); + await courseOverviewPage.clickOnStartCourse(); await coursePage.header.memberBadge.click(); assert.strictEqual(currentURL(), '/settings/billing', 'expect to be redirected to settings billing page'); From 5e42545df1188e8a1febe852e08f6699c1598aa7 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Fri, 25 Apr 2025 21:18:26 +0100 Subject: [PATCH 17/31] remove .settled calls which are unnecessary --- tests/acceptance/settings-page/billing-test.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index e95d152945..a0ad82b8f9 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -3,7 +3,6 @@ import { setupApplicationTest } from 'codecrafters-frontend/tests/helpers'; import { signIn, signInAsSubscriber, signInAsVipUser } from 'codecrafters-frontend/tests/support/authentication-helpers'; import testScenario from 'codecrafters-frontend/mirage/scenarios/test'; import billingPage from 'codecrafters-frontend/tests/pages/settings/billing-page'; -import { settled } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupWindowMock } from 'ember-window-mock/test-support'; import percySnapshot from '@percy/ember'; @@ -20,7 +19,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { subscription.update('pricingPlanName', 'Yearly Plan'); await billingPage.visit(); - await settled(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); assert.strictEqual(billingPage.membershipSection.text, 'Membership active', 'shows active plan'); @@ -33,7 +31,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { signIn(this.owner, this.server); await billingPage.visit(); - await settled(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); assert.notOk(billingPage.membershipSection.hasActivePlan, 'does not show active plan'); @@ -44,7 +41,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { signInAsVipUser(this.owner, this.server); await billingPage.visit(); - await settled(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); assert.strictEqual(billingPage.membershipSection.text, 'VIP Access', 'shows VIP access'); @@ -55,7 +51,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { signInAsSubscriber(this.owner, this.server); await billingPage.visit(); - await settled(); assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); assert.ok(billingPage.supportSection.clickOnContactButton, 'click on contact button'); @@ -66,7 +61,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { signInAsSubscriber(this.owner, this.server); await billingPage.visit(); - await settled(); assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); assert.strictEqual(billingPage.paymentHistorySection.charges.length, 0, 'shows no charges initially'); @@ -107,7 +101,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { }); await billingPage.visit(); - await settled(); assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); assert.strictEqual(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges after creation'); @@ -144,7 +137,6 @@ module('Acceptance | settings-page | billing-test', function (hooks) { }); await billingPage.visit(); - await settled(); assert.strictEqual(billingPage.paymentHistorySection.charges.length, 2, 'shows two charges'); assert.dom('[data-test-refund-text]').exists({ count: 2 }, 'shows refund text for both charges'); From e44227ae5657b7b53fcc6ac85f9b6606975d9ba3 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 15:39:09 +0100 Subject: [PATCH 18/31] addressed comments except VIP --- .../billing-page/membership-section.hbs | 8 +++---- .../billing-page/payment-history-section.hbs | 8 +------ .../settings/billing-page/renewal-section.hbs | 2 +- .../settings/billing-page/support-section.ts | 17 +++++++-------- app/routes/settings/billing.ts | 21 ------------------- tests/pages/settings/billing-page.ts | 1 - 6 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 app/routes/settings/billing.ts diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 7d062f7896..7d395101fd 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,7 +1,7 @@ {{#if @user.isVip}}
- + {{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
VIP Access
@@ -19,7 +19,7 @@
{{else if @user.hasActiveSubscription}}
- + {{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
Membership active
@@ -42,7 +42,7 @@
{{else if @user.expiredSubscription}}
- + {{svg-jar "x-circle" class="h-5 w-5 text-red-500"}}
Membership inactive
@@ -63,7 +63,7 @@ {{else}}
- + {{svg-jar "question-mark-circle" class="h-5 w-5 text-yellow-500"}}
No membership found
diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index c244cc74f8..bf5b4e68f6 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -7,13 +7,7 @@
- - - + {{svg-jar "x-circle" class="h-5 w-5 text-red-400"}}

{{this.errorMessage}}

diff --git a/app/components/settings/billing-page/renewal-section.hbs b/app/components/settings/billing-page/renewal-section.hbs index fdf2eb0b15..ca4d7ccc34 100644 --- a/app/components/settings/billing-page/renewal-section.hbs +++ b/app/components/settings/billing-page/renewal-section.hbs @@ -1,6 +1,6 @@
- + {{svg-jar "x-circle" class="h-5 w-5 text-gray-400"}}
Auto-renew disabled
diff --git a/app/components/settings/billing-page/support-section.ts b/app/components/settings/billing-page/support-section.ts index 0371e71090..112e576a0c 100644 --- a/app/components/settings/billing-page/support-section.ts +++ b/app/components/settings/billing-page/support-section.ts @@ -1,20 +1,17 @@ import Component from '@glimmer/component'; - -interface User { - username: string; - id?: string; - isVip?: boolean; -} +import type UserModel from 'codecrafters-frontend/models/user'; interface Signature { - Element: HTMLDivElement; - Args: { - user: User; + user: UserModel; }; } -export default class SupportSectionComponent extends Component {} +export default class SupportSectionComponent extends Component { + get supportEmail() { + return `support+${this.args.user.username}@codecrafters.io`; + } +} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/app/routes/settings/billing.ts b/app/routes/settings/billing.ts deleted file mode 100644 index d43949e5f5..0000000000 --- a/app/routes/settings/billing.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Route from '@ember/routing/route'; -import { inject as service } from '@ember/service'; -import type AuthenticatorService from 'codecrafters-frontend/services/authenticator'; -import type Store from '@ember-data/store'; - -export default class SettingsBillingRoute extends Route { - @service declare authenticator: AuthenticatorService; - @service declare store: Store; - - async model() { - const user = this.authenticator.currentUser; - - if (!user) { - throw new Error('User must be authenticated to access billing settings'); - } - - return { - user, - }; - } -} diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index 9258e14d02..15ca3fd84d 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -4,7 +4,6 @@ import createPage from 'codecrafters-frontend/tests/support/create-page'; export default createPage({ membershipSection: { scope: '[data-test-membership-section]', - text: text(), }, paymentHistorySection: { From 2324df51eacc5a5c753d3e3a1947a2dfb76a6aee Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 15:57:35 +0100 Subject: [PATCH 19/31] Show VIP + Active --- .../billing-page/membership-section.hbs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 7d395101fd..b2eb6cbd8a 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,20 +1,17 @@ - {{#if @user.isVip}} + {{#if (and @user.isVip @user.hasActiveSubscription)}}
{{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
- VIP Access + VIP Access + Membership Active
-

- {{#if @user.vipStatusExpiresAt}} - πŸŽ‰ You have VIP access to all CodeCrafters content, valid until - {{date-format @user.vipStatusExpiresAt format="PPPp"}}. - {{else}} - πŸŽ‰ You have VIP access to all CodeCrafters content. - {{/if}} + πŸŽ‰ You have VIP access to all CodeCrafters content{{#if @user.vipStatusExpiresAt}}, valid until {{date-format @user.vipStatusExpiresAt format="PPPp"}}{{/if}}. +

+

+ You are currently subscribed to the {{@user.activeSubscription.pricingPlanName}} plan{{#if @user.activeSubscription.cancelAt}}, valid until {{date-format @user.activeSubscription.cancelAt format="PPPp"}}{{/if}}.

{{else if @user.hasActiveSubscription}} @@ -24,14 +21,12 @@ Membership active
-
{{#if @user.activeSubscription.cancelAt}}

Your CodeCrafters membership is valid until {{date-format @user.activeSubscription.cancelAt format="PPPp"}}.

-

Your membership doesn't renew automatically. To restart your membership, make a new one-time payment.

{{else}}

You are currently subscribed to the @@ -40,6 +35,23 @@

{{/if}}
+ {{else if @user.isVip}} +
+ {{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}} +
+ VIP Access +
+
+
+

+ {{#if @user.vipStatusExpiresAt}} + πŸŽ‰ You have VIP access to all CodeCrafters content, valid until + {{date-format @user.vipStatusExpiresAt format="PPPp"}}. + {{else}} + πŸŽ‰ You have VIP access to all CodeCrafters content. + {{/if}} +

+
{{else if @user.expiredSubscription}}
{{svg-jar "x-circle" class="h-5 w-5 text-red-500"}} @@ -47,7 +59,6 @@ Membership inactive
-

Your CodeCrafters membership is @@ -68,7 +79,6 @@ No membership found

-

You don't have a CodeCrafters membership. Start one to get access to From f387a3be570d8c5ed89953a96ae2d5f2a939bb7f Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 15:57:45 +0100 Subject: [PATCH 20/31] [percy] forgot this From 31426ec45ee5f95d37be611c3e62c741e191d627 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 15:58:04 +0100 Subject: [PATCH 21/31] [percy] linter too --- .../settings/billing-page/membership-section.hbs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index b2eb6cbd8a..23617fba02 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -8,10 +8,14 @@

- πŸŽ‰ You have VIP access to all CodeCrafters content{{#if @user.vipStatusExpiresAt}}, valid until {{date-format @user.vipStatusExpiresAt format="PPPp"}}{{/if}}. + πŸŽ‰ You have VIP access to all CodeCrafters content{{#if @user.vipStatusExpiresAt}}, valid until + {{date-format @user.vipStatusExpiresAt format="PPPp"}}{{/if}}.

- You are currently subscribed to the {{@user.activeSubscription.pricingPlanName}} plan{{#if @user.activeSubscription.cancelAt}}, valid until {{date-format @user.activeSubscription.cancelAt format="PPPp"}}{{/if}}. + You are currently subscribed to the + {{@user.activeSubscription.pricingPlanName}} + plan{{#if @user.activeSubscription.cancelAt}}, valid until + {{date-format @user.activeSubscription.cancelAt format="PPPp"}}{{/if}}.

{{else if @user.hasActiveSubscription}} From c370b91347147871e502598a8785049ba465630e Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 16:08:57 +0100 Subject: [PATCH 22/31] Added Dark Methods --- .../billing-page/membership-section.hbs | 2 +- .../billing-page/payment-history-section.hbs | 26 +++++++++---------- .../settings/billing-page/renewal-section.hbs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 23617fba02..4d68ac7512 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,6 +1,6 @@ {{#if (and @user.isVip @user.hasActiveSubscription)}} -
+
{{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
VIP Access + Membership Active diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index bf5b4e68f6..6921e27358 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,36 +1,36 @@ {{#if this.isLoading}}
-
+
{{else if this.errorMessage}} -
+
- {{svg-jar "x-circle" class="h-5 w-5 text-red-400"}} + {{svg-jar "x-circle" class="h-5 w-5 text-red-400 dark:text-red-300"}}
-

{{this.errorMessage}}

+

{{this.errorMessage}}

{{else if (gt this.charges.length 0)}}
-
Date
-
Amount
+
Date
+
Amount
{{#each this.charges as |charge|}} -
+
{{date-format charge.createdAt format="PPP"}}
-
+
{{charge.displayString}} {{#if (gt charge.amountRefunded 0)}} {{#if charge.isFullyRefunded}} - (refunded) + (refunded) {{else}} - ({{charge.refundedAmountDisplayString}} + ({{charge.refundedAmountDisplayString}} refunded) {{/if}} {{/if}} @@ -40,20 +40,20 @@ Download Invoice {{else if charge.statusIsFailed}} - Payment failed + Payment failed {{/if}}
{{/each}}
{{else}} -
+
No payment history found.
{{/if}} diff --git a/app/components/settings/billing-page/renewal-section.hbs b/app/components/settings/billing-page/renewal-section.hbs index ca4d7ccc34..efa1582ac1 100644 --- a/app/components/settings/billing-page/renewal-section.hbs +++ b/app/components/settings/billing-page/renewal-section.hbs @@ -1,5 +1,5 @@ -
+
{{svg-jar "x-circle" class="h-5 w-5 text-gray-400"}}
Auto-renew disabled From 0fb27afdab1dc3a4b571a32c1b0b008d7c5ea029 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 16:09:18 +0100 Subject: [PATCH 23/31] linter ffs --- .../settings/billing-page/membership-section.hbs | 5 ++++- .../settings/billing-page/payment-history-section.hbs | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 4d68ac7512..8bf2dff8b6 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,6 +1,9 @@ {{#if (and @user.isVip @user.hasActiveSubscription)}} -
+
{{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
VIP Access + Membership Active diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index 6921e27358..d59344fc8c 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -24,13 +24,18 @@
{{date-format charge.createdAt format="PPP"}}
-
+
{{charge.displayString}} {{#if (gt charge.amountRefunded 0)}} {{#if charge.isFullyRefunded}} (refunded) {{else}} - ({{charge.refundedAmountDisplayString}} + ({{charge.refundedAmountDisplayString}} refunded) {{/if}} {{/if}} From 5fa3c1ab83c41817e3e5e23c2e70b881652e466a Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 16:16:49 +0100 Subject: [PATCH 24/31] Duplicate selectors again --- .../settings/billing-page/membership-section.hbs | 15 ++++++--------- tests/acceptance/settings-page/billing-test.js | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 8bf2dff8b6..38256f49b2 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,9 +1,6 @@ - + {{#if (and @user.isVip @user.hasActiveSubscription)}} -
+
{{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
VIP Access + Membership Active @@ -22,7 +19,7 @@

{{else if @user.hasActiveSubscription}} -
+
{{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
Membership active @@ -43,7 +40,7 @@ {{/if}}
{{else if @user.isVip}} -
+
{{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
VIP Access @@ -60,7 +57,7 @@

{{else if @user.expiredSubscription}} -
+
{{svg-jar "x-circle" class="h-5 w-5 text-red-500"}}
Membership inactive @@ -80,7 +77,7 @@ Start membership β†’ {{else}} -
+
{{svg-jar "question-mark-circle" class="h-5 w-5 text-yellow-500"}}
No membership found diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index a0ad82b8f9..eb2ebf5b0f 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -21,7 +21,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await billingPage.visit(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); - assert.strictEqual(billingPage.membershipSection.text, 'Membership active', 'shows active plan'); + assert.ok(billingPage.membershipSection.text.includes('Membership active'), 'shows active plan'); await percySnapshot('Billing Page - Active Subscription'); }); @@ -43,7 +43,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await billingPage.visit(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); - assert.strictEqual(billingPage.membershipSection.text, 'VIP Access', 'shows VIP access'); + assert.ok(billingPage.membershipSection.text.includes('VIP Access'), 'shows VIP access'); }); test('support section is visible', async function (assert) { From 96700abe4f91af7942bbd376188a4ede41133f38 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Mon, 28 Apr 2025 16:27:29 +0100 Subject: [PATCH 25/31] Added test for email client --- tests/acceptance/settings-page/billing-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index eb2ebf5b0f..45251f94d6 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -54,6 +54,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); assert.ok(billingPage.supportSection.clickOnContactButton, 'click on contact button'); + assert.dom('[data-test-support-section]').hasAttribute('href', /^mailto:hello@codecrafters.io/); }); test('payment history section shows empty state initially', async function (assert) { From 324665fd6e1310a8a574b54cd54472d58d36a908 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Wed, 30 Apr 2025 08:45:21 +0100 Subject: [PATCH 26/31] [percy] addressed comments --- .../billing-page/membership-section.hbs | 40 +++++-------------- .../billing-page/payment-history-section.hbs | 14 +++---- .../billing-page/payment-history-section.ts | 2 +- .../settings/billing-page/support-section.ts | 7 +--- .../acceptance/settings-page/billing-test.js | 4 +- tests/pages/settings/billing-page.ts | 1 + 6 files changed, 23 insertions(+), 45 deletions(-) diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 38256f49b2..6c32d10232 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,47 +1,29 @@ - {{#if (and @user.isVip @user.hasActiveSubscription)}} + {{#if @user.hasActiveSubscription}}
- {{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}} -
- VIP Access + Membership Active -
-
-
-

- πŸŽ‰ You have VIP access to all CodeCrafters content{{#if @user.vipStatusExpiresAt}}, valid until - {{date-format @user.vipStatusExpiresAt format="PPPp"}}{{/if}}. -

-

- You are currently subscribed to the - {{@user.activeSubscription.pricingPlanName}} - plan{{#if @user.activeSubscription.cancelAt}}, valid until - {{date-format @user.activeSubscription.cancelAt format="PPPp"}}{{/if}}. -

-
- {{else if @user.hasActiveSubscription}} -
- {{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
Membership active
- {{#if @user.activeSubscription.cancelAt}} -

- Your CodeCrafters membership is valid until + {{#if (and @user.isVip (gt @user.vipStatusExpiresAt @user.activeSubscription.cancelAt))}} +

+ You have access to all CodeCrafters content, valid until {{date-format @user.activeSubscription.cancelAt format="PPPp"}}.

+

+ πŸŽ‰ You have VIP access to all CodeCrafters content, valid until + {{date-format @user.vipStatusExpiresAt format="PPPp"}}. +

{{else}}

- You are currently subscribed to the - {{@user.activeSubscription.pricingPlanName}} - plan. + You have access to all CodeCrafters content, valid until + {{date-format @user.activeSubscription.cancelAt format="PPPp"}}.

{{/if}}
{{else if @user.isVip}}
- {{svg-jar "check-circle" class="h-5 w-5 text-teal-500"}}
VIP Access
@@ -58,7 +40,6 @@
{{else if @user.expiredSubscription}}
- {{svg-jar "x-circle" class="h-5 w-5 text-red-500"}}
Membership inactive
@@ -78,7 +59,6 @@ {{else}}
- {{svg-jar "question-mark-circle" class="h-5 w-5 text-yellow-500"}}
No membership found
diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index d59344fc8c..6d38f9257f 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,21 +1,19 @@ - + {{#if this.isLoading}}
{{else if this.errorMessage}}
-
+
- {{svg-jar "x-circle" class="h-5 w-5 text-red-400 dark:text-red-300"}} -
-
-

{{this.errorMessage}}

+ {{svg-jar "x-circle" class="h-5 w-5 text-red-700 dark:text-red-300"}}
+

{{this.errorMessage}}

{{else if (gt this.charges.length 0)}} -
+
Date
Amount
@@ -58,7 +56,7 @@ {{/each}}
{{else}} -
+
No payment history found.
{{/if}} diff --git a/app/components/settings/billing-page/payment-history-section.ts b/app/components/settings/billing-page/payment-history-section.ts index 7aca41dc89..77b54de261 100644 --- a/app/components/settings/billing-page/payment-history-section.ts +++ b/app/components/settings/billing-page/payment-history-section.ts @@ -17,8 +17,8 @@ export default class PaymentHistorySectionComponent extends Component @service declare store: Store; @tracked charges: ChargeModel[] = []; - @tracked isLoading = true; @tracked errorMessage: string | null = null; + @tracked isLoading = true; constructor(owner: unknown, args: Signature['Args']) { super(owner, args); diff --git a/app/components/settings/billing-page/support-section.ts b/app/components/settings/billing-page/support-section.ts index 112e576a0c..4295dea986 100644 --- a/app/components/settings/billing-page/support-section.ts +++ b/app/components/settings/billing-page/support-section.ts @@ -2,16 +2,13 @@ import Component from '@glimmer/component'; import type UserModel from 'codecrafters-frontend/models/user'; interface Signature { + Element: HTMLDivElement; Args: { user: UserModel; }; } -export default class SupportSectionComponent extends Component { - get supportEmail() { - return `support+${this.args.user.username}@codecrafters.io`; - } -} +export default class SupportSectionComponent extends Component {} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 45251f94d6..512f31ac8a 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -34,6 +34,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); assert.notOk(billingPage.membershipSection.hasActivePlan, 'does not show active plan'); + await percySnapshot('Billing Page - No Active Subscription'); }); test('membership section shows VIP access for subscriber with VIP access', async function (assert) { @@ -44,6 +45,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); assert.ok(billingPage.membershipSection.text.includes('VIP Access'), 'shows VIP access'); + await percySnapshot('Billing Page - VIP Access'); }); test('support section is visible', async function (assert) { @@ -65,7 +67,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { assert.ok(billingPage.paymentHistorySection.isVisible, 'payment history section is visible'); assert.strictEqual(billingPage.paymentHistorySection.charges.length, 0, 'shows no charges initially'); - assert.dom('[data-test-payment-history-section]').hasText('No payment history found.', 'shows empty state text'); + assert.dom('[data-test-payment-history-section] > div:last-child').hasText('No payment history found.', 'shows empty state text'); }); test('payment history section shows charges after creation', async function (assert) { diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index 15ca3fd84d..f4de6a38e1 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -12,6 +12,7 @@ export default createPage({ failed: hasClass('text-red-600'), }), scope: '[data-test-payment-history-section]', + }, supportSection: { From bd80766ad677ff4ae534fa6e787e23a8f23ecadc Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Wed, 30 Apr 2025 10:08:30 +0100 Subject: [PATCH 27/31] [percy] Update the dot to be just a dot --- app/components/settings/billing-page/membership-section.hbs | 6 +++++- app/components/settings/billing-page/renewal-section.hbs | 2 +- public/assets/images/svg-icons/circle.svg | 3 +++ tests/pages/settings/billing-page.ts | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 public/assets/images/svg-icons/circle.svg diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 6c32d10232..7212500d42 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,6 +1,7 @@ {{#if @user.hasActiveSubscription}}
+ {{svg-jar "circle" class="h-5 w-5 text-teal-500"}}
Membership active
@@ -24,6 +25,7 @@
{{else if @user.isVip}}
+ {{svg-jar "circle" class="h-5 w-5 text-teal-500"}}
VIP Access
@@ -40,7 +42,8 @@
{{else if @user.expiredSubscription}}
-
+ {{svg-jar "circle" class="h-5 w-5 text-red-700 dark:text-red-300"}} +
Membership inactive
@@ -59,6 +62,7 @@ {{else}}
+ {{svg-jar "circle" class="h-5 w-5 text-yellow-500"}}
No membership found
diff --git a/app/components/settings/billing-page/renewal-section.hbs b/app/components/settings/billing-page/renewal-section.hbs index efa1582ac1..47a73d24b0 100644 --- a/app/components/settings/billing-page/renewal-section.hbs +++ b/app/components/settings/billing-page/renewal-section.hbs @@ -1,6 +1,6 @@
- {{svg-jar "x-circle" class="h-5 w-5 text-gray-400"}} + {{svg-jar "circle" class="h-5 w-5 text-gray-400"}}
Auto-renew disabled
diff --git a/public/assets/images/svg-icons/circle.svg b/public/assets/images/svg-icons/circle.svg new file mode 100644 index 0000000000..a9fee858dc --- /dev/null +++ b/public/assets/images/svg-icons/circle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index f4de6a38e1..15ca3fd84d 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -12,7 +12,6 @@ export default createPage({ failed: hasClass('text-red-600'), }), scope: '[data-test-payment-history-section]', - }, supportSection: { From ac350c2b945e55b85e29fb82a2cad232a5b0b168 Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Thu, 1 May 2025 11:20:46 +0100 Subject: [PATCH 28/31] Update app/components/settings/billing-page/payment-history-section.ts Co-authored-by: Paul Kuruvilla --- app/components/settings/billing-page/payment-history-section.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings/billing-page/payment-history-section.ts b/app/components/settings/billing-page/payment-history-section.ts index 77b54de261..582814e175 100644 --- a/app/components/settings/billing-page/payment-history-section.ts +++ b/app/components/settings/billing-page/payment-history-section.ts @@ -36,7 +36,7 @@ export default class PaymentHistorySectionComponent extends Component this.charges = result.toArray(); } catch (error) { console.error('Failed to fetch charges:', error); - this.errorMessage = 'Failed to load payment history. Please try again later.'; + this.errorMessage = 'Failed to load payment history. Please contact us at hello@codecrafters.io if this error persists.'; Sentry.captureException(error); this.charges = []; } finally { From bc73fbae0d6b3a92b86be765c118ee6f58a04e7e Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Sat, 3 May 2025 09:39:40 +0100 Subject: [PATCH 29/31] Addressed Paul's Comments --- .../membership-page/actions-section.hbs | 26 ---------- .../membership-page/actions-section.js | 23 --------- .../membership-plan-section.hbs | 29 ----------- .../membership-plan-section.js | 3 -- .../payment-method-section.hbs | 9 ---- .../membership-page/payment-method-section.js | 3 -- .../recent-payments-section.hbs | 51 ------------------- .../recent-payments-section.js | 20 -------- app/components/membership-page/section.hbs | 10 ---- app/components/membership-page/section.js | 3 -- .../upcoming-payment-section.hbs | 17 ------- .../upcoming-payment-section.js | 19 ------- .../billing-page/membership-section.hbs | 36 +++++-------- .../membership-section/status-pill.hbs | 15 ++++++ .../billing-page/payment-history-section.hbs | 5 +- .../billing-page/payment-history-section.ts | 2 + .../settings/billing-page/support-section.hbs | 6 +-- public/assets/images/svg-icons/circle.svg | 3 -- .../acceptance/settings-page/billing-test.js | 3 +- tests/pages/settings/billing-page.ts | 4 +- 20 files changed, 38 insertions(+), 249 deletions(-) delete mode 100644 app/components/membership-page/actions-section.hbs delete mode 100644 app/components/membership-page/actions-section.js delete mode 100644 app/components/membership-page/membership-plan-section.hbs delete mode 100644 app/components/membership-page/membership-plan-section.js delete mode 100644 app/components/membership-page/payment-method-section.hbs delete mode 100644 app/components/membership-page/payment-method-section.js delete mode 100644 app/components/membership-page/recent-payments-section.hbs delete mode 100644 app/components/membership-page/recent-payments-section.js delete mode 100644 app/components/membership-page/section.hbs delete mode 100644 app/components/membership-page/section.js delete mode 100644 app/components/membership-page/upcoming-payment-section.hbs delete mode 100644 app/components/membership-page/upcoming-payment-section.js create mode 100644 app/components/settings/billing-page/membership-section/status-pill.hbs delete mode 100644 public/assets/images/svg-icons/circle.svg diff --git a/app/components/membership-page/actions-section.hbs b/app/components/membership-page/actions-section.hbs deleted file mode 100644 index 92bef9579f..0000000000 --- a/app/components/membership-page/actions-section.hbs +++ /dev/null @@ -1,26 +0,0 @@ - - {{#if @subscription.isInactive}} - - Start Membership - - {{/if}} - - - Update Payment Method - - -
- Questions? Write to us at - hello@codecrafters.io - and we'll help sort things out. -
-
\ No newline at end of file diff --git a/app/components/membership-page/actions-section.js b/app/components/membership-page/actions-section.js deleted file mode 100644 index 2359f4db90..0000000000 --- a/app/components/membership-page/actions-section.js +++ /dev/null @@ -1,23 +0,0 @@ -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; -import { inject as service } from '@ember/service'; -import Component from '@glimmer/component'; -import window from 'ember-window-mock'; - -export default class ActionsSectionComponent extends Component { - @tracked isCreatingPaymentMethodUpdateRequest = false; - @service router; - @service store; - - @action - async handleStartMembershipButtonClick() { - this.router.transitionTo('pay'); - } - - @action - async handleUpdatePaymentMethodButtonClick() { - this.isCreatingPaymentMethodUpdateRequest = true; - const paymentMethodUpdateRequest = await this.store.createRecord('individual-payment-method-update-request').save(); - window.location.href = paymentMethodUpdateRequest.url; - } -} diff --git a/app/components/membership-page/membership-plan-section.hbs b/app/components/membership-page/membership-plan-section.hbs deleted file mode 100644 index e1e18dc610..0000000000 --- a/app/components/membership-page/membership-plan-section.hbs +++ /dev/null @@ -1,29 +0,0 @@ -{{! @glint-nocheck: not typesafe yet }} - -
- {{#if @subscription.isActive}} - {{#if @subscription.cancelAt}} -

- Your CodeCrafters membership is valid until - {{date-format @subscription.cancelAt format="PPPp"}}. -

-

Your membership doesn’t renew automatically. To restart your membership, make a new one-time payment.

- {{else}} - You are currently subscribed to the - {{@subscription.pricingPlanName}} - plan. - {{/if}} - {{else if @subscription.isInactive}} - Your CodeCrafters membership is - currently inactive. - {{/if}} -
-
- {{#if (and @subscription.user.isVip @subscription.user.vipStatusExpiresAt)}} - πŸŽ‰ You have VIP access to all CodeCrafters content, valid until - {{date-format @subscription.user.vipStatusExpiresAt format="PPPp"}}. - {{else if @subscription.user.isVip}} - πŸŽ‰ You have VIP access to all CodeCrafters content. - {{/if}} -
-
\ No newline at end of file diff --git a/app/components/membership-page/membership-plan-section.js b/app/components/membership-page/membership-plan-section.js deleted file mode 100644 index 799d149ba9..0000000000 --- a/app/components/membership-page/membership-plan-section.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from '@glimmer/component'; - -export default class MembershipPlanSectionComponent extends Component {} diff --git a/app/components/membership-page/payment-method-section.hbs b/app/components/membership-page/payment-method-section.hbs deleted file mode 100644 index 85686837bf..0000000000 --- a/app/components/membership-page/payment-method-section.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{! @glint-nocheck: not typesafe yet }} - -
- The payment method linked to your account is a - Mastercard - ending in - 1234. -
-
\ No newline at end of file diff --git a/app/components/membership-page/payment-method-section.js b/app/components/membership-page/payment-method-section.js deleted file mode 100644 index 12b4b9ede8..0000000000 --- a/app/components/membership-page/payment-method-section.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from '@glimmer/component'; - -export default class PaymentMethodSectionComponent extends Component {} diff --git a/app/components/membership-page/recent-payments-section.hbs b/app/components/membership-page/recent-payments-section.hbs deleted file mode 100644 index cf14bb8802..0000000000 --- a/app/components/membership-page/recent-payments-section.hbs +++ /dev/null @@ -1,51 +0,0 @@ - - {{#if this.isLoading}} -
- Loading icon - Loading recent payments... -
- {{else if (gt this.charges.length 0)}} -
- {{! Header }} -
Date
-
Amount
-
- - {{#each this.charges as |charge|}} -
{{date-format charge.createdAt format="PPP"}}
- -
- {{charge.displayString}} - {{#if (gt charge.amountRefunded 0)}} - {{#if charge.isFullyRefunded}} - (refunded) - {{else}} - ({{charge.refundedAmountDisplayString}} - refunded) - {{/if}} - {{/if}} -
- -
- {{#if (and charge.invoiceId charge.statusIsSucceeded)}} - - Download Invoice - - {{else if charge.statusIsFailed}} - Payment failed - {{/if}} -
- {{/each}} -
- {{else}} -
- No recent payments found. -
- {{/if}} -
\ No newline at end of file diff --git a/app/components/membership-page/recent-payments-section.js b/app/components/membership-page/recent-payments-section.js deleted file mode 100644 index 7f48fc1989..0000000000 --- a/app/components/membership-page/recent-payments-section.js +++ /dev/null @@ -1,20 +0,0 @@ -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; -import { inject as service } from '@ember/service'; -import fade from 'ember-animated/transitions/fade'; -import Component from '@glimmer/component'; -import rippleSpinnerImage from '/assets/images/icons/ripple-spinner.svg'; - -export default class RecentPaymentsSectionComponent extends Component { - rippleSpinnerImage = rippleSpinnerImage; - @tracked charges = []; - @tracked isLoading = true; - @service store; - transition = fade; - - @action - async handleDidInsert() { - this.charges = await this.store.findAll('charge'); - this.isLoading = false; - } -} diff --git a/app/components/membership-page/section.hbs b/app/components/membership-page/section.hbs deleted file mode 100644 index 63ee6cb38d..0000000000 --- a/app/components/membership-page/section.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{! @glint-nocheck: not typesafe yet }} -
-
-
- {{@title}} -
-
- - {{yield}} -
\ No newline at end of file diff --git a/app/components/membership-page/section.js b/app/components/membership-page/section.js deleted file mode 100644 index 322f02f8f3..0000000000 --- a/app/components/membership-page/section.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from '@glimmer/component'; - -export default class SectionComponent extends Component {} diff --git a/app/components/membership-page/upcoming-payment-section.hbs b/app/components/membership-page/upcoming-payment-section.hbs deleted file mode 100644 index da3328e1b9..0000000000 --- a/app/components/membership-page/upcoming-payment-section.hbs +++ /dev/null @@ -1,17 +0,0 @@ - - {{#if this.isLoading}} -
- Loading icon - Loading upcoming payments... -
- {{else if this.nextInvoicePreview}} - Your payment method will be charged - ${{if this.isLoading "β‹―" this.nextInvoicePreview.amountDueInDollars}} - on - {{if this.isLoading "β‹―" (date-format this.nextInvoicePreview.createdAt format="PPP")}}. - {{else}} -
- No upcoming payment. -
- {{/if}} -
\ No newline at end of file diff --git a/app/components/membership-page/upcoming-payment-section.js b/app/components/membership-page/upcoming-payment-section.js deleted file mode 100644 index 4f8660319c..0000000000 --- a/app/components/membership-page/upcoming-payment-section.js +++ /dev/null @@ -1,19 +0,0 @@ -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; -import { inject as service } from '@ember/service'; -import Component from '@glimmer/component'; -import rippleSpinnerImage from '/assets/images/icons/ripple-spinner.svg'; - -export default class UpcomingPaymentSectionComponent extends Component { - rippleSpinnerImage = rippleSpinnerImage; - - @tracked isLoading = true; - @tracked nextInvoicePreview; - @service authenticator; - - @action - async handleDidInsert() { - this.nextInvoicePreview = await this.authenticator.currentUser.fetchNextInvoicePreview(); - this.isLoading = false; - } -} diff --git a/app/components/settings/billing-page/membership-section.hbs b/app/components/settings/billing-page/membership-section.hbs index 7212500d42..14b61c52a9 100644 --- a/app/components/settings/billing-page/membership-section.hbs +++ b/app/components/settings/billing-page/membership-section.hbs @@ -1,11 +1,8 @@ {{#if @user.hasActiveSubscription}} -
- {{svg-jar "circle" class="h-5 w-5 text-teal-500"}} -
- Membership active -
-
+ + Membership active +
{{#if (and @user.isVip (gt @user.vipStatusExpiresAt @user.activeSubscription.cancelAt))}}

@@ -24,12 +21,9 @@ {{/if}}

{{else if @user.isVip}} -
- {{svg-jar "circle" class="h-5 w-5 text-teal-500"}} -
- VIP Access -
-
+ + VIP Access +

{{#if @user.vipStatusExpiresAt}} @@ -41,12 +35,9 @@

{{else if @user.expiredSubscription}} -
- {{svg-jar "circle" class="h-5 w-5 text-red-700 dark:text-red-300"}} -
- Membership inactive -
-
+ + Membership inactive +

Your CodeCrafters membership is @@ -61,12 +52,9 @@ Start membership β†’ {{else}} -

- {{svg-jar "circle" class="h-5 w-5 text-yellow-500"}} -
- No membership found -
-
+ + No membership found +

You don't have a CodeCrafters membership. Start one to get access to diff --git a/app/components/settings/billing-page/membership-section/status-pill.hbs b/app/components/settings/billing-page/membership-section/status-pill.hbs new file mode 100644 index 0000000000..19b5850729 --- /dev/null +++ b/app/components/settings/billing-page/membership-section/status-pill.hbs @@ -0,0 +1,15 @@ +

+
+
+ {{yield}} +
+
\ No newline at end of file diff --git a/app/components/settings/billing-page/payment-history-section.hbs b/app/components/settings/billing-page/payment-history-section.hbs index 6d38f9257f..5b269690a8 100644 --- a/app/components/settings/billing-page/payment-history-section.hbs +++ b/app/components/settings/billing-page/payment-history-section.hbs @@ -1,7 +1,8 @@ {{#if this.isLoading}} -
-
+
+ Loading icon + Loading payment history...
{{else if this.errorMessage}}
diff --git a/app/components/settings/billing-page/payment-history-section.ts b/app/components/settings/billing-page/payment-history-section.ts index 582814e175..65961257b9 100644 --- a/app/components/settings/billing-page/payment-history-section.ts +++ b/app/components/settings/billing-page/payment-history-section.ts @@ -2,6 +2,7 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import * as Sentry from '@sentry/ember'; +import rippleSpinnerImage from '/assets/images/icons/ripple-spinner.svg'; import type Store from '@ember-data/store'; import type UserModel from 'codecrafters-frontend/models/user'; import type ChargeModel from 'codecrafters-frontend/models/charge'; @@ -16,6 +17,7 @@ interface Signature { export default class PaymentHistorySectionComponent extends Component { @service declare store: Store; + rippleSpinnerImage = rippleSpinnerImage; @tracked charges: ChargeModel[] = []; @tracked errorMessage: string | null = null; @tracked isLoading = true; diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs index fe3391e025..65fa39d3e7 100644 --- a/app/components/settings/billing-page/support-section.hbs +++ b/app/components/settings/billing-page/support-section.hbs @@ -1,6 +1,6 @@ - - - + + + Get help diff --git a/public/assets/images/svg-icons/circle.svg b/public/assets/images/svg-icons/circle.svg deleted file mode 100644 index a9fee858dc..0000000000 --- a/public/assets/images/svg-icons/circle.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 512f31ac8a..01dd3f3e95 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -55,8 +55,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await billingPage.visit(); assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); - assert.ok(billingPage.supportSection.clickOnContactButton, 'click on contact button'); - assert.dom('[data-test-support-section]').hasAttribute('href', /^mailto:hello@codecrafters.io/); + assert.strictEqual(billingPage.supportSection.contactButtonHref, 'mailto:hello@codecrafters.io?subject=Billing help (account: rohitpaulk)', 'contact button href is correct'); }); test('payment history section shows empty state initially', async function (assert) { diff --git a/tests/pages/settings/billing-page.ts b/tests/pages/settings/billing-page.ts index 15ca3fd84d..f43d816151 100644 --- a/tests/pages/settings/billing-page.ts +++ b/tests/pages/settings/billing-page.ts @@ -1,4 +1,4 @@ -import { clickable, hasClass, visitable, collection, text } from 'ember-cli-page-object'; +import { hasClass, visitable, collection, text, attribute } from 'ember-cli-page-object'; import createPage from 'codecrafters-frontend/tests/support/create-page'; export default createPage({ @@ -15,8 +15,8 @@ export default createPage({ }, supportSection: { - clickOnContactButton: clickable('[data-test-support-contact-button]'), scope: '[data-test-support-section]', + contactButtonHref: attribute('href', '[data-test-support-contact-button]'), }, visit: visitable('/settings/billing'), From 547b45b8d669f0bb384e024646a565892db032dd Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Sat, 3 May 2025 09:50:12 +0100 Subject: [PATCH 30/31] [percy] linter + other minor changes --- .../membership-section/status-pill.hbs | 23 +++++++++++-------- .../membership-section/status-pill.ts | 19 +++++++++++++++ .../settings/billing-page/renewal-section.hbs | 2 +- .../settings/billing-page/support-section.hbs | 6 ++++- .../acceptance/settings-page/billing-test.js | 6 ++++- 5 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 app/components/settings/billing-page/membership-section/status-pill.ts diff --git a/app/components/settings/billing-page/membership-section/status-pill.hbs b/app/components/settings/billing-page/membership-section/status-pill.hbs index 19b5850729..ea718ccdc6 100644 --- a/app/components/settings/billing-page/membership-section/status-pill.hbs +++ b/app/components/settings/billing-page/membership-section/status-pill.hbs @@ -1,15 +1,20 @@ -
-
{{yield}}
-
\ No newline at end of file +
\ No newline at end of file diff --git a/app/components/settings/billing-page/membership-section/status-pill.ts b/app/components/settings/billing-page/membership-section/status-pill.ts new file mode 100644 index 0000000000..25c9216955 --- /dev/null +++ b/app/components/settings/billing-page/membership-section/status-pill.ts @@ -0,0 +1,19 @@ +import Component from '@glimmer/component'; + +interface Signature { + Element: HTMLDivElement; + Args: { + variant: 'teal' | 'red' | 'yellow'; + }; + Blocks: { + default: []; + }; +} + +export default class StatusPillComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Settings::BillingPage::MembershipSection::StatusPill': typeof StatusPillComponent; + } +} diff --git a/app/components/settings/billing-page/renewal-section.hbs b/app/components/settings/billing-page/renewal-section.hbs index 47a73d24b0..953545b906 100644 --- a/app/components/settings/billing-page/renewal-section.hbs +++ b/app/components/settings/billing-page/renewal-section.hbs @@ -1,6 +1,6 @@
- {{svg-jar "circle" class="h-5 w-5 text-gray-400"}} +
Auto-renew disabled
diff --git a/app/components/settings/billing-page/support-section.hbs b/app/components/settings/billing-page/support-section.hbs index 65fa39d3e7..5e374a24fe 100644 --- a/app/components/settings/billing-page/support-section.hbs +++ b/app/components/settings/billing-page/support-section.hbs @@ -1,5 +1,9 @@ - + Get help diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 01dd3f3e95..7b8dceb374 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -55,7 +55,11 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await billingPage.visit(); assert.ok(billingPage.supportSection.isVisible, 'support section is visible'); - assert.strictEqual(billingPage.supportSection.contactButtonHref, 'mailto:hello@codecrafters.io?subject=Billing help (account: rohitpaulk)', 'contact button href is correct'); + assert.strictEqual( + billingPage.supportSection.contactButtonHref, + 'mailto:hello@codecrafters.io?subject=Billing help (account: rohitpaulk)', + 'contact button href is correct', + ); }); test('payment history section shows empty state initially', async function (assert) { From 263cab5fa1821850d32fb98e5393d61ba718b0cf Mon Sep 17 00:00:00 2001 From: Arpan Pandey Date: Sat, 3 May 2025 09:52:10 +0100 Subject: [PATCH 31/31] [percy] removed active plan one --- tests/acceptance/settings-page/billing-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/settings-page/billing-test.js b/tests/acceptance/settings-page/billing-test.js index 7b8dceb374..6aa6a13eed 100644 --- a/tests/acceptance/settings-page/billing-test.js +++ b/tests/acceptance/settings-page/billing-test.js @@ -33,7 +33,7 @@ module('Acceptance | settings-page | billing-test', function (hooks) { await billingPage.visit(); assert.ok(billingPage.membershipSection.isVisible, 'membership section is visible'); - assert.notOk(billingPage.membershipSection.hasActivePlan, 'does not show active plan'); + assert.notOk(billingPage.membershipSection.text.includes('Membership active'), 'does not show active plan'); await percySnapshot('Billing Page - No Active Subscription'); });