React Native Integration - Step by Step
Complete guide to integrating Loyalteez into your React Native mobile app.
Prerequisites
- React Native app (Expo or bare React Native)
- Node.js and npm installed
- iOS Simulator or Android Emulator
- Your Loyalteez Brand ID from Partner Portal
Step 1: Install Dependencies
# Install Privy SDK for authentication
npm install @privy-io/expo
# Install axios for API calls
npm install axios
# Install AsyncStorage for local storage
npm install @react-native-async-storage/async-storage
# Install push notifications (optional)
npm install expo-notifications
Step 2: Set Up Environment Variables
Create .env file:
# .env
EXPO_PUBLIC_LOYALTEEZ_BRAND_ID=your_brand_id_here
EXPO_PUBLIC_API_URL=https://api.loyalteez.app
EXPO_PUBLIC_GAS_RELAYER_URL=https://relayer.loyalteez.app
# Only needed if using Privy for gasless transactions
# Contact [email protected] for your Privy App ID
EXPO_PUBLIC_PRIVY_APP_ID=your_privy_app_id
Step 3: Create Loyalteez Service
Create services/loyalteez.ts:
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const GAS_RELAYER_URL = process.env.EXPO_PUBLIC_GAS_RELAYER_URL;
const BRAND_ID = process.env.EXPO_PUBLIC_LOYALTEEZ_BRAND_ID;
// Event queue for offline support
let eventQueue: any[] = [];
export const LoyalteezService = {
/**
* Track a user event
*/
async trackEvent(
eventName: string,
email: string,
metadata: Record<string, any> = {}
): Promise<void> {
const event = {
event: eventName,
email,
metadata: {
...metadata,
platform: 'mobile',
os: Platform.OS,
brandId: BRAND_ID,
timestamp: Date.now(),
},
};
try {
const response = await axios.post(
`${API_URL}/loyalteez-api/manual-event`,
event,
{
headers: {
'Content-Type': 'application/json',
},
timeout: 30000, // 30 second timeout
}
);
console.log('Event tracked:', response.data);
// Return LTZ amount for showing to user
return response.data;
} catch (error) {
console.error('Failed to track event:', error);
// Queue event for retry
await this.queueEvent(event);
throw error;
}
},
/**
* Execute gasless transaction
*/
async executeTransaction(
privyToken: string,
to: string,
data: string,
userAddress: string,
metadata?: Record<string, any>
): Promise<string> {
try {
const response = await axios.post(
`${GAS_RELAYER_URL}/relay`,
{
to,
data,
userAddress,
value: '0',
gasLimit: 500000,
metadata,
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${privyToken}`,
},
timeout: 60000, // 60 second timeout
}
);
return response.data.transactionHash;
} catch (error) {
console.error('Transaction failed:', error);
throw error;
}
},
/**
* Queue event for retry when offline
*/
async queueEvent(event: any): Promise<void> {
try {
const queue = await AsyncStorage.getItem('event_queue');
const events = queue ? JSON.parse(queue) : [];
events.push(event);
await AsyncStorage.setItem('event_queue', JSON.stringify(events));
} catch (error) {
console.error('Failed to queue event:', error);
}
},
/**
* Process queued events
*/
async processQueue(): Promise<void> {
try {
const queue = await AsyncStorage.getItem('event_queue');
if (!queue) return;
const events = JSON.parse(queue);
const failedEvents: any[] = [];
for (const event of events) {
try {
await axios.post(`${API_URL}/loyalteez-api/manual-event`, event);
} catch (error) {
failedEvents.push(event);
}
}
// Save failed events back to queue
await AsyncStorage.setItem('event_queue', JSON.stringify(failedEvents));
} catch (error) {
console.error('Failed to process queue:', error);
}
},
/**
* Check API health
*/
async checkHealth(): Promise<boolean> {
try {
const response = await axios.get(`${API_URL}/loyalteez-api/debug`, {
timeout: 10000,
});
return response.status === 200;
} catch (error) {
return false;
}
},
};
Step 4: Set Up Privy Authentication
Create contexts/AuthContext.tsx:
import React, { createContext, useContext, useEffect, useState } from 'react';
import { PrivyProvider, usePrivy } from '@privy-io/expo';
import AsyncStorage from '@react-native-async-storage/async-storage';
const PRIVY_APP_ID = process.env.EXPO_PUBLIC_PRIVY_APP_ID!;
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
return (
<PrivyProvider appId={PRIVY_APP_ID}>
{children}
</PrivyProvider>
);
};
export const useAuth = () => {
const { user, login, logout, getAccessToken, ready } = usePrivy();
return {
user,
isAuthenticated: !!user,
login,
logout,
getAccessToken,
ready,
walletAddress: user?.wallet?.address,
};
};
Step 5: Create Reward Notification Component
Create components/RewardNotification.tsx:
import React from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';
interface RewardNotificationProps {
amount: number;
visible: boolean;
onHide: () => void;
}
export const RewardNotification: React.FC<RewardNotificationProps> = ({
amount,
visible,
onHide,
}) => {
const fadeAnim = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
if (visible) {
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.delay(3000),
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
]).start(() => onHide());
}
}, [visible]);
if (!visible) return null;
return (
<Animated.View style={[styles.container, { opacity: fadeAnim }]}>
<View style={styles.content}>
<Text style={styles.emoji}>🎉</Text>
<Text style={styles.title}>You earned {amount} LTZ!</Text>
<Text style={styles.subtitle}>Keep up the great work</Text>
</View>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 100,
left: 20,
right: 20,
zIndex: 1000,
},
content: {
backgroundColor: '#8CBC99',
borderRadius: 12,
padding: 20,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
emoji: {
fontSize: 40,
marginBottom: 8,
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#FFFFFF',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#FFFFFF',
opacity: 0.9,
},
});
Step 6: Create Example Screen
Create screens/HomeScreen.tsx:
import React, { useState, useEffect } from 'react';
import {
View,
Text,
Button,
StyleSheet,
ActivityIndicator,
ScrollView,
} from 'react-native';
import { useAuth } from '../contexts/AuthContext';
import { LoyalteezService } from '../services/loyalteez';
import { RewardNotification } from '../components/RewardNotification';
export const HomeScreen = () => {
const { user, isAuthenticated, login, logout, walletAddress } = useAuth();
const [loading, setLoading] = useState(false);
const [reward, setReward] = useState<{ visible: boolean; amount: number }>({
visible: false,
amount: 0,
});
// Process queued events on mount
useEffect(() => {
if (isAuthenticated) {
LoyalteezService.processQueue();
}
}, [isAuthenticated]);
const handleLogin = async () => {
try {
setLoading(true);
await login();
// Track login event
if (user?.email) {
const result = await LoyalteezService.trackEvent(
'mobile_login',
user.email,
{ source: 'home_screen' }
);
if (result?.ltzDistributed) {
setReward({ visible: true, amount: result.ltzDistributed });
}
}
} catch (error) {
console.error('Login failed:', error);
Alert.alert('Error', 'Failed to login. Please try again.');
} finally {
setLoading(false);
}
};
const handleCompletAction = async () => {
if (!user?.email) return;
try {
setLoading(true);
const result = await LoyalteezService.trackEvent(
'action_completed',
user.email,
{
actionType: 'button_click',
screen: 'home',
}
);
if (result?.ltzDistributed) {
setReward({ visible: true, amount: result.ltzDistributed });
}
Alert.alert('Success', `You earned ${result.ltzDistributed} LTZ!`);
} catch (error) {
console.error('Failed to track action:', error);
Alert.alert('Error', 'Failed to record action. Will retry automatically.');
} finally {
setLoading(false);
}
};
return (
<ScrollView style={styles.container}>
<RewardNotification
visible={reward.visible}
amount={reward.amount}
onHide={() => setReward({ ...reward, visible: false })}
/>
<View style={styles.header}>
<Text style={styles.title}>Loyalteez Demo</Text>
<Text style={styles.subtitle}>Earn rewards for your actions</Text>
</View>
{!isAuthenticated ? (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Get Started</Text>
<Text style={styles.description}>
Login to start earning LTZ rewards
</Text>
<Button
title={loading ? 'Loading...' : 'Login with Email'}
onPress={handleLogin}
disabled={loading}
color="#8CBC99"
/>
</View>
) : (
<>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Your Account</Text>
<Text style={styles.label}>Email:</Text>
<Text style={styles.value}>{user?.email}</Text>
<Text style={styles.label}>Wallet:</Text>
<Text style={styles.value} numberOfLines={1}>
{walletAddress}
</Text>
<Button
title="Logout"
onPress={logout}
color="#6C33EA"
/>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Earn Rewards</Text>
<Text style={styles.description}>
Complete actions to earn LTZ
</Text>
<View style={styles.buttonSpacing}>
<Button
title={loading ? 'Processing...' : 'Complete Action'}
onPress={handleCompletAction}
disabled={loading}
color="#8CBC99"
/>
</View>
</View>
</>
)}
{loading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#8CBC99" />
</View>
)}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
header: {
backgroundColor: '#8CBC99',
padding: 20,
paddingTop: 60,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#FFFFFF',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#FFFFFF',
opacity: 0.9,
},
section: {
backgroundColor: '#FFFFFF',
margin: 16,
padding: 20,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 12,
color: '#0A0C1C',
},
description: {
fontSize: 14,
color: '#666',
marginBottom: 16,
},
label: {
fontSize: 12,
color: '#666',
marginTop: 12,
marginBottom: 4,
},
value: {
fontSize: 16,
color: '#0A0C1C',
marginBottom: 12,
},
buttonSpacing: {
marginTop: 8,
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
});
Step 7: Set Up App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { AuthProvider } from './contexts/AuthContext';
import { HomeScreen } from './screens/HomeScreen';
const Stack = createStackNavigator();
export default function App() {
return (
<AuthProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerShown: false }}
/>
</Stack.Navigator>
</NavigationContainer>
</AuthProvider>
);
}
Step 8: Test Your Integration
Run on iOS
npx expo run:ios
Run on Android
npx expo run:android
Test Event Tracking
- Open the app
- Click "Login with Email"
- Complete email verification
- Click "Complete Action"
- See reward notification appear
- Check Partner Portal analytics
Step 9: Add Push Notifications (Optional)
Create services/notifications.ts:
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
export const NotificationService = {
async requestPermissions() {
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';
},
async showRewardNotification(amount: number) {
await Notifications.scheduleNotificationAsync({
content: {
title: `You earned ${amount} LTZ! 🎉`,
body: 'Keep up the great work',
data: { type: 'reward', amount },
},
trigger: null, // Show immediately
});
},
async getPushToken() {
const token = await Notifications.getExpoPushTokenAsync();
return token.data;
},
};
// Configure notification handler
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
Step 10: Add Offline Support
Update App.tsx to handle network changes:
import NetInfo from '@react-native-community/netinfo';
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
if (state.isConnected) {
// Process queued events when back online
LoyalteezService.processQueue();
}
});
return () => unsubscribe();
}, []);
Complete Project Structure
your-app/
├── App.tsx
├── package.json
├── .env
├── contexts/
│ └── AuthContext.tsx
├── services/
│ ├── loyalteez.ts
│ └── notifications.ts
├── components/
│ └── RewardNotification.tsx
├── screens/
│ └── HomeScreen.tsx
└── types/
└── index.ts
Environment Variables for Production
# .env.production
EXPO_PUBLIC_LOYALTEEZ_BRAND_ID=your_production_brand_id
EXPO_PUBLIC_API_URL=https://api.loyalteez.app
EXPO_PUBLIC_GAS_RELAYER_URL=https://relayer.loyalteez.app
EXPO_PUBLIC_PRIVY_APP_ID=your_production_privy_app_id
Testing Checklist
- User can login with email
- Events are tracked successfully
- Rewards show in notification
- Offline events are queued
- Queued events sync when online
- Wallet address is displayed
- Push notifications work (if implemented)
- App doesn't crash on network errors
- Analytics appear in Partner Portal
Common Issues & Solutions
Issue: Privy login doesn't work
Solution: Make sure Privy App ID is correct and domain is whitelisted in Privy dashboard
Issue: Events not tracking
Solution: Check network connection, verify Brand ID, check Partner Portal analytics
Issue: "Network Error"
Solution: Enable offline queueing, check API URLs are correct
Issue: No rewards showing
Solution: Configure reward rules in Partner Portal → Settings → LTZ Distribution
Next Steps
- Add more screens - Implement wallet view, perks marketplace
- Add deep linking - Enable
loyalteez://URLs - Improve UI - Customize reward notifications
- Add analytics - Track user engagement
- Deploy to stores - Publish to App Store / Play Store
Support
- React Native Issues: Check React Native docs
- Privy SDK: Privy Mobile Docs
- Loyalteez API: API Reference
- Email Support: [email protected]
You're done! Your React Native app now rewards users with LTZ! 🎉