React Integration Example
Complete React application with Loyalteez integration.
🎯 What You'll Build: A React app with event tracking, user authentication, and gasless perk redemption.
Prerequisites
- Node.js 16+ installed
- React 18+ project
- Loyalteez Brand ID (Get yours)
Installation
npm install @privy-io/react-auth ethers
Environment Setup
Create .env file:
# Loyalteez Configuration
REACT_APP_BRAND_ID=your_brand_id
REACT_APP_API_URL=https://api.loyalteez.app
# Privy Configuration (only needed for gasless transactions)
# Contact [email protected] for your Privy App ID
REACT_APP_PRIVY_APP_ID=your_privy_app_id
# Blockchain Configuration
REACT_APP_CHAIN_ID=1868
REACT_APP_RPC_URL=https://rpc.soneium.org
# Smart Contracts
REACT_APP_LTZ_ADDRESS=0x5242b6DB88A72752ac5a54cFe6A7DB8244d743c9
REACT_APP_PERK_NFT_ADDRESS=0x6ae30d6Dcf3e75456B6582b057f1Bf98A90F2CA0
Project Structure
src/
├── App.jsx # Main app component
├── components/
│ ├── EventTracker.jsx # Event tracking component
│ ├── PerkClaimer.jsx # Gasless perk claiming
│ └── UserProfile.jsx # User wallet & balance
├── hooks/
│ └── useLoyalteez.js # Custom Loyalteez hook
├── utils/
│ └── loyalteez.js # Loyalteez utilities
└── config/
└── constants.js # Configuration constants
1. Configuration Constants
src/config/constants.js
export const LOYALTEEZ_CONFIG = {
brandId: process.env.REACT_APP_BRAND_ID,
apiUrl: process.env.REACT_APP_API_URL,
};
export const PRIVY_CONFIG = {
appId: process.env.REACT_APP_PRIVY_APP_ID,
};
export const BLOCKCHAIN_CONFIG = {
chainId: parseInt(process.env.REACT_APP_CHAIN_ID),
rpcUrl: process.env.REACT_APP_RPC_URL,
contracts: {
ltz: process.env.REACT_APP_LTZ_ADDRESS,
perkNFT: process.env.REACT_APP_PERK_NFT_ADDRESS,
},
};
export const EVENT_TYPES = {
ACCOUNT_CREATION: 'account_creation',
COMPLETE_SURVEY: 'complete_survey',
NEWSLETTER_SUBSCRIBE: 'newsletter_subscribe',
RATE_EXPERIENCE: 'rate_experience',
FORM_SUBMIT: 'form_submit',
};
2. Loyalteez Utilities
src/utils/loyalteez.js
import { LOYALTEEZ_CONFIG } from '../config/constants';
/**
* Track an event with Loyalteez
*/
export async function trackEvent(eventType, userEmail, metadata = {}) {
try {
const response = await fetch(
`${LOYALTEEZ_CONFIG.apiUrl}/loyalteez-api/manual-event`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
brandId: LOYALTEEZ_CONFIG.brandId,
eventType,
userEmail,
metadata: {
source: 'react_app',
timestamp: Date.now(),
...metadata,
},
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to track event');
}
const data = await response.json();
console.log('✅ Event tracked:', data);
return data;
} catch (error) {
console.error('❌ Event tracking failed:', error);
throw error;
}
}
/**
* Execute gasless transaction via gas relayer
*/
export async function executeGaslessTransaction(
privyToken,
{ to, data, value = '0', gasLimit = 300000, userAddress }
) {
try {
const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${privyToken}`,
},
body: JSON.stringify({
to,
data,
value,
gasLimit,
userAddress,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Transaction failed');
}
const result = await response.json();
console.log('✅ Transaction successful:', result.transactionHash);
return result;
} catch (error) {
console.error('❌ Transaction failed:', error);
throw error;
}
}
3. Custom Loyalteez Hook
src/hooks/useLoyalteez.js
import { useState, useCallback } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { trackEvent, executeGaslessTransaction } from '../utils/loyalteez';
export function useLoyalteez() {
const { user, getAccessToken, authenticated } = usePrivy();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const track = useCallback(
async (eventType, metadata = {}) => {
if (!user?.email) {
throw new Error('User email required');
}
setLoading(true);
setError(null);
try {
const result = await trackEvent(eventType, user.email, metadata);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
},
[user]
);
const executeTransaction = useCallback(
async (txData) => {
if (!authenticated) {
throw new Error('User not authenticated');
}
setLoading(true);
setError(null);
try {
const token = await getAccessToken();
const result = await executeGaslessTransaction(token, {
...txData,
userAddress: user.wallet.address,
});
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
},
[authenticated, getAccessToken, user]
);
return {
track,
executeTransaction,
loading,
error,
user,
authenticated,
};
}
4. App Setup with Privy
src/App.jsx
import React from 'react';
import { PrivyProvider } from '@privy-io/react-auth';
import { PRIVY_CONFIG, BLOCKCHAIN_CONFIG } from './config/constants';
import EventTracker from './components/EventTracker';
import PerkClaimer from './components/PerkClaimer';
import UserProfile from './components/UserProfile';
function App() {
return (
<PrivyProvider
appId={PRIVY_CONFIG.appId}
config={{
loginMethods: ['email', 'wallet', 'google'],
appearance: {
theme: 'light',
accentColor: '#6C33EA',
},
embeddedWallets: {
createOnLogin: 'users-without-wallets',
},
}}
>
<div className="App">
<header>
<h1>Loyalteez Demo App</h1>
<UserProfile />
</header>
<main>
<EventTracker />
<PerkClaimer />
</main>
</div>
</PrivyProvider>
);
}
export default App;
5. Event Tracker Component
src/components/EventTracker.jsx
import React, { useState } from 'react';
import { useLoyalteez } from '../hooks/useLoyalteez';
import { EVENT_TYPES } from '../config/constants';
function EventTracker() {
const { track, loading, error, authenticated } = useLoyalteez();
const [eventType, setEventType] = useState(EVENT_TYPES.ACCOUNT_CREATION);
const [result, setResult] = useState(null);
const handleTrackEvent = async () => {
try {
const data = await track(eventType, {
source: 'manual_button',
page: window.location.pathname,
});
setResult(data);
// Show success notification
alert(
`✅ Event tracked! You earned ${data.rewardAmount} LTZ tokens!`
);
} catch (err) {
console.error('Failed to track event:', err);
alert(`❌ Error: ${err.message}`);
}
};
if (!authenticated) {
return (
<div className="event-tracker">
<p>Please log in to track events</p>
</div>
);
}
return (
<div className="event-tracker">
<h2>Track Events</h2>
<select
value={eventType}
onChange={(e) => setEventType(e.target.value)}
disabled={loading}
>
<option value={EVENT_TYPES.ACCOUNT_CREATION}>
Account Creation (100 LTZ)
</option>
<option value={EVENT_TYPES.COMPLETE_SURVEY}>
Complete Survey (75 LTZ)
</option>
<option value={EVENT_TYPES.NEWSLETTER_SUBSCRIBE}>
Newsletter Subscribe (25 LTZ)
</option>
<option value={EVENT_TYPES.RATE_EXPERIENCE}>
Rate Experience (50 LTZ)
</option>
<option value={EVENT_TYPES.FORM_SUBMIT}>
Form Submit (10 LTZ)
</option>
</select>
<button onClick={handleTrackEvent} disabled={loading}>
{loading ? 'Tracking...' : 'Track Event'}
</button>
{error && <div className="error">{error}</div>}
{result && (
<div className="result">
<h3>Success!</h3>
<p>Event ID: {result.eventId}</p>
<p>Reward: {result.rewardAmount} LTZ</p>
{result.walletCreated && <p>✨ New wallet created!</p>}
</div>
)}
</div>
);
}
export default EventTracker;
6. Gasless Perk Claiming Component
src/components/PerkClaimer.jsx
import React, { useState } from 'react';
import { ethers } from 'ethers';
import { useLoyalteez } from '../hooks/useLoyalteez';
import { BLOCKCHAIN_CONFIG } from '../config/constants';
// PerkNFT contract ABI (claimPerk function)
const PERK_NFT_ABI = ['function claimPerk(uint256 perkId) external'];
function PerkClaimer() {
const { executeTransaction, loading, authenticated } = useLoyalteez();
const [perkId, setPerkId] = useState('1');
const [txHash, setTxHash] = useState(null);
const handleClaimPerk = async () => {
try {
// Encode the contract call
const iface = new ethers.Interface(PERK_NFT_ABI);
const data = iface.encodeFunctionData('claimPerk', [perkId]);
// Execute gasless transaction
const result = await executeTransaction({
to: BLOCKCHAIN_CONFIG.contracts.perkNFT,
data,
gasLimit: 300000,
});
setTxHash(result.transactionHash);
alert(
`✅ Perk claimed! Transaction: ${result.transactionHash.slice(0, 10)}...`
);
} catch (err) {
console.error('Failed to claim perk:', err);
alert(`❌ Error: ${err.message}`);
}
};
if (!authenticated) {
return (
<div className="perk-claimer">
<p>Please log in to claim perks</p>
</div>
);
}
return (
<div className="perk-claimer">
<h2>Claim Perk (Gasless)</h2>
<input
type="number"
value={perkId}
onChange={(e) => setPerkId(e.target.value)}
placeholder="Perk ID"
disabled={loading}
min="1"
/>
<button onClick={handleClaimPerk} disabled={loading}>
{loading ? 'Claiming...' : 'Claim Perk (No Gas Fees)'}
</button>
{txHash && (
<div className="result">
<h3>Perk Claimed!</h3>
<p>
<a
href={`https://soneium.blockscout.com/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
View on Explorer →
</a>
</p>
</div>
)}
</div>
);
}
export default PerkClaimer;
7. User Profile Component
src/components/UserProfile.jsx
import React, { useState, useEffect } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { ethers } from 'ethers';
import { BLOCKCHAIN_CONFIG } from '../config/constants';
const LTZ_ABI = ['function balanceOf(address) view returns (uint256)'];
function UserProfile() {
const { user, login, logout, authenticated } = usePrivy();
const [ltzBalance, setLtzBalance] = useState('0');
useEffect(() => {
if (authenticated && user?.wallet?.address) {
fetchLTZBalance();
}
}, [authenticated, user]);
const fetchLTZBalance = async () => {
try {
const provider = new ethers.JsonRpcProvider(BLOCKCHAIN_CONFIG.rpcUrl);
const ltzContract = new ethers.Contract(
BLOCKCHAIN_CONFIG.contracts.ltz,
LTZ_ABI,
provider
);
const balance = await ltzContract.balanceOf(user.wallet.address);
setLtzBalance(ethers.formatEther(balance));
} catch (error) {
console.error('Failed to fetch LTZ balance:', error);
}
};
if (!authenticated) {
return (
<div className="user-profile">
<button onClick={login} className="login-button">
Log In
</button>
</div>
);
}
return (
<div className="user-profile">
<div className="user-info">
<p>
<strong>Email:</strong> {user.email?.address}
</p>
<p>
<strong>Wallet:</strong>{' '}
{user.wallet?.address?.slice(0, 6)}...
{user.wallet?.address?.slice(-4)}
</p>
<p>
<strong>LTZ Balance:</strong> {parseFloat(ltzBalance).toFixed(2)} LTZ
</p>
<button onClick={fetchLTZBalance} className="refresh-button">
Refresh
</button>
</div>
<button onClick={logout} className="logout-button">
Log Out
</button>
</div>
);
}
export default UserProfile;
8. Basic Styling
src/App.css
.App {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 2px solid #6C33EA;
}
main {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 40px;
}
.event-tracker,
.perk-claimer,
.user-profile {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
button {
background: #6C33EA;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
button:hover:not(:disabled) {
background: #5522CC;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
select,
input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
.result {
margin-top: 20px;
padding: 15px;
background: #e8f5e9;
border-radius: 8px;
}
.error {
margin-top: 20px;
padding: 15px;
background: #ffebee;
color: #c62828;
border-radius: 8px;
}
9. Running the App
# Install dependencies
npm install
# Start development server
npm start
# Build for production
npm run build
10. Testing the Integration
Test Event Tracking:
- Log in with email/wallet
- Select an event type
- Click "Track Event"
- Check that you receive LTZ tokens
Test Gasless Perk Claiming:
- Ensure you're logged in
- Enter a valid perk ID
- Click "Claim Perk"
- Transaction executes without paying gas!
Next Steps
- Add more events: Create custom event types
- Build perk marketplace: Display available perks
- Add notifications: Show toast notifications for events
- Implement leaderboard: Show top users by LTZ earned
- Add transaction history: Display user's past transactions
Troubleshooting
"User email required" Error
- Ensure Privy is configured with email login
- Check that user is authenticated before tracking
"Transaction failed" Error
- Verify Privy access token is valid
- Check contract address is correct
- Ensure user has enough LTZ to claim perk
Balance not updating
- Click the "Refresh" button
- Check RPC URL is accessible
- Verify LTZ contract address
Related Documentation
Need Help? Join our Discord or email [email protected]