diff --git a/.changeset/serious-tools-double.md b/.changeset/serious-tools-double.md
new file mode 100644
index 00000000000..9abed2f0717
--- /dev/null
+++ b/.changeset/serious-tools-double.md
@@ -0,0 +1,8 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/types': minor
+---
+
+Add support for the `oauthFlow` prop on `` and ``, allowing developers to opt-in to using a popup for OAuth authorization instead of redirects.
+
+ With the new `oauthFlow` prop, developers can opt-in to using a popup window instead of redirects for their OAuth flows by setting `oauthFlow` to `"popup"`. While we still recommend the default `"redirect"` for most scenarios, the `"popup"` option is useful in environments where the redirect flow does not currently work, such as when your application is embedded into an `iframe`. We also opt applications into the `"popup"` flow when we detect that your application is running on a domain that's typically embedded into an `iframe`, such as `loveable.app`.
diff --git a/integration/templates/next-app-router/src/app/buttons/page.tsx b/integration/templates/next-app-router/src/app/buttons/page.tsx
index fa87107d7df..b565e35e2ad 100644
--- a/integration/templates/next-app-router/src/app/buttons/page.tsx
+++ b/integration/templates/next-app-router/src/app/buttons/page.tsx
@@ -11,6 +11,15 @@ export default function Home() {
Sign in button (force)
+
+ Sign in button (force, popup)
+
+
{
+ test('SignIn with oauthFlow=popup opens popup', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.page.goToRelative('/buttons');
+ await u.page.waitForClerkJsLoaded();
+ await u.po.expect.toBeSignedOut();
+
+ await u.page.getByText('Sign in button (force, popup)').click();
+
+ await u.po.signIn.waitForModal();
+
+ const popupPromise = context.waitForEvent('page');
+ await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
+ const popup = await popupPromise;
+ const popupUtils = createTestUtils({ app, page: popup, context });
+ await popupUtils.page.getByText('Sign in to oauth-provider').waitFor();
+
+ await popupUtils.po.signIn.setIdentifier(fakeUser.email);
+ await popupUtils.po.signIn.continue();
+ await popupUtils.po.signIn.enterTestOtpCode();
+
+ await u.page.waitForAppUrl('/protected');
+
+ await u.po.expect.toBeSignedIn();
+ });
+ });
});
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 629d66d37e4..659c6c18e7e 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,8 +1,8 @@
{
"files": [
- { "path": "./dist/clerk.js", "maxSize": "580kB" },
- { "path": "./dist/clerk.browser.js", "maxSize": "78.6kB" },
- { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
+ { "path": "./dist/clerk.js", "maxSize": "580.5kB" },
+ { "path": "./dist/clerk.browser.js", "maxSize": "79.25kB" },
+ { "path": "./dist/clerk.headless.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "94KB" },
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
@@ -11,8 +11,8 @@
{ "path": "./dist/organizationprofile*.js", "maxSize": "12KB" },
{ "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" },
{ "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" },
- { "path": "./dist/signin*.js", "maxSize": "12.4KB" },
- { "path": "./dist/signup*.js", "maxSize": "6.6KB" },
+ { "path": "./dist/signin*.js", "maxSize": "12.5KB" },
+ { "path": "./dist/signup*.js", "maxSize": "6.75KB" },
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "15KB" },
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts
index f0e373d4bed..50c34e563d3 100644
--- a/packages/clerk-js/src/core/__tests__/clerk.test.ts
+++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts
@@ -853,6 +853,17 @@ describe('Clerk singleton', () => {
});
describe('.handleRedirectCallback()', () => {
+ // handleRedirectCallback calls signIn/signUp.reload, which relies on the global fetch instance. We don't actually
+ // need a return value though, so we just mock a resolved promise.
+ const originalFetch = global.fetch;
+ beforeAll(() => {
+ global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({}) });
+ });
+
+ afterAll(() => {
+ global.fetch = originalFetch;
+ });
+
beforeEach(() => {
mockClientFetch.mockReset();
mockEnvironmentFetch.mockReset();
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 56897c72d3b..65cd4c2e923 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1489,6 +1489,25 @@ export class Clerk implements ClerkInterface {
return;
}
+ // If `handleRedirectCallback` is called on a window without an opener property (such as when the OAuth flow popup
+ // directs the opening page to navigate to the /sso-callback route), we need to reload the signIn and signUp resources
+ // to ensure that we have the latest state. This operation can fail when we try reloading a resource that doesn't
+ // exist (such as when reloading a signIn resource during a signUp attempt), but this can be safely ignored.
+ if (!window.opener) {
+ try {
+ await signIn.reload();
+ } catch (err) {
+ console.log('This can be safely ignored:');
+ console.error(err);
+ }
+ try {
+ await signUp.reload();
+ } catch (err) {
+ console.log('This can be safely ignored:');
+ console.error(err);
+ }
+ }
+
const { displayConfig } = this.environment;
const { firstFactorVerification } = signIn;
const { externalAccount } = signUp.verifications;
diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts
index 8a1f2c85049..10a01dfbd83 100644
--- a/packages/clerk-js/src/core/resources/SignIn.ts
+++ b/packages/clerk-js/src/core/resources/SignIn.ts
@@ -9,6 +9,7 @@ import type {
AttemptFirstFactorParams,
AttemptSecondFactorParams,
AuthenticateWithPasskeyParams,
+ AuthenticateWithPopupParams,
AuthenticateWithRedirectParams,
AuthenticateWithWeb3Params,
CreateEmailLinkFlowReturn,
@@ -48,6 +49,7 @@ import {
getOKXWalletIdentifier,
windowNavigate,
} from '../../utils';
+import { _authenticateWithPopup } from '../../utils/authenticateWithPopup';
import {
convertJSONToPublicKeyRequestOptions,
serializePublicKeyCredentialAssertion,
@@ -224,7 +226,10 @@ export class SignIn extends BaseResource implements SignInResource {
});
};
- public authenticateWithRedirect = async (params: AuthenticateWithRedirectParams): Promise => {
+ private authenticateWithRedirectOrPopup = async (
+ params: AuthenticateWithRedirectParams,
+ navigateCallback: (url: URL | string) => void,
+ ): Promise => {
const { strategy, redirectUrl, redirectUrlComplete, identifier } = params || {};
const { firstFactorVerification } =
@@ -244,12 +249,26 @@ export class SignIn extends BaseResource implements SignInResource {
const { status, externalVerificationRedirectURL } = firstFactorVerification;
if (status === 'unverified' && externalVerificationRedirectURL) {
- windowNavigate(externalVerificationRedirectURL);
+ navigateCallback(externalVerificationRedirectURL);
} else {
clerkInvalidFAPIResponse(status, SignIn.fapiClient.buildEmailAddress('support'));
}
};
+ public authenticateWithRedirect = async (params: AuthenticateWithRedirectParams): Promise => {
+ return this.authenticateWithRedirectOrPopup(params, windowNavigate);
+ };
+
+ public authenticateWithPopup = async (params: AuthenticateWithPopupParams): Promise => {
+ const { popup } = params || {};
+ if (!popup) {
+ clerkMissingOptionError('popup');
+ }
+ return _authenticateWithPopup(SignIn.clerk, this.authenticateWithRedirectOrPopup, params, url => {
+ popup.location.href = url.toString();
+ });
+ };
+
public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => {
if (__BUILD_DISABLE_RHC__) {
clerkUnsupportedEnvironmentWarning('Web3');
diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts
index 340dec1409b..8c499e85bc6 100644
--- a/packages/clerk-js/src/core/resources/SignUp.ts
+++ b/packages/clerk-js/src/core/resources/SignUp.ts
@@ -5,6 +5,7 @@ import type {
AttemptPhoneNumberVerificationParams,
AttemptVerificationParams,
AttemptWeb3WalletVerificationParams,
+ AuthenticateWithPopupParams,
AuthenticateWithRedirectParams,
AuthenticateWithWeb3Params,
CreateEmailLinkFlowReturn,
@@ -34,6 +35,7 @@ import {
getOKXWalletIdentifier,
windowNavigate,
} from '../../utils';
+import { _authenticateWithPopup } from '../../utils/authenticateWithPopup';
import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
import { createValidatePassword } from '../../utils/passwords/password';
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
@@ -290,19 +292,24 @@ export class SignUp extends BaseResource implements SignUpResource {
});
};
- public authenticateWithRedirect = async ({
- redirectUrl,
- redirectUrlComplete,
- strategy,
- continueSignUp = false,
- unsafeMetadata,
- emailAddress,
- legalAccepted,
- }: AuthenticateWithRedirectParams & {
- unsafeMetadata?: SignUpUnsafeMetadata;
- }): Promise => {
+ private authenticateWithRedirectOrPopup = async (
+ params: AuthenticateWithRedirectParams & {
+ unsafeMetadata?: SignUpUnsafeMetadata;
+ },
+ navigateCallback: (url: URL | string) => void,
+ ): Promise => {
+ const {
+ redirectUrl,
+ redirectUrlComplete,
+ strategy,
+ continueSignUp = false,
+ unsafeMetadata,
+ emailAddress,
+ legalAccepted,
+ } = params;
+
const authenticateFn = () => {
- const params = {
+ const authParams = {
strategy,
redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl),
actionCompleteRedirectUrl: redirectUrlComplete,
@@ -310,7 +317,7 @@ export class SignUp extends BaseResource implements SignUpResource {
emailAddress,
legalAccepted,
};
- return continueSignUp && this.id ? this.update(params) : this.create(params);
+ return continueSignUp && this.id ? this.update(authParams) : this.create(authParams);
};
const { verifications } = await authenticateFn().catch(async e => {
@@ -329,12 +336,35 @@ export class SignUp extends BaseResource implements SignUpResource {
const { status, externalVerificationRedirectURL } = externalAccount;
if (status === 'unverified' && !!externalVerificationRedirectURL) {
- windowNavigate(externalVerificationRedirectURL);
+ navigateCallback(externalVerificationRedirectURL);
} else {
clerkInvalidFAPIResponse(status, SignUp.fapiClient.buildEmailAddress('support'));
}
};
+ public authenticateWithRedirect = async (
+ params: AuthenticateWithRedirectParams & {
+ unsafeMetadata?: SignUpUnsafeMetadata;
+ },
+ ): Promise => {
+ return this.authenticateWithRedirectOrPopup(params, windowNavigate);
+ };
+
+ public authenticateWithPopup = async (
+ params: AuthenticateWithPopupParams & {
+ unsafeMetadata?: SignUpUnsafeMetadata;
+ },
+ ): Promise => {
+ const { popup } = params || {};
+ if (!popup) {
+ clerkMissingOptionError('popup');
+ }
+
+ return _authenticateWithPopup(SignUp.clerk, this.authenticateWithRedirectOrPopup, params, url => {
+ popup.location.href = url instanceof URL ? url.toString() : url;
+ });
+ };
+
update = (params: SignUpUpdateParams): Promise => {
return this._basePatch({
body: normalizeUnsafeMetadata(params),
diff --git a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Client.test.ts.snap b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Client.test.ts.snap
index e8fbec1216d..e7578beaa23 100644
--- a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Client.test.ts.snap
+++ b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Client.test.ts.snap
@@ -220,7 +220,9 @@ Client {
"authenticateWithMetamask": [Function],
"authenticateWithOKXWallet": [Function],
"authenticateWithPasskey": [Function],
+ "authenticateWithPopup": [Function],
"authenticateWithRedirect": [Function],
+ "authenticateWithRedirectOrPopup": [Function],
"authenticateWithWeb3": [Function],
"create": [Function],
"createEmailLinkFlow": [Function],
@@ -300,7 +302,9 @@ Client {
"authenticateWithCoinbaseWallet": [Function],
"authenticateWithMetamask": [Function],
"authenticateWithOKXWallet": [Function],
+ "authenticateWithPopup": [Function],
"authenticateWithRedirect": [Function],
+ "authenticateWithRedirectOrPopup": [Function],
"authenticateWithWeb3": [Function],
"create": [Function],
"createEmailLinkFlow": [Function],
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx
index dcab4d31679..5a3754a9db1 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx
@@ -8,7 +8,7 @@ import { useCardState } from '../../elements/contexts';
import type { SocialButtonsProps } from '../../elements/SocialButtons';
import { SocialButtons } from '../../elements/SocialButtons';
import { useRouter } from '../../router';
-import { handleError, web3CallbackErrorHandler } from '../../utils';
+import { handleError, originPrefersPopup, web3CallbackErrorHandler } from '../../utils';
export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => {
const clerk = useClerk();
@@ -19,11 +19,30 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => {
const signIn = useCoreSignIn();
const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl);
const redirectUrlComplete = ctx.afterSignInUrl || '/';
+ const shouldUsePopup = ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup());
return (
{
+ if (shouldUsePopup) {
+ // We create the popup window here with the `about:blank` URL since some browsers will block popups that are
+ // opened within async functions. The `signInWithPopup` method handles setting the URL of the popup.
+ const popup = window.open('about:blank', '', 'width=600,height=800');
+ // Unfortunately, there's no good way to detect when the popup is closed, so we simply poll and check if it's closed.
+ const interval = setInterval(() => {
+ if (!popup || popup.closed) {
+ clearInterval(interval);
+ card.setIdle();
+ }
+ }, 500);
+
+ return signIn
+ .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup })
+ .catch(err => handleError(err, [], card.setError));
+ }
+
return signIn
.authenticateWithRedirect({ strategy, redirectUrl, redirectUrlComplete })
.catch(err => handleError(err, [], card.setError));
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx
index 211c6dbaed4..2eaa170f5eb 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx
@@ -7,7 +7,7 @@ import { useCardState } from '../../elements';
import type { SocialButtonsProps } from '../../elements/SocialButtons';
import { SocialButtons } from '../../elements/SocialButtons';
import { useRouter } from '../../router';
-import { handleError, web3CallbackErrorHandler } from '../../utils';
+import { handleError, originPrefersPopup, web3CallbackErrorHandler } from '../../utils';
export type SignUpSocialButtonsProps = SocialButtonsProps & { continueSignUp?: boolean; legalAccepted?: boolean };
@@ -19,12 +19,39 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps)
const signUp = useCoreSignUp();
const redirectUrl = ctx.ssoCallbackUrl;
const redirectUrlComplete = ctx.afterSignUpUrl || '/';
+ const shouldUsePopup = ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup());
const { continueSignUp = false, ...rest } = props;
return (
{
+ if (shouldUsePopup) {
+ // We create the popup window here with the `about:blank` URL since some browsers will block popups that are
+ // opened within async functions. The `signUpWithPopup` method handles setting the URL of the popup.
+ const popup = window.open('about:blank', '', 'width=600,height=800');
+ // Unfortunately, there's no good way to detect when the popup is closed, so we simply poll and check if it's closed.
+ const interval = setInterval(() => {
+ if (!popup || popup.closed) {
+ clearInterval(interval);
+ card.setIdle();
+ }
+ }, 500);
+
+ return signUp
+ .authenticateWithPopup({
+ strategy,
+ redirectUrl,
+ redirectUrlComplete,
+ popup,
+ continueSignUp,
+ unsafeMetadata: ctx.unsafeMetadata,
+ legalAccepted: props.legalAccepted,
+ })
+ .catch(err => handleError(err, [], card.setError));
+ }
+
return signUp
.authenticateWithRedirect({
continueSignUp,
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index d709a0d14b4..3d5e77f50d7 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -129,6 +129,7 @@ export const useSignInContext = (): SignInContextType => {
return {
...(ctx as SignInCtx),
transferable: ctx.transferable ?? true,
+ oauthFlow: ctx.oauthFlow || 'auto',
componentName,
signUpUrl,
signInUrl,
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 59ce5f4ab09..7d1d58f6f88 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -123,6 +123,7 @@ export const useSignUpContext = (): SignUpContextType => {
return {
...ctx,
+ oauthFlow: ctx.oauthFlow || 'auto',
componentName,
signInUrl,
signUpUrl,
diff --git a/packages/clerk-js/src/ui/elements/SocialButtons.tsx b/packages/clerk-js/src/ui/elements/SocialButtons.tsx
index 8e55cdc2a89..5d33d4d77f7 100644
--- a/packages/clerk-js/src/ui/elements/SocialButtons.tsx
+++ b/packages/clerk-js/src/ui/elements/SocialButtons.tsx
@@ -35,6 +35,7 @@ export type SocialButtonsProps = React.PropsWithChildren<{
type SocialButtonsRootProps = SocialButtonsProps & {
oauthCallback: (strategy: OAuthStrategy) => Promise;
web3Callback: (strategy: Web3Strategy) => Promise;
+ idleAfterDelay?: boolean;
};
const isWeb3Strategy = (val: string): val is Web3Strategy => {
@@ -42,7 +43,13 @@ const isWeb3Strategy = (val: string): val is Web3Strategy => {
};
export const SocialButtons = React.memo((props: SocialButtonsRootProps) => {
- const { oauthCallback, web3Callback, enableOAuthProviders = true, enableWeb3Providers = true } = props;
+ const {
+ oauthCallback,
+ web3Callback,
+ enableOAuthProviders = true,
+ enableWeb3Providers = true,
+ idleAfterDelay = true,
+ } = props;
const { web3Strategies, authenticatableOauthStrategies, strategyToDisplayData } = useEnabledThirdPartyProviders();
const card = useCardState();
const { socialButtonsVariant } = useAppearance().parsedLayout;
@@ -78,8 +85,10 @@ export const SocialButtons = React.memo((props: SocialButtonsRootProps) => {
await sleep(1000);
card.setIdle();
}
- await sleep(5000);
- card.setIdle();
+ if (idleAfterDelay) {
+ await sleep(5000);
+ card.setIdle();
+ }
};
const ButtonElement = preferBlockButtons ? SocialButtonBlock : SocialButtonIcon;
diff --git a/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx b/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx
index 92523042b4f..58a12ce2f18 100644
--- a/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx
+++ b/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx
@@ -33,6 +33,7 @@ type FlowMetadata = {
| 'passwordPwnedMethods'
| 'havingTrouble'
| 'ssoCallback'
+ | 'popupCallback'
| 'popover'
| 'complete'
| 'accountSwitcher';
diff --git a/packages/clerk-js/src/ui/utils/index.ts b/packages/clerk-js/src/ui/utils/index.ts
index c946647108e..c846f8399c1 100644
--- a/packages/clerk-js/src/ui/utils/index.ts
+++ b/packages/clerk-js/src/ui/utils/index.ts
@@ -25,4 +25,5 @@ export * from './colorOptionToHslaScale';
export * from './createCustomMenuItems';
export * from './usernameUtils';
export * from './web3CallbackErrorHandler';
+export * from './originPrefersPopup';
export * from './normalizeColorString';
diff --git a/packages/clerk-js/src/ui/utils/originPrefersPopup.ts b/packages/clerk-js/src/ui/utils/originPrefersPopup.ts
new file mode 100644
index 00000000000..6fca8a25b3c
--- /dev/null
+++ b/packages/clerk-js/src/ui/utils/originPrefersPopup.ts
@@ -0,0 +1,10 @@
+const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io'];
+
+/**
+ * Returns `true` if the current origin is one that is typically embedded via an iframe, which would benefit from the
+ * popup flow.
+ * @returns {boolean} Whether the current origin prefers the popup flow.
+ */
+export function originPrefersPopup(): boolean {
+ return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin));
+}
diff --git a/packages/clerk-js/src/utils/authenticateWithPopup.ts b/packages/clerk-js/src/utils/authenticateWithPopup.ts
new file mode 100644
index 00000000000..0dfd0419eed
--- /dev/null
+++ b/packages/clerk-js/src/utils/authenticateWithPopup.ts
@@ -0,0 +1,77 @@
+import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl';
+import type { AuthenticateWithPopupParams, AuthenticateWithRedirectParams } from '@clerk/types';
+
+import type { Clerk } from '../core/clerk';
+
+export async function _authenticateWithPopup(
+ client: Clerk,
+ authenticateMethod: (
+ params: AuthenticateWithRedirectParams,
+ navigateCallback: (url: URL | string) => void,
+ ) => Promise,
+ params: AuthenticateWithPopupParams & {
+ unsafeMetadata?: SignUpUnsafeMetadata;
+ },
+ navigateCallback: (url: URL | string) => void,
+): Promise {
+ if (!client.client || !params.popup) {
+ return;
+ }
+
+ const accountPortalHost = buildAccountsBaseUrl(client.frontendApi);
+
+ const { redirectUrl } = params;
+
+ // We set the force_redirect_url query parameter to ensure that the user is redirected to the correct page even
+ // in situations like a modal transfer flow.
+ const r = new URL(redirectUrl);
+ r.searchParams.set('sign_in_force_redirect_url', params.redirectUrlComplete);
+ r.searchParams.set('sign_up_force_redirect_url', params.redirectUrlComplete);
+ // All URLs are decorated with the dev browser token in development mode since we're moving between AP and the app.
+ const redirectUrlWithForceRedirectUrl = client.buildUrlWithAuth(r.toString());
+
+ const popupRedirectUrlComplete = client.buildUrlWithAuth(`${accountPortalHost}/popup-callback`);
+ const popupRedirectUrl = client.buildUrlWithAuth(
+ `${accountPortalHost}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl)}`,
+ );
+
+ const messageHandler = async (event: MessageEvent) => {
+ if (event.origin !== accountPortalHost) return;
+
+ let shouldRemoveListener = false;
+
+ if (event.data.session) {
+ const existingSession = client.client?.sessions.find(x => x.id === event.data.session) || null;
+ if (!existingSession) {
+ try {
+ await client.client?.reload();
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ await client.setActive({
+ session: event.data.session,
+ redirectUrl: params.redirectUrlComplete,
+ });
+ shouldRemoveListener = true;
+ } else if (event.data.return_url) {
+ client.navigate(event.data.return_url);
+ shouldRemoveListener = true;
+ }
+
+ if (shouldRemoveListener) {
+ window.removeEventListener('message', messageHandler);
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+
+ await authenticateMethod(
+ {
+ ...params,
+ redirectUrlComplete: popupRedirectUrlComplete,
+ redirectUrl: popupRedirectUrl,
+ },
+ navigateCallback,
+ );
+}
diff --git a/packages/react/src/components/SignInButton.tsx b/packages/react/src/components/SignInButton.tsx
index d985bc2bcca..a584baa5521 100644
--- a/packages/react/src/components/SignInButton.tsx
+++ b/packages/react/src/components/SignInButton.tsx
@@ -15,6 +15,7 @@ export const SignInButton = withClerk(
mode,
initialValues,
withSignUp,
+ oauthFlow,
...rest
} = props;
children = normalizeWithDefaultValue(children, 'Sign in');
@@ -28,6 +29,7 @@ export const SignInButton = withClerk(
signUpForceRedirectUrl,
initialValues,
withSignUp,
+ oauthFlow,
};
if (mode === 'modal') {
diff --git a/packages/react/src/components/SignUpButton.tsx b/packages/react/src/components/SignUpButton.tsx
index b4b6ac27363..ccf5ce4f3c9 100644
--- a/packages/react/src/components/SignUpButton.tsx
+++ b/packages/react/src/components/SignUpButton.tsx
@@ -15,6 +15,7 @@ export const SignUpButton = withClerk(
mode,
unsafeMetadata,
initialValues,
+ oauthFlow,
...rest
} = props;
@@ -29,6 +30,7 @@ export const SignUpButton = withClerk(
signInForceRedirectUrl,
unsafeMetadata,
initialValues,
+ oauthFlow,
};
if (mode === 'modal') {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 5f7c6e24ce1..ff19882dd30 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -1031,6 +1031,10 @@ export type SignInProps = RoutingOptions & {
* Enable sign-in-or-up flow for `` component instance.
*/
withSignUp?: boolean;
+ /**
+ * Control whether OAuth flows use redirects or popups.
+ */
+ oauthFlow?: 'auto' | 'redirect' | 'popup';
} & TransferableOption &
SignUpForceRedirectUrl &
SignUpFallbackRedirectUrl &
@@ -1164,6 +1168,10 @@ export type SignUpProps = RoutingOptions & {
* Used to fill the "Join waitlist" link in the SignUp component.
*/
waitlistUrl?: string;
+ /**
+ * Control whether OAuth flows use redirects or popups.
+ */
+ oauthFlow?: 'auto' | 'redirect' | 'popup';
} & SignInFallbackRedirectUrl &
SignInForceRedirectUrl &
LegacyRedirectProps &
@@ -1541,6 +1549,7 @@ export type SignInButtonProps = ButtonProps &
| 'signUpFallbackRedirectUrl'
| 'initialValues'
| 'withSignUp'
+ | 'oauthFlow'
>;
export type SignUpButtonProps = {
@@ -1553,6 +1562,7 @@ export type SignUpButtonProps = {
| 'signInForceRedirectUrl'
| 'signInFallbackRedirectUrl'
| 'initialValues'
+ | 'oauthFlow'
>;
export type CreateOrganizationInvitationParams = {
diff --git a/packages/types/src/redirects.ts b/packages/types/src/redirects.ts
index b67d6742a9d..efeb4e9b432 100644
--- a/packages/types/src/redirects.ts
+++ b/packages/types/src/redirects.ts
@@ -83,6 +83,8 @@ export type AuthenticateWithRedirectParams = {
legalAccepted?: boolean;
};
+export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null };
+
export type RedirectUrlProp = {
/**
* Full URL or path to navigate after a successful action.
diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts
index ac6d662278c..7db407f303e 100644
--- a/packages/types/src/signIn.ts
+++ b/packages/types/src/signIn.ts
@@ -47,7 +47,7 @@ import type {
VerificationJSON,
} from './json';
import type { ValidatePasswordCallbacks } from './passwords';
-import type { AuthenticateWithRedirectParams } from './redirects';
+import type { AuthenticateWithPopupParams, AuthenticateWithRedirectParams } from './redirects';
import type { ClerkResource } from './resource';
import type { SignInJSONSnapshot } from './snapshots';
import type {
@@ -104,6 +104,8 @@ export interface SignInResource extends ClerkResource {
authenticateWithRedirect: (params: AuthenticateWithRedirectParams) => Promise;
+ authenticateWithPopup: (params: AuthenticateWithPopupParams) => Promise;
+
authenticateWithWeb3: (params: AuthenticateWithWeb3Params) => Promise;
authenticateWithMetamask: () => Promise;
diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts
index de7d3158718..77ce720c6b8 100644
--- a/packages/types/src/signUp.ts
+++ b/packages/types/src/signUp.ts
@@ -9,7 +9,7 @@ import type {
} from './identifiers';
import type { ValidatePasswordCallbacks } from './passwords';
import type { AttemptPhoneNumberVerificationParams, PreparePhoneNumberVerificationParams } from './phoneNumber';
-import type { AuthenticateWithRedirectParams } from './redirects';
+import type { AuthenticateWithPopupParams, AuthenticateWithRedirectParams } from './redirects';
import type { ClerkResource } from './resource';
import type { SignUpJSONSnapshot, SignUpVerificationJSONSnapshot, SignUpVerificationsJSONSnapshot } from './snapshots';
import type {
@@ -99,6 +99,10 @@ export interface SignUpResource extends ClerkResource {
params: AuthenticateWithRedirectParams & { unsafeMetadata?: SignUpUnsafeMetadata },
) => Promise;
+ authenticateWithPopup: (
+ params: AuthenticateWithPopupParams & { unsafeMetadata?: SignUpUnsafeMetadata },
+ ) => Promise;
+
authenticateWithWeb3: (
params: AuthenticateWithWeb3Params & {
unsafeMetadata?: SignUpUnsafeMetadata;