Skip to main content

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

  1. Open the app
  2. Click "Login with Email"
  3. Complete email verification
  4. Click "Complete Action"
  5. See reward notification appear
  6. 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

You're done! Your React Native app now rewards users with LTZ! 🎉