Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lovely-ghosts-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Marking root of subscription modal with `cl-subscriptionDetails-root`.
5 changes: 5 additions & 0 deletions .changeset/slow-zoos-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/testing': patch
---

Adding subscription details page object.
58 changes: 45 additions & 13 deletions integration/tests/pricing-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
await newFakeUser.deleteIfExists();
});

// test('can manage and cancel subscription', async ({ page, context }) => {
// const u = createTestUtils({ app, page, context });
// await u.po.signIn.goTo();
// await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
// await u.po.page.goToRelative('/pricing-table');

// await u.po.pricingTable.waitForMounted();
// await u.po.pricingTable.clickManageSubscription();
// await u.po.page.getByRole('button', { name: 'Cancel subscription' }).click();
// await u.po.page.getByRole('alertdialog').getByRole('button', { name: 'Cancel subscription' }).click();
// await expect(u.po.page.getByRole('button', { name: /resubscribe|re-subscribe/i }).first()).toBeVisible();
// });

test.describe('redirects', () => {
test('default navigates to afterSignInUrl', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
Expand Down Expand Up @@ -260,6 +247,51 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
await fakeUser.deleteIfExists();
});

test('can unsubscribe from a plan', 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 expect(u.po.page.getByText(/Free/i)).toBeVisible();
await u.po.page.getByRole('button', { name: 'Switch plans' }).click();
await u.po.pricingTable.startCheckout({ planSlug: 'plus' });
await u.po.checkout.waitForMounted();
await u.po.checkout.fillTestCard();
await u.po.checkout.clickPayOrSubscribe();
await expect(u.po.page.getByText('Payment was successful!')).toBeVisible();

await u.po.checkout.confirmAndContinue();
await u.po.page.locator('.cl-headerBackLink').getByText('Plans').click();

await u.page.waitForTimeout(1000);
await expect(u.po.page.locator('.cl-profileSectionContent__subscriptionsList').getByText('Plus')).toBeVisible();
await u.po.page.getByRole('button', { name: 'Manage subscription' }).first().click();
await u.po.subscriptionDetails.waitForMounted();
await u.po.subscriptionDetails.root.locator('.cl-menuButtonEllipsisBordered').click();
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();
await u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot').waitFor({ state: 'visible' });
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();
await u.po.subscriptionDetails.waitForUnmounted();

// Verify the Free plan with Upcoming status exists
await expect(
u.po.page
.locator('.cl-profileSectionContent__subscriptionsList')
.getByText('Free')
.locator('xpath=..')
.getByText('Upcoming'),
).toBeVisible();

await fakeUser.deleteIfExists();
});

test('checkout always revalidates on open', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

Expand Down
21 changes: 13 additions & 8 deletions packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
Col,
descriptors,
Flex,
Flow,
Heading,
localizationKeys,
Spinner,
Expand All @@ -40,7 +41,7 @@ import {
} from '../../customizables';
import { SubscriptionBadge } from '../Subscriptions/badge';

// We cannot derive the state of confrimation modal from the existance subscription, as it will make the animation laggy when the confimation closes.
// We cannot derive the state of confirmation modal from the existence subscription, as it will make the animation laggy when the confimation closes.
const SubscriptionForCancellationContext = React.createContext<{
subscription: CommerceSubscriptionResource | null;
setSubscription: (subscription: CommerceSubscriptionResource | null) => void;
Expand All @@ -55,13 +56,17 @@ const SubscriptionForCancellationContext = React.createContext<{

export const SubscriptionDetails = (props: __internal_SubscriptionDetailsProps) => {
return (
<Drawer.Content>
<SubscriptionDetailsContext.Provider value={{ componentName: 'SubscriptionDetails', ...props }}>
<SubscriberTypeContext.Provider value={props.for}>
<SubscriptionDetailsInternal {...props} />
</SubscriberTypeContext.Provider>
</SubscriptionDetailsContext.Provider>
</Drawer.Content>
<Flow.Root flow='subscriptionDetails'>
<Flow.Part>
<Drawer.Content>
<SubscriptionDetailsContext.Provider value={{ componentName: 'SubscriptionDetails', ...props }}>
<SubscriberTypeContext.Provider value={props.for}>
<SubscriptionDetailsInternal {...props} />
</SubscriberTypeContext.Provider>
</SubscriptionDetailsContext.Provider>
</Drawer.Content>
</Flow.Part>
</Flow.Root>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createPricingTablePageObject } from './pricingTable';
import { createSessionTaskComponentPageObject } from './sessionTask';
import { createSignInComponentPageObject } from './signIn';
import { createSignUpComponentPageObject } from './signUp';
import { createSubscriptionDetailsPageObject } from './subscriptionDetails';
import { createTestingTokenPageObject } from './testingToken';
import { createUserButtonPageObject } from './userButton';
import { createUserProfileComponentPageObject } from './userProfile';
Expand Down Expand Up @@ -48,5 +49,6 @@ export const createPageObjects = ({
userVerification: createUserVerificationComponentPageObject(testArgs),
waitlist: createWaitlistComponentPageObject(testArgs),
apiKeys: createAPIKeysComponentPageObject(testArgs),
subscriptionDetails: createSubscriptionDetailsPageObject(testArgs),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { EnhancedPage } from './app';
import { common } from './common';

export const createSubscriptionDetailsPageObject = (testArgs: { page: EnhancedPage }) => {
const { page } = testArgs;
const self = {
...common(testArgs),
waitForMounted: (selector = '.cl-subscriptionDetails-root') => {
return page.waitForSelector(selector, { state: 'attached' });
},
waitForUnmounted: () => {
return self.root.locator('.cl-drawerRoot').waitFor({ state: 'detached' });
},
closeDrawer: () => {
return self.root.locator('.cl-drawerClose').click();
},
root: page.locator('.cl-subscriptionDetails-root'),
};
return self;
};
Loading