From 1a9ebc72b9cb7db323ca4c7fdac3330b641d5234 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:05:44 -0800 Subject: [PATCH 01/32] wip --- packages/clerk-js/src/core/clerk.ts | 62 +++++++++++++++++++ .../clerk-js/src/core/resources/SignIn.ts | 31 ++++++++++ packages/clerk-js/src/ui/common/redirects.ts | 14 +++++ .../src/ui/components/SignIn/SignIn.tsx | 4 ++ .../components/SignIn/SignInPopupCallback.tsx | 38 ++++++++++++ .../components/SignIn/SignInSocialButtons.tsx | 11 ++-- .../elements/contexts/FlowMetadataContext.tsx | 1 + packages/types/src/clerk.ts | 5 ++ packages/types/src/signIn.ts | 2 + 9 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index dd946d23521..9a2fc116664 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -18,6 +18,7 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, + AuthenticateWithRedirectParams, Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, @@ -1312,10 +1313,26 @@ export class Clerk implements ClerkInterface { navigate: (to: string) => Promise; }, ): Promise => { + console.log('handling redirect callback'); if (!this.loaded || !this.environment || !this.client) { return; } + 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; @@ -1335,6 +1352,15 @@ export class Clerk implements ClerkInterface { firstFactorVerificationSessionId: firstFactorVerification.error?.meta?.sessionId, sessionId: signIn.createdSessionId, }; + console.log(JSON.stringify({ su, si })); + if (window.opener && window.opener.location.origin === window.location.origin) { + window.opener.postMessage( + { destination: window.location.href, metadata: JSON.stringify({ su, si }) }, + window.location.origin, + ); + window.close(); + return; + } const makeNavigate = (to: string) => () => navigate(to); @@ -1446,9 +1472,12 @@ export class Clerk implements ClerkInterface { return navigateToResetPassword(); } + console.log('here 1'); + const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable'; if (userNeedsToBeCreated) { + console.log('user needs to be created'); if (params.transferable === false) { return navigateToSignIn(); } @@ -1483,6 +1512,8 @@ export class Clerk implements ClerkInterface { su.externalAccountErrorCode === 'identifier_already_signed_in' && su.externalAccountSessionId; + console.log('here 2'); + const siUserAlreadySignedIn = si.firstFactorVerificationStatus === 'failed' && si.firstFactorVerificationErrorCode === 'identifier_already_signed_in' && @@ -1500,6 +1531,7 @@ export class Clerk implements ClerkInterface { } if (hasExternalAccountSignUpError(signUp)) { + console.log('has external account sign up error'); return navigateToSignUp(); } @@ -1507,6 +1539,8 @@ export class Clerk implements ClerkInterface { return navigateToNextStepSignUp({ missingFields: signUp.missingFields }); } + console.log('here 3'); + return navigateToSignIn(); }; @@ -1680,6 +1714,34 @@ export class Clerk implements ClerkInterface { } }; + public authenticateWithPopup = async ( + params: AuthenticateWithRedirectParams & { popupCallbackUrl: string; popup: Window | null }, + ): Promise => { + if (!this.client || !this.environment || !params.popup) { + return; + } + + const { redirectUrl, popupCallbackUrl } = params; + window.addEventListener('message', async event => { + if (event.origin !== window.location.origin) return; + if (event.data.session) { + console.log(`calling setActive with session ${event.data.session} adn redirectUrl ${redirectUrl}`); + await this.setActive({ + session: event.data.session, + redirectUrl: params.redirectUrlComplete, + }); + } else if (event.data.destination) { + console.log(`navigating to ${event.data.destination}`); + console.log(event.data.metadata); + this.navigate(event.data.destination); + } + }); + await this.client.signIn.authenticateWithPopup({ + ...params, + redirectUrlComplete: popupCallbackUrl, + }); + }; + public createOrganization = async ({ name, slug }: CreateOrganizationParams): Promise => { return Organization.create({ name, slug }); }; diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 8a1f2c85049..7101d4d4215 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -250,6 +250,37 @@ export class SignIn extends BaseResource implements SignInResource { } }; + public authenticateWithPopup = async ( + params: AuthenticateWithRedirectParams & { popup: Window | null }, + ): Promise => { + const { strategy, redirectUrl, redirectUrlComplete, identifier, popup } = params || {}; + if (!popup) { + clerkMissingOptionError('popup'); + } + + const { firstFactorVerification } = + (strategy === 'saml' || strategy === 'enterprise_sso') && this.id + ? await this.prepareFirstFactor({ + strategy, + redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl), + actionCompleteRedirectUrl: redirectUrlComplete, + }) + : await this.create({ + strategy, + identifier, + redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl), + actionCompleteRedirectUrl: redirectUrlComplete, + }); + + const { status, externalVerificationRedirectURL } = firstFactorVerification; + + if (status === 'unverified' && externalVerificationRedirectURL) { + popup.location.href = externalVerificationRedirectURL.toString(); + } else { + clerkInvalidFAPIResponse(status, SignIn.fapiClient.buildEmailAddress('support')); + } + }; + public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { if (__BUILD_DISABLE_RHC__) { clerkUnsupportedEnvironmentWarning('Web3'); diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 655e5fcefce..d1909dc2e22 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -39,6 +39,20 @@ export function buildSSOCallbackURL( }); } +export function buildPopupCallbackURL( + ctx: Partial, + baseUrl: string | undefined = '', +): string { + const { routing, authQueryString, path } = ctx; + return buildRedirectUrl({ + routing, + baseUrl, + authQueryString, + path, + endpoint: '/popup-callback', + }); +} + type AuthQueryString = string | null | undefined; type BuildRedirectUrlParams = { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index a767b6dbf1e..ba2a620aded 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -24,6 +24,7 @@ import { ResetPasswordSuccess } from './ResetPasswordSuccess'; import { SignInAccountSwitcher } from './SignInAccountSwitcher'; import { SignInFactorOne } from './SignInFactorOne'; import { SignInFactorTwo } from './SignInFactorTwo'; +import { SignInPopupCallback } from './SignInPopupCallback'; import { SignInSSOCallback } from './SignInSSOCallback'; import { SignInStart } from './SignInStart'; @@ -67,6 +68,9 @@ function SignInRoutes(): JSX.Element { resetPasswordUrl={'../reset-password'} /> + + + diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx new file mode 100644 index 00000000000..c8e0cf53833 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx @@ -0,0 +1,38 @@ +import { useClerk } from '@clerk/shared/react'; +import type { HandleOAuthCallbackParams, HandleSamlCallbackParams } from '@clerk/types'; +import React from 'react'; + +import { Flow } from '../../customizables'; +import { Card, LoadingCardContainer, useCardState, withCardStateProvider } from '../../elements'; +import { CaptchaElement } from '../../elements/CaptchaElement'; + +export const SignInPopupCallback = withCardStateProvider(() => { + return ( + + + + ); +}); + +export const SignInPopupCallbackCard = () => { + const clerk = useClerk(); + const card = useCardState(); + + React.useEffect(() => { + window.opener.postMessage({ session: clerk.session!.id }, window.location.origin); + window.close(); + }, []); + + return ( + + + + {card.error} + + + + + + + ); +}; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index dcab4d31679..d3f7e58ca23 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -1,8 +1,8 @@ import { useClerk } from '@clerk/shared/react'; import React from 'react'; -import { buildSSOCallbackURL } from '../../common/redirects'; -import { useCoreSignIn, useSignInContext } from '../../contexts'; +import { buildPopupCallbackURL, buildSSOCallbackURL } from '../../common/redirects'; +import { useSignInContext } from '../../contexts'; import { useEnvironment } from '../../contexts/EnvironmentContext'; import { useCardState } from '../../elements/contexts'; import type { SocialButtonsProps } from '../../elements/SocialButtons'; @@ -16,16 +16,17 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const card = useCardState(); const { displayConfig } = useEnvironment(); const ctx = useSignInContext(); - const signIn = useCoreSignIn(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl); + const popupCallbackUrl = buildPopupCallbackURL(ctx, displayConfig.signInUrl); const redirectUrlComplete = ctx.afterSignInUrl || '/'; return ( { - return signIn - .authenticateWithRedirect({ strategy, redirectUrl, redirectUrlComplete }) + const popup = window.open('about:blank', '', 'width=600,height=600'); + return clerk + .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popupCallbackUrl, popup }) .catch(err => handleError(err, [], card.setError)); }} web3Callback={strategy => { diff --git a/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx b/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx index a6937d6387a..73ef5adeee6 100644 --- a/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx @@ -32,6 +32,7 @@ type FlowMetadata = { | 'passwordPwnedMethods' | 'havingTrouble' | 'ssoCallback' + | 'popupCallback' | 'popover' | 'complete' | 'accountSwitcher'; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 1296685e084..a0bdb83dee9 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -23,6 +23,7 @@ import type { OrganizationCustomRoleKey } from './organizationMembership'; import type { AfterMultiSessionSingleSignOutUrl, AfterSignOutUrl, + AuthenticateWithRedirectParams, LegacyRedirectProps, RedirectOptions, RedirectUrlProp, @@ -587,6 +588,10 @@ export interface Clerk { params: AuthenticateWithGoogleOneTapParams, ) => Promise; + authenticateWithPopup: ( + params: AuthenticateWithRedirectParams & { popupCallbackUrl: string; popup: Window | null }, + ) => Promise; + /** * Creates an organization, adding the current user as admin. */ diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 6579bcbaf4b..e40ea9609a9 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -98,6 +98,8 @@ export interface SignInResource extends ClerkResource { authenticateWithRedirect: (params: AuthenticateWithRedirectParams) => Promise; + authenticateWithPopup: (params: AuthenticateWithRedirectParams & { popup: Window | null }) => Promise; + authenticateWithWeb3: (params: AuthenticateWithWeb3Params) => Promise; authenticateWithMetamask: () => Promise; From 6b35b897825412f0c357705d9ccfe28e1c2452e9 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:50:17 -0800 Subject: [PATCH 02/32] feat(clerk-js): Use Account Portal for popup callback --- packages/clerk-js/src/core/clerk.ts | 16 ++++++-- packages/clerk-js/src/ui/common/redirects.ts | 14 ------- .../src/ui/components/SignIn/SignIn.tsx | 4 -- .../components/SignIn/SignInPopupCallback.tsx | 38 ------------------- .../components/SignIn/SignInSocialButtons.tsx | 5 +-- packages/types/src/clerk.ts | 4 +- 6 files changed, 16 insertions(+), 65 deletions(-) delete mode 100644 packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index c7e368f1822..649ad6d7b4a 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1752,13 +1752,22 @@ export class Clerk implements ClerkInterface { }; public authenticateWithPopup = async ( - params: AuthenticateWithRedirectParams & { popupCallbackUrl: string; popup: Window | null }, + params: AuthenticateWithRedirectParams & { popup: Window | null }, ): Promise => { if (!this.client || !this.environment || !params.popup) { return; } - const { redirectUrl, popupCallbackUrl } = params; + const accountPortalDomain = this.frontendApi + // staging accounts + .replace(/clerk\.accountsstage\./, 'accountsstage.') + .replace(/clerk\.accounts\.|clerk\./, 'accounts.'); + + const { redirectUrl } = params; + + const popupRedirectUrlComplete = `https://${accountPortalDomain}/popup-callback`; + const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?destination=${encodeURIComponent(redirectUrl)}`; + window.addEventListener('message', async event => { if (event.origin !== window.location.origin) return; if (event.data.session) { @@ -1775,7 +1784,8 @@ export class Clerk implements ClerkInterface { }); await this.client.signIn.authenticateWithPopup({ ...params, - redirectUrlComplete: popupCallbackUrl, + redirectUrlComplete: popupRedirectUrlComplete, + redirectUrl: popupRedirectUrl, }); }; diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index d1909dc2e22..655e5fcefce 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -39,20 +39,6 @@ export function buildSSOCallbackURL( }); } -export function buildPopupCallbackURL( - ctx: Partial, - baseUrl: string | undefined = '', -): string { - const { routing, authQueryString, path } = ctx; - return buildRedirectUrl({ - routing, - baseUrl, - authQueryString, - path, - endpoint: '/popup-callback', - }); -} - type AuthQueryString = string | null | undefined; type BuildRedirectUrlParams = { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index ba2a620aded..a767b6dbf1e 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -24,7 +24,6 @@ import { ResetPasswordSuccess } from './ResetPasswordSuccess'; import { SignInAccountSwitcher } from './SignInAccountSwitcher'; import { SignInFactorOne } from './SignInFactorOne'; import { SignInFactorTwo } from './SignInFactorTwo'; -import { SignInPopupCallback } from './SignInPopupCallback'; import { SignInSSOCallback } from './SignInSSOCallback'; import { SignInStart } from './SignInStart'; @@ -68,9 +67,6 @@ function SignInRoutes(): JSX.Element { resetPasswordUrl={'../reset-password'} /> - - - diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx deleted file mode 100644 index c8e0cf53833..00000000000 --- a/packages/clerk-js/src/ui/components/SignIn/SignInPopupCallback.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useClerk } from '@clerk/shared/react'; -import type { HandleOAuthCallbackParams, HandleSamlCallbackParams } from '@clerk/types'; -import React from 'react'; - -import { Flow } from '../../customizables'; -import { Card, LoadingCardContainer, useCardState, withCardStateProvider } from '../../elements'; -import { CaptchaElement } from '../../elements/CaptchaElement'; - -export const SignInPopupCallback = withCardStateProvider(() => { - return ( - - - - ); -}); - -export const SignInPopupCallbackCard = () => { - const clerk = useClerk(); - const card = useCardState(); - - React.useEffect(() => { - window.opener.postMessage({ session: clerk.session!.id }, window.location.origin); - window.close(); - }, []); - - return ( - - - - {card.error} - - - - - - - ); -}; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index d3f7e58ca23..40ac3323105 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -1,7 +1,7 @@ import { useClerk } from '@clerk/shared/react'; import React from 'react'; -import { buildPopupCallbackURL, buildSSOCallbackURL } from '../../common/redirects'; +import { buildSSOCallbackURL } from '../../common/redirects'; import { useSignInContext } from '../../contexts'; import { useEnvironment } from '../../contexts/EnvironmentContext'; import { useCardState } from '../../elements/contexts'; @@ -17,7 +17,6 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const { displayConfig } = useEnvironment(); const ctx = useSignInContext(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl); - const popupCallbackUrl = buildPopupCallbackURL(ctx, displayConfig.signInUrl); const redirectUrlComplete = ctx.afterSignInUrl || '/'; return ( @@ -26,7 +25,7 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { oauthCallback={strategy => { const popup = window.open('about:blank', '', 'width=600,height=600'); return clerk - .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popupCallbackUrl, popup }) + .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) .catch(err => handleError(err, [], card.setError)); }} web3Callback={strategy => { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index a0bdb83dee9..4b86fd3321f 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -588,9 +588,7 @@ export interface Clerk { params: AuthenticateWithGoogleOneTapParams, ) => Promise; - authenticateWithPopup: ( - params: AuthenticateWithRedirectParams & { popupCallbackUrl: string; popup: Window | null }, - ) => Promise; + authenticateWithPopup: (params: AuthenticateWithRedirectParams & { popup: Window | null }) => Promise; /** * Creates an organization, adding the current user as admin. From 819e7ab3a52b75747a78e36851badd8a9ddd451f Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 3 Mar 2025 11:43:16 -0800 Subject: [PATCH 03/32] chore: Add changeset --- .changeset/serious-tools-double.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/serious-tools-double.md diff --git a/.changeset/serious-tools-double.md b/.changeset/serious-tools-double.md new file mode 100644 index 00000000000..c29c45feec6 --- /dev/null +++ b/.changeset/serious-tools-double.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Add new authenticateWithPopup method From 93c0dbb2d9c5f985b14159891dc5538796618758 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:10:39 -0800 Subject: [PATCH 04/32] fix(clerk-js): Only allow accountPortalDomain messages --- packages/clerk-js/src/core/clerk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 649ad6d7b4a..0c28403e151 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1769,7 +1769,7 @@ export class Clerk implements ClerkInterface { const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?destination=${encodeURIComponent(redirectUrl)}`; window.addEventListener('message', async event => { - if (event.origin !== window.location.origin) return; + if (event.origin !== `https://${accountPortalDomain}`) return; if (event.data.session) { console.log(`calling setActive with session ${event.data.session} adn redirectUrl ${redirectUrl}`); await this.setActive({ From 1c3f2cd038bce2da424111c29229c8be580be468 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:31:55 -0800 Subject: [PATCH 05/32] feat(clerk-js): Add oauthFlow prop --- packages/clerk-js/src/core/clerk.ts | 2 +- .../clerk-js/src/core/resources/SignIn.ts | 42 +++++++------------ .../components/SignIn/SignInSocialButtons.tsx | 20 +++++++-- .../src/ui/contexts/components/SignIn.ts | 1 + packages/types/src/clerk.ts | 4 ++ packages/types/src/signIn.ts | 4 +- 6 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 0c28403e151..49cb5e3fe80 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1765,7 +1765,7 @@ export class Clerk implements ClerkInterface { const { redirectUrl } = params; - const popupRedirectUrlComplete = `https://${accountPortalDomain}/popup-callback`; + const popupRedirectUrlComplete = this.buildUrlWithAuth(`https://${accountPortalDomain}/popup-callback`); const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?destination=${encodeURIComponent(redirectUrl)}`; window.addEventListener('message', async event => { diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 7101d4d4215..266f9b0010b 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, @@ -224,7 +225,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,41 +248,25 @@ 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 authenticateWithPopup = async ( - params: AuthenticateWithRedirectParams & { popup: Window | null }, - ): Promise => { - const { strategy, redirectUrl, redirectUrlComplete, identifier, popup } = params || {}; + public authenticateWithRedirect = async (params: AuthenticateWithRedirectParams): Promise => { + return this.authenticateWithRedirectOrPopup(params, windowNavigate); + }; + + public authenticateWithPopup = async (params: AuthenticateWithPopupParams): Promise => { + const { popup } = params || {}; if (!popup) { clerkMissingOptionError('popup'); } - const { firstFactorVerification } = - (strategy === 'saml' || strategy === 'enterprise_sso') && this.id - ? await this.prepareFirstFactor({ - strategy, - redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl), - actionCompleteRedirectUrl: redirectUrlComplete, - }) - : await this.create({ - strategy, - identifier, - redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl), - actionCompleteRedirectUrl: redirectUrlComplete, - }); - - const { status, externalVerificationRedirectURL } = firstFactorVerification; - - if (status === 'unverified' && externalVerificationRedirectURL) { - popup.location.href = externalVerificationRedirectURL.toString(); - } else { - clerkInvalidFAPIResponse(status, SignIn.fapiClient.buildEmailAddress('support')); - } + return this.authenticateWithRedirectOrPopup(params, url => { + popup.location.href = url instanceof URL ? url.toString() : url; + }); }; public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 40ac3323105..97accc65d05 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import React from 'react'; import { buildSSOCallbackURL } from '../../common/redirects'; -import { useSignInContext } from '../../contexts'; +import { useCoreSignIn, useSignInContext } from '../../contexts'; import { useEnvironment } from '../../contexts/EnvironmentContext'; import { useCardState } from '../../elements/contexts'; import type { SocialButtonsProps } from '../../elements/SocialButtons'; @@ -10,12 +10,18 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError, web3CallbackErrorHandler } from '../../utils'; +const POPUP_PREFERRED_ORIGINS = ['.lovable.app']; +function originPrefersPopup(): boolean { + return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); +} + export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const clerk = useClerk(); const { navigate } = useRouter(); const card = useCardState(); const { displayConfig } = useEnvironment(); const ctx = useSignInContext(); + const signIn = useCoreSignIn(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl); const redirectUrlComplete = ctx.afterSignInUrl || '/'; @@ -23,9 +29,15 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { { - const popup = window.open('about:blank', '', 'width=600,height=600'); - return clerk - .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) + if (ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup())) { + const popup = window.open('about:blank', '', 'width=600,height=800'); + return clerk + .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) + .catch(err => handleError(err, [], card.setError)); + } + + return signIn + .authenticateWithRedirect({ strategy, redirectUrl, redirectUrlComplete }) .catch(err => handleError(err, [], card.setError)); }} web3Callback={strategy => { diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 9bfdad969b2..1efda219966 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -114,6 +114,7 @@ export const useSignInContext = (): SignInContextType => { return { ...(ctx as SignInCtx), transferable: ctx.transferable ?? true, + oauthFlow: ctx.oauthFlow ?? 'auto', componentName, signUpUrl, signInUrl, diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 4b86fd3321f..4689c01f8bf 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -959,6 +959,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 & diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index e40ea9609a9..049edf56340 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -98,7 +98,7 @@ export interface SignInResource extends ClerkResource { authenticateWithRedirect: (params: AuthenticateWithRedirectParams) => Promise; - authenticateWithPopup: (params: AuthenticateWithRedirectParams & { popup: Window | null }) => Promise; + authenticateWithPopup: (params: AuthenticateWithPopupParams) => Promise; authenticateWithWeb3: (params: AuthenticateWithWeb3Params) => Promise; @@ -226,6 +226,8 @@ export type ResetPasswordParams = { signOutOfOtherSessions?: boolean; }; +export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null }; + export type AuthenticateWithPasskeyParams = { flow?: 'autofill' | 'discoverable'; }; From bc25b7f04c4e9026a0723c529964ae77b2a64a43 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:26:21 -0800 Subject: [PATCH 06/32] feat(clerk-js): Implement popup authentication flow for sign up --- packages/clerk-js/src/core/clerk.ts | 42 ++++++++++++-- .../clerk-js/src/core/resources/SignUp.ts | 57 ++++++++++++++----- .../components/SignIn/SignInSocialButtons.tsx | 2 +- .../components/SignUp/SignUpSocialButtons.tsx | 20 +++++++ .../src/ui/contexts/components/SignUp.ts | 1 + packages/types/src/clerk.ts | 9 ++- packages/types/src/redirects.ts | 2 + packages/types/src/signIn.ts | 4 +- packages/types/src/signUp.ts | 6 +- 9 files changed, 117 insertions(+), 26 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 49cb5e3fe80..e1532bff39f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -18,7 +18,7 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, - AuthenticateWithRedirectParams, + AuthenticateWithPopupParams, Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, @@ -1751,8 +1751,11 @@ export class Clerk implements ClerkInterface { } }; - public authenticateWithPopup = async ( - params: AuthenticateWithRedirectParams & { popup: Window | null }, + private authenticateWithPopup = async ( + authenticationType: 'signIn' | 'signUp', + params: AuthenticateWithPopupParams & { + unsafeMetadata?: SignUpUnsafeMetadata; + }, ): Promise => { if (!this.client || !this.environment || !params.popup) { return; @@ -1768,27 +1771,54 @@ export class Clerk implements ClerkInterface { const popupRedirectUrlComplete = this.buildUrlWithAuth(`https://${accountPortalDomain}/popup-callback`); const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?destination=${encodeURIComponent(redirectUrl)}`; - window.addEventListener('message', async event => { + const messageHandler = async (event: MessageEvent) => { if (event.origin !== `https://${accountPortalDomain}`) return; + + let shouldRemoveListener = false; + if (event.data.session) { console.log(`calling setActive with session ${event.data.session} adn redirectUrl ${redirectUrl}`); await this.setActive({ session: event.data.session, redirectUrl: params.redirectUrlComplete, }); + shouldRemoveListener = true; } else if (event.data.destination) { console.log(`navigating to ${event.data.destination}`); console.log(event.data.metadata); this.navigate(event.data.destination); + shouldRemoveListener = true; } - }); - await this.client.signIn.authenticateWithPopup({ + + if (shouldRemoveListener) { + window.removeEventListener('message', messageHandler); + } + }; + + window.addEventListener('message', messageHandler); + + const authenticateMethod = + authenticationType === 'signIn' + ? this.client.signIn.authenticateWithPopup + : this.client.signUp.authenticateWithPopup; + + await authenticateMethod({ ...params, redirectUrlComplete: popupRedirectUrlComplete, redirectUrl: popupRedirectUrl, }); }; + public signUpWithPopup = async ( + params: AuthenticateWithPopupParams & { unsafeMetadata?: SignUpUnsafeMetadata }, + ): Promise => { + return this.authenticateWithPopup('signUp', params); + }; + + public signInWithPopup = async (params: AuthenticateWithPopupParams): Promise => { + return this.authenticateWithPopup('signIn', params); + }; + public createOrganization = async ({ name, slug }: CreateOrganizationParams): Promise => { return Organization.create({ name, slug }); }; diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index f6ee3777dd7..da2c4cb9186 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, @@ -289,19 +290,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, @@ -309,7 +315,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 => { @@ -327,12 +333,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 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/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 97accc65d05..ad70d180768 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -32,7 +32,7 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { if (ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup())) { const popup = window.open('about:blank', '', 'width=600,height=800'); return clerk - .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) + .signInWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) .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..4eaff7e604e 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -9,6 +9,11 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError, web3CallbackErrorHandler } from '../../utils'; +const POPUP_PREFERRED_ORIGINS = ['.lovable.app', 'localhost:4000']; +function originPrefersPopup(): boolean { + return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); +} + export type SignUpSocialButtonsProps = SocialButtonsProps & { continueSignUp?: boolean; legalAccepted?: boolean }; export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) => { @@ -25,6 +30,21 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) { + if (ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup())) { + const popup = window.open('about:blank', '', 'width=600,height=800'); + return clerk + .signUpWithPopup({ + 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/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 32b4e4794c0..74701c6d603 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -109,6 +109,7 @@ export const useSignUpContext = (): SignUpContextType => { return { ...ctx, + oauthFlow: ctx.oauthFlow || 'auto', componentName, signInUrl, signUpUrl, diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 4689c01f8bf..86241e1c6e4 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -588,7 +588,10 @@ export interface Clerk { params: AuthenticateWithGoogleOneTapParams, ) => Promise; - authenticateWithPopup: (params: AuthenticateWithRedirectParams & { popup: Window | null }) => Promise; + signInWithPopup: (params: AuthenticateWithRedirectParams & { popup: Window | null }) => Promise; + signUpWithPopup: ( + params: AuthenticateWithRedirectParams & { popup: Window | null; unsafeMetadata?: SignUpUnsafeMetadata }, + ) => Promise; /** * Creates an organization, adding the current user as admin. @@ -1075,6 +1078,10 @@ export type SignUpProps = RoutingOptions & { * Used to fill the "Join waitlist" link in the SignUp component. */ waitlistUrl?: string; + /** + * + */ + oauthFlow?: 'auto' | 'redirect' | 'popup'; } & SignInFallbackRedirectUrl & SignInForceRedirectUrl & LegacyRedirectProps & diff --git a/packages/types/src/redirects.ts b/packages/types/src/redirects.ts index 346127eb241..e45b2472ae8 100644 --- a/packages/types/src/redirects.ts +++ b/packages/types/src/redirects.ts @@ -86,6 +86,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 049edf56340..f0cdbdd23fe 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 { @@ -226,8 +226,6 @@ export type ResetPasswordParams = { signOutOfOtherSessions?: boolean; }; -export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null }; - export type AuthenticateWithPasskeyParams = { flow?: 'autofill' | 'discoverable'; }; diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index b55b73a9f94..02354667cd4 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 { @@ -93,6 +93,10 @@ export interface SignUpResource extends ClerkResource { params: AuthenticateWithRedirectParams & { unsafeMetadata?: SignUpUnsafeMetadata }, ) => Promise; + authenticateWithPopup: ( + params: AuthenticateWithPopupParams & { unsafeMetadata?: SignUpUnsafeMetadata }, + ) => Promise; + authenticateWithWeb3: ( params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata; From b6ae69a5ad64140fc910bb00849160e20acb3402 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:25:09 -0800 Subject: [PATCH 07/32] feat(clerk-js): add webcontainer-api.io to popup origns --- .../clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx | 2 +- .../clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index ad70d180768..5f608b059cb 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -10,7 +10,7 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError, web3CallbackErrorHandler } from '../../utils'; -const POPUP_PREFERRED_ORIGINS = ['.lovable.app']; +const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.webcontainer-api.io']; function originPrefersPopup(): boolean { return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); } diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 4eaff7e604e..990bbad7104 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -9,7 +9,7 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError, web3CallbackErrorHandler } from '../../utils'; -const POPUP_PREFERRED_ORIGINS = ['.lovable.app', 'localhost:4000']; +const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.webcontainer-api.io']; function originPrefersPopup(): boolean { return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); } From aecbfa99b9f6478fc1156ea70932026fc5fea2fc Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:58:27 -0800 Subject: [PATCH 08/32] fix(clerk-js): Support modal sign in/sign up with return_url parameter --- packages/clerk-js/src/core/clerk.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index e1532bff39f..9232da170d5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1392,7 +1392,7 @@ export class Clerk implements ClerkInterface { console.log(JSON.stringify({ su, si })); if (window.opener && window.opener.location.origin === window.location.origin) { window.opener.postMessage( - { destination: window.location.href, metadata: JSON.stringify({ su, si }) }, + { return_url: window.location.href, metadata: JSON.stringify({ su, si }) }, window.location.origin, ); window.close(); @@ -1768,8 +1768,12 @@ export class Clerk implements ClerkInterface { const { redirectUrl } = params; + const redirectUrlWithForceRedirectUrl = new URL(redirectUrl); + redirectUrlWithForceRedirectUrl.searchParams.set('sign_in_force_redirect_url', params.redirectUrlComplete); + redirectUrlWithForceRedirectUrl.searchParams.set('sign_up_force_redirect_url', params.redirectUrlComplete); + const popupRedirectUrlComplete = this.buildUrlWithAuth(`https://${accountPortalDomain}/popup-callback`); - const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?destination=${encodeURIComponent(redirectUrl)}`; + const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl.toString())}`; const messageHandler = async (event: MessageEvent) => { if (event.origin !== `https://${accountPortalDomain}`) return; @@ -1783,10 +1787,10 @@ export class Clerk implements ClerkInterface { redirectUrl: params.redirectUrlComplete, }); shouldRemoveListener = true; - } else if (event.data.destination) { - console.log(`navigating to ${event.data.destination}`); + } else if (event.data.return_url) { + console.log(`navigating to ${event.data.return_url}`); console.log(event.data.metadata); - this.navigate(event.data.destination); + this.navigate(event.data.return_url); shouldRemoveListener = true; } From 5ca29b7978c0da876b079e508830372caddc7050 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 7 Mar 2025 11:10:59 -0800 Subject: [PATCH 09/32] feat: Add oauthFlow prop to SignInButton --- packages/react/src/components/SignInButton.tsx | 2 ++ packages/types/src/clerk.ts | 1 + 2 files changed, 3 insertions(+) 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/types/src/clerk.ts b/packages/types/src/clerk.ts index 9bf4bc37d25..a9267f71fa6 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -1432,6 +1432,7 @@ export type SignInButtonProps = ButtonProps & | 'signUpFallbackRedirectUrl' | 'initialValues' | 'withSignUp' + | 'oauthFlow' >; export type SignUpButtonProps = { From a8a07ca9b7284d705543edddf39650ac73f64974 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 7 Mar 2025 11:11:26 -0800 Subject: [PATCH 10/32] test(e2e): Add e2e test for oauth popup flow --- .../next-app-router/src/app/buttons/page.tsx | 9 ++++++ integration/tests/oauth-flows.test.ts | 28 +++++++++++++++++++ packages/clerk-js/src/core/clerk.ts | 8 ++++++ 3 files changed, 45 insertions(+) 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/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1c4a9b4a068..27e9bfab704 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1780,6 +1780,14 @@ export class Clerk implements ClerkInterface { if (event.data.session) { console.log(`calling setActive with session ${event.data.session} adn redirectUrl ${redirectUrl}`); + const existingSession = this.client?.sessions.find(x => x.id === event.data.session) || null; + if (!existingSession) { + try { + await this.client?.reload(); + } catch (e) { + console.error(e); + } + } await this.setActive({ session: event.data.session, redirectUrl: params.redirectUrlComplete, From f80fb863dfd3768ec527b05e43c9d26380b62d1e Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 12 Mar 2025 14:38:12 -0700 Subject: [PATCH 11/32] add new lovable preview domain to pop-up list --- .../clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx | 2 +- .../clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 5f608b059cb..a7355d2ec4d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -10,7 +10,7 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError, web3CallbackErrorHandler } from '../../utils'; -const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.webcontainer-api.io']; +const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io']; function originPrefersPopup(): boolean { return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); } diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 990bbad7104..40ee9c89e5e 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -9,7 +9,7 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError, web3CallbackErrorHandler } from '../../utils'; -const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.webcontainer-api.io']; +const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io']; function originPrefersPopup(): boolean { return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); } From f5aaaae0e511f48e5dbbe5bc76777c29c0deb4e5 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:55:53 -0700 Subject: [PATCH 12/32] fix(clerk-js): Use DB JWT on both URLs --- packages/clerk-js/src/core/clerk.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 27e9bfab704..c3be33e0685 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1766,12 +1766,15 @@ export class Clerk implements ClerkInterface { const { redirectUrl } = params; - const redirectUrlWithForceRedirectUrl = new URL(redirectUrl); - redirectUrlWithForceRedirectUrl.searchParams.set('sign_in_force_redirect_url', params.redirectUrlComplete); - redirectUrlWithForceRedirectUrl.searchParams.set('sign_up_force_redirect_url', params.redirectUrlComplete); + 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); + const redirectUrlWithForceRedirectUrl = this.buildUrlWithAuth(r.toString()); const popupRedirectUrlComplete = this.buildUrlWithAuth(`https://${accountPortalDomain}/popup-callback`); - const popupRedirectUrl = `https://${accountPortalDomain}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl.toString())}`; + const popupRedirectUrl = this.buildUrlWithAuth( + `https://${accountPortalDomain}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl)}`, + ); const messageHandler = async (event: MessageEvent) => { if (event.origin !== `https://${accountPortalDomain}`) return; From f8017d19650d8ffccaba50b33b3353f5d861ac53 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:03:17 -0700 Subject: [PATCH 13/32] fix(clerk-js): Remove popup handling now that AP handles it --- packages/clerk-js/src/core/clerk.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index c3be33e0685..d79a73e621c 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1387,15 +1387,6 @@ export class Clerk implements ClerkInterface { firstFactorVerificationSessionId: firstFactorVerification.error?.meta?.sessionId, sessionId: signIn.createdSessionId, }; - console.log(JSON.stringify({ su, si })); - if (window.opener && window.opener.location.origin === window.location.origin) { - window.opener.postMessage( - { return_url: window.location.href, metadata: JSON.stringify({ su, si }) }, - window.location.origin, - ); - window.close(); - return; - } const makeNavigate = (to: string) => () => navigate(to); From d105c04a6c1c4a159b73d9190cf32c6f8a39778c Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:10:41 -0700 Subject: [PATCH 14/32] fix(clerk-js): Update max size limits --- packages/clerk-js/bundlewatch.config.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 2c56eb95669..62bcdaf4a0b 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": "577.5kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "78.5kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "51KB" }, + { "path": "./dist/clerk.js", "maxSize": "578.5kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "79kB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "51.5KB" }, { "path": "./dist/ui-common*.js", "maxSize": "94KB" }, { "path": "./dist/vendors*.js", "maxSize": "30KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, @@ -11,7 +11,7 @@ { "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/signin*.js", "maxSize": "12.5KB" }, { "path": "./dist/signup*.js", "maxSize": "6.55KB" }, { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, { "path": "./dist/userprofile*.js", "maxSize": "15KB" }, From 1dfd8ecd07c219b81169bef92426c636b0f72579 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:22:39 -0700 Subject: [PATCH 15/32] fix(clerk-js): Update max size limits --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 62bcdaf4a0b..95766150e19 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -12,7 +12,7 @@ { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" }, { "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" }, { "path": "./dist/signin*.js", "maxSize": "12.5KB" }, - { "path": "./dist/signup*.js", "maxSize": "6.55KB" }, + { "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" }, From adfd1ad1fabdd20eedeec5e21b411963a36afb9e Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:20:14 -0700 Subject: [PATCH 16/32] fix(clerk-js): Update IsomorphicClerk type --- .../__snapshots__/Client.test.ts.snap | 4 ++++ packages/react/src/isomorphicClerk.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) 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/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index c2b85093152..215a1189aa9 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -10,6 +10,7 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, + AuthenticateWithPopupParams, Clerk, ClerkAuthenticateWithWeb3Params, ClerkOptions, @@ -1124,6 +1125,24 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + signUpWithPopup = async (params: AuthenticateWithPopupParams & { unsafeMetadata?: SignUpUnsafeMetadata }) => { + const callback = () => this.clerkjs?.signUpWithPopup(params); + if (this.clerkjs && this.#loaded) { + return callback() as Promise; + } else { + this.premountMethodCalls.set('signUpWithPopup', callback); + } + }; + + signInWithPopup = async (params: AuthenticateWithPopupParams) => { + const callback = () => this.clerkjs?.signInWithPopup(params); + if (this.clerkjs && this.#loaded) { + return callback() as Promise; + } else { + this.premountMethodCalls.set('signInWithPopup', callback); + } + }; + authenticateWithGoogleOneTap = async (params: AuthenticateWithGoogleOneTapParams) => { const clerkjs = await this.#waitForClerkJS(); return clerkjs.authenticateWithGoogleOneTap(params); From 73035da87e7e56241696dd6fed39b5cb0fcd861d Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:17:47 -0700 Subject: [PATCH 17/32] fix(clerk-js): Mock global fetch --- packages/clerk-js/src/core/__tests__/clerk.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index ea573685bd0..66732fec067 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -843,6 +843,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(); From f41aa5dd6adb8fa11f093b619876b7a428e2b721 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 12:00:09 -0700 Subject: [PATCH 18/32] cleanup(clerk-js): rm console.log --- packages/clerk-js/src/core/clerk.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 8948ef753db..d750dd3b67c 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1460,7 +1460,6 @@ export class Clerk implements ClerkInterface { navigate: (to: string) => Promise; }, ): Promise => { - console.log('handling redirect callback'); if (!this.loaded || !this.environment || !this.client) { return; } @@ -1610,12 +1609,9 @@ export class Clerk implements ClerkInterface { return navigateToResetPassword(); } - console.log('here 1'); - const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable'; if (userNeedsToBeCreated) { - console.log('user needs to be created'); if (params.transferable === false) { return navigateToSignIn(); } @@ -1650,8 +1646,6 @@ export class Clerk implements ClerkInterface { su.externalAccountErrorCode === 'identifier_already_signed_in' && su.externalAccountSessionId; - console.log('here 2'); - const siUserAlreadySignedIn = si.firstFactorVerificationStatus === 'failed' && si.firstFactorVerificationErrorCode === 'identifier_already_signed_in' && @@ -1669,7 +1663,6 @@ export class Clerk implements ClerkInterface { } if (hasExternalAccountSignUpError(signUp)) { - console.log('has external account sign up error'); return navigateToSignUp(); } @@ -1677,8 +1670,6 @@ export class Clerk implements ClerkInterface { return navigateToNextStepSignUp({ missingFields: signUp.missingFields }); } - console.log('here 3'); - return navigateToSignIn(); }; @@ -1885,7 +1876,6 @@ export class Clerk implements ClerkInterface { let shouldRemoveListener = false; if (event.data.session) { - console.log(`calling setActive with session ${event.data.session} adn redirectUrl ${redirectUrl}`); const existingSession = this.client?.sessions.find(x => x.id === event.data.session) || null; if (!existingSession) { try { @@ -1900,8 +1890,6 @@ export class Clerk implements ClerkInterface { }); shouldRemoveListener = true; } else if (event.data.return_url) { - console.log(`navigating to ${event.data.return_url}`); - console.log(event.data.metadata); this.navigate(event.data.return_url); shouldRemoveListener = true; } From 7f6ed875f4b3b884631d484082c7765ca1ed4bc6 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 12:03:14 -0700 Subject: [PATCH 19/32] fix(clerk-js): originPrefersPopup util function --- .../src/ui/components/SignIn/SignInSocialButtons.tsx | 7 +------ .../src/ui/components/SignUp/SignUpSocialButtons.tsx | 7 +------ packages/clerk-js/src/ui/utils/index.ts | 1 + packages/clerk-js/src/ui/utils/originPrefersPopup.ts | 4 ++++ 4 files changed, 7 insertions(+), 12 deletions(-) create mode 100644 packages/clerk-js/src/ui/utils/originPrefersPopup.ts diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index a7355d2ec4d..3f6ccb2dfdf 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -8,12 +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'; - -const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io']; -function originPrefersPopup(): boolean { - return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); -} +import { handleError, web3CallbackErrorHandler, originPrefersPopup } from '../../utils'; export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const clerk = useClerk(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 40ee9c89e5e..374a20c2e4e 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -7,12 +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'; - -const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io']; -function originPrefersPopup(): boolean { - return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); -} +import { handleError, web3CallbackErrorHandler, originPrefersPopup } from '../../utils'; export type SignUpSocialButtonsProps = SocialButtonsProps & { continueSignUp?: boolean; legalAccepted?: boolean }; diff --git a/packages/clerk-js/src/ui/utils/index.ts b/packages/clerk-js/src/ui/utils/index.ts index aeb8b01f264..71b8574ecdc 100644 --- a/packages/clerk-js/src/ui/utils/index.ts +++ b/packages/clerk-js/src/ui/utils/index.ts @@ -25,3 +25,4 @@ export * from './colorOptionToHslaScale'; export * from './createCustomMenuItems'; export * from './usernameUtils'; export * from './web3CallbackErrorHandler'; +export * from './originPrefersPopup'; 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..2a50e47e853 --- /dev/null +++ b/packages/clerk-js/src/ui/utils/originPrefersPopup.ts @@ -0,0 +1,4 @@ +const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io']; +export function originPrefersPopup(): boolean { + return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); +} From a3a3dc821a2f0218a6b81036863e74b43abb9972 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 12:13:59 -0700 Subject: [PATCH 20/32] fix(types,react): Add oauthFlow prop to SignUpButton --- packages/react/src/components/SignUpButton.tsx | 2 ++ packages/types/src/clerk.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) 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 60bd4de44e4..132087f99e6 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -1154,7 +1154,7 @@ export type SignUpProps = RoutingOptions & { */ waitlistUrl?: string; /** - * + * Control whether OAuth flows use redirects or popups. */ oauthFlow?: 'auto' | 'redirect' | 'popup'; } & SignInFallbackRedirectUrl & @@ -1535,6 +1535,7 @@ export type SignUpButtonProps = { | 'signInForceRedirectUrl' | 'signInFallbackRedirectUrl' | 'initialValues' + | 'oauthFlow' >; export type CreateOrganizationInvitationParams = { From c87acd11079211424537bf1b9f8a45cf33361322 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:11:13 -0700 Subject: [PATCH 21/32] feat(clerk-js): Improve loading state for popup auth --- .../ui/components/SignIn/SignInSocialButtons.tsx | 10 +++++++++- .../ui/components/SignUp/SignUpSocialButtons.tsx | 10 +++++++++- .../clerk-js/src/ui/elements/SocialButtons.tsx | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 3f6ccb2dfdf..624b3b8fbc5 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -19,13 +19,21 @@ 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 (ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup())) { + if (shouldUsePopup) { const popup = window.open('about:blank', '', 'width=600,height=800'); + const interval = setInterval(() => { + if (popup?.closed) { + clearInterval(interval); + card.setIdle(); + } + }, 500); return clerk .signInWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) .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 374a20c2e4e..2f3d1a8cd54 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -19,14 +19,22 @@ 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 (ctx.oauthFlow === 'popup' || (ctx.oauthFlow === 'auto' && originPrefersPopup())) { + if (shouldUsePopup) { const popup = window.open('about:blank', '', 'width=600,height=800'); + const interval = setInterval(() => { + if (popup?.closed) { + clearInterval(interval); + card.setIdle(); + } + }, 500); return clerk .signUpWithPopup({ strategy, 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; From a60b18df69185fad0d524feb01f376283c8bce0f Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:30:50 -0700 Subject: [PATCH 22/32] chore(repo): Update changeset --- .changeset/serious-tools-double.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/serious-tools-double.md b/.changeset/serious-tools-double.md index c29c45feec6..9abed2f0717 100644 --- a/.changeset/serious-tools-double.md +++ b/.changeset/serious-tools-double.md @@ -3,4 +3,6 @@ '@clerk/types': minor --- -Add new authenticateWithPopup method +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`. From a53c7b933a68e25bf4a392e0e45d67446cf0537b Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:43:11 -0700 Subject: [PATCH 23/32] chore(clerk-js): Add comments --- packages/clerk-js/src/core/clerk.ts | 7 +++++++ .../src/ui/components/SignIn/SignInSocialButtons.tsx | 4 ++++ .../src/ui/components/SignUp/SignUpSocialButtons.tsx | 4 ++++ packages/clerk-js/src/ui/utils/originPrefersPopup.ts | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d750dd3b67c..5ced296bcf4 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1464,6 +1464,10 @@ 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(); @@ -1860,9 +1864,12 @@ export class Clerk implements ClerkInterface { 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 = this.buildUrlWithAuth(r.toString()); const popupRedirectUrlComplete = this.buildUrlWithAuth(`https://${accountPortalDomain}/popup-callback`); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 624b3b8fbc5..a7a0ba5fbd9 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -27,13 +27,17 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { idleAfterDelay={!shouldUsePopup} oauthCallback={strategy => { 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?.closed) { clearInterval(interval); card.setIdle(); } }, 500); + return clerk .signInWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) .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 2f3d1a8cd54..71d67b7bba3 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -28,13 +28,17 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) idleAfterDelay={!shouldUsePopup} oauthCallback={(strategy: OAuthStrategy) => { 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?.closed) { clearInterval(interval); card.setIdle(); } }, 500); + return clerk .signUpWithPopup({ strategy, diff --git a/packages/clerk-js/src/ui/utils/originPrefersPopup.ts b/packages/clerk-js/src/ui/utils/originPrefersPopup.ts index 2a50e47e853..6fca8a25b3c 100644 --- a/packages/clerk-js/src/ui/utils/originPrefersPopup.ts +++ b/packages/clerk-js/src/ui/utils/originPrefersPopup.ts @@ -1,4 +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)); } From b9e50092084fa85adca6f83540cf0f62ff173640 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:49:28 -0700 Subject: [PATCH 24/32] chore(clerk-js): Sort imports --- .../clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx | 2 +- .../clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index a7a0ba5fbd9..801e75c9d29 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, originPrefersPopup } from '../../utils'; +import { handleError, originPrefersPopup, web3CallbackErrorHandler } from '../../utils'; export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const clerk = useClerk(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 71d67b7bba3..cbb541115e6 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, originPrefersPopup } from '../../utils'; +import { handleError, originPrefersPopup, web3CallbackErrorHandler } from '../../utils'; export type SignUpSocialButtonsProps = SocialButtonsProps & { continueSignUp?: boolean; legalAccepted?: boolean }; From 1d988beb97b8d9d2b5c30ed1d6781c9248ac291c Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:39:50 -0700 Subject: [PATCH 25/32] fix(clerk-js): Always call toString() --- packages/clerk-js/src/core/resources/SignIn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 266f9b0010b..4f41946df22 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -265,7 +265,7 @@ export class SignIn extends BaseResource implements SignInResource { } return this.authenticateWithRedirectOrPopup(params, url => { - popup.location.href = url instanceof URL ? url.toString() : url; + popup.location.href = url.toString(); }); }; From e9c24bf6ee1746baadc1384bbafc069e71057a04 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:41:16 -0700 Subject: [PATCH 26/32] fix(clerk-js): Restore idle state if popup is blocked --- .../clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx | 2 +- .../clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 801e75c9d29..04dab3c0b25 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -32,7 +32,7 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { 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?.closed) { + if (!popup || popup.closed) { clearInterval(interval); card.setIdle(); } diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index cbb541115e6..a13e753a332 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -33,7 +33,7 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) 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?.closed) { + if (!popup || popup.closed) { clearInterval(interval); card.setIdle(); } From b2cfec23bbbac6f9312d564dabb454f9ab32daaa Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:50:59 -0700 Subject: [PATCH 27/32] fix(clerk-js): Use shared buildAccountsBaseUrl utility --- packages/clerk-js/src/core/clerk.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 4b5cf1eb10b..19a15b5ddd8 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,4 +1,5 @@ import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; +import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, EmailLinkErrorCodeStatus, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -1883,10 +1884,7 @@ export class Clerk implements ClerkInterface { return; } - const accountPortalDomain = this.frontendApi - // staging accounts - .replace(/clerk\.accountsstage\./, 'accountsstage.') - .replace(/clerk\.accounts\.|clerk\./, 'accounts.'); + const accountPortalHost = buildAccountsBaseUrl(this.frontendApi); const { redirectUrl } = params; @@ -1898,13 +1896,13 @@ export class Clerk implements ClerkInterface { // All URLs are decorated with the dev browser token in development mode since we're moving between AP and the app. const redirectUrlWithForceRedirectUrl = this.buildUrlWithAuth(r.toString()); - const popupRedirectUrlComplete = this.buildUrlWithAuth(`https://${accountPortalDomain}/popup-callback`); + const popupRedirectUrlComplete = this.buildUrlWithAuth(`${accountPortalHost}/popup-callback`); const popupRedirectUrl = this.buildUrlWithAuth( - `https://${accountPortalDomain}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl)}`, + `${accountPortalHost}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl)}`, ); const messageHandler = async (event: MessageEvent) => { - if (event.origin !== `https://${accountPortalDomain}`) return; + if (event.origin !== accountPortalHost) return; let shouldRemoveListener = false; From 3b269a93cbfa0614098998e61fbe507e4e8df1a9 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:55:39 -0700 Subject: [PATCH 28/32] fix(clerk-js): Use logical OR instead of nullish coalescing --- packages/clerk-js/src/ui/contexts/components/SignIn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index deaf7dd857a..3d5e77f50d7 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -129,7 +129,7 @@ export const useSignInContext = (): SignInContextType => { return { ...(ctx as SignInCtx), transferable: ctx.transferable ?? true, - oauthFlow: ctx.oauthFlow ?? 'auto', + oauthFlow: ctx.oauthFlow || 'auto', componentName, signUpUrl, signInUrl, From 2f80ccb72e448466684df18c9cde70dbf2a2726b Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:17:30 -0700 Subject: [PATCH 29/32] feat(clerk-js): Move authenticateWithPopup methods to signIn/signUp resources --- packages/clerk-js/src/core/clerk.ts | 82 ------------------- .../clerk-js/src/core/resources/SignIn.ts | 4 +- .../clerk-js/src/core/resources/SignUp.ts | 3 +- .../components/SignIn/SignInSocialButtons.tsx | 4 +- .../components/SignUp/SignUpSocialButtons.tsx | 4 +- .../src/utils/authenticateWithPopup.ts | 77 +++++++++++++++++ packages/types/src/clerk.ts | 6 -- 7 files changed, 85 insertions(+), 95 deletions(-) create mode 100644 packages/clerk-js/src/utils/authenticateWithPopup.ts diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 19a15b5ddd8..10aada0228d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,5 +1,4 @@ import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; -import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, EmailLinkErrorCodeStatus, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -22,7 +21,6 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, - AuthenticateWithPopupParams, Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, @@ -1874,86 +1872,6 @@ export class Clerk implements ClerkInterface { } }; - private authenticateWithPopup = async ( - authenticationType: 'signIn' | 'signUp', - params: AuthenticateWithPopupParams & { - unsafeMetadata?: SignUpUnsafeMetadata; - }, - ): Promise => { - if (!this.client || !this.environment || !params.popup) { - return; - } - - const accountPortalHost = buildAccountsBaseUrl(this.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 = this.buildUrlWithAuth(r.toString()); - - const popupRedirectUrlComplete = this.buildUrlWithAuth(`${accountPortalHost}/popup-callback`); - const popupRedirectUrl = this.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 = this.client?.sessions.find(x => x.id === event.data.session) || null; - if (!existingSession) { - try { - await this.client?.reload(); - } catch (e) { - console.error(e); - } - } - await this.setActive({ - session: event.data.session, - redirectUrl: params.redirectUrlComplete, - }); - shouldRemoveListener = true; - } else if (event.data.return_url) { - this.navigate(event.data.return_url); - shouldRemoveListener = true; - } - - if (shouldRemoveListener) { - window.removeEventListener('message', messageHandler); - } - }; - - window.addEventListener('message', messageHandler); - - const authenticateMethod = - authenticationType === 'signIn' - ? this.client.signIn.authenticateWithPopup - : this.client.signUp.authenticateWithPopup; - - await authenticateMethod({ - ...params, - redirectUrlComplete: popupRedirectUrlComplete, - redirectUrl: popupRedirectUrl, - }); - }; - - public signUpWithPopup = async ( - params: AuthenticateWithPopupParams & { unsafeMetadata?: SignUpUnsafeMetadata }, - ): Promise => { - return this.authenticateWithPopup('signUp', params); - }; - - public signInWithPopup = async (params: AuthenticateWithPopupParams): Promise => { - return this.authenticateWithPopup('signIn', params); - }; - public createOrganization = async ({ name, slug }: CreateOrganizationParams): Promise => { return Organization.create({ name, slug }); }; diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 4f41946df22..10a01dfbd83 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -49,6 +49,7 @@ import { getOKXWalletIdentifier, windowNavigate, } from '../../utils'; +import { _authenticateWithPopup } from '../../utils/authenticateWithPopup'; import { convertJSONToPublicKeyRequestOptions, serializePublicKeyCredentialAssertion, @@ -263,8 +264,7 @@ export class SignIn extends BaseResource implements SignInResource { if (!popup) { clerkMissingOptionError('popup'); } - - return this.authenticateWithRedirectOrPopup(params, url => { + return _authenticateWithPopup(SignIn.clerk, this.authenticateWithRedirectOrPopup, params, url => { popup.location.href = url.toString(); }); }; diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index da2c4cb9186..1138e50f775 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -35,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'; @@ -357,7 +358,7 @@ export class SignUp extends BaseResource implements SignUpResource { clerkMissingOptionError('popup'); } - return this.authenticateWithRedirectOrPopup(params, url => { + return _authenticateWithPopup(SignUp.clerk, this.authenticateWithRedirectOrPopup, params, url => { popup.location.href = url instanceof URL ? url.toString() : url; }); }; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 04dab3c0b25..5a3754a9db1 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -38,8 +38,8 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { } }, 500); - return clerk - .signInWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) + return signIn + .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, popup }) .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 a13e753a332..2eaa170f5eb 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -39,8 +39,8 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) } }, 500); - return clerk - .signUpWithPopup({ + return signUp + .authenticateWithPopup({ strategy, redirectUrl, redirectUrlComplete, 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/types/src/clerk.ts b/packages/types/src/clerk.ts index 40ef80646da..114ba14721b 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -26,7 +26,6 @@ import type { OrganizationCustomRoleKey } from './organizationMembership'; import type { AfterMultiSessionSingleSignOutUrl, AfterSignOutUrl, - AuthenticateWithRedirectParams, LegacyRedirectProps, RedirectOptions, RedirectUrlProp, @@ -622,11 +621,6 @@ export interface Clerk { params: AuthenticateWithGoogleOneTapParams, ) => Promise; - signInWithPopup: (params: AuthenticateWithRedirectParams & { popup: Window | null }) => Promise; - signUpWithPopup: ( - params: AuthenticateWithRedirectParams & { popup: Window | null; unsafeMetadata?: SignUpUnsafeMetadata }, - ) => Promise; - /** * Creates an organization, adding the current user as admin. */ From d2fddec27852477db351158b4db3fad73b716bb1 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:26:46 -0700 Subject: [PATCH 30/32] cleanup(react): Remove isomorphic withPopup methods --- packages/react/src/isomorphicClerk.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 110c7d85959..ac2357c5fee 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -1134,24 +1134,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - signUpWithPopup = async (params: AuthenticateWithPopupParams & { unsafeMetadata?: SignUpUnsafeMetadata }) => { - const callback = () => this.clerkjs?.signUpWithPopup(params); - if (this.clerkjs && this.#loaded) { - return callback() as Promise; - } else { - this.premountMethodCalls.set('signUpWithPopup', callback); - } - }; - - signInWithPopup = async (params: AuthenticateWithPopupParams) => { - const callback = () => this.clerkjs?.signInWithPopup(params); - if (this.clerkjs && this.#loaded) { - return callback() as Promise; - } else { - this.premountMethodCalls.set('signInWithPopup', callback); - } - }; - authenticateWithGoogleOneTap = async (params: AuthenticateWithGoogleOneTapParams) => { const clerkjs = await this.#waitForClerkJS(); return clerkjs.authenticateWithGoogleOneTap(params); From 642664d94f365a8f96b0e06bd9052690203e75f1 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:31:21 -0700 Subject: [PATCH 31/32] fix(clerk-js): Increase bundlewatch thresholds --- packages/clerk-js/bundlewatch.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 6eef57f3176..659c6c18e7e 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "580kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "79kB" }, + { "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" }, From 5852ecb736967b5824c5a3dff0c8481be24ffce4 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:46:22 -0700 Subject: [PATCH 32/32] cleanup(react): Remove unused type --- packages/react/src/isomorphicClerk.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index ac2357c5fee..d08d84a124a 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -10,7 +10,6 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, - AuthenticateWithPopupParams, Clerk, ClerkAuthenticateWithWeb3Params, ClerkOptions,