expo-finance-kit
Version:
Native Expo module for Apple FinanceKit - Access financial data from Apple Card and other accounts
209 lines • 7.38 kB
JavaScript
/**
* Transaction monitoring module for Expo Finance Kit
* Handles real-time transaction change streams from FinanceKit
*/
import { NativeEventEmitter, Platform } from 'react-native';
import ExpoFinanceKit from '../ExpoFinanceKitModule';
import { FinanceKitErrorCode } from '../ExpoFinanceKit.types';
import { ensureAuthorized } from '../helpers';
import { createFinanceKitError } from '../utils/errors';
/**
* Transaction monitoring manager
* Provides real-time updates when transactions are inserted, updated, or deleted
*/
class TransactionMonitor {
eventEmitter = null;
listeners = new Map();
isMonitoring = false;
constructor() {
if (Platform.OS === 'ios') {
this.eventEmitter = new NativeEventEmitter(ExpoFinanceKit);
}
}
/**
* Start monitoring transactions for specified accounts
* If no account IDs are provided, monitors all accounts
* @param accountIds - Optional array of account IDs to monitor
* @returns Promise that resolves when monitoring starts
*/
async startMonitoring(accountIds) {
const isAuthorized = await ensureAuthorized();
if (!isAuthorized) {
throw createFinanceKitError(FinanceKitErrorCode.Unauthorized, 'User has not authorized access to financial data');
}
if (Platform.OS !== 'ios') {
throw createFinanceKitError(FinanceKitErrorCode.Unavailable, 'Transaction monitoring is only available on iOS');
}
try {
await ExpoFinanceKit.startMonitoringTransactions(accountIds);
this.isMonitoring = true;
}
catch (error) {
throw createFinanceKitError(FinanceKitErrorCode.Unknown, 'Failed to start transaction monitoring', { originalError: error });
}
}
/**
* Stop monitoring transactions
* @returns Promise that resolves when monitoring stops
*/
async stopMonitoring() {
if (Platform.OS !== 'ios') {
return;
}
try {
await ExpoFinanceKit.stopMonitoringTransactions();
this.isMonitoring = false;
}
catch (error) {
throw createFinanceKitError(FinanceKitErrorCode.Unknown, 'Failed to stop transaction monitoring', { originalError: error });
}
}
/**
* Add a listener for transaction changes
* @param callback - Callback function to invoke when transactions change
* @returns Unsubscribe function
*/
addListener(callback) {
if (Platform.OS !== 'ios' || !this.eventEmitter) {
return () => { };
}
const listenerId = `transaction_change_${Date.now()}_${Math.random()}`;
// Store the callback
this.listeners.set(listenerId, callback);
// Set up native event listener
const subscription = this.eventEmitter.addListener('onTransactionsChanged', (payload) => {
// Transform the payload to ensure proper types
const transformedPayload = {
accountId: payload.accountId,
timestamp: payload.timestamp,
inserted: payload.inserted?.map(transformTransaction) || [],
updated: payload.updated?.map(transformTransaction) || [],
deleted: payload.deleted || [],
hasHistoryToken: payload.hasHistoryToken,
};
callback(transformedPayload);
});
// Return unsubscribe function
return () => {
this.listeners.delete(listenerId);
subscription.remove();
};
}
/**
* Remove all listeners
*/
removeAllListeners() {
if (this.eventEmitter) {
this.eventEmitter.removeAllListeners('onTransactionsChanged');
this.listeners.clear();
}
}
/**
* Check if monitoring is currently active
*/
get monitoring() {
return this.isMonitoring;
}
}
/**
* Transform raw transaction data to ensure proper type safety
*/
function transformTransaction(data) {
return {
id: data.id,
accountId: data.accountId,
amount: typeof data.amount === 'number' ? data.amount : 0,
currencyCode: data.currencyCode || 'USD',
transactionDate: typeof data.transactionDate === 'number'
? data.transactionDate
: new Date(data.transactionDate || Date.now()).getTime(),
merchantName: data.merchantName,
transactionDescription: data.transactionDescription || '',
merchantCategoryCode: data.merchantCategoryCode,
status: data.status || 'unknown',
transactionType: data.transactionType || 'unknown',
creditDebitIndicator: data.creditDebitIndicator || 'debit',
};
}
// Export singleton instance
export const transactionMonitor = new TransactionMonitor();
/**
* Start monitoring transactions for specified accounts
* @param accountIds - Optional array of account IDs to monitor
*/
export async function startMonitoringTransactions(accountIds) {
return transactionMonitor.startMonitoring(accountIds);
}
/**
* Stop monitoring transactions
*/
export async function stopMonitoringTransactions() {
return transactionMonitor.stopMonitoring();
}
/**
* Add a listener for transaction changes
* @param callback - Callback function to invoke when transactions change
* @returns Unsubscribe function
*/
export function addTransactionChangeListener(callback) {
return transactionMonitor.addListener(callback);
}
/**
* Remove all transaction change listeners
*/
export function removeAllTransactionChangeListeners() {
transactionMonitor.removeAllListeners();
}
/**
* Check if transaction monitoring is currently active
*/
export function isMonitoringTransactions() {
return transactionMonitor.monitoring;
}
/**
* Clear history token for an account (resets monitoring state)
* @param accountId - Account ID to clear history token for
*/
export async function clearHistoryToken(accountId) {
if (Platform.OS !== 'ios') {
return;
}
try {
await ExpoFinanceKit.clearHistoryToken(accountId);
}
catch (error) {
throw createFinanceKitError(FinanceKitErrorCode.Unknown, 'Failed to clear history token', { originalError: error });
}
}
/**
* Set the app group identifier for background delivery
* This should match the identifier used in your Expo config plugin
* @param identifier - App group identifier (e.g., "group.com.yourapp.financekit")
*/
export async function setAppGroupIdentifier(identifier) {
if (Platform.OS !== 'ios') {
return;
}
try {
await ExpoFinanceKit.setAppGroupIdentifier(identifier);
}
catch (error) {
throw createFinanceKitError(FinanceKitErrorCode.Unknown, 'Failed to set app group identifier', { originalError: error });
}
}
/**
* Process pending changes that were stored during background sync
* This is automatically called when the app becomes active, but can be called manually
*/
export async function processPendingChanges() {
if (Platform.OS !== 'ios') {
return;
}
try {
await ExpoFinanceKit.processPendingChanges();
}
catch (error) {
throw createFinanceKitError(FinanceKitErrorCode.Unknown, 'Failed to process pending changes', { originalError: error });
}
}
//# sourceMappingURL=monitoring.js.map