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;