npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-native

Required Peer Dependencies

You must install async-storage manually as it is a peer dependency:

npm install @react-native-async-storage/async-storage

Included 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-polyfill

Critical 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 expiration

Transaction 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:

  1. Primary: Attempts to use Android Keystore / iOS Keychain (via react-native-keychain) for maximum security.
  2. Fallback: If hardware storage is unavailable or fails, it automatically falls back to AsyncStorage.
  3. Web: Uses localStorage automatically.

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 isAuthenticated state
    • Custom UI for session expiration
    • Platform-specific details (iOS Keychain, Android Keystore, Web)
    • Best practices for auth state observation

Setup & Configuration


Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

MIT License - see LICENSE file for details.

Support


Built with care by eXplorins for the tourism and loyalty industry.