UNPKG

expo-finance-kit

Version:

Native Expo module for Apple FinanceKit - Access financial data from Apple Card and other accounts

434 lines (386 loc) 10.2 kB
/** * React hooks for Expo Finance Kit * Provides easy integration with React components */ import { useEffect, useState, useCallback, useRef } from 'react'; import { Account, Transaction, AccountBalance, AuthorizationStatus, TransactionQueryOptions, AccountQueryOptions, } from '../ExpoFinanceKit.types'; import { getAccounts, getAccountsWithOptions, getAccountById, } from '../modules/accounts'; import { getTransactions, getRecentTransactions, getTransactionsByAccount, } from '../modules/transactions'; import { getBalances, getBalanceByAccount, getTotalBalance, } from '../modules/balances'; import { getAuthorizationStatus, requestAuthorization, authorizationListener, } from '../modules/authorization'; import { startMonitoringTransactions, stopMonitoringTransactions, addTransactionChangeListener, isMonitoringTransactions, } from '../modules/monitoring'; import { TransactionsChangedPayload } from '../ExpoFinanceKit.types'; /** * Hook for managing authorization status */ export function useAuthorizationStatus() { const [status, setStatus] = useState<AuthorizationStatus>('notDetermined'); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { // Get initial status getAuthorizationStatus() .then(setStatus) .catch(setError) .finally(() => setLoading(false)); // Listen for changes const unsubscribe = authorizationListener.addStatusChangeListener((payload) => { setStatus(payload.status); }); return unsubscribe; }, []); const requestAuth = useCallback(async () => { setLoading(true); setError(null); try { const result = await requestAuthorization(); setStatus(result.status); return result; } catch (err) { setError(err as Error); throw err; } finally { setLoading(false); } }, []); return { status, loading, error, requestAuthorization: requestAuth, isAuthorized: status === 'authorized', }; } /** * Hook for fetching accounts */ export function useAccounts(options?: AccountQueryOptions) { const [accounts, setAccounts] = useState<Account[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const fetchAccounts = useCallback(async () => { setLoading(true); setError(null); try { const data = options ? await getAccountsWithOptions(options) : await getAccounts(); setAccounts(data); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, [options]); useEffect(() => { fetchAccounts(); }, [fetchAccounts]); return { accounts, loading, error, refetch: fetchAccounts, }; } /** * Hook for fetching a single account */ export function useAccount(accountId: string) { const [account, setAccount] = useState<Account | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { if (!accountId) { setLoading(false); return; } setLoading(true); setError(null); getAccountById(accountId) .then(setAccount) .catch(setError) .finally(() => setLoading(false)); }, [accountId]); return { account, loading, error, }; } /** * Hook for fetching transactions */ export function useTransactions(options?: TransactionQueryOptions) { const [transactions, setTransactions] = useState<Transaction[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const optionsRef = useRef(options); const fetchTransactions = useCallback(async () => { setLoading(true); setError(null); try { const data = await getTransactions(optionsRef.current); setTransactions(data); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, []); useEffect(() => { optionsRef.current = options; fetchTransactions(); }, [options, fetchTransactions]); return { transactions, loading, error, refetch: fetchTransactions, }; } /** * Hook for fetching recent transactions */ export function useRecentTransactions(limit: number = 50) { const [transactions, setTransactions] = useState<Transaction[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const fetchTransactions = useCallback(async () => { setLoading(true); setError(null); try { const data = await getRecentTransactions(limit); setTransactions(data); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, [limit]); useEffect(() => { fetchTransactions(); }, [fetchTransactions]); return { transactions, loading, error, refetch: fetchTransactions, }; } /** * Hook for fetching account balance */ export function useAccountBalance(accountId?: string) { const [balance, setBalance] = useState<AccountBalance | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const fetchBalance = useCallback(async () => { setLoading(true); setError(null); try { const data = accountId ? await getBalanceByAccount(accountId) : (await getBalances())[0] || null; setBalance(data); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, [accountId]); useEffect(() => { fetchBalance(); }, [fetchBalance]); return { balance, loading, error, refetch: fetchBalance, }; } /** * Hook for fetching total balance across all accounts */ export function useTotalBalance() { const [totalBalance, setTotalBalance] = useState<{ total: number; byCurrency: Map<string, number>; accounts: AccountBalance[]; } | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const fetchBalance = useCallback(async () => { setLoading(true); setError(null); try { const data = await getTotalBalance(); setTotalBalance(data); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, []); useEffect(() => { fetchBalance(); }, [fetchBalance]); return { totalBalance, loading, error, refetch: fetchBalance, }; } /** * Hook for real-time transaction updates (polling-based) * @deprecated Use useTransactionMonitoring for FinanceKit native change streams */ export function useTransactionStream( accountId?: string, pollingInterval: number = 30000 // 30 seconds ) { const [transactions, setTransactions] = useState<Transaction[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); const fetchTransactions = useCallback(async () => { try { const data = accountId ? await getTransactionsByAccount(accountId, { limit: 100 }) : await getRecentTransactions(100); setTransactions(data); setError(null); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, [accountId]); useEffect(() => { // Initial fetch fetchTransactions(); // Set up polling intervalRef.current = setInterval(fetchTransactions, pollingInterval); // Cleanup return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [fetchTransactions, pollingInterval]); return { transactions, loading, error, refetch: fetchTransactions, }; } /** * Hook for real-time transaction monitoring using FinanceKit change streams * Provides batched updates when transactions are inserted, updated, or deleted */ export function useTransactionMonitoring( accountIds?: string[], options?: { autoStart?: boolean; onChanges?: (payload: TransactionsChangedPayload) => void; } ) { const [isMonitoring, setIsMonitoring] = useState(false); const [error, setError] = useState<Error | null>(null); const [changeCount, setChangeCount] = useState(0); const autoStart = options?.autoStart ?? true; const onChangesRef = useRef(options?.onChanges); // Update callback ref when it changes useEffect(() => { onChangesRef.current = options?.onChanges; }, [options?.onChanges]); // Set up monitoring useEffect(() => { let unsubscribe: (() => void) | null = null; const startMonitoring = async () => { try { setError(null); await startMonitoringTransactions(accountIds); setIsMonitoring(isMonitoringTransactions()); // Set up listener for changes unsubscribe = addTransactionChangeListener((payload) => { setChangeCount(prev => prev + 1); if (onChangesRef.current) { onChangesRef.current(payload); } }); } catch (err) { setError(err as Error); setIsMonitoring(false); } }; if (autoStart) { startMonitoring(); } // Cleanup return () => { if (unsubscribe) { unsubscribe(); } stopMonitoringTransactions().catch(() => { // Ignore cleanup errors }); setIsMonitoring(false); }; }, [accountIds?.join(','), autoStart]); const start = useCallback(async () => { try { setError(null); await startMonitoringTransactions(accountIds); setIsMonitoring(isMonitoringTransactions()); } catch (err) { setError(err as Error); throw err; } }, [accountIds]); const stop = useCallback(async () => { try { await stopMonitoringTransactions(); setIsMonitoring(false); } catch (err) { setError(err as Error); throw err; } }, []); return { isMonitoring, error, changeCount, start, stop, }; }