@explorins/pers-sdk-react-native
v2.1.3
Published
React Native SDK for PERS Platform - Tourism Loyalty System with Blockchain Transaction Signing and WebAuthn Authentication
Readme
PERS SDK React Native
A comprehensive React Native SDK for the PERS (Phygital Experience Rewards System) platform, designed specifically for tourism loyalty applications with blockchain transaction signing capabilities.
Features
- Secure Authentication: WebAuthn-based authentication with device biometrics
- Blockchain Integration: Complete transaction signing with PERS Signer SDK supporting EVM-compatible chains (Ethereum, Polygon, Camino, etc.)
- Token Operations: Balance checking, earning, redeeming, and transfers
- Business Management: Business profiles, campaigns, and operations
- Campaign System: Marketing campaigns, participation, and rewards
- Redemption Engine: Comprehensive reward redemption and fulfillment
- Analytics: Real-time analytics and transaction monitoring
- Donations: Charitable giving and donation management
- User Management: Profile management and user operations
- Web3 Support: EVM-compatible chain wallet operations and blockchain interactions
- Secure Storage: Hybrid storage strategy using Keychain (High Security) for secrets and AsyncStorage for public data.
- DPoP Integration: Native C++ bridge for high-performance ECDSA signing via
react-native-quick-crypto. - React Native Optimized: Full platform support with automatic polyfills.
Installation
npm install @explorins/pers-sdk-react-nativeRequired Peer Dependencies
You must install async-storage manually as it is a peer dependency:
npm install @react-native-async-storage/async-storageIncluded Native Dependencies
The SDK automatically installs the following native libraries. You must rebuild your native app (e.g., npx expo run:android or cd ios && pod install) after installing the SDK to link these native modules:
react-native-quick-crypto(High-performance Crypto)react-native-keychain(Secure Storage)react-native-passkey(WebAuthn/Passkeys)react-native-get-random-values(Crypto Primitives)
Optional Dependencies
# For URL Polyfills (Recommended if not already included in your app)
npm install react-native-url-polyfillCritical Setup Requirement: Passkeys
To enable Passkey authentication (WebAuthn) on iOS and Android, you must complete the setup in REACT_NATIVE_PASSKEY_SETUP.md.
This includes:
- Registering your app with the PERS backend (required for OS trust)
- Native configuration (Info.plist, AndroidManifest.xml)
- Expo/development build setup
Quick Start
1. Setup the Provider
import React from 'react';
import { PersSDKProvider } from '@explorins/pers-sdk-react-native';
export default function App() {
return (
<PersSDKProvider config={{
apiUrl: 'https://api.pers.ninja',
tenantId: 'your-tenant-id' // Optional
}}>
<NavigationContainer>
<YourAppContent />
</NavigationContainer>
</PersSDKProvider>
);
}2. Authentication & Token Operations
import {
useAuth,
useTokens,
useRedemptions,
useTransactionSigner
} from '@explorins/pers-sdk-react-native';
function RewardScreen() {
const { user, isAuthenticated, login } = useAuth();
const { getTokens } = useTokens();
const { redeem } = useRedemptions();
const { signAndSubmitTransactionWithJWT, isSignerAvailable } = useTransactionSigner();
const handleLogin = async () => {
try {
await login('your-jwt-token');
console.log('Authenticated:', user?.identifier);
} catch (error) {
console.error('Login failed:', error);
}
};
const handleLoadTokens = async () => {
try {
const tokens = await getTokens();
console.log('User tokens:', tokens);
} catch (error) {
console.error('Failed to load tokens:', error);
}
};
const handleRedemption = async () => {
try {
// Step 1: Create redemption
const redemptionId = 'redemption-id-from-ui';
const result = await redeem(redemptionId);
// Note: The redeem method automatically handles blockchain signing if required
// and if the signer is available.
if (result.isSigned) {
console.log('Redemption completed with blockchain signature!');
console.log('Transaction Hash:', result.transactionHash);
}
} catch (error) {
console.error('Redemption failed:', error);
}
};
return (
<ScrollView style={styles.container}>
{!isAuthenticated ? (
<TouchableOpacity onPress={handleLogin} style={styles.loginButton}>
<Text style={styles.buttonText}>Login with PERS</Text>
</TouchableOpacity>
) : (
<View>
<Text style={styles.welcome}>Welcome, {user?.identifier}!</Text>
<TouchableOpacity onPress={handleLoadTokens} style={styles.button}>
<Text style={styles.buttonText}>Load Tokens</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleRedemption}
disabled={!isSignerAvailable}
style={[styles.button, !isSignerAvailable && styles.disabled]}
>
<Text style={styles.buttonText}>
{isSignerAvailable ? 'Redeem Rewards' : 'Signer Loading...'}
</Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
);
}Core Hooks
Authentication & Users
// Authentication management
const {
isInitialized,
isAuthenticated,
user,
login,
loginWithRawData,
logout,
refreshUserData,
getCurrentUser,
checkIsAuthenticated,
refreshTokens,
clearAuth,
hasValidAuth
} = useAuth();
// User profile operations
const {
getCurrentUser,
updateCurrentUser,
getUserById,
getAllUsersPublic,
getAllUsers, // Admin
updateUser, // Admin
toggleUserStatus // Admin
} = useUsers();Token & Financial Operations
// Token management
const {
getTokens,
getActiveCreditToken,
getRewardTokens,
getTokenTypes,
getStatusTokens,
getTokenByContract
} = useTokens();
// Token balance loading (NEW in v2.1.1)
const {
tokenBalances, // Array of balances with token metadata
isLoading, // Loading state
error, // Error state
refresh // Manual refresh function
} = useTokenBalances({
availableTokens, // From useTokens()
autoLoad: true, // Auto-load on mount
refreshInterval: 30000 // Optional: refresh every 30s
});
// Transaction history
const {
createTransaction,
getTransactionById,
getUserTransactionHistory,
getTenantTransactions, // Admin
getPaginatedTransactions, // Admin
exportTransactionsCSV, // Admin
signingStatus, // UI feedback during blockchain signing
signingStatusMessage
} = useTransactions();
// Blockchain transaction signing
const {
signAndSubmitTransactionWithJWT,
isSignerAvailable,
isSignerInitialized,
currentStatus,
statusMessage
} = useTransactionSigner();Business & Campaign Management
// Business operations
const {
getActiveBusinesses,
getBusinessTypes,
getBusinesses,
getBusinessById,
getBusinessByAccount,
getBusinessesByType,
createBusiness, // Admin
updateBusiness, // Admin
toggleBusinessStatus // Admin
} = useBusiness();
// Campaign management
const {
getActiveCampaigns,
getCampaignById,
claimCampaign,
getUserClaims,
getCampaignTriggers,
getAllCampaigns, // Admin
getCampaignClaims, // Admin
getCampaignClaimsByUserId, // Admin
getCampaignClaimsByBusinessId // Admin
} = useCampaigns();
// Redemption system
const {
getActiveRedemptions,
getUserRedemptions,
redeem,
getRedemptionTypes,
signingStatus, // UI feedback during blockchain signing
signingStatusMessage,
createRedemption, // Admin
getAllRedemptions, // Admin
updateRedemption, // Admin
toggleRedemptionStatus // Admin
} = useRedemptions();Platform & Analytics
// Purchase processing
const {
createPaymentIntent,
getActivePurchaseTokens,
getAllUserPurchases
} = usePurchases();
// Multi-tenant support
const {
getTenantInfo,
getClientConfig,
getLoginToken,
getAdmins
} = useTenants();
// Analytics & reporting
const {
getTransactionAnalytics
} = useAnalytics();
// Web3 & blockchain (wallet addresses from user.wallets)
const {
getTokenBalance,
getTokenMetadata,
getTokenCollection,
resolveIPFSUrl,
fetchAndProcessMetadata,
getChainDataById,
getExplorerUrl,
// Helper methods for token collections
extractTokenIds, // Extract tokenIds from TokenDTO metadata
getAccountOwnedTokensFromContract, // Recommended: Get owned tokens automatically
buildCollectionRequest // Build request for getTokenCollection
} = useWeb3();
// User status & achievements
const {
getUserStatusTypes,
getEarnedUserStatus,
createUserStatusType // Admin
} = useUserStatus();
// File management
const {
getSignedPutUrl,
getSignedGetUrl,
getSignedUrl,
optimizeMedia
} = useFiles();
// Donations
const {
getDonationTypes
} = useDonations();
// Event subscriptions (notifications, logging)
const {
subscribe, // Subscribe to SDK events
once, // One-time event listener
clear, // Clear all subscriptions
isAvailable, // Event system available
subscriberCount // Active subscriber count
} = useEvents();Event System
The useEvents hook provides access to SDK-wide events for showing notifications, logging, and reacting to SDK operations. All events include a userMessage field ready for UI display.
Basic Usage
import { useEvents } from '@explorins/pers-sdk-react-native';
import { useEffect } from 'react';
function NotificationHandler() {
const { subscribe, isAvailable } = useEvents();
useEffect(() => {
if (!isAvailable) return;
// Subscribe to all events
const unsubscribe = subscribe((event) => {
showNotification(event.userMessage, event.level);
});
return () => unsubscribe(); // Cleanup on unmount
}, [subscribe, isAvailable]);
}Filtering Events
// Only transaction successes
subscribe(
(event) => {
playSuccessSound();
showConfetti();
},
{ domain: 'transaction', level: 'success' }
);
// Only errors (for logging)
subscribe(
(event) => {
logToSentry(event);
},
{ level: 'error' }
);
// One-time event (auto-unsubscribes)
once(
(event) => {
console.log('First transaction completed!');
},
{ domain: 'transaction', level: 'success' }
);Event Domains
| Domain | Events |
|--------|--------|
| auth | Login, logout, token refresh |
| user | Profile updates |
| transaction | Created, completed, failed |
| campaign | Claimed, activated |
| redemption | Redeemed, expired |
| business | Created, updated, membership |
| api | Network errors, validation errors |
Event Structure
interface PersEvent {
id: string; // Unique event ID
timestamp: number; // Unix timestamp (ms)
domain: string; // Event domain (transaction, auth, etc.)
type: string; // Event type within domain
level: 'success' | 'error';
userMessage: string; // Ready for UI display
action?: string; // Suggested action
details?: object; // Additional data
}POS Transaction Flow
For Point-of-Sale scenarios where a business submits a transaction on behalf of a user, use the buildPOSTransferRequest helper:
import {
useTransactions,
buildPOSTransferRequest,
useEvents
} from '@explorins/pers-sdk-react-native';
function POSScreen() {
const { createTransaction, signingStatus } = useTransactions();
const { subscribe, isAvailable } = useEvents();
// Listen for transaction events
useEffect(() => {
if (!isAvailable) return;
const unsubscribe = subscribe(
(event) => {
if (event.level === 'success') {
Alert.alert('Success', event.userMessage);
}
},
{ domain: 'transaction' }
);
return () => unsubscribe();
}, [subscribe, isAvailable]);
const handlePOSTransaction = async (
userId: string,
businessId: string,
amount: number,
token: TokenDTO
) => {
// Build POS transfer request
const request = buildPOSTransferRequest({
amount,
contractAddress: token.contractAddress,
chainId: token.chainId,
userId, // User sending tokens
businessId // Business receiving & authorized to submit
});
// Create and sign transaction
const result = await createTransaction(request, (status, message) => {
console.log(`Signing: ${status} - ${message}`);
});
console.log('Transaction created:', result.transaction?.id);
};
}POS Authorization Fields
The buildPOSTransferRequest helper automatically sets:
| Field | Value | Purpose |
|-------|-------|---------|
| engagedBusinessId | Business ID | Business commercially involved (for reporting) |
| authorizedSubmitterId | Business ID | Entity authorized to submit the signed tx |
| authorizedSubmitterType | BUSINESS | Type of authorized submitter |
For custom scenarios, use buildTransferRequest with manual POS fields:
import { buildTransferRequest, AccountOwnerType } from '@explorins/pers-sdk-react-native';
const request = buildTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER,
recipientAccountId: 'business-456',
recipientAccountType: AccountOwnerType.BUSINESS,
// POS authorization
engagedBusinessId: 'business-456',
authorizedSubmitterId: 'business-456',
authorizedSubmitterType: AccountOwnerType.BUSINESS
});Token Collection Helper Methods
The useWeb3 hook includes helper methods for querying token balances from any blockchain address. These work with all token standards (ERC-20, ERC-721, ERC-1155).
Recommended: getAccountOwnedTokensFromContract
A unified API to get all tokens owned by any blockchain address:
import { useWeb3, useTokens, usePersSDK } from '@explorins/pers-sdk-react-native';
function RewardTokensScreen() {
const { user } = usePersSDK();
const { getRewardTokens } = useTokens();
const { getAccountOwnedTokensFromContract } = useWeb3();
const loadUserRewards = async () => {
const walletAddress = user?.wallets?.[0]?.address;
if (!walletAddress) return;
const rewardTokens = await getRewardTokens();
for (const token of rewardTokens) {
// Works with ERC-20, ERC-721, and ERC-1155 automatically
const result = await getAccountOwnedTokensFromContract(walletAddress, token);
console.log(`Token: ${token.symbol}`);
console.log(`Owned: ${result.totalOwned}`);
result.ownedTokens.forEach(owned => {
console.log(` - ${owned.metadata?.name}: ${owned.balance}`);
});
}
};
return (
<TouchableOpacity onPress={loadUserRewards}>
<Text>Load My Rewards</Text>
</TouchableOpacity>
);
}How It Handles Each Token Standard
| Token Type | What It Does | |------------|--------------| | ERC-20 | Returns balance for fungible tokens | | ERC-721 | Enumerates all owned NFTs | | ERC-1155 | Extracts tokenIds from metadata, queries balances |
The helper abstracts away the complexity - especially for ERC-1155 which requires specific tokenIds that the helper extracts from token.metadata[].tokenMetadataIncrementalId.
Return Type
interface AccountOwnedTokensResult {
token: TokenDTO; // The token definition
ownedTokens: TokenBalance[]; // Tokens with balance > 0
totalOwned: number; // Count of owned tokens
}EVM Blockchain Transaction Signing
The useTransactionSigner hook provides secure EVM blockchain transaction signing:
import { useTransactionSigner } from '@explorins/pers-sdk-react-native';
function BlockchainRedemption() {
const {
signAndSubmitTransactionWithJWT,
isSignerAvailable,
isSignerInitialized
} = useTransactionSigner();
const handleBlockchainRedemption = async (jwtToken: string) => {
if (!isSignerAvailable) {
console.error('EVM blockchain signer not available');
return;
}
try {
// This handles the complete flow:
// 1. User authentication via WebAuthn
// 2. Transaction data fetching from PERS backend
// 3. Secure EVM transaction signing using device biometrics
// 4. EVM blockchain submission
const result = await signAndSubmitTransactionWithJWT(jwtToken);
if (result.success) {
console.log('Transaction completed!');
console.log('Hash:', result.transactionHash);
console.log('View on blockchain explorer: [Chain-specific URL]');
// Handle post-transaction flow
if (result.shouldRedirect && result.redirectUrl) {
// Navigate to success page or external URL
Linking.openURL(result.redirectUrl);
}
}
} catch (error) {
if (error.message.includes('expired')) {
// JWT token expired - redirect to login
navigation.navigate('Login');
} else if (error.message.includes('cancelled')) {
// User cancelled WebAuthn authentication
console.log('User cancelled transaction');
} else {
// Other errors
Alert.alert('Transaction Failed', error.message);
}
}
};
// Show loading state while signer initializes
if (!isSignerInitialized) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" />
<Text>Initializing EVM blockchain signer...</Text>
</View>
);
}
return (
<TouchableOpacity
onPress={() => handleBlockchainRedemption(redemptionJWT)}
disabled={!isSignerAvailable}
style={[styles.button, !isSignerAvailable && styles.disabled]}
>
<Text style={styles.buttonText}>
Sign & Submit Transaction
</Text>
</TouchableOpacity>
);
}Error Handling
The SDK provides comprehensive structured error handling with utilities for consistent error management:
import {
PersApiError,
ErrorUtils
} from '@explorins/pers-sdk-react-native';
import { Alert } from 'react-native';
// Structured error handling
try {
await sdk.campaigns.claimCampaign({ campaignId });
} catch (error) {
if (error instanceof PersApiError) {
// Structured error with backend details
console.log('Error code:', error.code); // 'CAMPAIGN_BUSINESS_REQUIRED'
console.log('Status:', error.status); // 400
console.log('User message:', error.userMessage); // User-friendly message
// Show user-friendly error
Alert.alert('Error', error.userMessage || error.message);
// Check if retryable
if (error.retryable) {
Alert.alert('Error', error.message, [
{ text: 'Cancel' },
{ text: 'Retry', onPress: () => retry() }
]);
}
} else {
// Generic error fallback with ErrorUtils
const message = ErrorUtils.getMessage(error);
Alert.alert('Error', message);
}
}
// Error utilities for any error type
const status = ErrorUtils.getStatus(error); // Extract HTTP status
const message = ErrorUtils.getMessage(error); // Extract user message
const retryable = ErrorUtils.isRetryable(error); // Check if retryable
const tokenExpired = ErrorUtils.isTokenExpiredError(error); // Detect token expirationTransaction Helpers
NEW in v2.1.1: Factory functions for client-side transaction workflows:
import {
ClientTransactionType,
buildPendingTransactionData,
extractDeadlineFromSigningData,
type PendingTransactionParams
} from '@explorins/pers-sdk-react-native';
// Build pending transaction data for QR codes, NFC, deep links, etc.
const qrData = buildPendingTransactionData(
signingResult.transactionId,
signingResult.signature,
'EIP-712' // Default format
);
console.log(qrData);
// {
// transactionId: '...',
// signature: '0x...',
// transactionFormat: 'EIP-712',
// txType: 'PENDING_SUBMISSION'
// }
// Serialize for transfer (QR code, NFC, etc.)
const qrCodeValue = JSON.stringify(qrData);
// Extract deadline from EIP-712 signing data
const deadline = extractDeadlineFromSigningData(signingResult.signingData);
if (deadline) {
const expiryTime = new Date(deadline * 1000);
console.log('Transaction expires at:', expiryTime);
}
// Client transaction types
if (transaction.txType === ClientTransactionType.PENDING_SUBMISSION) {
// Handle pending submission flow
}Security Features
WebAuthn Authentication
- Device biometric authentication (fingerprint, face ID, PIN)
- No passwords or private keys stored locally
- Secure hardware-backed authentication
Token Storage
- React Native Keychain integration for sensitive data
- Automatic token refresh and expiration handling
- Multiple storage strategies (AsyncStorage, Keychain, Memory)
Transaction Security
- JWT token validation and expiration checking
- Secure transaction signing without exposing private keys
- Automatic session management with secure caching
Platform Support
- iOS: Full support with Keychain integration
- Android: Full support with Keystore integration
- Expo: Compatible with Expo managed workflow
- Web: React Native Web compatibility
Advanced Configuration
SDK Initialization & DPoP
The PersSDKProvider accepts a config object allowing you to customize behavior, including DPoP (Distributed Proof of Possession).
DPoP Behavior:
- Enabled by Default: Automatically active on iOS/Android using a high-performance C++ crypto bridge (
react-native-quick-crypto). - Web Support: Uses standard WebCrypto API.
- Security: Binds access tokens to the device, preventing replay attacks.
Customizing Initialization:
<PersSDKProvider config={{
apiProjectKey: 'your-project-key',
// DPoP is enabled by default. To disable (not recommended):
dpop: {
enabled: false
}
}}>
<YourApp />
</PersSDKProvider>Storage Fallback Strategy
The SDK implements a robust Hybrid Storage Strategy:
- Primary: Attempts to use Android Keystore / iOS Keychain (via
react-native-keychain) for maximum security. - Fallback: If hardware storage is unavailable or fails, it automatically falls back to AsyncStorage.
- Web: Uses
localStorageautomatically.
This ensures your app works reliably across all devices while prioritizing security where available.
Custom Authentication Provider
The SDK automatically uses ReactNativeSecureStorage for React Native (iOS/Android) and LocalStorageTokenStorage for Web. You can customize this if needed:
import { createReactNativeAuthProvider, ReactNativeSecureStorage } from '@explorins/pers-sdk-react-native';
const authProvider = createReactNativeAuthProvider('your-project-key', {
keyPrefix: 'my_app_tokens_',
debug: true,
// Optional: Provide custom storage implementation
// customStorage: new ReactNativeSecureStorage('custom_prefix_')
});Error Handling Patterns
import { useTokens } from '@explorins/pers-sdk-react-native';
function ErrorHandlingExample() {
const { getTokens } = useTokens();
const [error, setError] = useState<string | null>(null);
const handleLoadTokensWithErrorHandling = async () => {
try {
setError(null);
await getTokens();
} catch (err) {
// Handle specific error types
if (err.code === 'NETWORK_ERROR') {
setError('Network connection required');
} else if (err.code === 'INVALID_TOKEN') {
setError('Please log in again');
} else {
setError(err.message || 'An unexpected error occurred');
}
}
};
}Analytics Integration
import { useAnalytics } from '@explorins/pers-sdk-react-native';
function AnalyticsExample() {
const { getTransactionAnalytics } = useAnalytics();
const loadAnalytics = async () => {
try {
const txAnalytics = await getTransactionAnalytics({
groupBy: ['day'],
metrics: ['count', 'sum'],
startDate: '2023-01-01',
endDate: '2023-12-31'
});
console.log('Transaction analytics:', txAnalytics.results);
console.log('Execution time:', txAnalytics.metadata?.executionTime);
} catch (error) {
console.error('Failed to load analytics:', error);
}
};
}Migration Guide
v2.0.0 Breaking Changes - Pagination
Version 2.0.0 introduces standardized pagination across all hooks that return lists. Previously, hooks returned raw arrays. Now they return PaginatedResponseDTO<T> with pagination metadata.
What Changed
All hooks returning lists now return paginated responses:
// ❌ OLD (v1.x) - Direct array
const businesses: BusinessDTO[] = await getActiveBusinesses();
// ✅ NEW (v2.x) - Paginated response
const response: PaginatedResponseDTO<BusinessDTO> = await getActiveBusinesses();
const businesses: BusinessDTO[] = response.data;Affected Hooks
| Hook | Method | Return Type |
|------|--------|-------------|
| useBusiness | getActiveBusinesses() | PaginatedResponseDTO<BusinessDTO> |
| useBusiness | getBusinessTypes() | PaginatedResponseDTO<BusinessTypeDTO> |
| useCampaigns | getActiveCampaigns() | PaginatedResponseDTO<CampaignDTO> |
| useCampaigns | getUserClaims() | PaginatedResponseDTO<CampaignClaimDTO> |
| useTokens | getTokens() | PaginatedResponseDTO<TokenDTO> |
| useTokens | getRewardTokens() | PaginatedResponseDTO<TokenDTO> |
| useTokens | getStatusTokens() | PaginatedResponseDTO<TokenDTO> |
| useRedemptions | getActiveRedemptions() | PaginatedResponseDTO<RedemptionDTO> |
| useRedemptions | getUserRedemptions() | PaginatedResponseDTO<RedemptionDTO> |
| useTransactions | getUserTransactionHistory() | PaginatedResponseDTO<TransactionDTO> |
| usePurchases | getAllUserPurchases() | PaginatedResponseDTO<PurchaseDTO> |
| usePurchases | getActivePurchaseTokens() | PaginatedResponseDTO<PurchaseTokenDTO> |
| useDonations | getDonationTypes() | PaginatedResponseDTO<DonationTypeDTO> |
PaginatedResponseDTO Structure
import type { PaginatedResponseDTO } from '@explorins/pers-shared';
interface PaginatedResponseDTO<T> {
data: T[]; // Array of results
pagination: {
currentPage: number; // Current page number (1-indexed)
pageSize: number; // Items per page
totalItems: number; // Total number of items across all pages
totalPages: number; // Total number of pages
};
}Migration Examples
Before (v1.x):
const businesses = await getActiveBusinesses();
console.log('Business count:', businesses.length);
businesses.forEach(b => console.log(b.name));After (v2.x):
const response = await getActiveBusinesses();
console.log('Business count:', response.data.length);
console.log('Total businesses:', response.pagination.totalItems);
response.data.forEach(b => console.log(b.name));With Pagination Parameters:
// Fetch page 2 with 20 items per page
const response = await getActiveBusinesses({ page: 2, pageSize: 20 });
console.log(`Page ${response.pagination.currentPage} of ${response.pagination.totalPages}`);
console.log(`Showing ${response.data.length} businesses`);React Native Component Example
import { useState, useEffect } from 'react';
import { useBusiness } from '@explorins/pers-sdk-react-native';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
function BusinessListScreen() {
const { getActiveBusinesses } = useBusiness();
const [businesses, setBusinesses] = useState<BusinessDTO[]>([]);
const [pagination, setPagination] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadBusinesses();
}, []);
const loadBusinesses = async (page = 1) => {
try {
setLoading(true);
const response = await getActiveBusinesses({ page, pageSize: 20 });
setBusinesses(response.data);
setPagination(response.pagination);
} catch (error) {
console.error('Failed to load businesses:', error);
} finally {
setLoading(false);
}
};
if (loading) return <ActivityIndicator />;
return (
<View>
<Text>
Showing {businesses.length} of {pagination?.totalItems} businesses
</Text>
<FlatList
data={businesses}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <BusinessCard business={item} />}
/>
<Text>Page {pagination?.currentPage} of {pagination?.totalPages}</Text>
</View>
);
}Quick Fix for Existing Code
If you want minimal code changes, extract .data immediately:
// Quick adaptation in your hooks
const businesses = (await getActiveBusinesses()).data;
const campaigns = (await getActiveCampaigns()).data;
const tokens = (await getTokens()).data;Benefits of Pagination
- Performance: Load only what you need, not entire datasets
- Consistency: All list endpoints follow the same pattern
- Metadata: Access total counts without loading all items
- Better UX: Build proper pagination UI components
- Memory Efficiency: Reduced memory footprint for large datasets
Documentation
Core Guides
- AUTH_STATE_HANDLING.md - Comprehensive guide for handling authentication state changes
- Pattern examples for observing
isAuthenticatedstate - Custom UI for session expiration
- Platform-specific details (iOS Keychain, Android Keystore, Web)
- Best practices for auth state observation
- Pattern examples for observing
Setup & Configuration
- REACT_NATIVE_PASSKEY_SETUP.md - Required setup for WebAuthn/Passkey authentication
- DPOP_IMPLEMENTATION_GUIDE.md - DPoP security implementation details
Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
MIT License - see LICENSE file for details.
Support
- Email: [email protected]
- Documentation: https://docs.pers.ninja
- Issues: GitHub Issues
Built with care by eXplorins for the tourism and loyalty industry.
