MoonKey’s React SDK lets you fully customize authentication to match your app’s brand and user experience. You can implement every authentication flow—including email OTP, OAuth (Google and Apple), and Web3 wallet signatures—with your own UI, while MoonKey manages security and backend logic.
All of MoonKey’s authentication methods can be whitelabeled, allowing you to build completely custom authentication flows without using the default UI components.
Available authentication methods
MoonKey provides headless authentication hooks that give you full control over your UI:
Email OTP Passwordless email authentication with one-time codes
OAuth Social login with Google and Apple
Wallet Auth Web3 authentication with SIWE and SIWS
Email OTP Authentication
To whitelabel MoonKey’s passwordless email flow, use the useLoginWithEmail hook. Call sendCode to send a verification code and loginWithCode to authenticate the user.
Basic usage
import { useLoginWithEmail } from '@moon-key/react-auth' ;
function CustomEmailLogin () {
const { sendCode , loginWithCode , state } = useLoginWithEmail ();
// Send code to user's email
await sendCode ({ email: 'user@example.com' });
// Login with the code
await loginWithCode ({ code: '123456' });
}
With callbacks
You can pass callbacks to handle success and error states:
import { useLoginWithEmail } from '@moon-key/react-auth' ;
import { useState } from 'react' ;
function CustomEmailLogin () {
const [ email , setEmail ] = useState ( '' );
const [ code , setCode ] = useState ( '' );
const { sendCode , loginWithCode , state } = useLoginWithEmail ({
onComplete : ( user ) => {
console . log ( '✅ Login successful:' , user );
// Redirect to dashboard or update UI
},
onError : ( error ) => {
console . error ( '❌ Login failed:' , error );
// Show error message to user
}
});
const handleSendCode = async () => {
try {
await sendCode ({ email });
alert ( `Code sent to ${ email } !` );
} catch ( error ) {
// Error handled by onError callback
}
};
const handleLogin = async () => {
try {
await loginWithCode ({ code });
} catch ( error ) {
// Error handled by onError callback
}
};
const isLoading = state . status === 'sending-code' ||
state . status === 'verifying-code' ;
return (
< div >
< input
type = "email"
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
placeholder = "Enter your email"
disabled = { isLoading }
/>
< button onClick = { handleSendCode } disabled = { isLoading } >
{ state . status === 'sending-code' ? 'Sending...' : 'Send Code' }
</ button >
{ state . status === 'waiting-code-input' && (
<>
< input
type = "text"
value = { code }
onChange = { ( e ) => setCode ( e . target . value ) }
placeholder = "Enter verification code"
maxLength = { 6 }
disabled = { isLoading }
/>
< button onClick = { handleLogin } disabled = { isLoading } >
{ state . status === 'verifying-code' ? 'Verifying...' : 'Login' }
</ button >
</>
) }
{ state . status === 'error' && state . error && (
< p className = "error" > Error: { state . error . message } </ p >
) }
</ div >
);
}
Flow state tracking
The state object tracks the current status of the authentication flow:
The current status of the authentication flow. Possible values:
'initial' - Flow hasn’t started
'sending-code' - Sending OTP to email
'waiting-code-input' - Code sent, waiting for user input
'verifying-code' - Verifying the entered code
'complete' - Authentication successful
'error' - An error occurred
Error object if an error occurred, null otherwise.
Learn more about email authentication .
OAuth Authentication
To whitelabel OAuth login with Google and Apple, use the useLoginWithOAuth hook and call initOAuth with your desired provider.
Basic usage
import { useLoginWithOAuth } from '@moon-key/react-auth' ;
function CustomOAuthLogin () {
const { initOAuth } = useLoginWithOAuth ();
// Initiate Google OAuth flow
initOAuth ({ provider: 'google' });
// Initiate Apple OAuth flow
initOAuth ({ provider: 'apple' });
}
With callbacks
import { useLoginWithOAuth } from '@moon-key/react-auth' ;
function CustomOAuthLogin () {
const { initOAuth , state } = useLoginWithOAuth ({
onSuccess : ( user ) => {
console . log ( '✅ OAuth login successful:' , user );
// User is authenticated and stays on current page
},
onError : ( error ) => {
console . error ( '❌ OAuth login failed:' , error );
// Handle error (e.g., show error message)
}
});
const handleGoogleLogin = () => {
initOAuth ({ provider: 'google' });
};
const handleAppleLogin = () => {
initOAuth ({ provider: 'apple' });
};
return (
< div >
< button
onClick = { handleGoogleLogin }
disabled = { state . status === 'loading' }
>
{ state . status === 'loading' ? 'Signing in...' : 'Sign in with Google' }
</ button >
< button
onClick = { handleAppleLogin }
disabled = { state . status === 'loading' }
>
{ state . status === 'loading' ? 'Signing in...' : 'Sign in with Apple' }
</ button >
{ state . status === 'error' && state . error && (
< p className = "error" > Error: { state . error . message } </ p >
) }
</ div >
);
}
OAuth state tracking
The state object tracks the OAuth flow:
The current status of the OAuth flow. Possible values:
'idle' - No OAuth flow in progress
'loading' - OAuth flow in progress
'done' - OAuth authentication successful
'error' - An error occurred
The authenticated user object after successful OAuth login.
Error object if an error occurred.
You must configure OAuth credentials in the MoonKey Dashboard before using OAuth authentication.
Learn more about OAuth authentication .
Complete example
Here’s a complete whitelabel authentication example with both email and OAuth:
'use client' ;
import { useLoginWithEmail , useLoginWithOAuth , useMoonKey } from '@moon-key/react-auth' ;
import { useState } from 'react' ;
export default function CustomAuthPage () {
const { isAuthenticated , user , logout , ready } = useMoonKey ();
const [ email , setEmail ] = useState ( '' );
const [ code , setCode ] = useState ( '' );
const [ message , setMessage ] = useState ( '' );
// Email authentication hook
const { sendCode , loginWithCode , state : emailState } = useLoginWithEmail ({
onComplete : ( user ) => {
console . log ( '✅ Email login successful:' , user );
setMessage ( 'Login successful!' );
},
onError : ( error ) => {
console . error ( '❌ Email login failed:' , error );
setMessage ( `Error: ${ error . message } ` );
}
});
// OAuth authentication hook
const { initOAuth , state : oauthState } = useLoginWithOAuth ({
onSuccess : ( user ) => {
console . log ( '✅ OAuth login successful:' , user );
setMessage ( 'OAuth login successful!' );
},
onError : ( error ) => {
console . error ( '❌ OAuth login failed:' , error );
setMessage ( `OAuth error: ${ error . message } ` );
}
});
const handleSendCode = async () => {
setMessage ( '' );
try {
await sendCode ({ email });
setMessage ( `Code sent to ${ email } !` );
} catch ( error ) {
// Handled by onError
}
};
const handleLogin = async () => {
setMessage ( '' );
try {
await loginWithCode ({ code });
} catch ( error ) {
// Handled by onError
}
};
const handleLogout = async () => {
await logout ();
setMessage ( 'Logged out successfully' );
setEmail ( '' );
setCode ( '' );
};
const isLoading = emailState . status === 'sending-code' ||
emailState . status === 'verifying-code' ||
oauthState . status === 'loading' ;
if ( ! ready ) {
return < div > Loading MoonKey SDK... </ div > ;
}
if ( isAuthenticated && user ) {
return (
< div className = "auth-container" >
< h1 > Welcome! </ h1 >
< p > User ID: { user . id } </ p >
< p > Email: { user . email ?. address } </ p >
< button onClick = { handleLogout } > Logout </ button >
</ div >
);
}
return (
< div className = "auth-container" >
< h1 > Sign In </ h1 >
{ message && < div className = "message" > { message } </ div > }
{ /* Email Authentication */ }
< div className = "auth-section" >
< h2 > Email Login </ h2 >
< div className = "form-group" >
< input
type = "email"
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
placeholder = "Enter your email"
disabled = { isLoading }
/>
< button onClick = { handleSendCode } disabled = { ! email || isLoading } >
{ emailState . status === 'sending-code' ? 'Sending...' : 'Send Code' }
</ button >
</ div >
{ emailState . status === 'waiting-code-input' && (
< div className = "form-group" >
< input
type = "text"
value = { code }
onChange = { ( e ) => setCode ( e . target . value ) }
placeholder = "Enter 6-digit code"
maxLength = { 6 }
disabled = { isLoading }
/>
< button onClick = { handleLogin } disabled = { ! code || isLoading } >
{ emailState . status === 'verifying-code' ? 'Verifying...' : 'Login' }
</ button >
</ div >
) }
{ emailState . status === 'error' && emailState . error && (
< p className = "error" > { emailState . error . message } </ p >
) }
</ div >
< div className = "divider" > OR </ div >
{ /* OAuth Authentication */ }
< div className = "auth-section" >
< h2 > Social Login </ h2 >
< button
className = "oauth-button google"
onClick = { () => initOAuth ({ provider: 'google' }) }
disabled = { isLoading }
>
< GoogleIcon />
{ oauthState . status === 'loading' ? 'Signing in...' : 'Continue with Google' }
</ button >
< button
className = "oauth-button apple"
onClick = { () => initOAuth ({ provider: 'apple' }) }
disabled = { isLoading }
>
< AppleIcon />
{ oauthState . status === 'loading' ? 'Signing in...' : 'Continue with Apple' }
</ button >
{ oauthState . status === 'error' && oauthState . error && (
< p className = "error" > { oauthState . error . message } </ p >
) }
</ div >
</ div >
);
}
// Icon components (simplified)
function GoogleIcon () {
return < span > G </ span > ;
}
function AppleIcon () {
return < span > 🍎 </ span > ;
}
Styling example
.auth-container {
max-width : 400 px ;
margin : 0 auto ;
padding : 2 rem ;
}
.auth-section {
margin : 2 rem 0 ;
}
.form-group {
display : flex ;
gap : 0.5 rem ;
margin-bottom : 1 rem ;
}
.form-group input {
flex : 1 ;
padding : 0.75 rem ;
border : 1 px solid #ddd ;
border-radius : 0.5 rem ;
}
.form-group button {
padding : 0.75 rem 1.5 rem ;
background : #6366f1 ;
color : white ;
border : none ;
border-radius : 0.5 rem ;
cursor : pointer ;
}
.form-group button :disabled {
background : #d1d5db ;
cursor : not-allowed ;
}
.oauth-button {
width : 100 % ;
padding : 0.75 rem ;
margin-bottom : 0.5 rem ;
border : 1 px solid #ddd ;
border-radius : 0.5 rem ;
background : white ;
cursor : pointer ;
display : flex ;
align-items : center ;
justify-content : center ;
gap : 0.5 rem ;
}
.oauth-button:hover {
background : #f9fafb ;
}
.oauth-button.apple {
background : #000 ;
color : white ;
}
.message {
padding : 1 rem ;
margin-bottom : 1 rem ;
background : #d1fae5 ;
border-radius : 0.5 rem ;
color : #065f46 ;
}
.error {
color : #dc2626 ;
font-size : 0.875 rem ;
margin-top : 0.5 rem ;
}
.divider {
text-align : center ;
margin : 2 rem 0 ;
position : relative ;
}
.divider::before ,
.divider::after {
content : '' ;
position : absolute ;
top : 50 % ;
width : 45 % ;
height : 1 px ;
background : #ddd ;
}
.divider::before {
left : 0 ;
}
.divider::after {
right : 0 ;
}
Best practices
Always show loading indicators during authentication: const isLoading = state . status === 'sending-code' ||
state . status === 'verifying-code' ;
< button disabled = { isLoading } >
{ isLoading ? 'Loading...' : 'Send Code' }
</ button >
Provide clear error messages
Display user-friendly error messages: { state . status === 'error' && state . error && (
< div className = "error" >
{ state . error . message }
</ div >
)}
Use callbacks for side effects
Handle navigation and UI updates in callbacks: const { sendCode } = useLoginWithEmail ({
onComplete : ( user ) => {
// Navigate to dashboard
router . push ( '/dashboard' );
},
onError : ( error ) => {
// Show toast notification
toast . error ( error . message );
}
});
Track authentication state
Use the flow state to show relevant UI: // Show code input only after sending code
{ state . status === 'waiting-code-input' && (
< input placeholder = "Enter code" />
)}
// Show success message
{ state . status === 'complete' && (
< p > Authentication successful! </ p >
)}
Disable inputs during loading
Check the ready state before rendering auth UI: const { ready } = useMoonKey ();
if ( ! ready ) {
return < div > Loading... </ div > ;
}
Advanced patterns
Build multi-step authentication flows:
function MultiStepAuth () {
const [ step , setStep ] = useState < 'email' | 'code' | 'complete' >( 'email' );
const { sendCode , loginWithCode , state } = useLoginWithEmail ({
onComplete : () => setStep ( 'complete' )
});
switch ( step ) {
case 'email' :
return < EmailStep onNext = { () => setStep ( 'code' ) } /> ;
case 'code' :
return < CodeStep onBack = { () => setStep ( 'email' ) } /> ;
case 'complete' :
return < CompleteStep /> ;
}
}
Conditional authentication
Show different auth methods based on user type:
function ConditionalAuth ({ userType } : { userType : 'new' | 'returning' }) {
if ( userType === 'new' ) {
return < EmailOnlyAuth /> ;
}
return < AllAuthMethods /> ;
}
Prefilled email
Pre-populate email for invited users:
function InviteAuth ({ inviteEmail } : { inviteEmail : string }) {
const [ email ] = useState ( inviteEmail );
const { sendCode } = useLoginWithEmail ();
useEffect (() => {
// Automatically send code to invited email
sendCode ({ email });
}, []);
return < CodeInput /> ;
}
TypeScript types
For TypeScript users, here are the relevant types:
// Email authentication
interface UseLoginWithEmailOptions {
onComplete ?: ( user : User ) => void ;
onError ?: ( error : Error ) => void ;
}
interface EmailAuthState {
status : 'initial' | 'sending-code' | 'waiting-code-input' | 'verifying-code' | 'complete' | 'error' ;
error : Error | null ;
}
// OAuth authentication
interface UseLoginWithOAuthOptions {
onSuccess ?: ( user : User ) => void ;
onError ?: ( error : Error ) => void ;
}
interface OAuthState {
status : 'idle' | 'loading' | 'done' | 'error' ;
user : User | null ;
error : Error | null ;
}
Troubleshooting
If users don’t receive the verification code:
Check spam/junk folders
Verify the email address is correct
Wait a few moments before resending
Check your email configuration in the MoonKey Dashboard
If OAuth redirects fail:
Verify redirect URLs in the MoonKey Dashboard
Check that OAuth credentials are configured
Ensure your domain is whitelisted
Test in an incognito window to rule out browser issues
If the state doesn’t update correctly:
Ensure you’re using the latest SDK version
Check that callbacks are properly defined
Verify the SDK is initialized correctly
Look for console errors