import { useCallback } from 'react'; import { usePersSDK } from '../providers/PersSDKProvider'; import type { CampaignClaimRequestDTO, CampaignDTO, CampaignClaimDTO, CampaignTriggerDTO, PaginatedResponseDTO, CampaignClaimIncludeRelation, CampaignIncludeRelation, SortOrder, CampaignFilterOptions, // DynamicContext } from '@explorins/pers-sdk'; import type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign'; // Re-export for consumers export type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign'; export type { CampaignFilterOptions } from '@explorins/pers-sdk'; /** * Options for fetching campaigns * @deprecated Use CampaignFilterOptions from '@explorins/pers-sdk/campaign' instead */ export interface GetCampaignsOptions { /** Filter by tag(s) - single tag or array for OR match */ tags?: string | string[]; /** Free text search across name and description (case-insensitive) */ search?: string; /** Filter by business ID */ businessId?: string; /** Filter campaigns starting on or after this date */ startDate?: Date | string; /** Filter campaigns ending on or before this date */ endDate?: Date | string; /** Page number (1-indexed) */ page?: number; /** Items per page */ limit?: number; /** Sort field */ sortBy?: 'name' | 'createdAt' | 'startDate'; /** Sort order */ sortOrder?: SortOrder; /** Relations to include: 'triggerSources', 'businesses', 'claimCount' */ include?: CampaignIncludeRelation[]; } export const useCampaigns = () => { const { sdk, isInitialized, isAuthenticated } = usePersSDK(); /** * Get active campaigns with optional filters and include relations * * @param options - Filter, pagination, and include options * @returns Promise resolving to paginated active campaigns * * @example * ```typescript * // Simple: get active campaigns with trigger sources * const { data: campaigns } = await getActiveCampaigns({ include: ['triggerSources'] }); * * // With pagination * const { data, pagination } = await getActiveCampaigns({ * page: 1, * limit: 10, * include: ['triggerSources', 'businesses'] * }); * * // Filter by date range * const { data } = await getActiveCampaigns({ * startDate: '2026-05-01', * endDate: '2026-12-31' * }); * * // Get campaigns with claim count * const { data } = await getActiveCampaigns({ include: ['claimCount'] }); * data.forEach(c => console.log(`${c.name}: ${c.claimCount} claims`)); * ``` */ const getActiveCampaigns = useCallback(async ( options?: GetCampaignsOptions ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaigns({ active: true, ...options }); return result; } catch (error) { console.error('Failed to fetch active campaigns:', error); throw error; } }, [sdk, isInitialized]); /** * Get campaign by ID with optional include relations * * @param campaignId - The campaign ID * @param include - Relations to include: 'triggerSources', 'businesses', 'claimCount' * @returns Promise resolving to campaign data * * @example * ```typescript * // Get campaign with all related data * const campaign = await getCampaignById('campaign-123', ['triggerSources', 'businesses']); * console.log('Trigger sources:', campaign.included?.triggerSources); * * // Get campaign with claim count * const campaign = await getCampaignById('campaign-123', ['claimCount']); * console.log('Total claims:', campaign.claimCount); * ``` */ const getCampaignById = useCallback(async ( campaignId: string, include?: CampaignIncludeRelation[] ): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaignById(campaignId, include); return result; } catch (error) { console.error('Failed to fetch campaign:', error); throw error; } }, [sdk, isInitialized]); /** * Claim a campaign reward with optional dynamic context * * Triggers a campaign claim for the authenticated user. The campaign must be active * and the user must meet all eligibility requirements. Supports dynamic context for * personalized NFT generation when the campaign issues ERC721 tokens. * * @param request - Campaign claim request with optional context * @param request.campaignId - ID of the campaign to claim (optional if using triggerSourceId) * @param request.triggerSourceId - ID of trigger source (QR, NFC, etc.) * @param request.context - Dynamic context for NFT personalization * @param request.metadata - Additional metadata for the claim * @param request.latitude - User's latitude for geo-validated claims * @param request.longitude - User's longitude for geo-validated claims * @returns Promise resolving to campaign claim result * * @example Basic campaign claim * ```typescript * const claim = await claimCampaign({ * triggerSourceId: 'qr-code-123', * }); * ``` * * @example Campaign claim with dynamic context (personalized NFT) * ```typescript * import { DynamicContext } from '@explorins/pers-sdk-react-native'; * * const context: DynamicContext = { * // Validity - token valid for 30 days * validityDate: new Date().toISOString(), * validityDuration: 30, * * // AI prompt context - available as {{context.xxx}} in prompts * visitorName: 'John Doe', * visitLocation: 'Grand Hotel Lobby', * membershipTier: 'Gold', * }; * * const claim = await claimCampaign({ * triggerSourceId: 'hotel-checkin-qr', * context, * }); * ``` * * @example Geo-validated claim with context * ```typescript * const claim = await claimCampaign({ * triggerSourceId: 'museum-entrance-nfc', * latitude: 48.8606, * longitude: 2.3376, * context: { * visitorType: 'Adult', * ticketCategory: 'General Admission', * } * }); * ``` */ const claimCampaign = useCallback(async (request: CampaignClaimRequestDTO): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } if (!isAuthenticated) { throw new Error('SDK not authenticated. claimCampaign requires authentication.'); } try { const result = await sdk.campaigns.claimCampaign(request); return result; } catch (error) { console.error('Failed to claim campaign:', error); throw error; } }, [sdk, isInitialized, isAuthenticated]); /** * Get user's campaign claims with optional pagination and include relations * * @param options - Optional options including pagination and include relations * @returns Promise resolving to paginated campaign claims * * @example * ```typescript * // Get user's claims with campaign details * const { data: claims } = await getUserClaims({ include: ['campaign', 'business'] }); * claims.forEach(claim => { * console.log('Campaign:', claim.included?.campaign?.name); * }); * * // With pagination * const { data } = await getUserClaims({ page: 1, limit: 10, include: ['campaign'] }); * ``` */ const getUserClaims = useCallback(async (options?: { page?: number; limit?: number; include?: CampaignClaimIncludeRelation[]; }): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } if (!isAuthenticated) { console.warn('SDK not authenticated. getUserClaims requires authentication.'); return { data: [], pagination: { page: 1, limit: 0, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } try { const result = await sdk.campaigns.getUserClaims(options); return result; } catch (error) { console.error('Failed to fetch user claims:', error); throw error; } }, [sdk, isInitialized, isAuthenticated]); const getCampaignTriggers = useCallback(async (): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaignTriggers(); return result; } catch (error) { console.error('Failed to fetch campaign triggers:', error); throw error; } }, [sdk, isInitialized]); // Admin methods /** * Admin: Get all campaigns with optional filters and include relations * * @param options - Filter, pagination, and include options (same as getActiveCampaigns but active is optional) * @returns Promise resolving to paginated campaigns * * @example * ```typescript * // Get all campaigns * const { data } = await getAllCampaigns(); * * // Get only active campaigns with trigger sources * const { data } = await getAllCampaigns({ active: true, include: ['triggerSources'] }); * * // Filter by date range with claim count * const { data } = await getAllCampaigns({ * startDate: '2026-05-01', * endDate: '2026-12-31', * include: ['claimCount'] * }); * data.forEach(c => console.log(`${c.name}: ${c.claimCount} claims`)); * * // With pagination and sorting * const { data } = await getAllCampaigns({ * page: 1, * limit: 20, * sortBy: 'createdAt', * sortOrder: 'DESC' * }); * ``` */ const getAllCampaigns = useCallback(async ( options?: GetCampaignsOptions & { active?: boolean } ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaigns(options); return result; } catch (error) { console.error('Failed to fetch all campaigns:', error); throw error; } }, [sdk, isInitialized]); /** * Admin: Get campaign claims with optional filters and include relations * * @param filters - Optional filters for campaign, user, business, and date range * @param filters.campaignId - Filter by specific campaign ID * @param filters.userId - Filter by user ID * @param filters.businessId - Filter by business ID * @param filters.dateFrom - Filter claims created on or after this date (ISO 8601 format) * @param filters.dateTo - Filter claims 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: 'campaign', 'user', 'business' * @returns Promise resolving to paginated campaign claims * * @example * ```typescript * // Get all claims with user details * const { data: claims } = await getCampaignClaims({}, ['user']); * * // Get claims for a specific campaign * const { data } = await getCampaignClaims({ campaignId: 'campaign-123' }, ['user', 'business']); * * // Filter by date range * const { data } = await getCampaignClaims({ * dateFrom: '2026-01-01', * dateTo: '2026-03-31' * }, ['campaign']); * ``` */ const getCampaignClaims = useCallback(async ( filters?: CampaignClaimFilters, include?: CampaignClaimIncludeRelation[] ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaignClaims(filters, include); return result; } catch (error) { console.error('Failed to fetch campaign claims:', error); throw error; } }, [sdk, isInitialized]); /** * Admin: Get campaign claims by user ID with optional include relations * * @param userId - The user ID * @param include - Relations to include: 'campaign', 'user', 'business' * @returns Promise resolving to paginated campaign claims */ const getCampaignClaimsByUserId = useCallback(async ( userId: string, include?: CampaignClaimIncludeRelation[] ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaignClaimsByUserId(userId, undefined, include); return result; } catch (error) { console.error('Failed to fetch campaign claims by user ID:', error); throw error; } }, [sdk, isInitialized]); /** * Admin: Get campaign claims by business ID with optional include relations * * @param businessId - The business ID * @param include - Relations to include: 'campaign', 'user', 'business' * @returns Promise resolving to paginated campaign claims */ const getCampaignClaimsByBusinessId = useCallback(async ( businessId: string, include?: CampaignClaimIncludeRelation[] ): Promise> => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const result = await sdk.campaigns.getCampaignClaimsByBusinessId(businessId, undefined, include); return result; } catch (error) { console.error('Failed to fetch campaign claims by business ID:', error); throw error; } }, [sdk, isInitialized]); // ========================================== // TRIGGER SOURCE ASSIGNMENT (Admin) // Note: TriggerSource CRUD is in useTriggerSources hook // ========================================== /** * Admin: Assign a trigger source to a campaign * * Associates a trigger source (QR code, NFC tag, etc.) with a campaign. * A campaign can have multiple trigger sources. * * Note: To create/update/delete trigger sources, use `useTriggerSources` hook. * * @param campaignId - Campaign UUID * @param triggerSourceId - Trigger source UUID * @returns Promise resolving to updated campaign * * @example * ```typescript * const { create } = useTriggerSources(); * const { assignTriggerSource } = useCampaigns(); * * // Create trigger source first * const source = await create({ type: 'QR_CODE', name: 'Store QR' }); * * // Then assign to campaign * const updated = await assignTriggerSource('campaign-123', source.id); * ``` */ const assignTriggerSource = useCallback(async ( campaignId: string, triggerSourceId: string ): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } if (!isAuthenticated) { throw new Error('SDK not authenticated. assignTriggerSource requires admin authentication.'); } try { const result = await sdk.campaigns.assignTriggerSource(campaignId, triggerSourceId); return result; } catch (error) { console.error('Failed to assign trigger source:', error); throw error; } }, [sdk, isInitialized, isAuthenticated]); /** * Admin: Remove a trigger source from a campaign * * Removes the association between a trigger source and a campaign. * The trigger source itself is not deleted. * * @param campaignId - Campaign UUID * @param triggerSourceId - Trigger source UUID * @returns Promise resolving to updated campaign */ const removeTriggerSource = useCallback(async ( campaignId: string, triggerSourceId: string ): Promise => { if (!isInitialized || !sdk) { throw new Error('SDK not initialized. Call initialize() first.'); } if (!isAuthenticated) { throw new Error('SDK not authenticated. removeTriggerSource requires admin authentication.'); } try { const result = await sdk.campaigns.removeTriggerSource(campaignId, triggerSourceId); return result; } catch (error) { console.error('Failed to remove trigger source:', error); throw error; } }, [sdk, isInitialized, isAuthenticated]); return { getActiveCampaigns, getCampaignById, claimCampaign, getUserClaims, getCampaignTriggers, getAllCampaigns, getCampaignClaims, getCampaignClaimsByUserId, getCampaignClaimsByBusinessId, assignTriggerSource, removeTriggerSource, isAvailable: isInitialized && !!sdk?.campaigns, }; }; export type CampaignHook = ReturnType;