Skip to main content
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:
state.status
string
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
state.error
Error | null
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:
state.status
string
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
state.user
User | null
The authenticated user object after successful OAuth login.
state.error
Error | null
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: 400px;
  margin: 0 auto;
  padding: 2rem;
}

.auth-section {
  margin: 2rem 0;
}

.form-group {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.form-group input {
  flex: 1;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 0.5rem;
}

.form-group button {
  padding: 0.75rem 1.5rem;
  background: #6366f1;
  color: white;
  border: none;
  border-radius: 0.5rem;
  cursor: pointer;
}

.form-group button:disabled {
  background: #d1d5db;
  cursor: not-allowed;
}

.oauth-button {
  width: 100%;
  padding: 0.75rem;
  margin-bottom: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 0.5rem;
  background: white;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.oauth-button:hover {
  background: #f9fafb;
}

.oauth-button.apple {
  background: #000;
  color: white;
}

.message {
  padding: 1rem;
  margin-bottom: 1rem;
  background: #d1fae5;
  border-radius: 0.5rem;
  color: #065f46;
}

.error {
  color: #dc2626;
  font-size: 0.875rem;
  margin-top: 0.5rem;
}

.divider {
  text-align: center;
  margin: 2rem 0;
  position: relative;
}

.divider::before,
.divider::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 45%;
  height: 1px;
  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>
Display user-friendly error messages:
{state.status === 'error' && state.error && (
  <div className="error">
    {state.error.message}
  </div>
)}
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);
  }
});
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>
)}
Prevent users from modifying inputs during operations:
<input
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  disabled={isLoading || !ready}
/>
Check the ready state before rendering auth UI:
const { ready } = useMoonKey();

if (!ready) {
  return <div>Loading...</div>;
}

Advanced patterns

Multi-step forms

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