Authentication Guide
Complete guide for authenticating users and managing wallets with Privy.
🔐 Powered by Privy: Loyalteez uses Privy for secure authentication and embedded wallet creation.
Overview
Loyalteez uses Privy for:
- User authentication (email, social, wallet)
- Embedded wallet creation
- Secure token management
- Gasless transaction signing
You don't need Privy credentials - Loyalteez handles authentication server-side. You only need to integrate Privy's client SDK.
Setup
✨ No Privy Account Needed: Loyalteez handles wallet creation server-side. You only need Privy SDK if you want to use our Gas Relayer for gasless transactions.
When You Need Privy
✅ You NEED Privy if:
- Using the Gas Relayer for gasless transactions
- Building a frontend with user authentication
✅ You DON'T NEED Privy if:
- Only tracking events (REST API)
- Only creating wallets via OAuth pregeneration
- Backend-only integration
Install Privy SDK (if needed)
npm install @privy-io/react-auth
Privy App ID: Contact [email protected] to get your Privy App ID for the Gas Relayer
Basic Setup
React App
App.jsx
import React from 'react';
import { PrivyProvider } from '@privy-io/react-auth';
const PRIVY_APP_ID = 'your-privy-app-id'; // Or use Loyalteez's shared ID
function App() {
return (
<PrivyProvider
appId={PRIVY_APP_ID}
config={{
// Login methods
loginMethods: ['email', 'wallet', 'google', 'twitter'],
// Appearance
appearance: {
theme: 'light',
accentColor: '#6C33EA',
logo: 'https://yoursite.com/logo.png',
},
// Embedded wallets
embeddedWallets: {
createOnLogin: 'users-without-wallets',
},
// Default chain (Soneium Mainnet)
defaultChain: {
id: 1868,
name: 'Soneium Mainnet',
network: 'soneium',
nativeCurrency: {
decimals: 18,
name: 'ETH',
symbol: 'ETH',
},
rpcUrls: {
default: { http: ['https://rpc.soneium.org'] },
public: { http: ['https://rpc.soneium.org'] },
},
blockExplorers: {
default: {
name: 'Soneium Explorer',
url: 'https://soneium.blockscout.com',
},
},
},
}}
>
<YourApp />
</PrivyProvider>
);
}
export default App;
Authentication Patterns
1. Email Authentication
Simplest method - recommended for most apps:
import { usePrivy } from '@privy-io/react-auth';
function LoginButton() {
const { login, ready, authenticated, user } = usePrivy();
if (!ready) {
return <div>Loading...</div>;
}
if (authenticated) {
return <div>Welcome, {user.email?.address}!</div>;
}
return (
<button onClick={login}>
Log in with Email
</button>
);
}
How it works:
- User enters email
- Privy sends verification code
- User enters code
- Embedded wallet auto-created
- User is authenticated
2. Social Authentication
Enable social login (Google, Twitter, Discord, etc.):
import { usePrivy } from '@privy-io/react-auth';
function SocialLogin() {
const { login, ready, authenticated } = usePrivy();
return (
<div>
<button onClick={() => login({ loginMethod: 'google' })}>
Continue with Google
</button>
<button onClick={() => login({ loginMethod: 'twitter' })}>
Continue with Twitter
</button>
<button onClick={() => login({ loginMethod: 'discord' })}>
Continue with Discord
</button>
</div>
);
}
Supported providers:
- Twitter (X)
- Discord
- GitHub
- Apple
- Spotify
- TikTok
3. Wallet Authentication
For users with existing wallets (MetaMask, WalletConnect, etc.):
import { usePrivy } from '@privy-io/react-auth';
function WalletLogin() {
const { connectWallet, ready } = usePrivy();
return (
<button
onClick={connectWallet}
disabled={!ready}
>
Connect Wallet
</button>
);
}
Supported wallets:
- MetaMask
- WalletConnect
- Coinbase Wallet
- Rainbow
- Phantom (Solana)
Accessing User Data
Get User Info
import { usePrivy } from '@privy-io/react-auth';
function UserProfile() {
const { user, authenticated } = usePrivy();
if (!authenticated || !user) {
return <div>Please log in</div>;
}
return (
<div>
{/* Email */}
{user.email && <p>Email: {user.email.address}</p>}
{/* Wallet */}
{user.wallet && (
<p>Wallet: {user.wallet.address}</p>
)}
{/* User ID */}
<p>Privy ID: {user.id}</p>
{/* Linked Accounts */}
<p>Accounts: {user.linkedAccounts?.length}</p>
</div>
);
}
Get Access Token
Required for Gas Relayer API:
import { usePrivy } from '@privy-io/react-auth';
function ClaimPerk() {
const { getAccessToken, authenticated } = usePrivy();
const handleClaimPerk = async () => {
if (!authenticated) {
alert('Please log in first');
return;
}
try {
// Get Privy access token
const token = await getAccessToken();
// Use token to call Gas Relayer
const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`, // ✅ Include token
},
body: JSON.stringify({
to: '0xContractAddress...',
data: '0xEncodedData...',
userAddress: user.wallet.address,
}),
});
const result = await response.json();
console.log('Transaction:', result.transactionHash);
} catch (error) {
console.error('Failed:', error);
}
};
return <button onClick={handleClaimPerk}>Claim Perk</button>;
}
Wallet Management
Check if User Has Wallet
import { usePrivy } from '@privy-io/react-auth';
function WalletStatus() {
const { user, authenticated } = usePrivy();
if (!authenticated) {
return <div>Not logged in</div>;
}
const hasWallet = user.wallet !== null;
return (
<div>
{hasWallet ? (
<div>
<p>✅ Wallet Connected</p>
<p>{user.wallet.address}</p>
</div>
) : (
<div>
<p>⚠️ No Wallet</p>
<button onClick={createWallet}>Create Wallet</button>
</div>
)}
</div>
);
}
Create Embedded Wallet
Automatically created on login if configured, or manually:
import { usePrivy } from '@privy-io/react-auth';
function CreateWalletButton() {
const { createWallet, user } = usePrivy();
const handleCreateWallet = async () => {
try {
const wallet = await createWallet();
console.log('Wallet created:', wallet.address);
} catch (error) {
console.error('Failed to create wallet:', error);
}
};
// Don't show if user already has wallet
if (user.wallet) {
return null;
}
return (
<button onClick={handleCreateWallet}>
Create Wallet
</button>
);
}
Export Wallet
Allow users to export their embedded wallet:
import { usePrivy } from '@privy-io/react-auth';
function ExportWalletButton() {
const { exportWallet } = usePrivy();
return (
<button onClick={exportWallet}>
Export Private Key
</button>
);
}
⚠️ Security Note: This shows the user's private key. Only use when explicitly requested by user.
Token Management
Get Fresh Token
import { usePrivy } from '@privy-io/react-auth';
async function callProtectedAPI() {
const { getAccessToken } = usePrivy();
// Always get fresh token
const token = await getAccessToken();
// Use token (valid for 1 hour)
const response = await fetch('https://api.example.com/protected', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
return response.json();
}
Handle Token Expiration
async function callAPIWithRetry(url, options) {
const { getAccessToken } = usePrivy();
let token = await getAccessToken();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
// If token expired, get new one and retry
if (response.status === 401) {
console.log('Token expired, refreshing...');
token = await getAccessToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
}
return response;
}
Logout
Simple Logout
import { usePrivy } from '@privy-io/react-auth';
function LogoutButton() {
const { logout, authenticated } = usePrivy();
if (!authenticated) {
return null;
}
return (
<button onClick={logout}>
Log Out
</button>
);
}
Logout with Cleanup
import { usePrivy } from '@privy-io/react-auth';
function LogoutButton() {
const { logout, authenticated } = usePrivy();
const handleLogout = async () => {
// Clear local storage
localStorage.clear();
// Clear session storage
sessionStorage.clear();
// Logout from Privy
await logout();
// Redirect to home
window.location.href = '/';
};
if (!authenticated) {
return null;
}
return (
<button onClick={handleLogout}>
Log Out
</button>
);
}
Authentication State
Listen to Auth Changes
import { useEffect } from 'react';
import { usePrivy } from '@privy-io/react-auth';
function AuthListener() {
const { authenticated, ready, user } = usePrivy();
useEffect(() => {
if (!ready) {
console.log('Privy is initializing...');
return;
}
if (authenticated) {
console.log('User logged in:', user.id);
// Track login event
trackLoginEvent(user.email?.address);
// Load user data
loadUserData(user.id);
} else {
console.log('User logged out');
// Clear user data
clearUserData();
}
}, [authenticated, ready, user]);
return null;
}
Protected Routes
React Router Example
import { Navigate } from 'react-router-dom';
import { usePrivy } from '@privy-io/react-auth';
function ProtectedRoute({ children }) {
const { authenticated, ready } = usePrivy();
// Show loading while Privy initializes
if (!ready) {
return <div>Loading...</div>;
}
// Redirect to login if not authenticated
if (!authenticated) {
return <Navigate to="/login" replace />;
}
// Render protected content
return children;
}
// Usage
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
Custom Login UI
Build Custom Login Form
import { useState } from 'react';
import { usePrivy } from '@privy-io/react-auth';
function CustomLoginForm() {
const { loginWithEmail } = usePrivy();
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
await loginWithEmail(email);
} catch (error) {
console.error('Login failed:', error);
alert('Login failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Continue with Email'}
</button>
</form>
);
}
Error Handling
Handle Authentication Errors
import { usePrivy } from '@privy-io/react-auth';
function LoginWithErrorHandling() {
const { login } = usePrivy();
const [error, setError] = useState(null);
const handleLogin = async () => {
setError(null);
try {
await login();
} catch (error) {
console.error('Login error:', error);
// Handle specific errors
if (error.message.includes('user_cancelled')) {
setError('Login cancelled');
} else if (error.message.includes('network')) {
setError('Network error. Please check your connection.');
} else {
setError('Login failed. Please try again.');
}
}
};
return (
<div>
<button onClick={handleLogin}>Log In</button>
{error && <div className="error">{error}</div>}
</div>
);
}
OAuth Pregeneration
Create Wallets Before Login
For viral growth mechanics, create wallets for users based on their social media IDs:
async function pregenerateWalletForDiscordUser(discordUserId, discordUsername) {
const response = await fetch(
'https://register.loyalteez.app/loyalteez-api/pregenerate-user',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brand_id: 'YOUR_BRAND_ID',
oauth_provider: 'discord',
oauth_user_id: discordUserId,
oauth_username: discordUsername,
}),
}
);
const data = await response.json();
console.log('Wallet pregenerated:', data.wallet_address);
console.log('Created new:', data.created_new);
return data.wallet_address;
}
Supported OAuth Providers:
- Discord
- GitHub
- Telegram
- Spotify
- TikTok
Learn more: OAuth Pregeneration Guide
Best Practices
1. Always Check ready State
const { ready, authenticated } = usePrivy();
if (!ready) {
return <div>Loading...</div>;
}
// Now safe to use authenticated state
2. Handle Missing Wallet
const { user } = usePrivy();
if (user && !user.wallet) {
// User logged in but no wallet
// Prompt to create wallet
}
3. Store Minimal User Data
// ❌ Don't store full user object
localStorage.setItem('user', JSON.stringify(user));
// ✅ Store only what you need
localStorage.setItem('userId', user.id);
localStorage.setItem('userEmail', user.email?.address);
4. Use Proper Loading States
function App() {
const { ready, authenticated } = usePrivy();
// Show loading during initialization
if (!ready) {
return <LoadingSpinner />;
}
// Render app once ready
return authenticated ? <Dashboard /> : <Landing />;
}
Security Considerations
✅ Do's
- ✅ Always validate access tokens server-side
- ✅ Use HTTPS for all API calls
- ✅ Refresh tokens before expiration
- ✅ Clear tokens on logout
- ✅ Implement rate limiting
❌ Don'ts
- ❌ Never expose Privy App Secret client-side
- ❌ Don't store private keys in localStorage
- ❌ Don't trust client-side authentication alone
- ❌ Don't hardcode sensitive data
- ❌ Don't skip token validation
Troubleshooting
User Can't Log In
Check:
- Privy App ID is correct
- Login methods are enabled in Privy dashboard
- Domain is whitelisted in Privy dashboard
- Network connection is stable
Wallet Not Created
Check:
embeddedWallets.createOnLoginis set correctly- User completed authentication flow
- Check browser console for errors
Token Expired
Solution:
// Get fresh token
const token = await getAccessToken();
Authentication Loop
Cause: Session cookie issues
Fix:
- Clear browser cookies
- Check CORS settings
- Verify Privy configuration
Related Documentation
Need Help?
- 📖 Privy Docs
- 💬 Discord Community
- 📧 Email: [email protected]