You must configure your application instance through the Clerk Dashboard for the social connection(s) that you want to use. Visit the appropriate guide for your platform to learn how to configure your instance.
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
First, in your .env file, set the NEXT_PUBLIC_CLERK_SIGN_IN_URL environment variable to tell Clerk where the sign-in page is being hosted. Otherwise, your app may default to using the Account Portal sign-in page instead. This guide uses the /sign-in route.
.env
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
The following example will both sign up and sign in users, eliminating the need for a separate sign-up page. However, if you want to have separate sign-up and sign-in pages, the sign-up and sign-in flows are equivalent, meaning that all you have to do is swap out the SignIn object for the SignUp object using the useSignUp() hook.
Starts the authentication process by calling SignIn.authenticateWithRedirect(params). This method requires a redirectUrl param, which is the URL that the browser will be redirected to once the user authenticates with the identity provider.
Creates a route at the URL that the redirectUrl param points to. The following example names this route /sso-callback. This route should either render the prebuilt <AuthenticateWithRedirectCallback/> component or call the Clerk.handleRedirectCallback() method if you're not using the prebuilt component.
The following example shows two files:
The sign-in page where the user can start the authentication flow.
The SSO callback page where the flow is completed.
Sign in page
SSO callback page
app/sign-in/[[...sign-in]]/page.tsx
'use client'import*as React from'react'import { OAuthStrategy } from'@clerk/types'import { useSignIn } from'@clerk/nextjs'exportdefaultfunctionPage() {const { signIn } =useSignIn()if (!signIn) returnnullconstsignInWith= (strategy:OAuthStrategy) => {return signIn.authenticateWithRedirect({ strategy, redirectUrl:'/sign-in/sso-callback', redirectUrlComplete:'/sign-in/tasks',// Learn more about session tasks at https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks }).then((res) => {console.log(res) }).catch((err:any) => {// See https://clerk.com/docs/guides/development/custom-flows/error-handling// for more info on error handlingconsole.log(err.errors)console.error(err,null,2) }) }// Render a button for each supported OAuth provider// you want to add to your app. This example uses only Google.return ( <div> <buttononClick={() =>signInWith('oauth_google')}>Sign in with Google</button> </div> )}
app/sign-in/sso-callback/page.tsx
import { AuthenticateWithRedirectCallback } from'@clerk/nextjs'exportdefaultfunctionPage() {// Handle the redirect flow by calling the Clerk.handleRedirectCallback() method// or rendering the prebuilt <AuthenticateWithRedirectCallback/> component.return ( <> <AuthenticateWithRedirectCallback /> {/* Required for sign-up flows Clerk's bot sign-up protection is enabled by default */} <divid="clerk-captcha" /> </> )}
To handle both sign-up and sign-in without a separate sign-up page, the following example:
Uses the useSSO()Expo Icon hook to access the startSSOFlow() method.
Calls the startSSOFlow() method with the strategy param set to oauth_google, but you can use any of the supported OAuth strategies. The optional redirect_url param is also set in order to redirect the user once they finish the authentication flow.
If authentication is successful, the setActive() method is called to set the active session with the new createdSessionId.
If authentication is not successful, you can handle the missing requirements, such as MFA, using the signIn or signUp object returned from startSSOFlow(), depending on if the user is signing in or signing up. These objects include properties, like status, that can be used to determine the next steps. See the respective linked references for more information.
app/(auth)/sign-in.tsx
import { ThemedText } from'@/components/themed-text'import { ThemedView } from'@/components/themed-view'import { useSSO } from'@clerk/clerk-expo'import*as AuthSession from'expo-auth-session'import { useRouter } from'expo-router'import*as WebBrowser from'expo-web-browser'import React, { useCallback, useEffect } from'react'import { Platform, Pressable, StyleSheet } from'react-native'// Preloads the browser for Android devices to reduce authentication load time// See: https://docs.expo.dev/guides/authentication/#improving-user-experienceexportconstuseWarmUpBrowser= () => {useEffect(() => {if (Platform.OS!=='android') returnvoidWebBrowser.warmUpAsync()return () => {// Cleanup: closes browser when component unmountsvoidWebBrowser.coolDownAsync() } }, [])}// Handle any pending authentication sessionsWebBrowser.maybeCompleteAuthSession()exportdefaultfunctionPage() {useWarmUpBrowser()constrouter=useRouter()// Use the `useSSO()` hook to access the `startSSOFlow()` methodconst { startSSOFlow } =useSSO()constonPress=useCallback(async () => {try {// Start the authentication process by calling `startSSOFlow()`const { createdSessionId,setActive,signIn,signUp } =awaitstartSSOFlow({ strategy:'oauth_google',// For web, defaults to current path// For native, you must pass a scheme, like AuthSession.makeRedirectUri({ scheme, path })// For more info, see https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions redirectUrl:AuthSession.makeRedirectUri(), })// If sign in was successful, set the active sessionif (createdSessionId) {setActive!({ session: createdSessionId,// Check for session tasks and navigate to custom UI to help users resolve them// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasksnavigate:async ({ session }) => {if (session?.currentTask) {console.log(session?.currentTask)router.push('/sign-in/tasks')return }router.push('/') }, }) } else {// If there is no `createdSessionId`,// there are missing requirements, such as MFA// See https://clerk.com/docs/guides/development/custom-flows/authentication/oauth-connections#handle-missing-requirements } } catch (err) {// See https://clerk.com/docs/guides/development/custom-flows/error-handling// for more info on error handlingconsole.error(JSON.stringify(err,null,2)) } }, [])return ( <ThemedViewstyle={styles.container}> <ThemedTexttype="title"style={styles.title}> Sign in </ThemedText> <ThemedTextstyle={styles.description}> Sign in with your Google account to continue </ThemedText> <Pressablestyle={({ pressed }) => [styles.button, pressed &&styles.buttonPressed]}onPress={onPress} > <ThemedTextstyle={styles.buttonText}>Sign in with Google</ThemedText> </Pressable> </ThemedView> )}conststyles=StyleSheet.create({ container: { flex:1, padding:20, gap:12, }, title: { marginBottom:8, }, description: { fontSize:14, marginBottom:16, opacity:0.8, }, button: { backgroundColor:'#0a7ea4', paddingVertical:12, paddingHorizontal:24, borderRadius:8, alignItems:'center', marginTop:8, }, buttonPressed: { opacity:0.7, }, buttonText: { color:'#fff', fontWeight:'600', },})
OAuthView.swift
importSwiftUIimportClerkKitstructOAuthView:View {@Environment(Clerk.self)privatevar clerkvar body: some View {// Render a button for each supported OAuth provider// you want to add to your app. This example uses Google.Button("Sign In with Google") {Task { awaitsignInWithOAuth(provider: .google) } } } }extensionOAuthView {funcsignInWithOAuth(provider: OAuthProvider) async {do {// Start the sign-in process using the selected OAuth provider.let result =tryawait clerk.auth.signInWithOAuth(provider: provider)// It is common for users who are authenticating with OAuth to use// a sign-in button when they mean to sign-up, and vice versa.// Clerk will handle this transfer for you if possible.// Therefore, a TransferFlowResult can be either a SignIn or SignUp.switch result {case .signIn(let signIn):switch signIn.status {case .complete:// If sign-in process is complete, navigate the user as needed.dump(clerk.session)default:// If the status is not complete, check why. User may need to// complete further steps.dump(signIn.status) }case .signUp(let signUp):switch signUp.status {case .complete:// If sign-up process is complete, navigate the user as needed.dump(clerk.session)default:// If the status is not complete, check why. User may need to// complete further steps.dump(signUp.status) } } } catch {// See https://clerk.com/docs/guides/development/custom-flows/error-handling// for more info on error handling.dump(error) } } }
OAuthViewModel.kt
OAuthActivity.kt
OAuthViewModel.kt
import android.util.Logimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport com.clerk.api.Clerkimport com.clerk.api.network.serialization.errorMessageimport com.clerk.api.network.serialization.onFailureimport com.clerk.api.network.serialization.onSuccessimport com.clerk.api.signin.SignInimport com.clerk.api.signup.SignUpimport com.clerk.api.sso.OAuthProviderimport com.clerk.api.sso.ResultTypeimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.flow.combineimport kotlinx.coroutines.flow.launchInimport kotlinx.coroutines.launchclassOAuthViewModel : ViewModel() {privateval _uiState =MutableStateFlow<UiState>(UiState.Loading)val uiState = _uiState.asStateFlow()init {combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> _uiState.value=when {!isInitialized -> UiState.Loading user !=null-> UiState.Authenticatedelse-> UiState.SignedOut } }.launchIn(viewModelScope) }funsignInWithOAuth(provider: OAuthProvider) { viewModelScope.launch { SignIn.authenticateWithRedirect(SignIn.AuthenticateWithRedirectParams.OAuth(provider)).onSuccess {when(it.resultType) { ResultType.SIGN_IN -> {// The OAuth flow resulted in a sign inif (it.signIn?.status == SignIn.Status.COMPLETE) { _uiState.value= UiState.Authenticated } else {// If the status is not complete, check why. User may need to// complete further steps. } } ResultType.SIGN_UP -> {// The OAuth flow resulted in a sign upif (it.signUp?.status == SignUp.Status.COMPLETE) { _uiState.value= UiState.Authenticated } else {// If the status is not complete, check why. User may need to// complete further steps. } } } }.onFailure {// See https://clerk.com/docs/guides/development/custom-flows/error-handling// for more info on error handling Log.e("OAuthViewModel", it.errorMessage, it.throwable) } } }sealedinterfaceUiState {dataobjectLoading : UiStatedataobjectSignedOut : UiStatedataobjectAuthenticated : UiState }}
Depending on your instance settings, users might need to provide extra information before their sign-up can be completed, such as when a username or accepting legal terms is required. In these cases, the SignUp object returns a status of "missing_requirements" along with a missingFields array. You can create a "Continue" page to collect these missing fields and complete the sign-up flow. Handling the missing requirements will depend on your instance settings. For example, if your instance settings require a phone number, you will need to handle verifying the phone number.
Quiz
Why does the "Continue" page use the useSignUp() hook? What if a user is using this flow to sign in?
With OAuth flows, it's common for users to try to sign in with an OAuth provider, but they don't have a Clerk account for your app yet. Clerk automatically transfers the flow from the SignIn object to the SignUp object, which returns the "missing_requirements" status and missingFields array needed to handle the missing requirements flow. This is why for the OAuth flow, the "Continue" page uses the useSignUp() hook and treats the missing requirements flow as a sign-up flow.
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
Continue page
SSO callback page
app/sign-in/continue/page.tsx
'use client'import { useState } from'react'import { useSignUp } from'@clerk/nextjs'import { useRouter } from'next/navigation'exportdefaultfunctionPage() {constrouter=useRouter()// Use `useSignUp()` hook to access the `SignUp` object// `missing_requirements` and `missingFields` are only available on the `SignUp` objectconst { isLoaded,signUp,setActive } =useSignUp()const [formData,setFormData] =useState<Record<string,string>>({})if (!isLoaded) return <div>Loading…</div>// Protect the page from users who are not in the sign-up flow// such as users who visited this route directlyif (!signUp.id) router.push('/sign-in')conststatus=signUp?.statusconstmissingFields=signUp?.missingFields ?? []consthandleChange= (field:string, value:string) => {setFormData((prev) => ({ ...prev, [field]: value })) }consthandleSubmit=async (e:React.FormEvent) => {e.preventDefault()try {// Update the `SignUp` object with the missing fields// The logic that goes here will depend on your instance settings// E.g. if your app requires a phone number, you will need to collect and verify it hereconstres=awaitsignUp?.update(formData)if (res?.status ==='complete') {awaitsetActive({ session:res.createdSessionId,navigate:async ({ session }) => {if (session?.currentTask) {// Handle pending session tasks// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasksconsole.log(session?.currentTask)router.push('/sign-in/tasks')return }router.push('/') }, }) } } catch (err) {// See https://clerk.com/docs/guides/development/custom-flows/error-handling// for more info on error handlingconsole.error(JSON.stringify(err,null,2)) } }if (status ==='missing_requirements') {// For simplicity, all missing fields in this example are text inputs.// In a real app, you might want to handle them differently:// - legal_accepted: checkbox// - username: text with validation// - phone_number: phone input, etc.return ( <div> <h1>Continue sign-up</h1> <formonSubmit={handleSubmit}> {missingFields.map((field) => ( <divkey={field}> <label> {field}: <inputtype="text"value={formData[field] ||''}onChange={(e) =>handleChange(field,e.target.value)} /> </label> </div> ))} {/* Required for sign-up flows Clerk's bot sign-up protection is enabled by default */} <divid="clerk-captcha" /> <buttontype="submit">Submit</button> </form> </div> ) }// Handle other statuses if neededreturn ( <> {/* Required for sign-up flows Clerk's bot sign-up protection is enabled by default */} <divid="clerk-captcha" /> </> )}
app/sign-in/sso-callback/page.tsx
import { AuthenticateWithRedirectCallback } from'@clerk/nextjs'exportdefaultfunctionPage() {// Set the `continueSignUpUrl` to the route of your "Continue" page// Once a user authenticates with the OAuth provider, they will be redirected to that routereturn ( <> <AuthenticateWithRedirectCallbackcontinueSignUpUrl="/sign-in/continue" /> {/* Required for sign-up flows Clerk's bot sign-up protection is enabled by default */} <divid="clerk-captcha" /> </> )}
app/(auth)/sign-in/continue/page.tsx
import { ThemedText } from'@/components/themed-text'import { ThemedView } from'@/components/themed-view'import { useSignUp } from'@clerk/clerk-expo'import { useRouter } from'expo-router'import { useState } from'react'import { Pressable, StyleSheet, TextInput, View } from'react-native'exportdefaultfunctionPage() {constrouter=useRouter()// Use `useSignUp()` hook to access the `SignUp` object// `missing_requirements` and `missingFields` are only available on the `SignUp` objectconst { isLoaded,signUp,setActive } =useSignUp()const [formData,setFormData] =useState<Record<string,string>>({})if (!isLoaded)return ( <ThemedViewstyle={styles.container}> <ThemedText>Loading…</ThemedText> </ThemedView> )// Protect the page from users who are not in the sign-up flow// such as users who visited this route directlyif (!signUp.id) router.push('/sign-in')conststatus=signUp?.statusconstmissingFields=signUp?.missingFields ?? []consthandleChange= (field:string, value:string) => {setFormData((prev) => ({ ...prev, [field]: value })) }consthandleSubmit=async () => {try {// Update the `SignUp` object with the missing fields// The logic that goes here will depend on your instance settings// E.g. if your app requires a phone number, you will need to collect and verify it hereconsole.log(formData)constres=awaitsignUp?.update(formData)if (res?.status ==='complete') {awaitsetActive({ session:res.createdSessionId,navigate:async ({ session }) => {if (session?.currentTask) {// Handle pending session tasks// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasksconsole.log(session?.currentTask)return }router.push('/') }, }) } } catch (err) {// See https://clerk.com/docs/guides/development/custom-flows/error-handling// for more info on error handlingconsole.error(JSON.stringify(err,null,2)) } }if (status ==='missing_requirements') {// For simplicity, all missing fields in this example are text inputs.// In a real app, you might want to handle them differently:// - legal_accepted: checkbox// - username: text with validation// - phone_number: phone input, etc.return ( <ThemedViewstyle={styles.container}> <ThemedTexttype="title"style={styles.title}> Continue sign-up </ThemedText> <ThemedTextstyle={styles.description}> Please complete the required fields to continue </ThemedText> {missingFields.map((field) => ( <Viewkey={field} style={styles.fieldContainer}> <ThemedTextstyle={styles.label}>{field.replace(/_/g,' ')}</ThemedText> <TextInputstyle={styles.input}value={formData[field] ||''}onChangeText={(value) =>handleChange(field, value)}placeholder={`Enter ${field.replace(/_/g,' ')}`}placeholderTextColor="#666666" /> </View> ))} {/* Required for sign-up flows Clerk's bot sign-up protection is enabled by default */} <Viewid="clerk-captcha" /> <Pressablestyle={({ pressed }) => [styles.button, pressed &&styles.buttonPressed]}onPress={handleSubmit} > <ThemedTextstyle={styles.buttonText}>Submit</ThemedText> </Pressable> </ThemedView> ) }// Handle other statuses if neededreturn ( <ThemedViewstyle={styles.container}> {/* Required for sign-up flows Clerk's bot sign-up protection is enabled by default */} <Viewid="clerk-captcha" /> </ThemedView> )}conststyles=StyleSheet.create({ container: { flex:1, padding:20, gap:12, }, title: { marginBottom:8, }, description: { fontSize:14, marginBottom:16, opacity:0.8, }, fieldContainer: { gap:4, }, label: { fontWeight:'600', fontSize:14, textTransform:'capitalize', }, input: { borderWidth:1, borderColor:'#ccc', borderRadius:8, padding:12, fontSize:16, backgroundColor:'#fff', }, button: { backgroundColor:'#0a7ea4', paddingVertical:12, paddingHorizontal:24, borderRadius:8, alignItems:'center', marginTop:8, }, buttonPressed: { opacity:0.7, }, buttonText: { color:'#fff', fontWeight:'600', },})
Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.