import { useCallback } from 'react'; import { usePersSDK } from '../providers/PersSDKProvider'; import { SubmissionResult, useTransactionSigner, OnStatusUpdateFn } from './useTransactionSigner'; import type { RedemptionCreateRequestDTO, RedemptionDTO, RedemptionRedeemDTO, RedemptionRedeemRequestResponseDTO, RedemptionTypeDTO, PaginatedResponseDTO, RedemptionRedeemIncludeRelation, RedemptionIncludeRelation, ProcessRecordStatus, DynamicContext, SortOrder } from '@explorins/pers-sdk'; import type { RedemptionRedeemQueryParams, RedemptionQueryParams } from '@explorins/pers-sdk/redemption'; import type { PaginationOptions } from '@explorins/pers-sdk'; // Re-export for consumers (new names + deprecated aliases) export type { RedemptionRedeemQueryParams, RedemptionQueryParams } from '@explorins/pers-sdk/redemption'; /** @deprecated Use RedemptionRedeemQueryParams instead */ export type RedemptionRedeemFilters = RedemptionRedeemQueryParams; /** @deprecated Use RedemptionQueryParams instead */ export type RedemptionFilterOptions = RedemptionQueryParams; /** * Options for fetching redemptions with filtering and pagination */ export interface GetRedemptionsOptions { /** Filter by active status (Admin only - regular users always see active offers) */ active?: boolean; /** Free text search across name, description, tags (case-insensitive) */ search?: string; /** Filter by tag(s) - single tag or array for OR match */ tags?: string | string[]; /** Filter redemptions starting on or after this date */ startDate?: Date | string; /** Filter redemptions ending on or before this date */ endDate?: Date | string; /** Relations to include: 'redeemCount' */ include?: RedemptionIncludeRelation[]; /** Page number (1-indexed) */ page?: number; /** Results per page */ limit?: number; /** Field to sort by */ sortBy?: 'name' | 'createdAt' | 'startDate'; /** Sort direction */ sortOrder?: SortOrder; } export const useRedemptions = () => { const { sdk, isInitialized, isAuthenticated } = usePersSDK(); const { signAndSubmitTransactionWithJWT, isSignerAvailable, currentStatus, statusMessage } = useTransactionSigner(); /** * Get redemption offers based on user permissions * * **Regular Users:** Automatically see only active redemptions available for purchase * **Administrators:** Can see all redemptions and filter by active/inactive status * * The backend enforces permission-based filtering automatically. * * @param options - Filter and pagination options * @param options.active - Filter by active status (Admin only) * @param options.search - Free text search across name, description, tags * @param options.startDate - Filter redemptions starting on or after this date * @param options.endDate - Filter redemptions ending on or before this date * @param options.include - Relations to include: 'redeemCount' * @param options.page - Page number (1-indexed) * @param options.limit - Results per page * @param options.sortBy - Field to sort by ('name' | 'createdAt') * @param options.sortOrder - Sort direction ('ASC' | 'DESC') * * @example Basic usage * ```typescript * const { data: offers } = await getRedemptions(); * ``` * * @example Filter by date range * ```typescript * const { data: dateFiltered } = await getRedemptions({ * startDate: '2026-05-01', * endDate: '2026-06-30' * }); * ``` * * @example Get redemptions with redeem count * ```typescript * const { data: offers } = await getRedemptions({ include: ['redeemCount'] }); * offers.forEach(o => console.log(`${o.name}: ${o.redeemCount} redeems`)); * ``` * * @example Search with pagination * ```typescript * const { data, pagination } = await getRedemptions({ * search: 'discount', * page: 1, * limit: 10, * sortBy: 'name', * sortOrder: 'ASC' * }); * ``` */ const getRedemptions = useCallback(async (options?: GetRedemptionsOptions): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { // Convert Date to ISO string for API compatibility const apiOptions = options ? { ...options, startDate: options.startDate instanceof Date ? options.startDate.toISOString() : options.startDate, endDate: options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate, } : undefined; const result = await sdk.redemptions.getRedemptions(apiOptions); return result; } catch (error) { console.error('Failed to fetch redemptions:', error); throw error; } }, [sdk, isInitialized]); /** * Get user's redemption history with optional filters, pagination and include relations * * @param filters - Optional filters (redemptionId, status) * @param options - Pagination options (page, limit, sortBy, sortOrder) * @param include - Relations to include: 'redemption', 'user', 'business', 'transactions' * @returns Promise resolving to paginated redemption history * * @example * ```typescript * // Get first page of user redemptions * const { data: redeems, pagination } = await getUserRedemptions({}, { page: 1, limit: 10 }); * console.log(`Page ${pagination.page} of ${pagination.pages}`); * * // Get only completed redemptions * import { ProcessRecordStatus } from '@explorins/pers-shared'; * const { data: completed } = await getUserRedemptions({ status: ProcessRecordStatus.COMPLETED }); * * // Get user redemptions with full redemption details * const { data: redeems } = await getUserRedemptions({}, {}, ['redemption', 'business']); * redeems.forEach(redeem => { * console.log('Redemption:', redeem.included?.redemption?.name); * console.log('Business:', redeem.included?.business?.displayName); * }); * ``` */ const getUserRedemptions = useCallback(async ( filters?: { redemptionId?: string; status?: ProcessRecordStatus }, options?: PaginationOptions, include?: RedemptionRedeemIncludeRelation[] ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } if (!isAuthenticated) { console.warn('SDK not authenticated. getUserRedemptions requires authentication.'); return { data: [], pagination: { page: 1, limit: 0, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } try { const result = await sdk.redemptions.getUserRedemptions(filters, options, include); return result; } catch (error) { console.error('Failed to fetch user redemptions:', error); throw error; } }, [sdk, isInitialized, isAuthenticated]); /** * Redeem a redemption offer with optional dynamic context * * ## When to use `redeem()` vs `createTransaction()` * * | Use Case | Method | Auto-Signs | * |----------|--------|------------| * | **Voucher redemption** | `redeem()` | ✅ Yes | * | **Reward redemption** | `redeem()` | ✅ Yes | * | **Direct token transfer** | `createTransaction()` | ⚠️ Depends | * | **POS burn with QR** | `buildPOSBurnRequest()` | ❌ Manual | * * **Use `redeem()` for voucher/reward redemptions.** It automatically: * 1. Creates redemption record * 2. Triggers passkey/biometric prompt * 3. Signs the transaction * 4. Submits to blockchain * 5. Returns result with `transactionHash` * * **No admin approval required** - The "PENDING" state is brief (seconds). * The user's passkey signature IS the approval. * * Supports dynamic context for personalized NFT generation when the redemption * issues ERC721 tokens. Context can include validity dates, AI prompt placeholders, * and custom metadata. * * @param redemptionId - ID of the redemption to redeem * @param options - Optional settings * @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts) * @param options.onStatusUpdate - Callback for transaction signing status updates * @returns Promise resolving to redemption result with `transactionHash` on success * * @example Basic redemption (most common) * ```typescript * const { redeem } = useRedemptions(); * * const result = await redeem('redemption-123', { * onStatusUpdate: (status, message) => { * console.log(`Status: ${status} - ${message}`); * } * }); * * if (result.transactionHash) { * console.log('Redemption complete!', result.transactionHash); * } * ``` * * @example Redemption with dynamic context (personalized NFT) * ```typescript * const result = await redeem('event-ticket-123', { * context: { * // Validity - token valid for event duration * validityDate: new Date().toISOString(), * validityDuration: 8, // 8 hours * * // AI prompt context - available as {{context.xxx}} in prompts * attendeeName: 'John Doe', * ticketTier: 'VIP', * seatSection: 'A12', * } * }); * ``` * * @example Hotel stay NFT with check-in/check-out dates * ```typescript * const result = await redeem('hotel-nft-456', { * context: { * validityDate: '2026-04-15T14:00:00Z', // Check-in * validityEndDate: '2026-04-20T11:00:00Z', // Check-out * guestName: 'Jane Smith', * roomNumber: '305', * roomType: 'Suite', * } * }); * ``` * * @see {@link useTransactions.createTransaction | createTransaction()} for direct transfers without redemption flow */ const redeem = useCallback(async ( redemptionId: string, options?: { context?: DynamicContext; onStatusUpdate?: OnStatusUpdateFn } ): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } if (!isAuthenticated) { throw new Error('SDK not authenticated. redeem requires authentication.'); } try { const result = await sdk.redemptions.redeem(redemptionId, options?.context); const txToken = result.senderTransaction?.actionable?.authToken; if (txToken && isSignerAvailable) { try { const signingResult: SubmissionResult = await signAndSubmitTransactionWithJWT(txToken, options?.onStatusUpdate); if (signingResult.success) { // Return enhanced result with signing information return { ...result, transactionHash: signingResult.transactionHash, // signature: signingResult.signature, isSigned: true, signedAt: new Date().toISOString() } as RedemptionRedeemRequestResponseDTO; } else { console.error('Transaction signing failed:', signingResult.error); throw new Error(signingResult.error || 'Transaction signing failed'); } } catch (signingError) { console.error('Blockchain signing error:', signingError); throw new Error(`Transaction signing failed: ${signingError}`); } } else if (txToken && !isSignerAvailable) { console.warn('Transaction requires signature but signer is not available'); throw new Error('Transaction requires signature but blockchain signer is not initialized'); } // Return original result if no signing was required return result; } catch (error) { console.error('Failed to redeem redemption:', error); throw error; } }, [sdk, isInitialized, isAuthenticated, signAndSubmitTransactionWithJWT, isSignerAvailable]); // Admin methods /** * Admin: Get all redemption redeems with filters and include relations * * Retrieves all redemption redeems across the platform with filtering. * * @param filters - Optional filters for user, redemption, date range, and pagination * @param filters.userId - Filter by user ID * @param filters.redemptionId - Filter by redemption offer ID * @param filters.businessId - Filter by business ID (admin only) * @param filters.status - Filter by processing status (PENDING, PROCESSING, COMPLETED, FAILED) * @param filters.dateFrom - Filter redeems created on or after this date (ISO 8601 format) * @param filters.dateTo - Filter redeems created on or before this date (ISO 8601 format) * @param filters.page - Page number (1-indexed) * @param filters.limit - Items per page * @param filters.sortBy - Field to sort by * @param filters.sortOrder - Sort direction * @param include - Relations to include: 'redemption', 'user', 'business' * @returns Promise resolving to paginated redemption redeems * * @example * ```typescript * // Get all redeems with user details * const { data: redeems } = await getRedemptionRedeems({}, ['user', 'redemption']); * * // Filter by specific user * const { data: userRedeems } = await getRedemptionRedeems( * { userId: 'user-123' }, * ['redemption'] * ); * * // Filter by date range * const { data } = await getRedemptionRedeems({ * dateFrom: '2026-01-01', * dateTo: '2026-03-31' * }, ['user', 'redemption']); * ``` */ const getRedemptionRedeems = useCallback(async ( filters?: RedemptionRedeemQueryParams, include?: RedemptionRedeemIncludeRelation[] ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.redemptions.getRedemptionRedeems(filters, include); return result; } catch (error) { console.error('Failed to fetch redemption redeems:', error); throw error; } }, [sdk, isInitialized]); const createRedemption = useCallback(async (redemptionData: RedemptionCreateRequestDTO): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.redemptions.createRedemption(redemptionData); return result; } catch (error) { console.error('Failed to create redemption offer:', error); throw error; } }, [sdk, isInitialized]); const getRedemptionTypes = useCallback(async (): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.redemptions.getRedemptionTypes(); return result; } catch (error) { console.error('Failed to fetch redemption types:', error); throw error; } }, [sdk, isInitialized]); const updateRedemption = useCallback(async (redemptionId: string, redemptionData: RedemptionCreateRequestDTO): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.redemptions.updateRedemption(redemptionId, redemptionData); return result; } catch (error) { console.error('Failed to update redemption:', error); throw error; } }, [sdk, isInitialized]); const toggleRedemptionStatus = useCallback(async (redemptionId: string): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.redemptions.toggleRedemptionStatus(redemptionId); return result; } catch (error) { console.error('Failed to toggle redemption status:', error); throw error; } }, [sdk, isInitialized]); /** * Get a specific redemption by ID * * Retrieves detailed information about a specific redemption offer. * * @param id - Redemption UUID * @param include - Optional relations to include: 'redeemCount' * @returns Promise resolving to the redemption * * @example Basic usage * ```typescript * const redemption = await getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3'); * console.log(redemption.name); * ``` * * @example With redeem count * ```typescript * const redemption = await getRedemptionById('redemption-123', ['redeemCount']); * console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`); * ``` */ const getRedemptionById = useCallback(async ( id: string, include?: RedemptionIncludeRelation[] ): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.redemptions.getRedemptionById(id, include); return result; } catch (error) { console.error('Failed to fetch redemption by ID:', error); throw error; } }, [sdk, isInitialized]); return { getRedemptions, getRedemptionById, getUserRedemptions, redeem, getRedemptionTypes, getRedemptionRedeems, createRedemption, updateRedemption, toggleRedemptionStatus, isAvailable: isInitialized && !!sdk?.redemptions, // Expose signing status for UI feedback signingStatus: currentStatus, signingStatusMessage: statusMessage, }; }; export type RedemptionHook = ReturnType;