Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/flat-ravens-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@clerk/clerk-js': minor
'@clerk/shared': minor
'@clerk/clerk-react': minor
'@clerk/vue': minor
---

Introduce in-app development prompt to enable the Organizations feature

In development instances, when using organization components or hooks for the first time, developers will see a prompt to enable the Organizations feature directly in their app, eliminating the need to visit the Clerk Dashboard.
1 change: 1 addition & 0 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.5KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" },
Expand Down
137 changes: 115 additions & 22 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import {
import type {
__experimental_CheckoutInstance,
__experimental_CheckoutOptions,
__internal_AttemptToEnableEnvironmentSettingParams,
__internal_CheckoutProps,
__internal_EnableOrganizationsPromptProps,
__internal_OAuthConsentProps,
__internal_PlanDetailsProps,
__internal_SubscriptionDetailsProps,
Expand All @@ -35,9 +37,9 @@ import type {
AuthenticateWithMetamaskParams,
AuthenticateWithOKXWalletParams,
BillingNamespace,
Clerk as ClerkInterface,
ClerkAPIError,
ClerkAuthenticateWithWeb3Params,
Clerk as ClerkInterface,
ClerkOptions,
ClientJSONSnapshot,
ClientResource,
Expand Down Expand Up @@ -739,6 +741,50 @@ export class Clerk implements ClerkInterface {
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification'));
};

public __internal_attemptToEnableEnvironmentSetting = (
params: __internal_AttemptToEnableEnvironmentSettingParams,
): { status: 'enabled' | 'prompt-shown' } => {
const { for: setting, caller } = params;

// If not in development instance, return enabled status in order to not open the prompt
if (this.#instanceType !== 'development') {
return { status: 'enabled' };
}

switch (setting) {
case 'organizations':
if (!disabledOrganizationsFeature(this, this.environment)) {
return { status: 'enabled' };
}

this.__internal_openEnableOrganizationsPrompt({
caller,
// Reload current window to all invalidate all resources
// related to organizations, eg: roles
onSuccess: () => window.location.reload(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still some delay in which the window is reloading and the screen kepts showing a blank state without having the component mounted due to the stale environment data.

I'm thinking of applying an optimistic update, at least on the "enabled" status, to have the components mounted meanwhile, or not closing the modal and introduce a loading status until the window gets reloaded.

onClose: params.onClose,
} as __internal_EnableOrganizationsPromptProps);

return { status: 'prompt-shown' };
default:
return { status: 'enabled' };
}
};

public __internal_openEnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls
.ensureMounted({ preloadHint: 'EnableOrganizationsPrompt' })
.then(controls => controls.openModal('enableOrganizationsPrompt', props || {}));

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

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

public __internal_openBlankCaptchaModal = (): Promise<unknown> => {
this.assertComponentsReady(this.#componentControls);
return this.#componentControls
Expand Down Expand Up @@ -810,14 +856,21 @@ export class Clerk implements ClerkInterface {

public openOrganizationProfile = (props?: OrganizationProfileProps): void => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {

const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'OrganizationProfile',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

if (noOrganizationExists(this)) {
if (this.#instanceType === 'development') {
throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenOrgDoesNotExist, {
Expand All @@ -840,14 +893,21 @@ export class Clerk implements ClerkInterface {

public openCreateOrganization = (props?: CreateOrganizationProps): void => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {

const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'CreateOrganization',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

void this.#componentControls
.ensureMounted({ preloadHint: 'CreateOrganization' })
.then(controls => controls.openModal('createOrganization', props || {}));
Expand Down Expand Up @@ -982,14 +1042,21 @@ export class Clerk implements ClerkInterface {

public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {

const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'OrganizationProfile',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

const userExists = !noUserExists(this);
if (noOrganizationExists(this) && userExists) {
if (this.#instanceType === 'development') {
Expand Down Expand Up @@ -1022,14 +1089,21 @@ export class Clerk implements ClerkInterface {

public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {

const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'CreateOrganization',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

void this.#componentControls?.ensureMounted({ preloadHint: 'CreateOrganization' }).then(controls =>
controls.mountComponent({
name: 'CreateOrganization',
Expand All @@ -1053,14 +1127,21 @@ export class Clerk implements ClerkInterface {

public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {

const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'OrganizationSwitcher',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }).then(controls =>
controls.mountComponent({
name: 'OrganizationSwitcher',
Expand Down Expand Up @@ -1092,14 +1173,21 @@ export class Clerk implements ClerkInterface {

public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {

const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'OrganizationList',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationList'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationList' }).then(controls =>
controls.mountComponent({
name: 'OrganizationList',
Expand Down Expand Up @@ -1279,12 +1367,17 @@ export class Clerk implements ClerkInterface {
public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => {
this.assertComponentsReady(this.#componentControls);

if (disabledOrganizationsFeature(this, this.environment)) {
if (this.#instanceType === 'development') {
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'TaskChooseOrganization',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('TaskChooseOrganization'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
}
},
});

if (status === 'prompt-shown') {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/fapiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export interface FapiClient {
}

// List of paths that should not receive the session ID parameter in the URL
const unauthorizedPathPrefixes = ['/client', '/waitlist'];
const unauthorizedPathPrefixes = ['/client', '/waitlist', '/dev_tools'];

type FapiClientOptions = {
frontendApi: string;
Expand Down
21 changes: 21 additions & 0 deletions packages/clerk-js/src/core/resources/DevTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ClerkResourceJSON, DevToolsResource, EnableEnvironmentSettingParams } from '@clerk/shared/types';

import { BaseResource } from './Base';

/**
* @internal
*/
export class DevTools extends BaseResource implements DevToolsResource {
pathRoot = '/dev_tools';

protected fromJSON(_data: ClerkResourceJSON | null): this {
return this;
}

async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) {
await this._basePatch({
path: `${this.pathRoot}/enable_environment_setting`,
body: params,
});
}
}
Loading
Loading