diff --git a/.changeset/thin-wolves-camp.md b/.changeset/thin-wolves-camp.md
new file mode 100644
index 00000000000..20e9f51b749
--- /dev/null
+++ b/.changeset/thin-wolves-camp.md
@@ -0,0 +1,7 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/nextjs': minor
+'@clerk/types': minor
+---
+
+Display keyless prompt until the developer manually dismisses it.
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 5bab893c45a..7335d9c08dd 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -18,6 +18,6 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
- { "path": "./dist/keylessPrompt*.js", "maxSize": "4.9KB" }
+ { "path": "./dist/keylessPrompt*.js", "maxSize": "5.5KB" }
]
}
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 8862630bca0..dc1701b95cd 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -2081,12 +2081,14 @@ export class Clerk implements ClerkInterface {
};
#handleKeylessPrompt = () => {
- if (this.#options.__internal_claimKeylessApplicationUrl) {
+ if (this.#options.__internal_keyless_claimKeylessApplicationUrl) {
void this.#componentControls?.ensureMounted().then(controls => {
+ // TODO(@pantelis): Investigate if this resets existing props
controls.updateProps({
options: {
- __internal_claimKeylessApplicationUrl: this.#options.__internal_claimKeylessApplicationUrl,
- __internal_copyInstanceKeysUrl: this.#options.__internal_copyInstanceKeysUrl,
+ __internal_keyless_claimKeylessApplicationUrl: this.#options.__internal_keyless_claimKeylessApplicationUrl,
+ __internal_keyless_copyInstanceKeysUrl: this.#options.__internal_keyless_copyInstanceKeysUrl,
+ __internal_keyless_dismissPrompt: this.#options.__internal_keyless_dismissPrompt,
},
});
});
diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx
index 0e35b0a7236..eac3243a9b5 100644
--- a/packages/clerk-js/src/ui/Components.tsx
+++ b/packages/clerk-js/src/ui/Components.tsx
@@ -517,14 +517,16 @@ const Components = (props: ComponentsProps) => {
)}
- {state.options?.__internal_claimKeylessApplicationUrl && state.options?.__internal_copyInstanceKeysUrl && (
-
-
-
- )}
+ {state.options?.__internal_keyless_claimKeylessApplicationUrl &&
+ state.options?.__internal_keyless_copyInstanceKeysUrl && (
+
+
+
+ )}
{state.organizationSwitcherPrefetch && }
diff --git a/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx b/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx
index 9f13df41e1f..125a1a1bd50 100644
--- a/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx
+++ b/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx
@@ -1,7 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { css } from '@emotion/react';
import type { PropsWithChildren } from 'react';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { Flex } from '../../customizables';
@@ -14,6 +14,7 @@ import { useRevalidateEnvironment } from './use-revalidate-environment';
type KeylessPromptProps = {
claimUrl: string;
copyKeysUrl: string;
+ onDismiss: (() => Promise) | undefined;
};
const buttonIdentifierPrefix = `--clerk-keyless-prompt`;
@@ -25,11 +26,22 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
const environment = useRevalidateEnvironment();
const claimed = Boolean(environment.authConfig.claimedAt);
- const success = false;
+ const success = typeof _props.onDismiss === 'function' && claimed;
const appName = environment.displayConfig.applicationName;
const isForcedExpanded = claimed || success || isExpanded;
+ const urlToDashboard = useMemo(() => {
+ if (claimed) {
+ return _props.copyKeysUrl;
+ }
+
+ const url = new URL(_props.claimUrl);
+ // Clerk Dashboard accepts a `return_url` query param when visiting `/apps/claim`.
+ url.searchParams.append('return_url', window.location.href);
+ return url.href;
+ }, [claimed, _props.copyKeysUrl, _props.claimUrl]);
+
const baseElementStyles = css`
box-sizing: border-box;
padding: 0;
@@ -71,6 +83,7 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.32);
white-space: nowrap;
user-select: none;
+ cursor: pointer;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 30.5%, rgba(0, 0, 0, 0.05) 100%), #454545;
box-shadow:
0px 0px 0px 1px rgba(255, 255, 255, 0.04) inset,
@@ -279,6 +292,7 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
color: #8c8c8c;
transition: color 130ms ease-out;
display: ${isExpanded && !claimed && !success ? 'block' : 'none'};
+ cursor: pointer;
:hover {
color: #eeeeee;
@@ -374,6 +388,10 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
(success ? (
) : (
m.removeKeyless());
+ return;
+}
diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx
index 6f7b8f8c97d..541d8c405f8 100644
--- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx
+++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx
@@ -4,11 +4,13 @@ import React from 'react';
import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider';
import { getDynamicAuthData } from '../../server/buildClerkProps';
+import { safeParseClerkFile } from '../../server/keyless-node';
import type { NextClerkProviderProps } from '../../types';
import { canUseKeyless } from '../../utils/feature-flags';
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
import { isNext13 } from '../../utils/sdk-versions';
import { ClientClerkProvider } from '../client/ClerkProvider';
+import { deleteKeylessAction } from '../keyless-actions';
import { buildRequestLike, getScriptNonceFromHeader } from './utils';
const getDynamicClerkState = React.cache(async function getDynamicClerkState() {
@@ -69,7 +71,8 @@ export async function ClerkProvider(
);
- const shouldRunAsKeyless = !propsWithEnvs.publishableKey && canUseKeyless;
+ const runningWithClaimedKeys = propsWithEnvs.publishableKey === safeParseClerkFile()?.publishableKey;
+ const shouldRunAsKeyless = (!propsWithEnvs.publishableKey || runningWithClaimedKeys) && canUseKeyless;
if (shouldRunAsKeyless) {
// NOTE: Create or read keys on every render. Usually this means only on hard refresh or hard navigations.
@@ -77,22 +80,27 @@ export async function ClerkProvider(
if (newOrReadKeys) {
const KeylessCookieSync = await import('../client/keyless-cookie-sync.js').then(mod => mod.KeylessCookieSync);
- output = (
-
-
- {children}
-
-
+ const clientProvider = (
+
+ {children}
+
);
+
+ if (runningWithClaimedKeys) {
+ output = clientProvider;
+ } else {
+ output = {clientProvider};
+ }
}
}
diff --git a/packages/nextjs/src/server/keyless-node.ts b/packages/nextjs/src/server/keyless-node.ts
index 28226dba7a6..21c9dee3987 100644
--- a/packages/nextjs/src/server/keyless-node.ts
+++ b/packages/nextjs/src/server/keyless-node.ts
@@ -25,20 +25,29 @@ const throwMissingFsModule = () => {
throw "Clerk: fsModule.fs is missing. This is an internal error. Please contact Clerk's support.";
};
-/**
- * The `.clerk/` is NOT safe to be commited as it may include sensitive information about a Clerk instance.
- * It may include an instance's secret key and the secret token for claiming that instance.
- */
-function updateGitignore() {
+const safeNodeRuntimeFs = () => {
if (!nodeRuntime.fs) {
throwMissingFsModule();
}
- const { existsSync, writeFileSync, readFileSync, appendFileSync } = nodeRuntime.fs;
+ return nodeRuntime.fs;
+};
+const safeNodeRuntimePath = () => {
if (!nodeRuntime.path) {
throwMissingFsModule();
}
- const gitignorePath = nodeRuntime.path.join(process.cwd(), '.gitignore');
+ return nodeRuntime.path;
+};
+
+/**
+ * The `.clerk/` directory is NOT safe to be committed as it may include sensitive information about a Clerk instance.
+ * It may include an instance's secret key and the secret token for claiming that instance.
+ */
+function updateGitignore() {
+ const { existsSync, writeFileSync, readFileSync, appendFileSync } = safeNodeRuntimeFs();
+
+ const path = safeNodeRuntimePath();
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
if (!existsSync(gitignorePath)) {
writeFileSync(gitignorePath, '');
}
@@ -52,10 +61,8 @@ function updateGitignore() {
}
const generatePath = (...slugs: string[]) => {
- if (!nodeRuntime.path) {
- throwMissingFsModule();
- }
- return nodeRuntime.path.join(process.cwd(), CLERK_HIDDEN, ...slugs);
+ const path = safeNodeRuntimePath();
+ return path.join(process.cwd(), CLERK_HIDDEN, ...slugs);
};
const _TEMP_DIR_NAME = '.tmp';
@@ -64,11 +71,8 @@ const getKeylessReadMePath = () => generatePath(_TEMP_DIR_NAME, 'README.md');
let isCreatingFile = false;
-function safeParseClerkFile(): AccountlessApplication | undefined {
- if (!nodeRuntime.fs) {
- throwMissingFsModule();
- }
- const { readFileSync } = nodeRuntime.fs;
+export function safeParseClerkFile(): AccountlessApplication | undefined {
+ const { readFileSync } = safeNodeRuntimeFs();
try {
const CONFIG_PATH = getKeylessConfigurationPath();
let fileAsString;
@@ -87,20 +91,11 @@ const createMessage = (keys: AccountlessApplication) => {
return `\n\x1b[35m\n[Clerk]:\x1b[0m You are running in keyless mode.\nYou can \x1b[35mclaim your keys\x1b[0m by visiting ${keys.claimUrl}\n`;
};
-async function createOrReadKeyless(): Promise {
- if (!nodeRuntime.fs) {
- // This should never happen.
- throwMissingFsModule();
- }
- const { existsSync, writeFileSync, mkdirSync, rmSync } = nodeRuntime.fs;
-
- /**
- * If another request is already in the process of acquiring keys return early.
- * Using both an in-memory and file system lock seems to be the most effective solution.
- */
- if (isCreatingFile || existsSync(CLERK_LOCK)) {
- return undefined;
- }
+/**
+ * Using both an in-memory and file system lock seems to be the most effective solution.
+ */
+const lockFileWriting = () => {
+ const { writeFileSync } = safeNodeRuntimeFs();
isCreatingFile = true;
@@ -114,6 +109,37 @@ async function createOrReadKeyless(): Promise {
+ const { rmSync } = safeNodeRuntimeFs();
+
+ try {
+ rmSync(CLERK_LOCK, { force: true, recursive: true });
+ } catch (e) {
+ // Simply ignore if the removal of the directory/file fails
+ }
+
+ isCreatingFile = false;
+};
+
+const isFileWritingLocked = () => {
+ const { existsSync } = safeNodeRuntimeFs();
+ return isCreatingFile || existsSync(CLERK_LOCK);
+};
+
+async function createOrReadKeyless(): Promise {
+ const { writeFileSync, mkdirSync } = safeNodeRuntimeFs();
+
+ /**
+ * If another request is already in the process of acquiring keys return early.
+ * Using both an in-memory and file system lock seems to be the most effective solution.
+ */
+ if (isFileWritingLocked()) {
+ return undefined;
+ }
+
+ lockFileWriting();
const CONFIG_PATH = getKeylessConfigurationPath();
const README_PATH = getKeylessReadMePath();
@@ -126,8 +152,7 @@ async function createOrReadKeyless(): Promise Promise;
};
export interface NavigateOptions {