import { useCallback, useEffect, useRef } from 'react'; import { usePersSDK } from '../providers/PersSDKProvider'; import type { PersEvent, EventHandler, EventFilter, Unsubscribe } from '@explorins/pers-sdk/core'; // Re-export event types for convenience export type { PersEvent, EventHandler, EventFilter, Unsubscribe }; /** * Return interface for the useEvents hook * * Provides event subscription capabilities for the PERS SDK event system. * Use this to display notifications, update UI, or trigger side effects. * * @interface EventsHook * @property {Function} subscribe - Subscribe to SDK events with optional filtering * @property {Function} once - Subscribe to a single event (auto-unsubscribes) * @property {Function} clear - Clear all subscriptions * @property {boolean} isAvailable - Whether the event system is available * @property {number} subscriberCount - Current number of active subscribers */ export interface EventsHook { subscribe: (handler: EventHandler, filter?: EventFilter) => Unsubscribe; once: (handler: EventHandler, filter?: EventFilter) => Unsubscribe; clear: () => void; isAvailable: boolean; subscriberCount: number; } /** * React Native hook for PERS SDK event system * * This hook provides access to the platform-agnostic event system for subscribing to * transaction, authentication, campaign, blockchain, and system events. All events * include a `userMessage` field ready for display to end users. * * **Event Domains:** * - `auth` - Authentication events (login, logout, token refresh) * - `user` - User profile events (update, create) * - `transaction` - Transaction events (created, submitted, confirmed) * - `campaign` - Campaign events (claimed, activated) * - `redemption` - Redemption events (redeemed, expired) * - `business` - Business events (created, updated, membership) * - `wallet` - Real-time blockchain events (Transfer, Approval, etc.) - Auto-enabled on auth * - `api` - API error events (network, validation, server errors) * * **Blockchain Events (Wallet Domain):** * When `sdk.connectWalletEvents()` is called (or auto-enabled via `captureWalletEvents: true`), * real-time blockchain events are automatically routed through the same event system: * ```typescript * const { subscribe } = useEvents(); * * subscribe((event) => { * if (event.domain === 'wallet') { * console.log('Blockchain event:', event.type); // wallet_transfer, wallet_approval, etc. * } * }); * ``` * * **Notification Levels:** * - `success` - Operation completed successfully * - `error` - Operation failed * * **Cleanup:** * All subscriptions created through this hook are automatically cleaned up when * the component unmounts, preventing memory leaks and stale event handlers. * * @returns {EventsHook} Hook interface with event subscription methods * * @example * **Basic Usage - Show All Notifications** * ```typescript * import { useEvents } from '@explorins/pers-sdk-react-native'; * * function NotificationComponent() { * const { subscribe, isAvailable } = useEvents(); * * useEffect(() => { * if (!isAvailable) return; * * const unsubscribe = subscribe((event) => { * // userMessage is always present and UI-ready * showNotification(event.userMessage, event.level); * }); * * return () => unsubscribe(); * }, [subscribe, isAvailable]); * * return ; * } * ``` * * @example * **Filter by Domain and Level** * ```typescript * function TransactionListener() { * const { subscribe, isAvailable } = useEvents(); * * useEffect(() => { * if (!isAvailable) return; * * // Only listen to successful transactions * const unsubscribe = subscribe( * (event) => { * if (event.level === 'success') { * playSuccessSound(); * showConfetti(); * } * }, * { domain: 'transaction' } // Filter * ); * * return () => unsubscribe(); * }, [subscribe, isAvailable]); * } * ``` * * @example * **Filter Errors for Logging** * ```typescript * function ErrorLogger() { * const { subscribe, isAvailable } = useEvents(); * * useEffect(() => { * if (!isAvailable) return; * * const unsubscribe = subscribe( * (event) => { * // Log errors to crash reporting * logToSentry({ * message: event.userMessage, * domain: event.domain, * type: event.type, * }); * }, * { level: 'error' } // Only errors * ); * * return () => unsubscribe(); * }, [subscribe, isAvailable]); * } * ``` * * @example * **One-time Event** * ```typescript * function FirstTransactionHandler() { * const { once, isAvailable } = useEvents(); * * useEffect(() => { * if (!isAvailable) return; * * // Auto-unsubscribe after first transaction event * const unsubscribe = once( * (event) => { * console.log('First transaction event:', event.type); * showOnboardingComplete(); * }, * { domain: 'transaction', level: 'success' } * ); * * return () => unsubscribe(); * }, [once, isAvailable]); * } * ``` * * @example * **Combined Domain and Level Filter** * ```typescript * function AuthErrorHandler() { * const { subscribe, isAvailable } = useEvents(); * * useEffect(() => { * if (!isAvailable) return; * * const unsubscribe = subscribe( * (event) => { * Alert.alert('Authentication Error', event.userMessage); * navigation.navigate('Login'); * }, * { domain: 'auth', level: 'error' } * ); * * return () => unsubscribe(); * }, [subscribe, isAvailable]); * } * ``` * * @see {@link PersEvent} for event structure * @see {@link EventFilter} for filtering options * @see {@link EventHandler} for handler function type * * @since 1.6.49 */ export const useEvents = (): EventsHook => { const { sdk, isInitialized } = usePersSDK(); // Track subscriptions for cleanup const subscriptionsRef = useRef([]); // Cleanup all subscriptions on unmount useEffect(() => { return () => { subscriptionsRef.current.forEach(unsubscribe => { try { unsubscribe(); } catch (e) { // Ignore cleanup errors } }); subscriptionsRef.current = []; }; }, []); /** * Subscribe to SDK events with optional filtering * * Creates a subscription that will be called for every matching event. * The subscription is automatically tracked for cleanup when the component unmounts. * * @param handler - Callback function invoked for each matching event * @param filter - Optional filter to limit events (by domain, level, or both) * @returns Unsubscribe function to manually cancel the subscription * * @example * ```typescript * const unsubscribe = subscribe((event) => { * console.log('Event:', event.type, event.userMessage); * }); * * // Later: manual cleanup * unsubscribe(); * ``` */ const subscribe = useCallback((handler: EventHandler, filter?: EventFilter): Unsubscribe => { if (!isInitialized || !sdk) { console.warn('[useEvents] SDK not initialized. Cannot subscribe to events.'); return () => {}; // Return no-op unsubscribe } const unsubscribe = sdk.events.subscribe(handler, filter); // Track for cleanup subscriptionsRef.current.push(unsubscribe); // Return wrapped unsubscribe that also removes from tracking return () => { unsubscribe(); const index = subscriptionsRef.current.indexOf(unsubscribe); if (index > -1) { subscriptionsRef.current.splice(index, 1); } }; }, [sdk, isInitialized]); /** * Subscribe to a single event (auto-unsubscribes after first match) * * Useful for one-time event handling, like waiting for a specific action to complete. * * @param handler - Callback function invoked for the first matching event * @param filter - Optional filter to limit events (by domain, level, or both) * @returns Unsubscribe function to manually cancel before event fires * * @example * ```typescript * once((event) => { * console.log('First event:', event.type); * }, { domain: 'transaction', level: 'success' }); * ``` */ const once = useCallback((handler: EventHandler, filter?: EventFilter): Unsubscribe => { if (!isInitialized || !sdk) { console.warn('[useEvents] SDK not initialized. Cannot subscribe to events.'); return () => {}; } const unsubscribe = sdk.events.once(handler, filter); // Track for cleanup subscriptionsRef.current.push(unsubscribe); return () => { unsubscribe(); const index = subscriptionsRef.current.indexOf(unsubscribe); if (index > -1) { subscriptionsRef.current.splice(index, 1); } }; }, [sdk, isInitialized]); /** * Clear all event subscriptions from this hook instance * * Useful when you want to reset all event listeners without unmounting the component. * Note: This only clears subscriptions created through this hook instance. * * @example * ```typescript * const { clear, subscribe } = useEvents(); * * // Create some subscriptions * subscribe((event) => console.log(event)); * subscribe((event) => logToServer(event)); * * // Later: clear all subscriptions from this hook * clear(); * ``` */ const clear = useCallback((): void => { subscriptionsRef.current.forEach(unsubscribe => { try { unsubscribe(); } catch (e) { // Ignore cleanup errors } }); subscriptionsRef.current = []; }, []); /** * Get current subscriber count from the event emitter * * Useful for debugging or showing active listener count in dev tools. */ const subscriberCount = sdk?.events?.subscriberCount ?? 0; return { /** * Subscribe to SDK events with optional filtering * * @param handler - Callback function for each matching event * @param filter - Optional filter by domain and/or level * @returns Unsubscribe function */ subscribe, /** * Subscribe to a single event (auto-unsubscribes after first match) * * @param handler - Callback function for the first matching event * @param filter - Optional filter by domain and/or level * @returns Unsubscribe function */ once, /** * Clear all subscriptions created through this hook instance */ clear, /** * Whether the event system is available * * Returns `true` when SDK is initialized and events can be subscribed to. */ isAvailable: isInitialized && !!sdk?.events, /** * Current number of active subscribers across all components * * Useful for debugging event system usage. */ subscriberCount, }; }; export type EventsHook_ = ReturnType;