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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
7 changes: 7 additions & 0 deletions .changeset/three-ducks-hang.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add test step for `<__experimental_ConfigureSSO />`
68 changes: 68 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,6 +274,74 @@ export const enUS: LocalizationResource = {
subtitle: "Contact the application's administrator to get access through the existing connection.",
},
},
testConfigurationStep: {
Comment thread
LauraBeatris marked this conversation as resolved.
title: 'Test your SSO connection',
subtitle: 'Authenticate using the test SSO URL to verify you configured the connection correctly.',
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test SSO URL and complete the sign-in flow.',
testUrl: {
title: 'Test your SSO URL',
subtitle: 'Generate and copy a test SSO URL to authenticate with.',
actionLabel__copy: 'Copy test URL',
},
testResults: {
title: 'Test results',
actionLabel__refresh: 'Refresh logs',
polling: 'Waiting for the test run to complete…',
status__success: 'Success',
status__failed: 'Failed',
status__pending: 'Pending',
},
testRunDetails: {
title: 'Test run',
runDetails: {
sectionTitle: 'Run details',
timestamp: 'Timestamp',
status: 'Status',
errorCode: 'Error code',
fullMessage: 'Full message',
actionLabel__copy: 'Copy message',
actionLabel__copied: 'Copied',
},
parsedUserInfo: {
sectionTitle: 'Parsed user info',
email: 'Email',
firstName: 'First name',
},
howToFix: {
sectionTitle: 'How to fix',
actionLabel__viewDocumentation: 'View documentation',
saml_user_attribute_missing: {
intro: 'To fix this error, follow these steps:',
step1: "Access your identity provider's configuration dashboard.",
step2: "Navigate to your application's SAML settings or attribute mapping configuration.",
step3: "Ensure that the 'mail' attribute is properly mapped to the user's email address field.",
},
saml_response_relaystate_missing: {
description:
'Check that your identity provider is correctly returning the RelayState parameter that was sent in the original request.',
},
saml_email_address_domain_mismatch: {
description:
'Verify that the user is signing in with an email address that matches one of the allowed domains for this connection. If you need to add additional domains, update the allowed domains in your connection settings.',
},
oauth_access_denied: {
description:
"This error occurs when the user clicked Cancel or Deny on the OAuth provider's authorization screen, or the provider rejected the authorization request. Verify that the OAuth application credentials (Client ID and Client Secret) are correctly configured.",
},
oauth_token_exchange_error: {
description:
"Verify that your OAuth application's Client ID and Client Secret are correctly configured and match the credentials from your OAuth provider's dashboard.",
},
oauth_fetch_user_error: {
intro: 'To fix this error, follow these steps:',
step1:
'Verify that the OAuth scopes configured in your connection settings include the necessary permissions to read user profile information.',
step2: 'Ensure that the user info endpoint URL is correctly configured.',
},
},
},
},
configureStep: {
spFields: {
acsUrl: {
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ export type {
UseUserEnterpriseConnectionsParams,
UseUserEnterpriseConnectionsReturn,
} from './useUserEnterpriseConnections';
export { __internal_useEnterpriseConnectionTestRuns } from './useEnterpriseConnectionTestRuns';
export type {
UseEnterpriseConnectionTestRunsParams,
UseEnterpriseConnectionTestRunsReturn,
} from './useEnterpriseConnectionTestRuns';

export { useUserBase as __internal_useUserBase } from './base/useUserBase';
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react';

import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
import { createCacheKeys } from './createCacheKeys';

/**
* @internal
*/
export function useEnterpriseConnectionTestRunsCacheKeys(params: {
userId: string | null;
enterpriseConnectionId: string | null;
args: GetEnterpriseConnectionTestRunsParams;
}) {
const { userId, enterpriseConnectionId, args } = params;
return useMemo(() => {
return createCacheKeys({
stablePrefix: INTERNAL_STABLE_KEYS.ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
authenticated: Boolean(userId),
tracked: {
userId: userId ?? null,
enterpriseConnectionId: enterpriseConnectionId ?? null,
},
untracked: {
args,
},
});
// The args object is intentionally serialized via the consumer to keep stability.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, enterpriseConnectionId, JSON.stringify(args)]);
}
139 changes: 139 additions & 0 deletions packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';

import type {
EnterpriseConnectionTestRunResource,
GetEnterpriseConnectionTestRunsParams,
} from '../../types/enterpriseConnectionTestRun';
import { useClerkInstanceContext } from '../contexts';
import { useClerkQueryClient } from '../query/use-clerk-query-client';
import { useClerkQuery } from '../query/useQuery';
import { useUserBase } from './base/useUserBase';
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
import { useEnterpriseConnectionTestRunsCacheKeys } from './useEnterpriseConnectionTestRuns.shared';

const DEFAULT_POLL_INTERVAL_MS = 2_000;

export type UseEnterpriseConnectionTestRunsParams = {
enterpriseConnectionId: string | null;
/**
* Pass-through fetch parameters (pagination, status filter).
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
*/
params?: GetEnterpriseConnectionTestRunsParams;
/**
* Polling interval (ms) applied between `revalidate()` and the moment the
* first record arrives in the response.
*
* @default 2000
*/
pollIntervalMs?: number;
/**
* If `false`, the hook is dormant — no fetch, no polling.
*
* @default true
*/
enabled?: boolean;
};

export type UseEnterpriseConnectionTestRunsReturn = {
data: EnterpriseConnectionTestRunResource[] | undefined;
totalCount: number | undefined;
error: Error | null;
isLoading: boolean;
isFetching: boolean;
/**
* `true` while the hook is actively polling for the first record to appear
*/
isPolling: boolean;
/**
* Force a refetch and (if the list is currently empty) arm polling
*/
revalidate: () => Promise<void>;
};

/**
* Subscribes to the list of enterprise-connection test runs for the signed-in user
*
* @internal
*/
function useEnterpriseConnectionTestRuns(
params: UseEnterpriseConnectionTestRunsParams,
): UseEnterpriseConnectionTestRunsReturn {
const {
enterpriseConnectionId,
params: fetchParams = { initialPage: 1, pageSize: 10 },
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
enabled = true,
} = params;

const clerk = useClerkInstanceContext();
const user = useUserBase();
const [queryClient] = useClerkQueryClient();

const { queryKey, invalidationKey, stableKey, authenticated } = useEnterpriseConnectionTestRunsCacheKeys({
userId: user?.id ?? null,
enterpriseConnectionId,
args: fetchParams,
});

useClearQueriesOnSignOut({
isSignedOut: user === null,
authenticated,
stableKeys: stableKey,
});

const queryEnabled = enabled && clerk.loaded && Boolean(user) && Boolean(enterpriseConnectionId);

const [shouldPoll, setShouldPoll] = useState(false);

const query = useClerkQuery({
queryKey,
queryFn: () => {
if (!enterpriseConnectionId) {
throw new Error('enterpriseConnectionId is required to fetch test runs');
}
return user?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
},
refetchInterval: q => {
if (!shouldPoll) {
return false;
}

const hasRows = (q.state.data?.data?.length ?? 0) > 0;
return hasRows ? false : pollIntervalMs;
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
});
Comment thread
LauraBeatris marked this conversation as resolved.

const hasRows = (query.data?.data?.length ?? 0) > 0;

useEffect(() => {
if (shouldPoll && hasRows) {
setShouldPoll(false);
}
}, [shouldPoll, hasRows]);

const revalidate = useCallback(async () => {
// Only arm polling when there is nothing in the list yet — once any record
// has been seen, this is a one-shot refetch.
if (!hasRows) {
setShouldPoll(true);
}
await queryClient.invalidateQueries({ queryKey: invalidationKey });
}, [queryClient, invalidationKey, hasRows]);

const isPolling = queryEnabled && shouldPoll && !hasRows;

return {
data: query.data?.data,
totalCount: query.data?.total_count,
error: query.error ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isPolling,
revalidate,
};
}

export { useEnterpriseConnectionTestRuns as __internal_useEnterpriseConnectionTestRuns };
2 changes: 2 additions & 0 deletions packages/shared/src/react/stable-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,12 +73,14 @@ const PAYMENT_ATTEMPT_KEY = 'billing-payment-attempt';
const BILLING_PLANS_KEY = 'billing-plan';
const BILLING_STATEMENTS_KEY = 'billing-statement';
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';

export const INTERNAL_STABLE_KEYS = {
PAYMENT_ATTEMPT_KEY,
BILLING_PLANS_KEY,
BILLING_STATEMENTS_KEY,
USER_ENTERPRISE_CONNECTIONS_KEY,
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
} as const;

export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];
4 changes: 3 additions & 1 deletion packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,9 @@ export type ProfileSectionId =
| 'ssoDomain'
| 'ssoConfiguration'
| 'configureAgain'
| 'resetSso';
| 'resetSso'
| 'testSsoUrl'
| 'testResults';
export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing';

export type UserPreviewId = 'userButton' | 'personalWorkspace';
Expand Down
62 changes: 62 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1338,6 +1338,68 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
testConfigurationStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
error__noSuccessfulTestRun: LocalizationValue;
testUrl: {
title: LocalizationValue;
subtitle: LocalizationValue;
actionLabel__copy: LocalizationValue;
};
testResults: {
title: LocalizationValue;
actionLabel__refresh: LocalizationValue;
polling: LocalizationValue;
status__success: LocalizationValue;
status__failed: LocalizationValue;
status__pending: LocalizationValue;
};
testRunDetails: {
title: LocalizationValue;
runDetails: {
sectionTitle: LocalizationValue;
timestamp: LocalizationValue;
status: LocalizationValue;
errorCode: LocalizationValue;
fullMessage: LocalizationValue;
actionLabel__copy: LocalizationValue;
actionLabel__copied: LocalizationValue;
};
parsedUserInfo: {
sectionTitle: LocalizationValue;
email: LocalizationValue;
firstName: LocalizationValue;
};
howToFix: {
sectionTitle: LocalizationValue;
actionLabel__viewDocumentation: LocalizationValue;
saml_user_attribute_missing: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
};
saml_response_relaystate_missing: {
description: LocalizationValue;
};
saml_email_address_domain_mismatch: {
description: LocalizationValue;
};
oauth_access_denied: {
description: LocalizationValue;
};
oauth_token_exchange_error: {
description: LocalizationValue;
};
oauth_fetch_user_error: {
intro: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
};
};
};
configureStep: {
spFields: {
acsUrl: {
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,15 +55,15 @@ const AuthenticatedContent = withCoreUserGuard(() => {
})}
>
<ConfigureSSOCardProtect>
<ConfigureSSOCardContent />
<ConfigureSSOCardContent contentRef={contentRef} />
</ConfigureSSOCardProtect>
</Col>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOCardContent = () => {
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });

// Currently FAPI only supports one enterprise connection per user
Expand All@@ -74,7 +74,10 @@ const ConfigureSSOCardContent = () => {
}

return (
<ConfigureSSOProvider enterpriseConnection={enterpriseConnection}>
<ConfigureSSOProvider
enterpriseConnection={enterpriseConnection}
contentRef={contentRef}
>
<ConfigureSSOSteps />
</ConfigureSSOProvider>
);
Expand Down
Loading
Loading