Skip to main content

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:

  1. User enters email
  2. Privy sends verification code
  3. User enters code
  4. Embedded wallet auto-created
  5. 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:

  • Google
  • Twitter (X)
  • Discord
  • GitHub
  • Apple
  • LinkedIn
  • Spotify
  • TikTok
  • Instagram

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
  • Twitter
  • GitHub
  • Google
  • Telegram
  • Spotify
  • Instagram
  • 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:

  1. Privy App ID is correct
  2. Login methods are enabled in Privy dashboard
  3. Domain is whitelisted in Privy dashboard
  4. Network connection is stable

Wallet Not Created

Check:

  1. embeddedWallets.createOnLogin is set correctly
  2. User completed authentication flow
  3. Check browser console for errors

Token Expired

Solution:

// Get fresh token
const token = await getAccessToken();

Authentication Loop

Cause: Session cookie issues

Fix:

  1. Clear browser cookies
  2. Check CORS settings
  3. Verify Privy configuration


Need Help?