diff --git a/.changeset/slimy-baths-wait.md b/.changeset/slimy-baths-wait.md new file mode 100644 index 00000000000..87d8ab329b0 --- /dev/null +++ b/.changeset/slimy-baths-wait.md @@ -0,0 +1,6 @@ +--- +'@clerk/testing': patch +--- + +Add `closeDrawer` to the checkout object. +Update `startCheckout` to allow choosing between monthly and annually. diff --git a/.changeset/strong-years-bathe.md b/.changeset/strong-years-bathe.md new file mode 100644 index 00000000000..a09b1e86fe2 --- /dev/null +++ b/.changeset/strong-years-bathe.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Bug fix: Always invalidate checkout object when `` unmounts. diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts index 8a866932e3b..f690fce35f7 100644 --- a/integration/tests/pricing-table.test.ts +++ b/integration/tests/pricing-table.test.ts @@ -188,5 +188,33 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await fakeUser.deleteIfExists(); }); + + test('checkout always revalidates on open', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const fakeUser = u.services.users.createFakeUser(); + await u.services.users.createBapiUser(fakeUser); + + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.page.goToRelative('/user'); + + await u.po.userProfile.waitForMounted(); + await u.po.userProfile.switchToBillingTab(); + await u.po.page.getByRole('button', { name: 'Switch plans' }).click(); + await u.po.pricingTable.startCheckout({ planSlug: 'pro', period: 'monthly' }); + await u.po.checkout.waitForMounted(); + await u.po.checkout.closeDrawer(); + + await u.po.checkout.waitForMounted(); + await u.po.pricingTable.startCheckout({ planSlug: 'plus', period: 'monthly' }); + await u.po.checkout.fillTestCard(); + await u.po.checkout.clickPayOrSubscribe(); + await u.po.checkout.confirmAndContinue(); + await u.po.pricingTable.startCheckout({ planSlug: 'pro', period: 'monthly' }); + await expect(u.po.page.getByText('- $9.99')).toBeVisible(); + + await fakeUser.deleteIfExists(); + }); }); }); diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx index b329934062f..0f6c355b87d 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx @@ -22,7 +22,7 @@ export const CheckoutPage = (props: __internal_CheckoutProps) => { const { translateError } = useLocalizations(); const { t } = useLocalizations(); const { planId, planPeriod, subscriberType, onSubscriptionComplete } = props; - const { setIsOpen, isOpen } = useDrawerContext(); + const { setIsOpen } = useDrawerContext(); const prefersReducedMotion = usePrefersReducedMotion(); const { animations: layoutAnimations } = useAppearance().parsedLayout; const isMotionSafe = !prefersReducedMotion && layoutAnimations === true; @@ -45,10 +45,8 @@ export const CheckoutPage = (props: __internal_CheckoutProps) => { }; useEffect(() => { - if (isOpen) { - revalidate(); - } - }, [isOpen]); + return invalidate; + }, []); if (isLoading || plansLoading) { return ( diff --git a/packages/clerk-js/src/ui/components/Checkout/useCheckout.ts b/packages/clerk-js/src/ui/components/Checkout/useCheckout.ts index 2fbf2ca8452..61b57b23a73 100644 --- a/packages/clerk-js/src/ui/components/Checkout/useCheckout.ts +++ b/packages/clerk-js/src/ui/components/Checkout/useCheckout.ts @@ -24,16 +24,7 @@ export const useCheckout = (props: __internal_CheckoutProps) => { planPeriod, ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), }, - { - // TODO: remove this once we move to useSWR - // Some more context: - // The checkout is already invalidated on complete, but the way `useFetch` is implemented - // there is no way to invalidate the cache by resourceId only (e.g. commerce-checkout-user_xxx), - // we can only invalidate the cache by resourceId + params as this is how the cache key is constructed. - // With SWR, we will be able to invalidate the cache by the `commerce-checkout-user_xxx` key, - // so only invalidation on checkout completion will be needed. - staleTime: 0, - }, + undefined, `commerce-checkout-${user?.id}`, ); diff --git a/packages/testing/src/playwright/unstable/page-objects/checkout.ts b/packages/testing/src/playwright/unstable/page-objects/checkout.ts index 4571cb9e65e..2ece5613fdd 100644 --- a/packages/testing/src/playwright/unstable/page-objects/checkout.ts +++ b/packages/testing/src/playwright/unstable/page-objects/checkout.ts @@ -8,6 +8,9 @@ export const createCheckoutPageObject = (testArgs: { page: EnhancedPage }) => { waitForMounted: (selector = '.cl-checkout-root') => { return page.waitForSelector(selector, { state: 'attached' }); }, + closeDrawer: () => { + return page.locator('.cl-drawerClose').click(); + }, fillTestCard: async () => { await self.fillCard({ number: '4242424242424242', diff --git a/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts b/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts index f1248fa139a..67a4402dd91 100644 --- a/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts +++ b/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts @@ -14,10 +14,30 @@ export const createPricingTablePageObject = (testArgs: { page: EnhancedPage }) = clickResubscribe: async () => { await page.getByText('Re-subscribe').click(); }, - startCheckout: async ({ planSlug, shouldSwitch }: { planSlug: string; shouldSwitch?: boolean }) => { + startCheckout: async ({ + planSlug, + shouldSwitch, + period, + }: { + planSlug: string; + shouldSwitch?: boolean; + period?: 'monthly' | 'annually'; + }) => { const targetButtonName = shouldSwitch === true ? 'Switch to this plan' : shouldSwitch === false ? /subscribe/i : /get|switch|subscribe/i; + if (period) { + await page.locator(`.cl-pricingTableCard__${planSlug} .cl-pricingTableCardPeriodToggle`).click(); + + const billedAnnuallyChecked = await page + .locator(`.cl-pricingTableCard__${planSlug} .cl-switchIndicator`) + .getAttribute('data-checked'); + + if (billedAnnuallyChecked === 'true' && period === 'monthly') { + await page.locator(`.cl-pricingTableCard__${planSlug} .cl-pricingTableCardPeriodToggle`).click(); + } + } + await page .locator(`.cl-pricingTableCard__${planSlug} .cl-pricingTableCardFooter`) .getByRole('button', {