Skip to main content

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:

  1. Log in with email/wallet
  2. Select an event type
  3. Click "Track Event"
  4. Check that you receive LTZ tokens

Test Gasless Perk Claiming:

  1. Ensure you're logged in
  2. Enter a valid perk ID
  3. Click "Claim Perk"
  4. 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


Need Help? Join our Discord or email [email protected]