Skip to content

Commit

Permalink
feat(clerk-js,nextjs,react,types): Introduce <Waitlist /> component
Browse files Browse the repository at this point in the history
  • Loading branch information
nikospapcom committed Oct 24, 2024
1 parent 7a63290 commit fb7aedf
Show file tree
Hide file tree
Showing 50 changed files with 813 additions and 17 deletions.
14 changes: 14 additions & 0 deletions .changeset/nine-squids-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@clerk/localizations": minor
"@clerk/clerk-js": minor
"@clerk/nextjs": minor
"@clerk/clerk-react": minor
"@clerk/types": minor
---

Show `Join waitlist` promt from `<SignIn />` component when mode is `waitlist`
Change the text in `RestrictedAccess` component when mode is `waitlist`
Introduce `<Waitlist />` component to allow users to join in the waitlist via email
Introduce `joinWaitlist` method in `clerk` class
Introduce `redirectToWaitlist` function in `clerk` class to allow user to redirect to waitlist page

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ exports[`public exports should not include a breaking change 1`] = `
"SignedOut",
"UserButton",
"UserProfile",
"Waitlist",
"useAuth",
"useClerk",
"useEmailLink",
Expand Down
11 changes: 10 additions & 1 deletion packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const mockDisplayConfigWithSameOrigin = {
homeUrl: 'http://test.host/home',
createOrganizationUrl: 'http://test.host/create-organization',
organizationProfileUrl: 'http://test.host/organization-profile',
waitlistUrl: 'http://test.host/waitlist',
} as DisplayConfig;

const mockDisplayConfigWithDifferentOrigin = {
Expand All @@ -45,6 +46,7 @@ const mockDisplayConfigWithDifferentOrigin = {
homeUrl: 'http://another-test.host/home',
createOrganizationUrl: 'http://another-test.host/create-organization',
organizationProfileUrl: 'http://another-test.host/organization-profile',
waitlistUrl: 'http://another-test.host/waitlist',
} as DisplayConfig;

const mockUserSettings = {
Expand Down Expand Up @@ -96,7 +98,7 @@ describe('Clerk singleton - Redirects', () => {

afterEach(() => mockNavigate.mockReset());

describe('.redirectTo(SignUp|SignIn|UserProfile|AfterSignIn|AfterSignUp|CreateOrganization|OrganizationProfile)', () => {
describe('.redirectTo(SignUp|SignIn|UserProfile|AfterSignIn|AfterSignUp|CreateOrganization|OrganizationProfile|Waitlist)', () => {
let clerkForProductionInstance: Clerk;
let clerkForDevelopmentInstance: Clerk;

Expand Down Expand Up @@ -189,6 +191,13 @@ describe('Clerk singleton - Redirects', () => {
expect(mockNavigate.mock.calls[0][0]).toBe('/organization-profile');
expect(mockNavigate.mock.calls[1][0]).toBe('/organization-profile');
});

it('redirects to waitlitUrl', async () => {
await clerkForDevelopmentInstance.redirectToWaitlist();
expect(mockNavigate).toHaveBeenCalledWith('/waitlist', {
windowNavigate: expect.any(Function),
});
});
});

describe('when redirects point to different origin urls', () => {
Expand Down
55 changes: 55 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
HandleEmailLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
JoinWaitlistParams,
ListenerCallback,
LoadedClerk,
NavigateOptions,
Expand All @@ -61,6 +62,8 @@ import type {
UserButtonProps,
UserProfileProps,
UserResource,
WaitlistProps,
WaitlistResource,
Web3Provider,
} from '@clerk/types';

Expand Down Expand Up @@ -119,6 +122,7 @@ import {
EmailLinkErrorCode,
Environment,
Organization,
Waitlist,
} from './resources/internal';
import { warnings } from './warnings';

Expand Down Expand Up @@ -509,6 +513,18 @@ export class Clerk implements ClerkInterface {
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('createOrganization'));
};

public openWaitlist = (props?: WaitlistProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls
.ensureMounted({ preloadHint: 'Waitlist' })
.then(controls => controls.openModal('waitlist', props || {}));
};

public closeWaitlist = (): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('waitlist'));
};

public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => {
if (props && props.__experimental?.newComponents && this.__experimental_ui) {
this.__experimental_ui.mount('SignIn', node, props);
Expand Down Expand Up @@ -743,6 +759,25 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public mountWaitlist = (node: HTMLDivElement, props?: WaitlistProps) => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls?.ensureMounted({ preloadHint: 'Waitlist' }).then(controls =>
controls.mountComponent({
name: 'Waitlist',
appearanceKey: 'waitlist',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('Waitlist', props));
};

public unmountWaitlist = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down Expand Up @@ -964,6 +999,16 @@ export class Clerk implements ClerkInterface {
return this.buildUrlWithAuth(this.#options.afterSignOutUrl);
}

public buildWaitlistUrl(): string {
if (!this.environment || !this.environment.displayConfig) {
return '';
}

const waitlistUrl = this.#options['waitlistUrl'] || this.environment.displayConfig.waitlistUrl;

return buildURL({ base: waitlistUrl }, { stringify: true });
}

public buildAfterMultiSessionSingleSignOutUrl(): string {
if (!this.#options.afterMultiSessionSingleSignOutUrl) {
return this.buildUrlWithAuth(
Expand Down Expand Up @@ -1078,6 +1123,13 @@ export class Clerk implements ClerkInterface {
return;
};

public redirectToWaitlist = async (): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.buildWaitlistUrl());
}
return;
};

public handleEmailLinkVerification = async (
params: HandleEmailLinkVerificationParams,
customNavigate?: (to: string) => Promise<unknown>,
Expand Down Expand Up @@ -1479,6 +1531,9 @@ export class Clerk implements ClerkInterface {
public getOrganization = async (organizationId: string): Promise<OrganizationResource> =>
Organization.get(organizationId);

public joinWaitlist = async ({ emailAddress }: JoinWaitlistParams): Promise<WaitlistResource> =>
Waitlist.join({ emailAddress });

public updateEnvironment(environment: EnvironmentResource): asserts this is { environment: EnvironmentResource } {
this.environment = environment;
this.#authService?.setEnvironment(environment);
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export const DEBOUNCE_MS = 350;
export const SIGN_UP_MODES: Record<string, SignUpModes> = {
PUBLIC: 'public',
RESTRICTED: 'restricted',
WAITLIST: 'waitlist',
};
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/resources/DisplayConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
showDevModeWarning!: boolean;
termsUrl!: string;
privacyPolicyUrl!: string;
waitlistUrl!: string;

public constructor(data: DisplayConfigJSON) {
super();
Expand Down Expand Up @@ -91,6 +92,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
this.showDevModeWarning = data.show_devmode_warning;
this.termsUrl = data.terms_url;
this.privacyPolicyUrl = data.privacy_policy_url;
this.waitlistUrl = data.waitlist_url;
return this;
}
}
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/resources/Waitlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { JoinWaitlistParams, WaitlistJSON, WaitlistResource } from '@clerk/types';

import { unixEpochToDate } from '../../utils/date';
import { BaseResource } from './internal';

export class Waitlist extends BaseResource implements WaitlistResource {
pathRoot = '/waitlist';

id = '';
updatedAt: Date | null = null;
createdAt: Date | null = null;

constructor(data: WaitlistJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: WaitlistJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.updatedAt = unixEpochToDate(data.updated_at);
this.createdAt = unixEpochToDate(data.created_at);
return this;
}

static async join(params: JoinWaitlistParams): Promise<WaitlistResource> {
const json = (
await BaseResource._fetch<WaitlistJSON>({
path: '/waitlist',
method: 'POST',
body: params as any,
})
)?.response as unknown as WaitlistJSON;

return new Waitlist(json);
}
}
14 changes: 14 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/Waitlist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Waitlist } from '../internal';

describe('Organization', () => {
it('has the same initial properties', () => {
const organization = new Waitlist({
object: 'waitlist',
id: 'test_id',
created_at: 12345,
updated_at: 5678,
});

expect(organization).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Organization has the same initial properties 1`] = `
Waitlist {
"createdAt": 1970-01-01T00:00:12.345Z,
"id": "test_id",
"pathRoot": "/waitlist",
"updatedAt": 1970-01-01T00:00:05.678Z,
}
`;
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './Token';
export * from './User';
export * from './Verification';
export * from './Web3Wallet';
export * from './Waitlist';
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export * from './User';
export * from './UserOrganizationInvitation';
export * from './Verification';
export * from './Web3Wallet';
export * from './Waitlist';
35 changes: 32 additions & 3 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
SignInProps,
SignUpProps,
UserProfileProps,
WaitlistProps,
} from '@clerk/types';
import React, { Suspense } from 'react';

Expand All @@ -30,6 +31,7 @@ import {
SignUpModal,
UserProfileModal,
UserVerificationModal,
WaitlistModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand Down Expand Up @@ -65,7 +67,8 @@ export type ComponentControls = {
| 'userProfile'
| 'organizationProfile'
| 'createOrganization'
| 'userVerification',
| 'userVerification'
| 'waitlist',
>(
modal: T,
props: T extends 'signIn'
Expand All @@ -74,7 +77,9 @@ export type ComponentControls = {
? SignUpProps
: T extends 'userVerification'
? __experimental_UserVerificationProps
: UserProfileProps,
: T extends 'waitlist'
? WaitlistProps
: UserProfileProps,
) => void;
closeModal: (
modal:
Expand All @@ -84,7 +89,8 @@ export type ComponentControls = {
| 'userProfile'
| 'organizationProfile'
| 'createOrganization'
| 'userVerification',
| 'userVerification'
| 'waitlist',
options?: {
notify?: boolean;
},
Expand Down Expand Up @@ -119,6 +125,7 @@ interface ComponentsState {
organizationProfileModal: null | OrganizationProfileProps;
createOrganizationModal: null | CreateOrganizationProps;
organizationSwitcherPrefetch: boolean;
waitlistModal: null | WaitlistProps;
nodes: Map<HTMLDivElement, HtmlNodeOptions>;
impersonationFab: boolean;
}
Expand Down Expand Up @@ -183,6 +190,7 @@ const componentNodes = Object.freeze({
UserProfile: 'userProfileModal',
OrganizationProfile: 'organizationProfileModal',
CreateOrganization: 'createOrganizationModal',
Waitlist: 'waitlistModal',
}) as any;

const Components = (props: ComponentsProps) => {
Expand All @@ -197,6 +205,7 @@ const Components = (props: ComponentsProps) => {
organizationProfileModal: null,
createOrganizationModal: null,
organizationSwitcherPrefetch: false,
waitlistModal: null,
nodes: new Map(),
impersonationFab: false,
});
Expand All @@ -209,6 +218,7 @@ const Components = (props: ComponentsProps) => {
userVerificationModal,
organizationProfileModal,
createOrganizationModal,
waitlistModal,
nodes,
} = state;

Expand Down Expand Up @@ -334,6 +344,7 @@ const Components = (props: ComponentsProps) => {
>
<SignInModal {...signInModal} />
<SignUpModal {...signInModal} />
<WaitlistModal {...waitlistModal} />
</LazyModalRenderer>
);

Expand All @@ -350,6 +361,7 @@ const Components = (props: ComponentsProps) => {
>
<SignInModal {...signUpModal} />
<SignUpModal {...signUpModal} />
<WaitlistModal {...waitlistModal} />
</LazyModalRenderer>
);

Expand Down Expand Up @@ -427,6 +439,22 @@ const Components = (props: ComponentsProps) => {
</LazyModalRenderer>
);

const mountedWaitlistModal = (
<LazyModalRenderer
globalAppearance={state.appearance}
appearanceKey={'waitlist'}
componentAppearance={waitlistModal?.appearance}
flowName={'waitlist'}
onClose={() => componentsControls.closeModal('waitlist')}
onExternalNavigate={() => componentsControls.closeModal('waitlist')}
startPath={buildVirtualRouterUrl({ base: '/waitlist', path: urlStateParam?.path })}
componentName={'WaitlistModal'}
>
<WaitlistModal {...waitlistModal} />
<SignInModal {...waitlistModal} />
</LazyModalRenderer>
);

return (
<Suspense fallback={''}>
<LazyProviders
Expand Down Expand Up @@ -455,6 +483,7 @@ const Components = (props: ComponentsProps) => {
{userVerificationModal && mountedUserVerificationModal}
{organizationProfileModal && mountedOrganizationProfileModal}
{createOrganizationModal && mountedCreateOrganizationModal}
{waitlistModal && mountedWaitlistModal}
{state.impersonationFab && (
<LazyImpersonationFabProvider globalAppearance={state.appearance}>
<ImpersonationFab />
Expand Down
Loading

0 comments on commit fb7aedf

Please sign in to comment.