This tutorial extends the authentication system built in the previous parts by adding Sign in with Apple functionality. You’ll learn how to configure Apple Sign In in the Nhost dashboard, implement the native iOS integration, and handle the authentication flow in your React Native application.
This is Part 6 in the Full-Stack React Native Development with Nhost series. This part focuses on integrating Apple’s native authentication system with your existing Nhost authentication flow.
Full-Stack React Native Development with Nhost
Prerequisites
Complete the User Authentication part first
Access to an Apple Developer account (required for Sign in with Apple)
An iOS device or simulator for testing (Sign in with Apple requires iOS)
The project from the previous parts set up and running
What You’ll Build
By the end of this part, you’ll have:
Apple Developer Console configuration for Sign in with Apple
Nhost Auth provider setup for Apple authentication
Native Sign in with Apple button in your React Native app
Seamless integration with your existing authentication flow
Error handling for Apple Sign In specific scenarios
Step-by-Step Guide
Before we start we need to configure Apple Sign In. To do so, follow the sign in with Apple guide. In addition, you will have to configure the audience in the nhost dashboard. To do so, set the audience with your application’s bundle identifier (e.g. com.yourcompany.yourapp
). If you are testing this flow in Expo Go, you will need to configure the audience to be host.exp.Exponent
.
Install Required Dependencies Install the necessary packages for Apple Sign In in React Native: npx expo install expo-apple-authentication@7 expo-crypto@14 --legacy-peer-deps
The required packages provide:
expo-apple-authentication : Native Apple Sign In functionality and Apple-styled sign-in buttons
expo-crypto : Cryptographic utilities needed for secure nonce generation and hashing
Update App Configuration Add the Sign in with Apple configuration to your app.json
or app.config.js
: {
"expo" : {
"name" : "nhost-reactnative-tutorial2" ,
"slug" : "nhost-reactnative-tutorial2" ,
"version" : "1.0.0" ,
"orientation" : "portrait" ,
"icon" : "./assets/icon.png" ,
"userInterfaceStyle" : "light" ,
"newArchEnabled" : true ,
"splash" : {
"image" : "./assets/splash-icon.png" ,
"resizeMode" : "contain" ,
"backgroundColor" : "#ffffff"
},
"ios" : {
"supportsTablet" : true ,
"bundleIdentifier" : "io.example.nhost-reactnative-tutorial" ,
"jsEngine" : "jsc" ,
"infoPlist" : {
"NSFaceIDUsageDescription" : "This app uses Face ID for signing in" ,
"CFBundleURLTypes" : [
{
"CFBundleURLSchemes" : [ "nhost-reactnative-tutorial" ]
}
]
}
},
"android" : {
"adaptiveIcon" : {
"foregroundImage" : "./assets/adaptive-icon.png" ,
"backgroundColor" : "#ffffff"
},
"edgeToEdgeEnabled" : true ,
"predictiveBackGestureEnabled" : false
},
"web" : {
"favicon" : "./assets/favicon.png"
},
"plugins" : [
"expo-router" ,
[ "expo-apple-authentication" ]
],
"extra" : {
"NHOST_REGION" : <region> ,
"NHOST_SUBDOMAIN" : <subdomain>
}
}
}
Create Apple Sign In Component Create a reusable component for Apple Sign In functionality following the ReactNativeDemo approach: app/components/AppleSignInButton.tsx
import * as AppleAuthentication from "expo-apple-authentication" ;
import * as Crypto from "expo-crypto" ;
import { useRouter } from "expo-router" ;
import { useEffect , useState } from "react" ;
import { Alert , Platform , StyleSheet } from "react-native" ;
import { useAuth } from "../lib/nhost/AuthProvider" ;
interface AppleSignInButtonProps {
isLoading : boolean ;
setIsLoading : ( isLoading : boolean ) => void ;
}
export default function AppleSignInButton ({
setIsLoading ,
} : AppleSignInButtonProps ) {
const { nhost } = useAuth ();
const router = useRouter ();
const [ appleAuthAvailable , setAppleAuthAvailable ] = useState ( false );
useEffect (() => {
const checkAvailability = async () => {
if ( Platform . OS === "ios" ) {
const isAvailable = await AppleAuthentication . isAvailableAsync ();
setAppleAuthAvailable ( isAvailable );
}
};
void checkAvailability ();
}, []);
const handleAppleSignIn = async () => {
try {
setIsLoading ( true );
// Generate a random nonce for security
const nonce = Math . random (). toString ( 36 ). substring ( 2 , 15 );
// Hash the nonce for Apple Authentication
const hashedNonce = await Crypto . digestStringAsync (
Crypto . CryptoDigestAlgorithm . SHA256 ,
nonce ,
);
// Request Apple authentication with our hashed nonce
const credential = await AppleAuthentication . signInAsync ({
requestedScopes: [
AppleAuthentication . AppleAuthenticationScope . FULL_NAME ,
AppleAuthentication . AppleAuthenticationScope . EMAIL ,
],
nonce: hashedNonce ,
});
if ( credential . identityToken ) {
// Use the identity token to sign in with Nhost
// Pass the original unhashed nonce to the SDK
// so the server can verify it
const response = await nhost . auth . signInIdToken ({
provider: "apple" ,
idToken: credential . identityToken ,
nonce ,
});
if ( response . body ?. session ) {
router . replace ( "/profile" );
} else {
Alert . alert (
"Authentication Error" ,
"Failed to authenticate with Nhost" ,
);
}
} else {
Alert . alert (
"Authentication Error" ,
"No identity token received from Apple" ,
);
}
} catch ( error : unknown ) {
// Handle user cancellation gracefully
if ( error instanceof Error && error . message . includes ( "canceled" )) {
// User cancelled the sign-in flow, don't show an error
return ;
}
// Handle other errors
const message =
error instanceof Error
? error . message
: "Failed to authenticate with Apple" ;
Alert . alert ( "Authentication Error" , message );
} finally {
setIsLoading ( false );
}
};
// Only show the button on iOS devices where Apple authentication is available
if ( Platform . OS !== "ios" || ! appleAuthAvailable ) {
return null ;
}
return (
< AppleAuthentication.AppleAuthenticationButton
buttonType = { AppleAuthentication . AppleAuthenticationButtonType . SIGN_IN }
buttonStyle = { AppleAuthentication . AppleAuthenticationButtonStyle . BLACK }
cornerRadius = { 5 }
style = { styles . appleButton }
onPress = { handleAppleSignIn }
/>
);
}
const styles = StyleSheet . create ({
appleButton: {
width: "100%" ,
height: 45 ,
marginBottom: 10 ,
},
});
Update Sign In Screen Now integrate the Apple Sign In button into your existing sign-in screen following the tutorial structure: import { Link , router } from "expo-router" ;
import { useEffect , useState } from "react" ;
import {
ActivityIndicator ,
KeyboardAvoidingView ,
Platform ,
ScrollView ,
Text ,
TextInput ,
TouchableOpacity ,
View ,
} from "react-native" ;
import AppleSignInButton from "./components/AppleSignInButton" ;
import { useAuth } from "./lib/nhost/AuthProvider" ;
import { commonStyles } from "./styles/commonStyles" ;
import { colors } from "./styles/theme" ;
export default function SignIn () {
const { nhost , isAuthenticated } = useAuth ();
const [ email , setEmail ] = useState ( "" );
const [ password , setPassword ] = useState ( "" );
const [ isLoading , setIsLoading ] = useState ( false );
const [ error , setError ] = useState < string | null >( null );
// Use useEffect for navigation after authentication is confirmed
useEffect (() => {
if ( isAuthenticated ) {
router . replace ( "/profile" );
}
}, [ isAuthenticated ]);
const handleSubmit = async () => {
setIsLoading ( true );
setError ( null );
try {
// Use the signIn function from auth context
const response = await nhost . auth . signInEmailPassword ({
email ,
password ,
});
// If we have a session, sign in was successful
if ( response . body ?. session ) {
router . replace ( "/profile" );
} else {
setError ( "Failed to sign in. Please check your credentials." );
}
} catch ( err ) {
const message = ( err as Error ). message || "Unknown error" ;
setError ( `An error occurred during sign in: ${ message } ` );
} finally {
setIsLoading ( false );
}
};
return (
< KeyboardAvoidingView
behavior = { Platform . OS === "ios" ? "padding" : "height" }
style = { commonStyles . container }
>
< ScrollView
contentContainerStyle = { commonStyles . centerContent }
keyboardShouldPersistTaps = "handled"
>
< Text style = { commonStyles . title } > Sign In </ Text >
< View style = { commonStyles . card } >
{ /* Apple Sign In Button */ }
< AppleSignInButton
isLoading = { isLoading }
setIsLoading = { setIsLoading }
/>
{ /* Divider */ }
< View style = { commonStyles . dividerContainer } >
< View style = { commonStyles . divider } />
< Text style = { commonStyles . dividerText } > or </ Text >
< View style = { commonStyles . divider } />
</ View >
< View style = { commonStyles . formField } >
< Text style = { commonStyles . labelText } > Email </ Text >
< TextInput
style = { commonStyles . input }
value = { email }
onChangeText = { setEmail }
placeholder = "Enter your email"
keyboardType = "email-address"
autoCapitalize = "none"
autoComplete = "email"
/>
</ View >
< View style = { commonStyles . formField } >
< Text style = { commonStyles . labelText } > Password </ Text >
< TextInput
style = { commonStyles . input }
value = { password }
onChangeText = { setPassword }
placeholder = "Enter your password"
secureTextEntry
autoCapitalize = "none"
/>
</ View >
{ error && (
< View style = { commonStyles . errorContainer } >
< Text style = { commonStyles . errorText } > { error } </ Text >
</ View >
) }
< TouchableOpacity
style = { [ commonStyles . button , commonStyles . fullWidth ] }
onPress = { handleSubmit }
disabled = { isLoading }
>
{ isLoading ? (
< ActivityIndicator size = "small" color = { colors . surface } />
) : (
< Text style = { commonStyles . buttonText } > Sign In </ Text >
) }
</ TouchableOpacity >
</ View >
< View style = { commonStyles . linkContainer } >
< Text style = { commonStyles . linkText } >
Don't have an account? { " " }
< Link href = "/signup" style = { commonStyles . link } >
Sign Up
</ Link >
</ Text >
</ View >
</ ScrollView >
</ KeyboardAvoidingView >
);
}
Update Sign Up Screen Also add Apple Sign In to your sign-up screen for consistency: import * as Linking from "expo-linking" ;
import { Link , router } from "expo-router" ;
import { useEffect , useState } from "react" ;
import {
ActivityIndicator ,
KeyboardAvoidingView ,
Platform ,
ScrollView ,
Text ,
TextInput ,
TouchableOpacity ,
View ,
} from "react-native" ;
import AppleSignInButton from "./components/AppleSignInButton" ;
import { useAuth } from "./lib/nhost/AuthProvider" ;
import { commonStyles } from "./styles/commonStyles" ;
import { colors } from "./styles/theme" ;
export default function SignUp () {
const { nhost , isAuthenticated } = useAuth ();
const [ email , setEmail ] = useState ( "" );
const [ password , setPassword ] = useState ( "" );
const [ displayName , setDisplayName ] = useState ( "" );
const [ isLoading , setIsLoading ] = useState ( false );
const [ error , setError ] = useState < string | null >( null );
const [ success , setSuccess ] = useState ( false );
// Redirect authenticated users to profile
useEffect (() => {
if ( isAuthenticated ) {
router . replace ( "/profile" );
}
}, [ isAuthenticated ]);
const handleSubmit = async () => {
setIsLoading ( true );
setError ( null );
setSuccess ( false );
try {
const response = await nhost . auth . signUpEmailPassword ({
email ,
password ,
options: {
displayName ,
// Set the redirect URL for email verification
redirectTo: Linking . createURL ( "verify" ),
},
});
if ( response . body ?. session ) {
// Successfully signed up and automatically signed in
router . replace ( "/profile" );
} else {
// Verification email sent
setSuccess ( true );
}
} catch ( err ) {
const message = ( err as Error ). message || "Unknown error" ;
setError ( `An error occurred during sign up: ${ message } ` );
} finally {
setIsLoading ( false );
}
};
if ( success ) {
return (
< View style = { commonStyles . centerContent } >
< Text style = { commonStyles . title } > Check Your Email </ Text >
< View style = { commonStyles . successContainer } >
< Text style = { commonStyles . successText } >
We've sent a verification link to { " " }
< Text style = { commonStyles . emailText } > { email } </ Text >
</ Text >
< Text style = { [ commonStyles . bodyText , commonStyles . textCenter ] } >
Please check your email and click the verification link to activate
your account.
</ Text >
</ View >
< TouchableOpacity
style = { [ commonStyles . button , commonStyles . fullWidth ] }
onPress = { () => router . replace ( "/signin" ) }
>
< Text style = { commonStyles . buttonText } > Back to Sign In </ Text >
</ TouchableOpacity >
</ View >
);
}
return (
< KeyboardAvoidingView
behavior = { Platform . OS === "ios" ? "padding" : "height" }
style = { commonStyles . container }
>
< ScrollView
contentContainerStyle = { commonStyles . centerContent }
keyboardShouldPersistTaps = "handled"
>
< Text style = { commonStyles . title } > Sign Up </ Text >
< View style = { commonStyles . card } >
{ /* Apple Sign In Button */ }
< AppleSignInButton
isLoading = { isLoading }
setIsLoading = { setIsLoading }
/>
{ /* Divider */ }
< View style = { commonStyles . dividerContainer } >
< View style = { commonStyles . divider } />
< Text style = { commonStyles . dividerText } > or </ Text >
< View style = { commonStyles . divider } />
</ View >
< View style = { commonStyles . formField } >
< Text style = { commonStyles . labelText } > Display Name </ Text >
< TextInput
style = { commonStyles . input }
value = { displayName }
onChangeText = { setDisplayName }
placeholder = "Enter your name"
autoCapitalize = "words"
/>
</ View >
< View style = { commonStyles . formField } >
< Text style = { commonStyles . labelText } > Email </ Text >
< TextInput
style = { commonStyles . input }
value = { email }
onChangeText = { setEmail }
placeholder = "Enter your email"
keyboardType = "email-address"
autoCapitalize = "none"
autoComplete = "email"
/>
</ View >
< View style = { commonStyles . formField } >
< Text style = { commonStyles . labelText } > Password </ Text >
< TextInput
style = { commonStyles . input }
value = { password }
onChangeText = { setPassword }
placeholder = "Enter your password"
secureTextEntry
autoCapitalize = "none"
/>
< Text style = { commonStyles . helperText } > Minimum 8 characters </ Text >
</ View >
{ error && (
< View style = { commonStyles . errorContainer } >
< Text style = { commonStyles . errorText } > { error } </ Text >
</ View >
) }
< TouchableOpacity
style = { [ commonStyles . button , commonStyles . fullWidth ] }
onPress = { handleSubmit }
disabled = { isLoading }
>
{ isLoading ? (
< ActivityIndicator size = "small" color = { colors . surface } />
) : (
< Text style = { commonStyles . buttonText } > Sign Up </ Text >
) }
</ TouchableOpacity >
</ View >
< View style = { commonStyles . linkContainer } >
< Text style = { commonStyles . linkText } >
Already have an account? { " " }
< Link href = "/signin" style = { commonStyles . link } >
Sign In
</ Link >
</ Text >
</ View >
</ ScrollView >
</ KeyboardAvoidingView >
);
}
Test Sign in with Apple Build and test your application on an iOS device or simulator: Things to test:
Apple Sign In Flow : Tap the “Sign in with Apple” button and complete the authentication
User Creation : Sign in with Apple for the first time to create a new user account
Returning Users : Sign in again with the same Apple ID to verify user recognition
Profile Information : Check that user name and email are properly populated
Integration : Verify that Apple Sign In users can access todos, files, and other protected features
Error Handling : Test network errors and authentication failures
Sign in with Apple only works on physical iOS devices or iOS simulators running iOS 13+. It will not work on Android or web platforms in this implementation.
Key Features Implemented
Apple Developer Configuration
Complete setup of Apple Developer Console including App ID configuration, Service ID creation, and private key generation for secure authentication.
Nhost Provider Integration
Seamless integration with Nhost Auth system using Apple’s identity tokens and authorization codes for secure user authentication.
Native Apple Sign In button using expo-apple-authentication with proper iOS styling and user experience guidelines.
Comprehensive error handling for Apple Sign In failures, network issues, and user cancellations with appropriate user feedback.