NVLP Mobile App Integration Guide

Complete guide for integrating NVLP into React Native and mobile applications with offline support, authentication, and performance optimization.

Overview

NVLP provides comprehensive support for mobile applications through the unified client library. This guide covers integration patterns, offline capabilities, authentication flows, and performance optimization specifically for mobile development.

📱 Cross-Platform Support

Works with React Native CLI, Expo, and hybrid applications across iOS and Android.

🔒 Mobile Authentication

Deep linking, biometric authentication, and secure session management.

📡 Offline-First

Built-in request queuing, local storage, and automatic synchronization.

⚡ Performance Optimized

Lazy loading, request batching, and memory-efficient data handling.

🔄 Real-time Updates

WebSocket support for live data updates and collaborative features.

🛡️ Security Built-in

Secure storage, automatic token refresh, and certificate validation.

Platform Support

React Native CLI

✅ Fully Supported

  • Full native module access
  • Custom native code integration
  • Advanced offline storage options
  • Biometric authentication
  • Push notifications

Expo Managed

✅ Supported

  • Zero native code required
  • Over-the-air updates
  • Simplified deployment
  • Limited to Expo SDK features
  • AsyncStorage for persistence

Expo Bare Workflow

✅ Fully Supported

  • Best of both worlds
  • Expo tooling + native access
  • Full customization
  • Advanced security features
  • Custom native modules

Project Setup

React Native CLI Setup

1. Install Dependencies

# Core NVLP client
npm install @nvlp/client

# React Native dependencies
npm install @react-native-async-storage/async-storage
npm install @supabase/supabase-js
npm install react-native-url-polyfill

# Optional: Enhanced features
npm install react-native-keychain  # Secure storage
npm install @react-native-community/netinfo  # Network status
npm install react-native-biometrics  # Biometric auth

2. Platform Configuration

// metro.config.js - Add polyfills
const { getDefaultConfig } = require('metro-config');

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig();

  return {
    resolver: {
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      sourceExts: [...sourceExts, 'svg'],
    },
    transformer: {
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
  };
})();
// index.js - Add polyfills at the top
import 'react-native-url-polyfill/auto';
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';

AppRegistry.registerComponent(appName, () => App);

Expo Setup

1. Install Dependencies

# Core NVLP client
npx expo install @nvlp/client

# Expo-compatible dependencies
npx expo install @react-native-async-storage/async-storage
npx expo install @supabase/supabase-js
npx expo install expo-secure-store  # For secure token storage
npx expo install expo-auth-session  # For OAuth flows
npx expo install expo-linking  # For deep linking
npx expo install expo-network  # For network status

2. App Configuration

// app.json - Configure deep linking and permissions
{
  "expo": {
    "name": "NVLP Budget App",
    "slug": "nvlp-budget",
    "scheme": "nvlp",
    "platforms": ["ios", "android"],
    "ios": {
      "bundleIdentifier": "com.yourcompany.nvlp",
      "buildNumber": "1.0.0",
      "infoPlist": {
        "NSFaceIDUsageDescription": "Use Face ID to secure your budget data"
      }
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png"
      },
      "permissions": [
        "USE_FINGERPRINT",
        "USE_BIOMETRIC"
      ]
    }
  }
}

Mobile Authentication

Complete Authentication Setup

// src/services/AuthService.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createNVLPClient, NVLPClient, NVLPSessionProvider } from '@nvlp/client';
import { Platform, Linking } from 'react-native';

export class MobileAuthService implements NVLPSessionProvider {
  private supabase: SupabaseClient;
  private nvlp: NVLPClient;
  private sessionListeners: Set<(session: any) => void> = new Set();

  constructor() {
    // Initialize Supabase with custom storage
    this.supabase = createClient(
      process.env.EXPO_PUBLIC_SUPABASE_URL!,
      process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
      {
        auth: {
          storage: AsyncStorage,
          autoRefreshToken: true,
          persistSession: true,
          detectSessionInUrl: Platform.OS === 'web',
        },
      }
    );

    // Initialize NVLP client
    this.nvlp = createNVLPClient({
      supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL!,
      supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
      sessionProvider: this,
      offlineQueue: {
        enabled: true,
        maxSize: 100,
        retryOnReconnect: true,
      },
    });

    // Listen for auth state changes
    this.supabase.auth.onAuthStateChange((event, session) => {
      console.log('Auth state changed:', event, session?.user?.id);
      this.notifySessionListeners(session);
    });

    // Handle deep linking for magic links
    this.setupDeepLinking();
  }

  // NVLPSessionProvider implementation
  async getSession() {
    const { data: { session } } = await this.supabase.auth.getSession();
    return session;
  }

  async ensureValidSession() {
    const { data: { session }, error } = await this.supabase.auth.getSession();
    
    if (!session) {
      throw new Error('No active session');
    }

    // Check if token needs refresh
    if (session.expires_at && session.expires_at * 1000 < Date.now()) {
      const { data: refreshData, error: refreshError } = 
        await this.supabase.auth.refreshSession();
      if (refreshError || !refreshData.session) {
        throw new Error('Failed to refresh session');
      }
      return refreshData.session;
    }

    return session;
  }

  onSessionChange(handler: (session: any) => void) {
    this.sessionListeners.add(handler);
    return () => this.sessionListeners.delete(handler);
  }

  private notifySessionListeners(session: any) {
    this.sessionListeners.forEach(listener => listener(session));
  }

  // Magic link authentication
  async signInWithMagicLink(email: string) {
    const { data, error } = await this.supabase.auth.signInWithOtp({
      email,
      options: {
        emailRedirectTo: 'nvlp://auth/callback',
      },
    });

    if (error) throw error;
    return data;
  }

  // OAuth authentication
  async signInWithOAuth(provider: 'google' | 'apple') {
    const { data, error } = await this.supabase.auth.signInWithOAuth({
      provider,
      options: {
        redirectTo: 'nvlp://auth/callback',
      },
    });

    if (error) throw error;
    return data;
  }

  // Sign out
  async signOut() {
    await this.nvlp.clearOfflineQueue();
    const { error } = await this.supabase.auth.signOut();
    if (error) throw error;
  }

  // Deep linking setup
  private setupDeepLinking() {
    const handleDeepLink = (url: string) => {
      if (url.includes('auth/callback')) {
        const urlParams = new URL(url);
        const accessToken = urlParams.searchParams.get('access_token');
        const refreshToken = urlParams.searchParams.get('refresh_token');
        
        if (accessToken && refreshToken) {
          this.supabase.auth.setSession({
            access_token: accessToken,
            refresh_token: refreshToken,
          });
        }
      }
    };

    Linking.addEventListener('url', (event) => {
      handleDeepLink(event.url);
    });

    Linking.getInitialURL().then((url) => {
      if (url) handleDeepLink(url);
    });
  }

  getNVLPClient(): NVLPClient {
    return this.nvlp;
  }
}

Offline Support

Complete Offline Implementation

// src/services/OfflineService.ts
import { createNVLPClient, AsyncStorageImpl } from '@nvlp/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';

export class OfflineService {
  private nvlp: any;
  private isOnline: boolean = true;
  private offlineListeners: Set<(isOnline: boolean) => void> = new Set();

  constructor(authService: MobileAuthService) {
    // Configure NVLP client with offline support
    this.nvlp = createNVLPClient({
      supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL!,
      supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
      sessionProvider: authService,
      offlineQueue: {
        enabled: true,
        maxSize: 200,
        maxRetries: 3,
        retryDelay: 2000,
        storage: new AsyncStorageImpl(AsyncStorage),
        retryOnReconnect: true,
      },
    });

    this.setupNetworkMonitoring();
  }

  private setupNetworkMonitoring() {
    NetInfo.addEventListener(state => {
      const wasOffline = !this.isOnline;
      this.isOnline = state.isConnected && state.isInternetReachable;
      
      console.log('Network status:', {
        connected: state.isConnected,
        reachable: state.isInternetReachable,
        type: state.type,
      });

      // Notify listeners
      this.offlineListeners.forEach(listener => listener(this.isOnline));

      // Process queue when coming back online
      if (wasOffline && this.isOnline) {
        this.processOfflineQueue();
      }
    });
  }

  async processOfflineQueue() {
    try {
      console.log('Processing offline queue...');
      const processed = await this.nvlp.processOfflineQueue();
      console.log(`Processed ${processed} queued requests`);
      return processed;
    } catch (error) {
      console.error('Error processing offline queue:', error);
      return 0;
    }
  }

  onNetworkChange(listener: (isOnline: boolean) => void) {
    this.offlineListeners.add(listener);
    return () => this.offlineListeners.delete(listener);
  }

  getNetworkStatus(): boolean {
    return this.isOnline;
  }
}

State Management Integration

React Context + Hooks Pattern

// src/context/NVLPContext.tsx
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { MobileAuthService } from '../services/AuthService';
import { OfflineService } from '../services/OfflineService';

interface NVLPContextType {
  session: any;
  user: any;
  signIn: (email: string) => Promise;
  signOut: () => Promise;
  isOnline: boolean;
  queueSize: number;
  authService: MobileAuthService;
  offlineService: OfflineService;
  isLoading: boolean;
}

const NVLPContext = createContext(null);

export function NVLPProvider({ children }: { children: ReactNode }) {
  const [authService] = useState(() => new MobileAuthService());
  const [offlineService] = useState(() => new OfflineService(authService));
  
  const [session, setSession] = useState(null);
  const [user, setUser] = useState(null);
  const [isOnline, setIsOnline] = useState(true);
  const [queueSize, setQueueSize] = useState(0);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Initialize auth state
    const initAuth = async () => {
      try {
        const currentSession = await authService.getSession();
        setSession(currentSession);
        setUser(currentSession?.user || null);
      } catch (error) {
        console.error('Error initializing auth:', error);
      } finally {
        setIsLoading(false);
      }
    };

    initAuth();

    // Listen for auth changes
    const unsubscribeAuth = authService.onSessionChange((newSession) => {
      setSession(newSession);
      setUser(newSession?.user || null);
    });

    // Listen for network changes
    const unsubscribeNetwork = offlineService.onNetworkChange((online) => {
      setIsOnline(online);
    });

    return () => {
      unsubscribeAuth();
      unsubscribeNetwork();
    };
  }, [authService, offlineService]);

  const signIn = async (email: string) => {
    await authService.signInWithMagicLink(email);
  };

  const signOut = async () => {
    await authService.signOut();
  };

  const value: NVLPContextType = {
    session,
    user,
    signIn,
    signOut,
    isOnline,
    queueSize,
    authService,
    offlineService,
    isLoading,
  };

  return (
    
      {children}
    
  );
}

export function useNVLP() {
  const context = useContext(NVLPContext);
  if (!context) {
    throw new Error('useNVLP must be used within NVLPProvider');
  }
  return context;
}

Performance Optimization

📊 Mobile Performance Tips

Key strategies for optimal mobile performance:

Data Loading Optimization

// src/hooks/useBudgetData.ts
import { useState, useEffect, useCallback } from 'react';
import { useNVLP } from '../context/NVLPContext';
import AsyncStorage from '@react-native-async-storage/async-storage';

export function useBudgetData(budgetId: string) {
  const { authService } = useNVLP();
  const nvlp = authService.getNVLPClient();
  
  const [data, setData] = useState({
    budget: null,
    categories: [],
    envelopes: [],
    recentTransactions: [],
  });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const cacheKey = `budget_data_${budgetId}`;

  const loadCachedData = useCallback(async () => {
    try {
      const cached = await AsyncStorage.getItem(cacheKey);
      if (cached) {
        const parsedData = JSON.parse(cached);
        const cacheAge = Date.now() - parsedData.timestamp;
        if (cacheAge < 5 * 60 * 1000) { // 5 minutes
          setData(parsedData.data);
          return true;
        }
      }
    } catch (error) {
      console.error('Error loading cached data:', error);
    }
    return false;
  }, [cacheKey]);

  const fetchFreshData = useCallback(async () => {
    try {
      setError(null);
      
      // Load data in parallel for better performance
      const [budget, categories, envelopes, recentTransactions] = await Promise.all([
        nvlp.budgets.eq('id', budgetId).single(),
        nvlp.categories.eq('budget_id', budgetId).order('display_order').get(),
        nvlp.envelopes.eq('budget_id', budgetId).eq('is_active', true).get(),
        nvlp.transactions.eq('budget_id', budgetId).order('created_at', false).limit(20).get(),
      ]);

      const freshData = { budget, categories, envelopes, recentTransactions };
      setData(freshData);

      // Cache the fresh data
      await AsyncStorage.setItem(cacheKey, JSON.stringify({
        data: freshData,
        timestamp: Date.now(),
      }));

    } catch (err) {
      console.error('Error fetching budget data:', err);
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [nvlp, budgetId, cacheKey]);

  const refresh = useCallback(async () => {
    setLoading(true);
    await fetchFreshData();
  }, [fetchFreshData]);

  useEffect(() => {
    const loadData = async () => {
      const hasCachedData = await loadCachedData();
      if (hasCachedData) {
        setLoading(false);
        fetchFreshData(); // Background refresh
      } else {
        await fetchFreshData();
      }
    };

    loadData();
  }, [budgetId, loadCachedData, fetchFreshData]);

  return { ...data, loading, error, refresh };
}

Security Considerations

🔐 Mobile Security Best Practices

Essential security measures for mobile apps:

Secure Token Storage

// src/services/SecureStorage.ts
import * as Keychain from 'react-native-keychain';
// import * as SecureStore from 'expo-secure-store'; // For Expo

export class SecureTokenStorage {
  private static readonly TOKEN_KEY = 'nvlp_auth_token';

  static async storeTokens(accessToken: string, refreshToken: string): Promise {
    try {
      // React Native CLI with Keychain
      await Keychain.setInternetCredentials(
        this.TOKEN_KEY,
        'nvlp_user',
        JSON.stringify({ accessToken, refreshToken })
      );
      
      // Expo alternative:
      // await SecureStore.setItemAsync(this.TOKEN_KEY, accessToken);
    } catch (error) {
      console.error('Failed to store tokens securely:', error);
      throw new Error('Token storage failed');
    }
  }

  static async getTokens(): Promise<{ accessToken: string; refreshToken: string } | null> {
    try {
      const credentials = await Keychain.getInternetCredentials(this.TOKEN_KEY);
      if (credentials) {
        return JSON.parse(credentials.password);
      }
    } catch (error) {
      console.error('Failed to retrieve tokens:', error);
    }
    return null;
  }

  static async clearTokens(): Promise {
    try {
      await Keychain.resetInternetCredentials(this.TOKEN_KEY);
    } catch (error) {
      console.error('Failed to clear tokens:', error);
    }
  }
}

Complete App Example

Main App Component

// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { StatusBar, Platform } from 'react-native';
import { NVLPProvider, useNVLP } from './src/context/NVLPContext';
import { LoadingScreen } from './src/screens/LoadingScreen';
import { AuthScreen } from './src/screens/AuthScreen';
import { BudgetDashboard } from './src/screens/BudgetDashboard';

const Stack = createStackNavigator();

function AppNavigator() {
  const { session, isLoading } = useNVLP();

  if (isLoading) {
    return ;
  }

  return (
    
      
      
        {session ? (
          
        ) : (
          
        )}
      
    
  );
}

export default function App() {
  return (
    
      
    
  );
}

🎉 Complete Mobile Integration

You now have a complete mobile app setup with:

  • ✅ Cross-platform authentication with deep linking
  • ✅ Offline-first architecture with automatic sync
  • ✅ Secure token storage and session management
  • ✅ Performance optimized data loading and caching
  • ✅ Production-ready deployment configuration

🚀 Next Steps

Ready to build your mobile app? Explore these resources: