Skip to content

Commit 308e220

Browse files
committed
Extract button to generic component
1 parent 88582a3 commit 308e220

File tree

9 files changed

+342
-133
lines changed

9 files changed

+342
-133
lines changed

packages/clerk-js/src/core/clerk.ts

Lines changed: 156 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import type {
2323
__experimental_CheckoutInstance,
2424
__experimental_CheckoutOptions,
25+
__internal_AttemptToEnableEnvironmentSettingParams,
2526
__internal_CheckoutProps,
2627
__internal_EnableOrganizationsPromptProps,
2728
__internal_OAuthConsentProps,
@@ -98,6 +99,7 @@ import { allSettled, handleValueOrFn, noop } from '@clerk/shared/utils';
9899
import type { QueryClient } from '@tanstack/query-core';
99100

100101
import { debugLogger, initDebugLogger } from '@/utils/debug';
102+
import { CLERK_ENVIRONMENT_SETTING_PROMPT_REJECTED_KEY, SafeSessionStorage } from '@/utils/sessionStorage';
101103

102104
import type { MountComponentRenderer } from '../ui/Components';
103105
import {
@@ -176,6 +178,7 @@ declare global {
176178

177179
const CANNOT_RENDER_BILLING_DISABLED_ERROR_CODE = 'cannot_render_billing_disabled';
178180
const CANNOT_RENDER_USER_MISSING_ERROR_CODE = 'cannot_render_user_missing';
181+
const CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE = 'cannot_render_organizations_disabled';
179182
const CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE = 'cannot_render_organization_missing';
180183
const CANNOT_RENDER_SINGLE_SESSION_ENABLED_ERROR_CODE = 'cannot_render_single_session_enabled';
181184
const CANNOT_RENDER_API_KEYS_DISABLED_ERROR_CODE = 'cannot_render_api_keys_disabled';
@@ -739,6 +742,42 @@ export class Clerk implements ClerkInterface {
739742
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification'));
740743
};
741744

745+
public __internal_attemptToEnableEnvironmentSetting = (
746+
params: __internal_AttemptToEnableEnvironmentSettingParams,
747+
): { status: 'enabled' | 'prompt-shown' | 'rejected' } => {
748+
const { for: setting, caller, onSuccess } = params;
749+
750+
// If not in development instance, return enabled status in order to not open the in-app prompt
751+
if (this.#instanceType !== 'development') {
752+
return { status: 'enabled' };
753+
}
754+
755+
const rejectedCallers = SafeSessionStorage.getItem<string[]>(CLERK_ENVIRONMENT_SETTING_PROMPT_REJECTED_KEY, []);
756+
757+
switch (setting) {
758+
case 'organizations':
759+
if (!disabledOrganizationsFeature(this, this.environment)) {
760+
return { status: 'enabled' };
761+
}
762+
763+
if (rejectedCallers.includes(caller)) {
764+
return { status: 'rejected' };
765+
}
766+
767+
this.__internal_openEnableOrganizationsPrompt({
768+
caller,
769+
onSuccess: () => window.location.reload(),
770+
onClose: () => {
771+
SafeSessionStorage.setItem(CLERK_ENVIRONMENT_SETTING_PROMPT_REJECTED_KEY, [caller]);
772+
},
773+
} as __internal_EnableOrganizationsPromptProps);
774+
775+
return { status: 'prompt-shown' };
776+
default:
777+
return { status: 'enabled' };
778+
}
779+
};
780+
742781
public __internal_openEnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps): void => {
743782
this.assertComponentsReady(this.#componentControls);
744783
void this.#componentControls
@@ -822,17 +861,25 @@ export class Clerk implements ClerkInterface {
822861

823862
public openOrganizationProfile = (props?: OrganizationProfileProps): void => {
824863
this.assertComponentsReady(this.#componentControls);
825-
if (disabledOrganizationsFeature(this, this.environment)) {
826-
if (this.#instanceType === 'development') {
827-
this.__internal_openEnableOrganizationsPrompt({
828-
componentName: 'OrganizationProfile',
829-
onComplete: () => {
830-
this.openOrganizationProfile();
831-
},
832-
});
833-
}
864+
865+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
866+
for: 'organizations',
867+
caller: 'OrganizationProfile',
868+
onSuccess: () => {
869+
this.openOrganizationProfile(props);
870+
},
871+
});
872+
873+
if (status === 'rejected') {
874+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), {
875+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
876+
});
877+
}
878+
879+
if (status === 'prompt-shown') {
834880
return;
835881
}
882+
836883
if (noOrganizationExists(this)) {
837884
if (this.#instanceType === 'development') {
838885
throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenOrgDoesNotExist, {
@@ -855,17 +902,25 @@ export class Clerk implements ClerkInterface {
855902

856903
public openCreateOrganization = (props?: CreateOrganizationProps): void => {
857904
this.assertComponentsReady(this.#componentControls);
858-
if (disabledOrganizationsFeature(this, this.environment)) {
859-
if (this.#instanceType === 'development') {
860-
this.__internal_openEnableOrganizationsPrompt({
861-
componentName: 'OrganizationSwitcher',
862-
onComplete: () => {
863-
this.openCreateOrganization();
864-
},
865-
});
866-
}
905+
906+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
907+
for: 'organizations',
908+
caller: 'CreateOrganization',
909+
onSuccess: () => {
910+
this.openCreateOrganization(props);
911+
},
912+
});
913+
914+
if (status === 'rejected') {
915+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), {
916+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
917+
});
918+
}
919+
920+
if (status === 'prompt-shown') {
867921
return;
868922
}
923+
869924
void this.#componentControls
870925
.ensureMounted({ preloadHint: 'CreateOrganization' })
871926
.then(controls => controls.openModal('createOrganization', props || {}));
@@ -1000,17 +1055,25 @@ export class Clerk implements ClerkInterface {
10001055

10011056
public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => {
10021057
this.assertComponentsReady(this.#componentControls);
1003-
if (disabledOrganizationsFeature(this, this.environment)) {
1004-
if (this.#instanceType === 'development') {
1005-
this.__internal_openEnableOrganizationsPrompt({
1006-
componentName: 'OrganizationProfile',
1007-
onComplete: () => {
1008-
this.mountOrganizationProfile(node, props);
1009-
},
1010-
});
1011-
}
1058+
1059+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
1060+
for: 'organizations',
1061+
caller: 'OrganizationProfile',
1062+
onSuccess: () => {
1063+
this.mountOrganizationProfile(node, props);
1064+
},
1065+
});
1066+
1067+
if (status === 'rejected') {
1068+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationProfile'), {
1069+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1070+
});
1071+
}
1072+
1073+
if (status === 'prompt-shown') {
10121074
return;
10131075
}
1076+
10141077
const userExists = !noUserExists(this);
10151078
if (noOrganizationExists(this) && userExists) {
10161079
if (this.#instanceType === 'development') {
@@ -1043,17 +1106,25 @@ export class Clerk implements ClerkInterface {
10431106

10441107
public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => {
10451108
this.assertComponentsReady(this.#componentControls);
1046-
if (disabledOrganizationsFeature(this, this.environment)) {
1047-
if (this.#instanceType === 'development') {
1048-
this.__internal_openEnableOrganizationsPrompt({
1049-
componentName: 'OrganizationSwitcher',
1050-
onComplete: () => {
1051-
this.mountCreateOrganization();
1052-
},
1053-
});
1054-
}
1109+
1110+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
1111+
for: 'organizations',
1112+
caller: 'CreateOrganization',
1113+
onSuccess: () => {
1114+
this.mountCreateOrganization(node, props);
1115+
},
1116+
});
1117+
1118+
if (status === 'rejected') {
1119+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('CreateOrganization'), {
1120+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1121+
});
1122+
}
1123+
1124+
if (status === 'prompt-shown') {
10551125
return;
10561126
}
1127+
10571128
void this.#componentControls?.ensureMounted({ preloadHint: 'CreateOrganization' }).then(controls =>
10581129
controls.mountComponent({
10591130
name: 'CreateOrganization',
@@ -1077,17 +1148,25 @@ export class Clerk implements ClerkInterface {
10771148

10781149
public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => {
10791150
this.assertComponentsReady(this.#componentControls);
1080-
if (disabledOrganizationsFeature(this, this.environment)) {
1081-
if (this.#instanceType === 'development') {
1082-
this.__internal_openEnableOrganizationsPrompt({
1083-
componentName: 'OrganizationSwitcher',
1084-
onComplete: () => {
1085-
this.mountOrganizationSwitcher(node, props);
1086-
},
1087-
});
1088-
}
1151+
1152+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
1153+
for: 'organizations',
1154+
caller: 'OrganizationSwitcher',
1155+
onSuccess: () => {
1156+
this.mountOrganizationSwitcher(node, props);
1157+
},
1158+
});
1159+
1160+
if (status === 'rejected') {
1161+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationSwitcher'), {
1162+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1163+
});
1164+
}
1165+
1166+
if (status === 'prompt-shown') {
10891167
return;
10901168
}
1169+
10911170
void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }).then(controls =>
10921171
controls.mountComponent({
10931172
name: 'OrganizationSwitcher',
@@ -1119,17 +1198,25 @@ export class Clerk implements ClerkInterface {
11191198

11201199
public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => {
11211200
this.assertComponentsReady(this.#componentControls);
1122-
if (disabledOrganizationsFeature(this, this.environment)) {
1123-
if (this.#instanceType === 'development') {
1124-
this.__internal_openEnableOrganizationsPrompt({
1125-
componentName: 'OrganizationList',
1126-
onComplete: () => {
1127-
this.mountOrganizationList(node, props);
1128-
},
1129-
});
1130-
}
1201+
1202+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
1203+
for: 'organizations',
1204+
caller: 'OrganizationList',
1205+
onSuccess: () => {
1206+
this.mountOrganizationList(node, props);
1207+
},
1208+
});
1209+
1210+
if (status === 'rejected') {
1211+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('OrganizationList'), {
1212+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1213+
});
1214+
}
1215+
1216+
if (status === 'prompt-shown') {
11311217
return;
11321218
}
1219+
11331220
void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationList' }).then(controls =>
11341221
controls.mountComponent({
11351222
name: 'OrganizationList',
@@ -1309,15 +1396,21 @@ export class Clerk implements ClerkInterface {
13091396
public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => {
13101397
this.assertComponentsReady(this.#componentControls);
13111398

1312-
if (disabledOrganizationsFeature(this, this.environment)) {
1313-
if (this.#instanceType === 'development') {
1314-
this.__internal_openEnableOrganizationsPrompt({
1315-
componentName: 'OrganizationSwitcher',
1316-
onComplete: () => {
1317-
this.mountTaskChooseOrganization(node, props);
1318-
},
1319-
});
1320-
}
1399+
const { status } = this.__internal_attemptToEnableEnvironmentSetting({
1400+
for: 'organizations',
1401+
caller: 'TaskChooseOrganization',
1402+
onSuccess: () => {
1403+
this.mountTaskChooseOrganization(node, props);
1404+
},
1405+
});
1406+
1407+
if (status === 'rejected') {
1408+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('TaskChooseOrganization'), {
1409+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1410+
});
1411+
}
1412+
1413+
if (status === 'prompt-shown') {
13211414
return;
13221415
}
13231416

packages/clerk-js/src/core/warnings.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ const formatWarning = (msg: string) => {
44
return `🔒 Clerk:\n${msg.trim()}\n(This notice only appears in development)`;
55
};
66

7+
const createMessageForDisabledOrganizations = (
8+
componentName:
9+
| 'OrganizationProfile'
10+
| 'OrganizationSwitcher'
11+
| 'OrganizationList'
12+
| 'CreateOrganization'
13+
| 'TaskChooseOrganization',
14+
) => {
15+
return formatWarning(
16+
`The <${componentName}/> cannot be rendered when the feature is turned off. Visit 'dashboard.clerk.com' to enable the feature. Since the feature is turned off, this is no-op.`,
17+
);
18+
};
719
const createMessageForDisabledBilling = (componentName: 'PricingTable' | 'Checkout' | 'PlanDetails') => {
820
return formatWarning(
921
`The <${componentName}/> component cannot be rendered when billing is disabled. Visit 'https://dashboard.clerk.com/last-active?path=billing/settings' to follow the necessary steps to enable billing. Since billing is disabled, this is no-op.`,
@@ -25,6 +37,7 @@ const warnings = {
2537
cannotRenderComponentWhenUserDoesNotExist:
2638
'<UserProfile/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
2739
cannotRenderComponentWhenOrgDoesNotExist: `<OrganizationProfile/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
40+
cannotRenderAnyOrganizationComponent: createMessageForDisabledOrganizations,
2841
cannotRenderAnyBillingComponent: createMessageForDisabledBilling,
2942
cannotOpenUserProfile:
3043
'The UserProfile modal cannot render unless a user is signed in. Since no user is signed in, this is no-op.',

0 commit comments

Comments
 (0)