From 0df15643d09b451489f711b1071b727fb82d6da1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 18 Apr 2024 11:21:45 +0300 Subject: [PATCH 01/16] feat(clerk-js): WIP PSU --- packages/clerk-js/src/core/clerk.ts | 18 +++++++++++++++--- packages/clerk-js/src/core/resources/SignIn.ts | 3 +++ packages/clerk-js/src/ui/common/redirects.ts | 11 +++++++++++ .../components/GoogleOneTap/one-tap-start.tsx | 14 ++++++++++++++ .../components/SignIn/SignInSocialButtons.tsx | 1 + .../components/SignUp/SignUpSocialButtons.tsx | 1 + 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index b4734e9e9a0..cd41f6e3d5a 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1089,8 +1089,14 @@ export class Clerk implements ClerkInterface { const userExistsButNeedsToSignIn = su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists'; + console.log('userExistsButNeedsToSignIn', userExistsButNeedsToSignIn); + if (userExistsButNeedsToSignIn) { - const res = await signIn.create({ transfer: true }); + let res = signIn; + if (!res.status) { + res = await signIn.create({ transfer: true }); + } + switch (res.status) { case 'complete': return this.setActive({ @@ -1131,10 +1137,16 @@ export class Clerk implements ClerkInterface { return navigateToResetPassword(); } - const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable'; + const userNeedsToBeCreated = + si.firstFactorVerificationStatus === 'transferable' || su.status === 'missing_requirements'; + console.log('userNeedsToBeCreated', userNeedsToBeCreated); if (userNeedsToBeCreated) { - const res = await signUp.create({ transfer: true }); + // const res = await signUp.create({ transfer: true }); + let res = signUp; + if (!res.status) { + res = await signUp.create({ transfer: true }); + } switch (res.status) { case 'complete': return this.setActive({ diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 8ebed20aeaa..d0be87cdffd 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -234,7 +234,10 @@ export class SignIn extends BaseResource implements SignInResource { strategy: 'google_one_tap', googleOneTapToken: params.token, }).catch(err => { + console.log('Error', err); + console.log('Error', isClerkAPIResponseError(err), err.errors[0].code); if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') { + console.log('is external_account_not_found'); return SignIn.clerk.client?.signUp.create({ // TODO-ONETAP: Add new types when feature is ready for public beta // @ts-expect-error diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 212dfeca9dc..9b5e941d1dc 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -2,6 +2,7 @@ import { buildURL } from '../../utils/url'; import type { SignInContextType, SignUpContextType, UserProfileContextType } from './../contexts'; const SSO_CALLBACK_PATH_ROUTE = '/sso-callback'; +const SIGN_UP_CONTINUE_PATH_ROUTE = '/sign-up/continue'; const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify'; export function buildEmailLinkRedirectUrl( @@ -32,6 +33,16 @@ export function buildSSOCallbackURL( }); } +export function buildSignUpContinueURL(baseUrl: string | undefined = ''): string { + return buildRedirectUrl({ + routing: 'hash', + baseUrl, + authQueryString: '', + path: '', + endpoint: SIGN_UP_CONTINUE_PATH_ROUTE, + }); +} + type AuthQueryString = string | null | undefined; type BuildRedirectUrlParams = { diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index 591e2e65bc1..2818f076513 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -8,6 +8,7 @@ import { useCoreSignIn, useEnvironment, useGoogleOneTapContext } from '../../con import { withCardStateProvider } from '../../elements'; import { useFetch } from '../../hooks'; import { useSupportEmail } from '../../hooks/useSupportEmail'; +import { buildSignUpContinueURL } from '../../common'; function _OneTapStart(): JSX.Element | null { const clerk = useClerk(); @@ -17,6 +18,7 @@ function _OneTapStart(): JSX.Element | null { const supportEmail = useSupportEmail(); const ctx = useGoogleOneTapContext(); + const { displayConfig } = useEnvironment(); async function oneTapCallback(response: GISCredentialResponse) { try { @@ -24,12 +26,24 @@ function _OneTapStart(): JSX.Element | null { token: response.credential, }); + await clerk.handleRedirectCallback(); + switch (res.status) { case 'complete': await clerk.setActive({ session: res.createdSessionId, }); break; + case 'missing_requirements': { + console.log('missing_requirements'); + // const url = new URL(buildSignUpContinueURL(displayConfig.signUpUrl)).pathname; + // const finalUrl = clerk.buildUrlWithAuth(new URL(url, displayConfig.signUpUrl).href); + const url = new URL('/sign-up#/continue', displayConfig.signUpUrl); + console.log('URL', url, displayConfig.signUpUrl); + const finalUrl = clerk.buildUrlWithAuth(url); + // await clerk.navigate(finalUrl, { replace: false }); + break; + } // TODO-ONETAP: Add a new case in order to handle the `missing_requirements` status and the PSU flow default: clerkInvalidFAPIResponse(res.status, supportEmail); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 2ba1b6aae35..ed9bbec858f 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -18,6 +18,7 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const ctx = useSignInContext(); const signIn = useCoreSignIn(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl); + console.log('Sign in SSO Callback', redirectUrl); const redirectUrlComplete = ctx.afterSignInUrl || '/'; return ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 4dd84f36039..6d6ac8760e4 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -21,6 +21,7 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) const ctx = useSignUpContext(); const signUp = useCoreSignUp(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signUpUrl); + console.log('SSO Callback', redirectUrl); const redirectUrlComplete = ctx.afterSignUpUrl || '/'; const { continueSignUp = false, ...rest } = props; From 9dba1ee73bb5b0e112d8bd6fb5f7f8be1f0e56ba Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 18 Apr 2024 18:24:37 +0300 Subject: [PATCH 02/16] chore(clerk-js): WIP exclude pages --- .../components/GoogleOneTap/one-tap-start.tsx | 65 ++++++------------- packages/clerk-js/src/utils/one-tap.ts | 4 +- playground/nextjs/middleware.ts | 2 +- playground/nextjs/pages/_app.tsx | 2 + 4 files changed, 24 insertions(+), 49 deletions(-) diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index 2818f076513..779d69fe06b 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -1,61 +1,27 @@ import { useClerk, useUser } from '@clerk/shared/react'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; -import { clerkInvalidFAPIResponse } from '../../../core/errors'; import type { GISCredentialResponse } from '../../../utils/one-tap'; import { loadGIS } from '../../../utils/one-tap'; import { useCoreSignIn, useEnvironment, useGoogleOneTapContext } from '../../contexts'; import { withCardStateProvider } from '../../elements'; import { useFetch } from '../../hooks'; -import { useSupportEmail } from '../../hooks/useSupportEmail'; -import { buildSignUpContinueURL } from '../../common'; function _OneTapStart(): JSX.Element | null { const clerk = useClerk(); const signIn = useCoreSignIn(); const { user } = useUser(); const environment = useEnvironment(); + const isPromptedRef = useRef(false); - const supportEmail = useSupportEmail(); const ctx = useGoogleOneTapContext(); - const { displayConfig } = useEnvironment(); async function oneTapCallback(response: GISCredentialResponse) { - try { - const res = await signIn.__experimental_authenticateWithGoogleOneTap({ - token: response.credential, - }); - - await clerk.handleRedirectCallback(); + await signIn.__experimental_authenticateWithGoogleOneTap({ + token: response.credential, + }); - switch (res.status) { - case 'complete': - await clerk.setActive({ - session: res.createdSessionId, - }); - break; - case 'missing_requirements': { - console.log('missing_requirements'); - // const url = new URL(buildSignUpContinueURL(displayConfig.signUpUrl)).pathname; - // const finalUrl = clerk.buildUrlWithAuth(new URL(url, displayConfig.signUpUrl).href); - const url = new URL('/sign-up#/continue', displayConfig.signUpUrl); - console.log('URL', url, displayConfig.signUpUrl); - const finalUrl = clerk.buildUrlWithAuth(url); - // await clerk.navigate(finalUrl, { replace: false }); - break; - } - // TODO-ONETAP: Add a new case in order to handle the `missing_requirements` status and the PSU flow - default: - clerkInvalidFAPIResponse(res.status, supportEmail); - break; - } - } catch (err) { - /** - * Currently it is not possible to display an error in the UI. - * As a fallback we simply open the SignIn modal for the user to sign in. - */ - clerk.openSignIn(); - } + await clerk.handleRedirectCallback(); } const environmentClientID = environment.displayConfig.googleOneTapClientId; @@ -64,7 +30,7 @@ function _OneTapStart(): JSX.Element | null { /** * Prevent GIS from initializing multiple times */ - const { data: google } = useFetch(shouldLoadGIS ? loadGIS : undefined, 'google-identity-services-script', { + useFetch(shouldLoadGIS ? loadGIS : undefined, 'google-identity-services-script', { onSuccess(google) { google.accounts.id.initialize({ client_id: environmentClientID!, @@ -76,20 +42,27 @@ function _OneTapStart(): JSX.Element | null { }); google.accounts.id.prompt(); + isPromptedRef.current = true; }, }); // Trigger only on mount/unmount. Above we handle the logic for the initial fetch + initialization useEffect(() => { - if (google && !user?.id) { - google.accounts.id.prompt(); + if (window.google && !user?.id && !isPromptedRef.current) { + window.google.accounts.id.prompt(); + isPromptedRef.current = true; } + }, [user?.id]); + + // Trigger only on mount/unmount. Above we handle the logic for the initial fetch + initialization + useEffect(() => { return () => { - if (google) { - google.accounts.id.cancel(); + if (window.google && isPromptedRef.current) { + isPromptedRef.current = false; + window.google.accounts.id.cancel(); } }; - }, [user?.id]); + }, []); return null; } diff --git a/packages/clerk-js/src/utils/one-tap.ts b/packages/clerk-js/src/utils/one-tap.ts index 4698b64445a..54ffd6cdf66 100644 --- a/packages/clerk-js/src/utils/one-tap.ts +++ b/packages/clerk-js/src/utils/one-tap.ts @@ -31,7 +31,7 @@ interface Google { declare global { export interface Window { - google: Google; + google?: Google; } } @@ -44,7 +44,7 @@ async function loadGIS() { clerkFailedToLoadThirdPartyScript('Google Identity Services'); } } - return window.google; + return window.google as Google; } export { loadGIS }; diff --git a/playground/nextjs/middleware.ts b/playground/nextjs/middleware.ts index 7c387d67ed0..f90e1441caf 100644 --- a/playground/nextjs/middleware.ts +++ b/playground/nextjs/middleware.ts @@ -9,4 +9,4 @@ export default authMiddleware({ export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], -} \ No newline at end of file +} diff --git a/playground/nextjs/pages/_app.tsx b/playground/nextjs/pages/_app.tsx index 6bb74d3c773..38e716ff2a1 100644 --- a/playground/nextjs/pages/_app.tsx +++ b/playground/nextjs/pages/_app.tsx @@ -13,6 +13,7 @@ import { import { dark, experimental__simple, neobrutalism, shadesOfPurple } from '@clerk/themes'; import Link from 'next/link'; import React, { FunctionComponent, useEffect, useState } from 'react'; +import { GoogleOneTap } from '../components/google-one-tap'; const themes = { default: undefined, dark, neobrutalism, shadesOfPurple }; @@ -77,6 +78,7 @@ function MyApp({ Component, pageProps }: AppProps) { smooth={selectedSmoothing} onPrimaryColorChange={setPrimaryColor} /> + ); From 395fada6a78472e37a900933f1007ed31cdd3cd1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 19 Apr 2024 15:52:57 +0300 Subject: [PATCH 03/16] feat(clerk-js): Handle PSU and MFA flows with GoogleOneTap --- packages/clerk-js/src/core/clerk.ts | 55 ++++++++++++++++--- .../components/GoogleOneTap/one-tap-start.tsx | 30 +++++++--- .../ui/contexts/ClerkUIComponentsContext.tsx | 20 +++++++ packages/types/src/clerk.ts | 24 ++++++++ packages/types/src/signIn.ts | 5 +- 5 files changed, 114 insertions(+), 20 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index cd41f6e3d5a..077bfd684d1 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -4,6 +4,7 @@ import { handleValueOrFn, inBrowser as inClientSide, is4xxError, + isClerkAPIResponseError, isHttpOrHttps, isValidBrowserOnline, isValidProxyUrl, @@ -16,6 +17,7 @@ import { } from '@clerk/shared'; import { eventComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import type { + __experimental_AuthenticateWithGoogleOneTapParams, ActiveSessionResource, AuthenticateWithMetamaskParams, Clerk as ClerkInterface, @@ -1011,8 +1013,16 @@ export class Clerk implements ClerkInterface { return null; }; + public __experimental__handleGoogleOneTapCallback = async ( + signInOrUp: SignInResource | SignUpResource, + params: HandleOAuthCallbackParams, + customNavigate?: (to: string) => Promise, + ): Promise => { + return this.handleRedirectCallback({ ...params, signInOrUp }, customNavigate); + }; + public handleRedirectCallback = async ( - params: HandleOAuthCallbackParams = {}, + params: HandleOAuthCallbackParams & { signInOrUp?: SignInResource | SignUpResource } = {}, customNavigate?: (to: string) => Promise, ): Promise => { if (!this.loaded || !this.#environment || !this.client) { @@ -1088,15 +1098,20 @@ export class Clerk implements ClerkInterface { }; const userExistsButNeedsToSignIn = - su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists'; + (su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists') || + (!!params.signInOrUp && params.signInOrUp.status !== 'needs_identifier'); + console.log('userExistsButNeedsToSignIn', userExistsButNeedsToSignIn); if (userExistsButNeedsToSignIn) { - let res = signIn; - if (!res.status) { + let res = params.signInOrUp || signIn; + console.log('TRANFER', !params.signInOrUp); + if (!params.signInOrUp) { res = await signIn.create({ transfer: true }); } + console.log('status', res); + switch (res.status) { case 'complete': return this.setActive({ @@ -1138,13 +1153,12 @@ export class Clerk implements ClerkInterface { } const userNeedsToBeCreated = - si.firstFactorVerificationStatus === 'transferable' || su.status === 'missing_requirements'; + si.firstFactorVerificationStatus === 'transferable' || + (!!params.signInOrUp && params.signInOrUp.status === 'missing_requirements'); - console.log('userNeedsToBeCreated', userNeedsToBeCreated); if (userNeedsToBeCreated) { - // const res = await signUp.create({ transfer: true }); - let res = signUp; - if (!res.status) { + let res = params.signInOrUp || signUp; + if (!params.signInOrUp) { res = await signUp.create({ transfer: true }); } switch (res.status) { @@ -1211,6 +1225,29 @@ export class Clerk implements ClerkInterface { return this.setActive({ session: null }); }; + public __experimental_authenticateWithGoogleOneTap = async ( + params: __experimental_AuthenticateWithGoogleOneTapParams, + ): Promise => { + return this.client?.signIn + .create({ + // TODO-ONETAP: Add new types when feature is ready for public beta + // @ts-expect-error + strategy: 'google_one_tap', + googleOneTapToken: params.token, + }) + .catch(err => { + if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') { + return this.client?.signUp.create({ + // TODO-ONETAP: Add new types when feature is ready for public beta + // @ts-expect-error + strategy: 'google_one_tap', + googleOneTapToken: params.token, + }); + } + throw err; + }) as Promise; + }; + public authenticateWithMetamask = async ({ redirectUrl, signUpContinueUrl, diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index 779d69fe06b..f353527b998 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -3,25 +3,41 @@ import { useEffect, useRef } from 'react'; import type { GISCredentialResponse } from '../../../utils/one-tap'; import { loadGIS } from '../../../utils/one-tap'; -import { useCoreSignIn, useEnvironment, useGoogleOneTapContext } from '../../contexts'; +import { useEnvironment, useGoogleOneTapContext } from '../../contexts'; import { withCardStateProvider } from '../../elements'; import { useFetch } from '../../hooks'; +import { useRouter } from '../../router'; function _OneTapStart(): JSX.Element | null { const clerk = useClerk(); - const signIn = useCoreSignIn(); const { user } = useUser(); const environment = useEnvironment(); const isPromptedRef = useRef(false); + const { navigate } = useRouter(); const ctx = useGoogleOneTapContext(); + const { signInUrl, signUpUrl, continueSignUpUrl, secondFactorUrl, firstFactorUrl } = ctx; async function oneTapCallback(response: GISCredentialResponse) { - await signIn.__experimental_authenticateWithGoogleOneTap({ - token: response.credential, - }); - - await clerk.handleRedirectCallback(); + isPromptedRef.current = false; + try { + const res = await clerk.__experimental_authenticateWithGoogleOneTap({ + token: response.credential, + }); + await clerk.__experimental__handleGoogleOneTapCallback( + res, + { + signInUrl, + signUpUrl, + continueSignUpUrl, + secondFactorUrl, + firstFactorUrl, + }, + navigate, + ); + } catch (e) { + console.error(e); + } } const environmentClientID = environment.displayConfig.googleOneTapClientId; diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index a5f58f755ed..1e0d2444760 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -495,13 +495,33 @@ export const useCreateOrganizationContext = () => { export const useGoogleOneTapContext = () => { const { componentName, ...ctx } = (React.useContext(ComponentContext) || {}) as OneTapCtx; + const options = useOptions(); + const { displayConfig } = useEnvironment(); + const { queryParams } = useRouter(); if (componentName !== 'OneTap') { throw new Error('Clerk: useGoogleOneTapContext called outside GoogleOneTap.'); } + const redirectUrls = new RedirectUrls(options, {}, queryParams); + + let signUpUrl = options.signUpUrl || displayConfig.signUpUrl; + let signInUrl = options.signInUrl || displayConfig.signInUrl; + + signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams); + signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams); + + const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true }); + const firstFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-one' }, { stringify: true }); + const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true }); + return { ...ctx, componentName, + signInUrl, + signUpUrl, + firstFactorUrl, + secondFactorUrl, + continueSignUpUrl: signUpContinueUrl, }; }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 6f1f1f678e5..1d623388004 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -27,6 +27,8 @@ import type { SignUpForceRedirectUrl, } from './redirects'; import type { ActiveSessionResource } from './session'; +import type { SignInResource } from './signIn'; +import type { SignUpResource } from './signUp'; import type { UserResource } from './user'; import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; @@ -438,6 +440,16 @@ export interface Clerk { */ redirectToAfterSignOut: () => void; + /** + * Completes an Google One Tap redirection flow started by + * {@link Clerk.__experimental_authenticateWithGoogleOneTap} + */ + __experimental__handleGoogleOneTapCallback: ( + signInOrUp: SignInResource | SignUpResource, + params: HandleOAuthCallbackParams, + customNavigate?: (to: string) => Promise, + ) => Promise; + /** * Completes an OAuth or SAML redirection flow started by * {@link Clerk.client.signIn.authenticateWithRedirect} or {@link Clerk.client.signUp.authenticateWithRedirect} @@ -460,6 +472,14 @@ export interface Clerk { */ authenticateWithMetamask: (params?: AuthenticateWithMetamaskParams) => Promise; + /** + * @experimental + * Authenticates user using a google token generated from google identity services. + */ + __experimental_authenticateWithGoogleOneTap: ( + params: __experimental_AuthenticateWithGoogleOneTapParams, + ) => Promise; + /** * Creates an organization, adding the current user as admin. */ @@ -1036,6 +1056,10 @@ export interface AuthenticateWithMetamaskParams { unsafeMetadata?: SignUpUnsafeMetadata; } +export interface __experimental_AuthenticateWithGoogleOneTapParams { + token: string; +} + export interface LoadedClerk extends Clerk { client: ClientResource; } diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 8cc66a8dd5c..2812c98e8a9 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -1,3 +1,4 @@ +import type { __experimental_AuthenticateWithGoogleOneTapParams } from './clerk'; import type { BackupCodeAttempt, BackupCodeFactor, @@ -218,10 +219,6 @@ export type AuthenticateWithPasskeyParams = { flow?: 'autofill' | 'discoverable'; }; -export type __experimental_AuthenticateWithGoogleOneTapParams = { - token: string; -}; - export interface SignInStartEmailLinkFlowParams extends StartEmailLinkFlowParams { emailAddressId: string; } From 49e10e26794275d92ee81e44997334ba233a2056 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 19 Apr 2024 21:02:51 +0300 Subject: [PATCH 04/16] feat(clerk-js): Handle after sign in/up after one tap - Use buildURLWithAuth - Build urls with correct redirect urls when leaving app for AP --- packages/clerk-js/src/core/clerk.ts | 30 +++++++++++---- .../ui/contexts/ClerkUIComponentsContext.tsx | 37 +++++++++++++++++-- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 077bfd684d1..397e6399b4c 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -109,6 +109,8 @@ import { EmailLinkErrorCode, Environment, Organization, + SignIn, + SignUp, } from './resources/internal'; import { SessionCookieService } from './services'; import { warnings } from './warnings'; @@ -1050,34 +1052,44 @@ export class Clerk implements ClerkInterface { const navigate = (to: string) => customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); - const makeNavigate = (to: string) => () => navigate(to); + const makeNavigate = (to: string, withAuth: boolean) => () => navigate(withAuth ? this.buildUrlWithAuth(to) : to); - const navigateToSignIn = makeNavigate(params.signInUrl || displayConfig.signInUrl); + const navigateToSignIn = makeNavigate(params.signInUrl || displayConfig.signInUrl, !!params.signInOrUp); - const navigateToSignUp = makeNavigate(params.signUpUrl || displayConfig.signUpUrl); + const navigateToSignUp = makeNavigate(params.signUpUrl || displayConfig.signUpUrl, !!params.signInOrUp); const navigateToFactorOne = makeNavigate( params.firstFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-one' }, { stringify: true }), + !!params.signInOrUp, ); const navigateToFactorTwo = makeNavigate( params.secondFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }), + !!params.signInOrUp, ); const navigateToResetPassword = makeNavigate( params.resetPasswordUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/reset-password' }, { stringify: true }), + !!params.signInOrUp, ); const redirectUrls = new RedirectUrls(this.#options, params); - const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl()); - const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl()); + const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl(), !!params.signInOrUp); + const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl(), !!params.signInOrUp); const navigateToContinueSignUp = makeNavigate( params.continueSignUpUrl || - buildURL({ base: displayConfig.signUpUrl, hashPath: '/continue' }, { stringify: true }), + buildURL( + { + base: displayConfig.signUpUrl, + hashPath: '/continue', + }, + { stringify: true }, + ), + !!params.signInOrUp, ); const navigateToNextStepSignUp = ({ missingFields }: { missingFields: SignUpField[] }) => { @@ -1099,7 +1111,7 @@ export class Clerk implements ClerkInterface { const userExistsButNeedsToSignIn = (su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists') || - (!!params.signInOrUp && params.signInOrUp.status !== 'needs_identifier'); + (!!params.signInOrUp && params.signInOrUp instanceof SignIn && params.signInOrUp.status !== 'needs_identifier'); console.log('userExistsButNeedsToSignIn', userExistsButNeedsToSignIn); @@ -1154,7 +1166,9 @@ export class Clerk implements ClerkInterface { const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable' || - (!!params.signInOrUp && params.signInOrUp.status === 'missing_requirements'); + (!!params.signInOrUp && + params.signInOrUp instanceof SignUp && + params.signInOrUp.status === 'missing_requirements'); if (userNeedsToBeCreated) { let res = params.signInOrUp || signUp; diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 1e0d2444760..cd8228d718e 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -511,9 +511,40 @@ export const useGoogleOneTapContext = () => { signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams); signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams); - const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true }); - const firstFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-one' }, { stringify: true }); - const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true }); + const signUpContinueUrl = buildURL( + { + base: signUpUrl, + hashPath: '/continue', + hashSearch: new URLSearchParams({ + // or // sign_up_force_redirect_url: window.location.href, + sign_up_force_redirect_url: redirectUrls.getAfterSignUpUrl(), + }).toString(), + }, + { stringify: true }, + ); + + const firstFactorUrl = buildURL( + { + base: signInUrl, + hashPath: '/factor-one', + hashSearch: new URLSearchParams({ + // or // sign_in_force_redirect_url: window.location.href, + sign_in_force_redirect_url: redirectUrls.getAfterSignInUrl(), + }).toString(), + }, + { stringify: true }, + ); + const secondFactorUrl = buildURL( + { + base: signInUrl, + hashPath: '/factor-two', + hashSearch: new URLSearchParams({ + // or // sign_in_force_redirect_url: window.location.href, + sign_in_force_redirect_url: redirectUrls.getAfterSignInUrl(), + }).toString(), + }, + { stringify: true }, + ); return { ...ctx, From 2a15ad7fd5c2988440cf0bf85a944945932f9bf5 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 11:40:41 +0300 Subject: [PATCH 05/16] feat(clerk-js): GoogleOneTap return back to same location --- packages/clerk-js/src/core/clerk.ts | 10 +++++++-- .../components/GoogleOneTap/one-tap-start.tsx | 5 ++++- .../ui/contexts/ClerkUIComponentsContext.tsx | 22 +++++++++++++------ packages/types/src/clerk.ts | 3 ++- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 397e6399b4c..ab0f50ceefa 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1077,8 +1077,14 @@ export class Clerk implements ClerkInterface { ); const redirectUrls = new RedirectUrls(this.#options, params); - const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl(), !!params.signInOrUp); - const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl(), !!params.signInOrUp); + const navigateAfterSignIn = makeNavigate( + params.afterSignInUrl || redirectUrls.getAfterSignInUrl(), + !!params.signInOrUp, + ); + const navigateAfterSignUp = makeNavigate( + params.afterSignUpUrl || redirectUrls.getAfterSignUpUrl(), + !!params.signInOrUp, + ); const navigateToContinueSignUp = makeNavigate( params.continueSignUpUrl || diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index f353527b998..5d3f4894d27 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -16,7 +16,8 @@ function _OneTapStart(): JSX.Element | null { const { navigate } = useRouter(); const ctx = useGoogleOneTapContext(); - const { signInUrl, signUpUrl, continueSignUpUrl, secondFactorUrl, firstFactorUrl } = ctx; + const { signInUrl, signUpUrl, continueSignUpUrl, secondFactorUrl, firstFactorUrl, afterSignUpUrl, afterSignInUrl } = + ctx; async function oneTapCallback(response: GISCredentialResponse) { isPromptedRef.current = false; @@ -32,6 +33,8 @@ function _OneTapStart(): JSX.Element | null { continueSignUpUrl, secondFactorUrl, firstFactorUrl, + afterSignUpUrl, + afterSignInUrl, }, navigate, ); diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index cd8228d718e..614f5276300 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -503,7 +503,13 @@ export const useGoogleOneTapContext = () => { throw new Error('Clerk: useGoogleOneTapContext called outside GoogleOneTap.'); } - const redirectUrls = new RedirectUrls(options, {}, queryParams); + const redirectUrls = new RedirectUrls( + options, + { + ...ctx, + }, + queryParams, + ); let signUpUrl = options.signUpUrl || displayConfig.signUpUrl; let signInUrl = options.signInUrl || displayConfig.signInUrl; @@ -511,13 +517,15 @@ export const useGoogleOneTapContext = () => { signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams); signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams); + const afterSignInUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignInUrl(); + const afterSignUpUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignUpUrl(); + const signUpContinueUrl = buildURL( { base: signUpUrl, hashPath: '/continue', hashSearch: new URLSearchParams({ - // or // sign_up_force_redirect_url: window.location.href, - sign_up_force_redirect_url: redirectUrls.getAfterSignUpUrl(), + sign_up_force_redirect_url: afterSignUpUrl, }).toString(), }, { stringify: true }, @@ -528,8 +536,7 @@ export const useGoogleOneTapContext = () => { base: signInUrl, hashPath: '/factor-one', hashSearch: new URLSearchParams({ - // or // sign_in_force_redirect_url: window.location.href, - sign_in_force_redirect_url: redirectUrls.getAfterSignInUrl(), + sign_in_force_redirect_url: afterSignInUrl, }).toString(), }, { stringify: true }, @@ -539,8 +546,7 @@ export const useGoogleOneTapContext = () => { base: signInUrl, hashPath: '/factor-two', hashSearch: new URLSearchParams({ - // or // sign_in_force_redirect_url: window.location.href, - sign_in_force_redirect_url: redirectUrls.getAfterSignInUrl(), + sign_in_force_redirect_url: afterSignInUrl, }).toString(), }, { stringify: true }, @@ -554,5 +560,7 @@ export const useGoogleOneTapContext = () => { firstFactorUrl, secondFactorUrl, continueSignUpUrl: signUpContinueUrl, + afterSignInUrl, + afterSignUpUrl, }; }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 1d623388004..2b8acde0c71 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -730,7 +730,8 @@ export type SignInProps = RoutingOptions & { export type SignInModalProps = WithoutRouting; -export type OneTapProps = { +export type OneTapProps = RedirectOptions & { + returnToCurrentLocation?: boolean; cancelOnTapOutside?: boolean; appearance?: SignInTheme; }; From ecb29a7b8d063e4294e7c3bcf2fc338bbbd836e0 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 13:20:01 +0300 Subject: [PATCH 06/16] chore(clerk-js): Cleanup code --- packages/clerk-js/src/core/clerk.ts | 120 +++++++++++------- .../components/GoogleOneTap/one-tap-start.tsx | 15 ++- .../ui/contexts/ClerkUIComponentsContext.tsx | 14 +- 3 files changed, 92 insertions(+), 57 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ab0f50ceefa..7ee2f9bb3d7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -109,8 +109,6 @@ import { EmailLinkErrorCode, Environment, Organization, - SignIn, - SignUp, } from './resources/internal'; import { SessionCookieService } from './services'; import { warnings } from './warnings'; @@ -1020,17 +1018,42 @@ export class Clerk implements ClerkInterface { params: HandleOAuthCallbackParams, customNavigate?: (to: string) => Promise, ): Promise => { - return this.handleRedirectCallback({ ...params, signInOrUp }, customNavigate); + if (!this.loaded || !this.#environment || !this.client) { + return; + } + const { signIn: _signIn, signUp: _signUp } = this.client; + + const signIn = 'identifier' in (signInOrUp || {}) ? (signInOrUp as SignInResource) : _signIn; + const signUp = 'missingFields' in (signInOrUp || {}) ? (signInOrUp as SignUpResource) : _signUp; + + const navigate = (to: string) => + customNavigate && typeof customNavigate === 'function' + ? customNavigate(this.buildUrlWithAuth(to)) + : this.navigate(this.buildUrlWithAuth(to)); + + return this._handleRedirectCallback(params, { + signUp, + signIn, + navigate, + }); }; - public handleRedirectCallback = async ( - params: HandleOAuthCallbackParams & { signInOrUp?: SignInResource | SignUpResource } = {}, - customNavigate?: (to: string) => Promise, + private _handleRedirectCallback = async ( + params: HandleOAuthCallbackParams, + { + signIn, + signUp, + navigate, + }: { + signIn: SignInResource; + signUp: SignUpResource; + navigate: (to: string) => Promise; + }, ): Promise => { if (!this.loaded || !this.#environment || !this.client) { return; } - const { signIn, signUp } = this.client; + const { displayConfig } = this.#environment; const { firstFactorVerification } = signIn; const { externalAccount } = signUp.verifications; @@ -1040,6 +1063,7 @@ export class Clerk implements ClerkInterface { externalAccountStatus: externalAccount.status, externalAccountErrorCode: externalAccount.error?.code, externalAccountSessionId: externalAccount.error?.meta?.sessionId, + sessionId: signUp.createdSessionId, }; const si = { @@ -1047,44 +1071,33 @@ export class Clerk implements ClerkInterface { firstFactorVerificationStatus: firstFactorVerification.status, firstFactorVerificationErrorCode: firstFactorVerification.error?.code, firstFactorVerificationSessionId: firstFactorVerification.error?.meta?.sessionId, + sessionId: signIn.createdSessionId, }; - const navigate = (to: string) => - customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); - - const makeNavigate = (to: string, withAuth: boolean) => () => navigate(withAuth ? this.buildUrlWithAuth(to) : to); + const makeNavigate = (to: string) => () => navigate(to); - const navigateToSignIn = makeNavigate(params.signInUrl || displayConfig.signInUrl, !!params.signInOrUp); + const navigateToSignIn = makeNavigate(params.signInUrl || displayConfig.signInUrl); - const navigateToSignUp = makeNavigate(params.signUpUrl || displayConfig.signUpUrl, !!params.signInOrUp); + const navigateToSignUp = makeNavigate(params.signUpUrl || displayConfig.signUpUrl); const navigateToFactorOne = makeNavigate( params.firstFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-one' }, { stringify: true }), - !!params.signInOrUp, ); const navigateToFactorTwo = makeNavigate( params.secondFactorUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }), - !!params.signInOrUp, ); const navigateToResetPassword = makeNavigate( params.resetPasswordUrl || buildURL({ base: displayConfig.signInUrl, hashPath: '/reset-password' }, { stringify: true }), - !!params.signInOrUp, ); const redirectUrls = new RedirectUrls(this.#options, params); - const navigateAfterSignIn = makeNavigate( - params.afterSignInUrl || redirectUrls.getAfterSignInUrl(), - !!params.signInOrUp, - ); - const navigateAfterSignUp = makeNavigate( - params.afterSignUpUrl || redirectUrls.getAfterSignUpUrl(), - !!params.signInOrUp, - ); + const navigateAfterSignIn = makeNavigate(redirectUrls.getAfterSignInUrl()); + const navigateAfterSignUp = makeNavigate(redirectUrls.getAfterSignUpUrl()); const navigateToContinueSignUp = makeNavigate( params.continueSignUpUrl || @@ -1095,7 +1108,6 @@ export class Clerk implements ClerkInterface { }, { stringify: true }, ), - !!params.signInOrUp, ); const navigateToNextStepSignUp = ({ missingFields }: { missingFields: SignUpField[] }) => { @@ -1115,21 +1127,18 @@ export class Clerk implements ClerkInterface { }); }; - const userExistsButNeedsToSignIn = - (su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists') || - (!!params.signInOrUp && params.signInOrUp instanceof SignIn && params.signInOrUp.status !== 'needs_identifier'); + if (si.status === 'complete') { + return this.setActive({ + session: si.sessionId, + beforeEmit: navigateAfterSignIn, + }); + } - console.log('userExistsButNeedsToSignIn', userExistsButNeedsToSignIn); + const userExistsButNeedsToSignIn = + su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists'; if (userExistsButNeedsToSignIn) { - let res = params.signInOrUp || signIn; - console.log('TRANFER', !params.signInOrUp); - if (!params.signInOrUp) { - res = await signIn.create({ transfer: true }); - } - - console.log('status', res); - + const res = await signIn.create({ transfer: true }); switch (res.status) { case 'complete': return this.setActive({ @@ -1170,17 +1179,10 @@ export class Clerk implements ClerkInterface { return navigateToResetPassword(); } - const userNeedsToBeCreated = - si.firstFactorVerificationStatus === 'transferable' || - (!!params.signInOrUp && - params.signInOrUp instanceof SignUp && - params.signInOrUp.status === 'missing_requirements'); + const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable'; if (userNeedsToBeCreated) { - let res = params.signInOrUp || signUp; - if (!params.signInOrUp) { - res = await signUp.create({ transfer: true }); - } + const res = await signUp.create({ transfer: true }); switch (res.status) { case 'complete': return this.setActive({ @@ -1194,6 +1196,13 @@ export class Clerk implements ClerkInterface { } } + if (su.status === 'complete') { + return this.setActive({ + session: si.sessionId, + beforeEmit: navigateAfterSignUp, + }); + } + if (si.status === 'needs_second_factor') { return navigateToFactorTwo(); } @@ -1230,6 +1239,25 @@ export class Clerk implements ClerkInterface { return navigateToSignIn(); }; + public handleRedirectCallback = async ( + params: HandleOAuthCallbackParams = {}, + customNavigate?: (to: string) => Promise, + ): Promise => { + if (!this.loaded || !this.#environment || !this.client) { + return; + } + const { signIn, signUp } = this.client; + + const navigate = (to: string) => + customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); + + return this._handleRedirectCallback(params, { + signUp, + signIn, + navigate, + }); + }; + public handleUnauthenticated = async (opts = { broadcast: true }): Promise => { if (!this.client || !this.session) { return; diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index 5d3f4894d27..5a8993927de 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -16,8 +16,15 @@ function _OneTapStart(): JSX.Element | null { const { navigate } = useRouter(); const ctx = useGoogleOneTapContext(); - const { signInUrl, signUpUrl, continueSignUpUrl, secondFactorUrl, firstFactorUrl, afterSignUpUrl, afterSignInUrl } = - ctx; + const { + signInUrl, + signUpUrl, + continueSignUpUrl, + secondFactorUrl, + firstFactorUrl, + signUpForceRedirectUrl, + signInForceRedirectUrl, + } = ctx; async function oneTapCallback(response: GISCredentialResponse) { isPromptedRef.current = false; @@ -33,8 +40,8 @@ function _OneTapStart(): JSX.Element | null { continueSignUpUrl, secondFactorUrl, firstFactorUrl, - afterSignUpUrl, - afterSignInUrl, + signUpForceRedirectUrl, + signInForceRedirectUrl, }, navigate, ); diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 614f5276300..9de97028084 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -517,15 +517,15 @@ export const useGoogleOneTapContext = () => { signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams); signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams); - const afterSignInUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignInUrl(); - const afterSignUpUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignUpUrl(); + const signInForceRedirectUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignInUrl(); + const signUpForceRedirectUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignUpUrl(); const signUpContinueUrl = buildURL( { base: signUpUrl, hashPath: '/continue', hashSearch: new URLSearchParams({ - sign_up_force_redirect_url: afterSignUpUrl, + sign_up_force_redirect_url: signUpForceRedirectUrl, }).toString(), }, { stringify: true }, @@ -536,7 +536,7 @@ export const useGoogleOneTapContext = () => { base: signInUrl, hashPath: '/factor-one', hashSearch: new URLSearchParams({ - sign_in_force_redirect_url: afterSignInUrl, + sign_in_force_redirect_url: signInForceRedirectUrl, }).toString(), }, { stringify: true }, @@ -546,7 +546,7 @@ export const useGoogleOneTapContext = () => { base: signInUrl, hashPath: '/factor-two', hashSearch: new URLSearchParams({ - sign_in_force_redirect_url: afterSignInUrl, + sign_in_force_redirect_url: signInForceRedirectUrl, }).toString(), }, { stringify: true }, @@ -560,7 +560,7 @@ export const useGoogleOneTapContext = () => { firstFactorUrl, secondFactorUrl, continueSignUpUrl: signUpContinueUrl, - afterSignInUrl, - afterSignUpUrl, + signInForceRedirectUrl, + signUpForceRedirectUrl, }; }; From e7d26a117c6c9a858bb08bd2caf4a60c4d284e25 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 14:45:29 +0300 Subject: [PATCH 07/16] fix(clerk-js): Set active with correct session from sign up --- 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 7ee2f9bb3d7..2e48291a729 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1198,7 +1198,7 @@ export class Clerk implements ClerkInterface { if (su.status === 'complete') { return this.setActive({ - session: si.sessionId, + session: su.sessionId, beforeEmit: navigateAfterSignUp, }); } From 3acc6ff7d21c19318e162d4bce79dc45b67eeebd Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 15:06:30 +0300 Subject: [PATCH 08/16] chore(clerk-js): Cleanup --- packages/clerk-js/src/core/clerk.ts | 2 +- packages/clerk-js/src/core/resources/SignIn.ts | 3 --- packages/clerk-js/src/ui/common/redirects.ts | 11 ----------- .../src/ui/components/GoogleOneTap/one-tap-start.tsx | 2 +- .../src/ui/components/SignIn/SignInSocialButtons.tsx | 1 - .../src/ui/components/SignUp/SignUpSocialButtons.tsx | 1 - packages/types/src/clerk.ts | 2 +- 7 files changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2e48291a729..21276081947 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1013,7 +1013,7 @@ export class Clerk implements ClerkInterface { return null; }; - public __experimental__handleGoogleOneTapCallback = async ( + public __experimental_handleGoogleOneTapCallback = async ( signInOrUp: SignInResource | SignUpResource, params: HandleOAuthCallbackParams, customNavigate?: (to: string) => Promise, diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index d0be87cdffd..8ebed20aeaa 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -234,10 +234,7 @@ export class SignIn extends BaseResource implements SignInResource { strategy: 'google_one_tap', googleOneTapToken: params.token, }).catch(err => { - console.log('Error', err); - console.log('Error', isClerkAPIResponseError(err), err.errors[0].code); if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') { - console.log('is external_account_not_found'); return SignIn.clerk.client?.signUp.create({ // TODO-ONETAP: Add new types when feature is ready for public beta // @ts-expect-error diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 9b5e941d1dc..212dfeca9dc 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -2,7 +2,6 @@ import { buildURL } from '../../utils/url'; import type { SignInContextType, SignUpContextType, UserProfileContextType } from './../contexts'; const SSO_CALLBACK_PATH_ROUTE = '/sso-callback'; -const SIGN_UP_CONTINUE_PATH_ROUTE = '/sign-up/continue'; const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify'; export function buildEmailLinkRedirectUrl( @@ -33,16 +32,6 @@ export function buildSSOCallbackURL( }); } -export function buildSignUpContinueURL(baseUrl: string | undefined = ''): string { - return buildRedirectUrl({ - routing: 'hash', - baseUrl, - authQueryString: '', - path: '', - endpoint: SIGN_UP_CONTINUE_PATH_ROUTE, - }); -} - type AuthQueryString = string | null | undefined; type BuildRedirectUrlParams = { diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index 5a8993927de..32fc53c3ecb 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -32,7 +32,7 @@ function _OneTapStart(): JSX.Element | null { const res = await clerk.__experimental_authenticateWithGoogleOneTap({ token: response.credential, }); - await clerk.__experimental__handleGoogleOneTapCallback( + await clerk.__experimental_handleGoogleOneTapCallback( res, { signInUrl, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index ed9bbec858f..2ba1b6aae35 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -18,7 +18,6 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { const ctx = useSignInContext(); const signIn = useCoreSignIn(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl); - console.log('Sign in SSO Callback', redirectUrl); const redirectUrlComplete = ctx.afterSignInUrl || '/'; return ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 6d6ac8760e4..4dd84f36039 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -21,7 +21,6 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) const ctx = useSignUpContext(); const signUp = useCoreSignUp(); const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signUpUrl); - console.log('SSO Callback', redirectUrl); const redirectUrlComplete = ctx.afterSignUpUrl || '/'; const { continueSignUp = false, ...rest } = props; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 2b8acde0c71..d1dc3482405 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -444,7 +444,7 @@ export interface Clerk { * Completes an Google One Tap redirection flow started by * {@link Clerk.__experimental_authenticateWithGoogleOneTap} */ - __experimental__handleGoogleOneTapCallback: ( + __experimental_handleGoogleOneTapCallback: ( signInOrUp: SignInResource | SignUpResource, params: HandleOAuthCallbackParams, customNavigate?: (to: string) => Promise, From a5c2fe7e05d78f1549f12f3eb701913d224189c7 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 15:08:15 +0300 Subject: [PATCH 09/16] chore(clerk-js): Cleanup --- .../clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index 32fc53c3ecb..bce4b3e07b3 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -72,7 +72,6 @@ function _OneTapStart(): JSX.Element | null { }, }); - // Trigger only on mount/unmount. Above we handle the logic for the initial fetch + initialization useEffect(() => { if (window.google && !user?.id && !isPromptedRef.current) { window.google.accounts.id.prompt(); From 2af0f56958c8bb05aca811328742a300c18aac55 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 15:10:10 +0300 Subject: [PATCH 10/16] chore(clerk-js): Mark as deprecated --- packages/types/src/signIn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 2812c98e8a9..52c203207bc 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -102,7 +102,7 @@ export interface SignInResource extends ClerkResource { authenticateWithPasskey: (params?: AuthenticateWithPasskeyParams) => Promise; /** - * @experimental + * @deprecated Use `Clerk.__experimental_authenticateWithGoogleOneTap` */ __experimental_authenticateWithGoogleOneTap: ( params: __experimental_AuthenticateWithGoogleOneTapParams, From bdc0e45fdf73d05da9e967b3bc38053658190552 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 15:11:45 +0300 Subject: [PATCH 11/16] chore(clerk-js): Add changeset --- .changeset/breezy-monkeys-develop.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .changeset/breezy-monkeys-develop.md diff --git a/.changeset/breezy-monkeys-develop.md b/.changeset/breezy-monkeys-develop.md new file mode 100644 index 00000000000..9095db5f293 --- /dev/null +++ b/.changeset/breezy-monkeys-develop.md @@ -0,0 +1,25 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Updates related to experimental Google One Tap support +- Comply with Clerk's redirection logic ( & priorities ) +```tsx +<__experimental_GoogleOneTap + signInForceRedirectUrl="" + signUpForceRedirectUrl="" + signInFallbackRedirectUrl="" + signUpFallbackRedirectUrl="" +/> +``` + +- Opt-in for user to return back where they started ( this overrides the above) +```tsx +<__experimental_GoogleOneTap returnToCurrentLocation /> +``` + +- Moved handleGoogleOneTapCallback to Clerk singleton +```ts +Clerk.__experimental_handleGoogleOneTapCallback +``` From c62c7e299be21a9700adf8a9c5015d9b7e42ee56 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 24 Apr 2024 15:32:21 +0300 Subject: [PATCH 12/16] chore(*): Revert playground changes --- playground/nextjs/middleware.ts | 2 +- playground/nextjs/pages/_app.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/playground/nextjs/middleware.ts b/playground/nextjs/middleware.ts index f90e1441caf..e41136e55ed 100644 --- a/playground/nextjs/middleware.ts +++ b/playground/nextjs/middleware.ts @@ -9,4 +9,4 @@ export default authMiddleware({ export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], -} +}; diff --git a/playground/nextjs/pages/_app.tsx b/playground/nextjs/pages/_app.tsx index 38e716ff2a1..6bb74d3c773 100644 --- a/playground/nextjs/pages/_app.tsx +++ b/playground/nextjs/pages/_app.tsx @@ -13,7 +13,6 @@ import { import { dark, experimental__simple, neobrutalism, shadesOfPurple } from '@clerk/themes'; import Link from 'next/link'; import React, { FunctionComponent, useEffect, useState } from 'react'; -import { GoogleOneTap } from '../components/google-one-tap'; const themes = { default: undefined, dark, neobrutalism, shadesOfPurple }; @@ -78,7 +77,6 @@ function MyApp({ Component, pageProps }: AppProps) { smooth={selectedSmoothing} onPrimaryColorChange={setPrimaryColor} /> - ); From 94fd9fc151be61e1fd27b54a9b2b4ed3fd602ef2 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 13 May 2024 13:09:51 +0300 Subject: [PATCH 13/16] fix(clerk-js): Drop `returnToCurrentLocation` --- .../src/ui/contexts/ClerkUIComponentsContext.tsx | 5 +++-- packages/types/src/clerk.ts | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 9de97028084..a2d8e794e84 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -507,6 +507,7 @@ export const useGoogleOneTapContext = () => { options, { ...ctx, + redirectUrl: window.location.href, }, queryParams, ); @@ -517,8 +518,8 @@ export const useGoogleOneTapContext = () => { signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams); signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams); - const signInForceRedirectUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignInUrl(); - const signUpForceRedirectUrl = ctx.returnToCurrentLocation ? window.location.href : redirectUrls.getAfterSignUpUrl(); + const signInForceRedirectUrl = redirectUrls.getAfterSignInUrl(); + const signUpForceRedirectUrl = redirectUrls.getAfterSignUpUrl(); const signUpContinueUrl = buildURL( { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index d1dc3482405..7c14004d431 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -730,8 +730,13 @@ export type SignInProps = RoutingOptions & { export type SignInModalProps = WithoutRouting; -export type OneTapProps = RedirectOptions & { - returnToCurrentLocation?: boolean; +type OneTapRedirectUrlProps = SignInForceRedirectUrl & SignUpForceRedirectUrl; + +export type OneTapProps = OneTapRedirectUrlProps & { + /** + * Whether to cancel the Google One Tap request if a user clicks outside the prompt. + * @default true + */ cancelOnTapOutside?: boolean; appearance?: SignInTheme; }; From bf262a2cc5c10e02f25b29343ea0e7de07a2b818 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 13 May 2024 14:15:49 +0300 Subject: [PATCH 14/16] chore(clerk-js): Add support for `itpSupport` and `fedCmSupport` props --- .../ui/components/GoogleOneTap/one-tap-start.tsx | 4 ++-- packages/types/src/clerk.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx index bce4b3e07b3..750b703c759 100644 --- a/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx +++ b/packages/clerk-js/src/ui/components/GoogleOneTap/one-tap-start.tsx @@ -61,10 +61,10 @@ function _OneTapStart(): JSX.Element | null { google.accounts.id.initialize({ client_id: environmentClientID!, callback: oneTapCallback, - itp_support: true, + itp_support: ctx.itpSupport, cancel_on_tap_outside: ctx.cancelOnTapOutside, auto_select: false, - use_fedcm_for_prompt: true, + use_fedcm_for_prompt: ctx.fedCmSupport, }); google.accounts.id.prompt(); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 7c14004d431..6c85f37947b 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -738,6 +738,19 @@ export type OneTapProps = OneTapRedirectUrlProps & { * @default true */ cancelOnTapOutside?: boolean; + /** + * Enables upgraded One Tap UX on ITP browsers. + * Turning this options off, would hide any One Tap UI in such browsers. + * @default true + */ + itpSupport?: boolean; + /** + * FedCM enables more private sign-in flows without requiring the use of third-party cookies. + * The browser controls user settings, displays user prompts, and only contacts an Identity Provider such as Google after explicit user consent is given. + * Backwards compatible with browsers that still support third-party cookies. + * @default true + */ + fedCmSupport?: boolean; appearance?: SignInTheme; }; From 180cfbaccb2e1351ea7cdbf17f0cc7c693319122 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 13 May 2024 14:22:01 +0300 Subject: [PATCH 15/16] chore(clerk-js): Update changeset --- .changeset/breezy-monkeys-develop.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/.changeset/breezy-monkeys-develop.md b/.changeset/breezy-monkeys-develop.md index 9095db5f293..36f161d3e0f 100644 --- a/.changeset/breezy-monkeys-develop.md +++ b/.changeset/breezy-monkeys-develop.md @@ -4,22 +4,35 @@ --- Updates related to experimental Google One Tap support -- Comply with Clerk's redirection logic ( & priorities ) +- By default we are returning back to the location where the flow started. + To accomplish that internally we will use the redirect_url query parameter to build the url. +```tsx +<__experimental_GoogleOneTap /> +``` + +- In the above example if there is a SIGN_UP_FORCE_REDIRECT_URL or SIGN_IN_FORCE_REDIRECT_URL set then the developer would need to pass new values as props like this ```tsx <__experimental_GoogleOneTap signInForceRedirectUrl="" signUpForceRedirectUrl="" - signInFallbackRedirectUrl="" - signUpFallbackRedirectUrl="" /> ``` -- Opt-in for user to return back where they started ( this overrides the above) +- Let the developer configure the experience they want to offer. (All these values are true by default) ```tsx -<__experimental_GoogleOneTap returnToCurrentLocation /> +<__experimental_GoogleOneTap + cancelOnTapOutside={false} + itpSupport={false} + fedCmSupport={false} +/> +``` + +- Moved authenticateWithGoogleOneTap to Clerk singleton +```ts +Clerk.__experimental_authenticateWithGoogleOneTap ``` -- Moved handleGoogleOneTapCallback to Clerk singleton +- Created the handleGoogleOneTapCallback in Clerk singleton ```ts Clerk.__experimental_handleGoogleOneTapCallback ``` From 3c8594f5ffd26f395979de61c66cb0034b4867c2 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 14 May 2024 12:00:14 +0300 Subject: [PATCH 16/16] fix(clerk-js): Missing utils --- .../clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 71a4e8a7c22..fbeb992a31c 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -518,8 +518,9 @@ export const useGoogleOneTapContext = () => { let signUpUrl = options.signUpUrl || displayConfig.signUpUrl; let signInUrl = options.signInUrl || displayConfig.signInUrl; - signUpUrl = redirectUrls.appendPreservedPropsToUrl(signUpUrl, queryParams); - signInUrl = redirectUrls.appendPreservedPropsToUrl(signInUrl, queryParams); + const preservedParams = redirectUrls.getPreservedSearchParams(); + signInUrl = buildURL({ base: signInUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); + signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); const signInForceRedirectUrl = redirectUrls.getAfterSignInUrl(); const signUpForceRedirectUrl = redirectUrls.getAfterSignUpUrl();