@explorins/pers-sdk-react-native 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorins/pers-sdk-react-native",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "React Native SDK for PERS Platform - Tourism Loyalty System with Blockchain Transaction Signing and WebAuthn Authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "author": "eXplorins",
38
38
  "license": "MIT",
39
39
  "dependencies": {
40
- "@explorins/pers-sdk": "^2.1.3",
40
+ "@explorins/pers-sdk": "^2.1.6",
41
41
  "@explorins/pers-signer": "^1.0.33",
42
42
  "buffer": "^6.0.3",
43
43
  "ethers": "^6.15.0",
@@ -16,6 +16,7 @@ export { useFiles } from './useFiles';
16
16
  export { useAnalytics } from './useAnalytics';
17
17
  export { useDonations } from './useDonations';
18
18
  export { useEvents } from './useEvents';
19
+ export { useTriggerSources } from './useTriggerSources';
19
20
 
20
21
  // Re-export auth-related types for convenience
21
22
  export type { RawUserData } from './useAuth';
@@ -34,4 +35,19 @@ export type {
34
35
  TokenBalanceWithToken,
35
36
  UseTokenBalancesOptions,
36
37
  UseTokenBalancesResult
37
- } from './useTokenBalances';
38
+ } from './useTokenBalances';
39
+
40
+ // Export campaign-related types
41
+ export type { CampaignClaimFilters, CampaignHook } from './useCampaigns';
42
+
43
+ // Export redemption-related types
44
+ export type { RedemptionRedeemFilters, RedemptionHook } from './useRedemptions';
45
+
46
+ // Export transaction-related types
47
+ export type { TransactionQueryOptions, TransactionHook } from './useTransactions';
48
+
49
+ // Export trigger source-related types
50
+ export type { TriggerSourceQueryOptions, TriggerSourceHook } from './useTriggerSources';
51
+
52
+ // Export analytics-related types
53
+ export type { AnalyticsHook } from './useAnalytics';
@@ -2,35 +2,63 @@ import { useCallback } from 'react';
2
2
  import { usePersSDK } from '../providers/PersSDKProvider';
3
3
  import {
4
4
  TransactionAnalyticsRequestDTO,
5
- TransactionAnalyticsResponseDTO
5
+ TransactionAnalyticsResponseDTO,
6
+ CampaignClaimAnalyticsRequestDTO,
7
+ CampaignClaimAnalyticsResponseDTO,
8
+ UserAnalyticsRequestDTO,
9
+ UserAnalyticsResponseDTO,
10
+ UserRankingAnalyticsRequestDTO,
11
+ UserRankingAnalyticsResponseDTO,
12
+ BusinessRankingAnalyticsRequestDTO,
13
+ BusinessRankingAnalyticsResponseDTO,
14
+ RetentionAnalyticsRequestDTO,
15
+ RetentionAnalyticsResponseDTO
6
16
  } from '@explorins/pers-shared';
7
17
 
8
18
  /**
9
19
  * React hook for analytics operations in the PERS SDK
10
20
  *
11
- * Provides methods for retrieving transaction analytics and insights.
12
- * Supports various analytics queries for business intelligence and reporting.
21
+ * Provides comprehensive analytics and business intelligence capabilities:
22
+ * - **Transaction Analytics**: Volume, trends, business performance
23
+ * - **Campaign Claim Analytics**: Campaign performance and claim patterns
24
+ * - **User Analytics**: Engagement metrics with per-active-user averages
25
+ * - **User Ranking**: Leaderboards with full user details
26
+ * - **Business Ranking**: Business performance rankings
27
+ * - **Retention Analytics**: Monthly retention metrics
13
28
  *
14
29
  * @returns Analytics hook with methods for data analysis
15
30
  *
16
- * @example
31
+ * @example Basic Transaction Analytics
17
32
  * ```typescript
18
33
  * function AnalyticsComponent() {
19
34
  * const { getTransactionAnalytics } = useAnalytics();
20
35
  *
21
36
  * const loadAnalytics = async () => {
22
- * try {
23
- * const analytics = await getTransactionAnalytics({
24
- * timeRange: 'last_30_days',
25
- * groupBy: 'day'
26
- * });
27
- * console.log('Transaction analytics:', analytics);
28
- * } catch (error) {
29
- * console.error('Failed to load analytics:', error);
30
- * }
37
+ * const analytics = await getTransactionAnalytics({
38
+ * startDate: '2024-01-01',
39
+ * endDate: '2024-01-31',
40
+ * groupBy: 'day'
41
+ * });
42
+ * console.log('Transaction analytics:', analytics);
31
43
  * };
44
+ * }
45
+ * ```
32
46
  *
33
- * return <button onClick={loadAnalytics}>Load Analytics</button>;
47
+ * @example User Leaderboard
48
+ * ```typescript
49
+ * function LeaderboardScreen() {
50
+ * const { getUserRanking } = useAnalytics();
51
+ *
52
+ * const loadLeaderboard = async () => {
53
+ * const ranking = await getUserRanking({
54
+ * sortBy: 'totalTransactions',
55
+ * sortOrder: 'DESC',
56
+ * limit: 50
57
+ * });
58
+ * ranking.results.forEach((user, i) => {
59
+ * console.log(`#${i + 1}: ${user.email} - ${user.totalTransactions} txns`);
60
+ * });
61
+ * };
34
62
  * }
35
63
  * ```
36
64
  */
@@ -46,19 +74,17 @@ export const useAnalytics = () => {
46
74
  *
47
75
  * @example
48
76
  * ```typescript
49
- * const { getTransactionAnalytics } = useAnalytics();
50
77
  * const analytics = await getTransactionAnalytics({
51
- * groupBy: ['day'],
52
- * metrics: ['count', 'sum'],
53
78
  * startDate: '2024-01-01',
54
79
  * endDate: '2024-01-31',
55
- * filters: { status: 'completed' }
80
+ * groupBy: 'day',
81
+ * metrics: ['count', 'sum']
56
82
  * });
57
- * console.log('Daily transaction analytics:', analytics.results);
58
- * console.log('Execution time:', analytics.metadata.executionTime);
59
83
  * ```
60
84
  */
61
- const getTransactionAnalytics = useCallback(async (request: TransactionAnalyticsRequestDTO): Promise<TransactionAnalyticsResponseDTO> => {
85
+ const getTransactionAnalytics = useCallback(async (
86
+ request: TransactionAnalyticsRequestDTO
87
+ ): Promise<TransactionAnalyticsResponseDTO> => {
62
88
  if (!isInitialized || !sdk) {
63
89
  throw new Error('SDK not initialized. Call initialize() first.');
64
90
  }
@@ -72,8 +98,229 @@ export const useAnalytics = () => {
72
98
  }
73
99
  }, [sdk, isInitialized]);
74
100
 
101
+ /**
102
+ * Retrieves campaign claim analytics with aggregation
103
+ *
104
+ * Provides insights into campaign performance, claim patterns, and user engagement.
105
+ *
106
+ * @param request - Analytics request with filters, groupBy, and metrics
107
+ * @returns Promise resolving to campaign claim analytics data
108
+ *
109
+ * @example Claims per campaign
110
+ * ```typescript
111
+ * const analytics = await getCampaignClaimAnalytics({
112
+ * filters: { status: 'COMPLETED' },
113
+ * groupBy: ['campaignId'],
114
+ * metrics: ['count'],
115
+ * sortBy: 'count',
116
+ * sortOrder: 'DESC',
117
+ * limit: 10
118
+ * });
119
+ * console.log('Top campaigns:', analytics.results);
120
+ * ```
121
+ */
122
+ const getCampaignClaimAnalytics = useCallback(async (
123
+ request: CampaignClaimAnalyticsRequestDTO
124
+ ): Promise<CampaignClaimAnalyticsResponseDTO> => {
125
+ if (!isInitialized || !sdk) {
126
+ throw new Error('SDK not initialized. Call initialize() first.');
127
+ }
128
+
129
+ try {
130
+ const result = await sdk.analytics.getCampaignClaimAnalytics(request);
131
+ return result;
132
+ } catch (error) {
133
+ console.error('Failed to fetch campaign claim analytics:', error);
134
+ throw error;
135
+ }
136
+ }, [sdk, isInitialized]);
137
+
138
+ /**
139
+ * Retrieves user analytics with engagement metrics
140
+ *
141
+ * Includes both per-user and per-active-user metrics for accurate engagement insights.
142
+ * Per-active-user metrics show concentrated engagement among active users.
143
+ *
144
+ * @param request - Analytics request with optional filters and date range
145
+ * @returns Promise resolving to user analytics data
146
+ *
147
+ * @example Compare per-user vs per-active-user metrics
148
+ * ```typescript
149
+ * const analytics = await getUserAnalytics({
150
+ * startDate: new Date('2026-02-01'),
151
+ * endDate: new Date('2026-02-28')
152
+ * });
153
+ *
154
+ * console.log(`Active users: ${analytics.activeUsers} / ${analytics.totalUsers}`);
155
+ * console.log(`Engagement rate: ${analytics.engagementRate.toFixed(1)}%`);
156
+ *
157
+ * // Per-active-user metrics (more meaningful)
158
+ * console.log(`Avg transactions per active user: ${analytics.averageTransactionsPerActiveUser}`);
159
+ * ```
160
+ *
161
+ * @example Business-specific analytics
162
+ * ```typescript
163
+ * const analytics = await getUserAnalytics({
164
+ * filters: { businessId: 'business-123' },
165
+ * startDate: new Date('2026-01-01'),
166
+ * endDate: new Date('2026-12-31')
167
+ * });
168
+ * ```
169
+ */
170
+ const getUserAnalytics = useCallback(async (
171
+ request: UserAnalyticsRequestDTO = {}
172
+ ): Promise<UserAnalyticsResponseDTO> => {
173
+ if (!isInitialized || !sdk) {
174
+ throw new Error('SDK not initialized. Call initialize() first.');
175
+ }
176
+
177
+ try {
178
+ const result = await sdk.analytics.getUserAnalytics(request);
179
+ return result;
180
+ } catch (error) {
181
+ console.error('Failed to fetch user analytics:', error);
182
+ throw error;
183
+ }
184
+ }, [sdk, isInitialized]);
185
+
186
+ /**
187
+ * Retrieves user transaction ranking with enriched user data
188
+ *
189
+ * Returns ranked list of users with full user details (email, externalUserId)
190
+ * and transaction metrics. Ideal for leaderboards, engagement analysis,
191
+ * and identifying power users.
192
+ *
193
+ * @param request - Ranking request with filters, sorting, and limit
194
+ * @returns Promise resolving to ranked user list with transaction metrics
195
+ *
196
+ * @example Top 50 users by transaction count
197
+ * ```typescript
198
+ * const ranking = await getUserRanking({
199
+ * sortBy: 'totalTransactions',
200
+ * sortOrder: 'DESC',
201
+ * limit: 50
202
+ * });
203
+ *
204
+ * ranking.results.forEach((user, index) => {
205
+ * console.log(`#${index + 1}: ${user.email || user.externalUserId}`);
206
+ * console.log(` Transactions: ${user.totalTransactions}`);
207
+ * console.log(` Token spent: ${user.tokenSpent}`);
208
+ * });
209
+ * ```
210
+ *
211
+ * @example Top STAMP spenders
212
+ * ```typescript
213
+ * const ranking = await getUserRanking({
214
+ * filters: { tokenType: 'STAMP' },
215
+ * sortBy: 'tokenSpent',
216
+ * sortOrder: 'DESC',
217
+ * limit: 20
218
+ * });
219
+ * ```
220
+ */
221
+ const getUserRanking = useCallback(async (
222
+ request: UserRankingAnalyticsRequestDTO = {}
223
+ ): Promise<UserRankingAnalyticsResponseDTO> => {
224
+ if (!isInitialized || !sdk) {
225
+ throw new Error('SDK not initialized. Call initialize() first.');
226
+ }
227
+
228
+ try {
229
+ const result = await sdk.analytics.getUserRanking(request);
230
+ return result;
231
+ } catch (error) {
232
+ console.error('Failed to fetch user ranking:', error);
233
+ throw error;
234
+ }
235
+ }, [sdk, isInitialized]);
236
+
237
+ /**
238
+ * Retrieves business transaction ranking with enriched business data
239
+ *
240
+ * Returns ranked list of businesses with transaction metrics for
241
+ * partner analytics and performance dashboards.
242
+ *
243
+ * @param request - Ranking request with filters, sorting, and limit
244
+ * @returns Promise resolving to ranked business list
245
+ *
246
+ * @example Top businesses by transaction count
247
+ * ```typescript
248
+ * const ranking = await getBusinessRanking({
249
+ * sortBy: 'totalTransactions',
250
+ * sortOrder: 'DESC',
251
+ * limit: 20
252
+ * });
253
+ *
254
+ * ranking.results.forEach((business, index) => {
255
+ * console.log(`#${index + 1}: ${business.businessId}`);
256
+ * console.log(` Transactions: ${business.totalTransactions}`);
257
+ * console.log(` Token spent: ${business.tokenSpent}`);
258
+ * });
259
+ * ```
260
+ */
261
+ const getBusinessRanking = useCallback(async (
262
+ request: BusinessRankingAnalyticsRequestDTO = {}
263
+ ): Promise<BusinessRankingAnalyticsResponseDTO> => {
264
+ if (!isInitialized || !sdk) {
265
+ throw new Error('SDK not initialized. Call initialize() first.');
266
+ }
267
+
268
+ try {
269
+ const result = await sdk.analytics.getBusinessRanking(request);
270
+ return result;
271
+ } catch (error) {
272
+ console.error('Failed to fetch business ranking:', error);
273
+ throw error;
274
+ }
275
+ }, [sdk, isInitialized]);
276
+
277
+ /**
278
+ * Retrieves monthly user retention analytics
279
+ *
280
+ * Returns monthly retention data with active, new, and returning users
281
+ * along with retention rates. Useful for churn analysis and engagement trends.
282
+ *
283
+ * @param request - Retention request with monthsBack and filters
284
+ * @returns Promise resolving to monthly retention data
285
+ *
286
+ * @example Get 12 months of retention data
287
+ * ```typescript
288
+ * const retention = await getRetentionAnalytics({
289
+ * monthsBack: 12
290
+ * });
291
+ *
292
+ * retention.results.forEach(month => {
293
+ * console.log(`${month.month}:`);
294
+ * console.log(` Active: ${month.activeUsers}`);
295
+ * console.log(` New: ${month.newUsers}`);
296
+ * console.log(` Returning: ${month.returningUsers}`);
297
+ * console.log(` Retention Rate: ${month.retentionRate.toFixed(1)}%`);
298
+ * });
299
+ * ```
300
+ */
301
+ const getRetentionAnalytics = useCallback(async (
302
+ request: RetentionAnalyticsRequestDTO = {}
303
+ ): Promise<RetentionAnalyticsResponseDTO> => {
304
+ if (!isInitialized || !sdk) {
305
+ throw new Error('SDK not initialized. Call initialize() first.');
306
+ }
307
+
308
+ try {
309
+ const result = await sdk.analytics.getRetentionAnalytics(request);
310
+ return result;
311
+ } catch (error) {
312
+ console.error('Failed to fetch retention analytics:', error);
313
+ throw error;
314
+ }
315
+ }, [sdk, isInitialized]);
316
+
75
317
  return {
76
318
  getTransactionAnalytics,
319
+ getCampaignClaimAnalytics,
320
+ getUserAnalytics,
321
+ getUserRanking,
322
+ getBusinessRanking,
323
+ getRetentionAnalytics,
77
324
  isAvailable: isInitialized && !!sdk?.analytics,
78
325
  };
79
326
  };
@@ -5,11 +5,14 @@ import type {
5
5
  CampaignDTO,
6
6
  CampaignClaimDTO,
7
7
  CampaignTriggerDTO,
8
- CampaignCreateRequestDTO,
9
- TokenUnitCreateRequestDTO,
10
- CampaignBusinessEngagementCreateRequestDTO,
11
- PaginatedResponseDTO
8
+ PaginatedResponseDTO,
9
+ CampaignClaimIncludeRelation,
10
+ CampaignIncludeRelation
12
11
  } from '@explorins/pers-shared';
12
+ import type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign';
13
+
14
+ // Re-export for consumers
15
+ export type { CampaignClaimFilters } from '@explorins/pers-sdk/campaign';
13
16
 
14
17
  export const useCampaigns = () => {
15
18
  const { sdk, isInitialized, isAuthenticated } = usePersSDK();
@@ -28,13 +31,30 @@ export const useCampaigns = () => {
28
31
  }
29
32
  }, [sdk, isInitialized]);
30
33
 
31
- const getCampaignById = useCallback(async (campaignId: string): Promise<CampaignDTO | null> => {
34
+ /**
35
+ * Get campaign by ID with optional include relations
36
+ *
37
+ * @param campaignId - The campaign ID
38
+ * @param include - Relations to include: 'triggerSources', 'businesses'
39
+ * @returns Promise resolving to campaign data
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // Get campaign with all related data
44
+ * const campaign = await getCampaignById('campaign-123', ['triggerSources', 'businesses']);
45
+ * console.log('Trigger sources:', campaign.included?.triggerSources);
46
+ * ```
47
+ */
48
+ const getCampaignById = useCallback(async (
49
+ campaignId: string,
50
+ include?: CampaignIncludeRelation[]
51
+ ): Promise<CampaignDTO | null> => {
32
52
  if (!isInitialized || !sdk) {
33
53
  throw new Error('SDK not initialized. Call initialize() first.');
34
54
  }
35
55
 
36
56
  try {
37
- const result = await sdk.campaigns.getCampaignById(campaignId);
57
+ const result = await sdk.campaigns.getCampaignById(campaignId, include);
38
58
  return result;
39
59
  } catch (error) {
40
60
  console.error('Failed to fetch campaign:', error);
@@ -59,7 +79,29 @@ export const useCampaigns = () => {
59
79
  }
60
80
  }, [sdk, isInitialized, isAuthenticated]);
61
81
 
62
- const getUserClaims = useCallback(async (): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
82
+ /**
83
+ * Get user's campaign claims with optional pagination and include relations
84
+ *
85
+ * @param options - Optional options including pagination and include relations
86
+ * @returns Promise resolving to paginated campaign claims
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Get user's claims with campaign details
91
+ * const { data: claims } = await getUserClaims({ include: ['campaign', 'business'] });
92
+ * claims.forEach(claim => {
93
+ * console.log('Campaign:', claim.included?.campaign?.title);
94
+ * });
95
+ *
96
+ * // With pagination
97
+ * const { data } = await getUserClaims({ page: 1, limit: 10, include: ['campaign'] });
98
+ * ```
99
+ */
100
+ const getUserClaims = useCallback(async (options?: {
101
+ page?: number;
102
+ limit?: number;
103
+ include?: CampaignClaimIncludeRelation[];
104
+ }): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
63
105
  if (!isInitialized || !sdk) {
64
106
  throw new Error('SDK not initialized. Call initialize() first.');
65
107
  }
@@ -69,7 +111,7 @@ export const useCampaigns = () => {
69
111
  }
70
112
 
71
113
  try {
72
- const result = await sdk.campaigns.getUserClaims();
114
+ const result = await sdk.campaigns.getUserClaims(options);
73
115
  return result;
74
116
  } catch (error) {
75
117
  console.error('Failed to fetch user claims:', error);
@@ -106,13 +148,32 @@ export const useCampaigns = () => {
106
148
  }
107
149
  }, [sdk, isInitialized]);
108
150
 
109
- const getCampaignClaims = useCallback(async (): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
151
+ /**
152
+ * Admin: Get campaign claims with optional filters and include relations
153
+ *
154
+ * @param filters - Optional filters for campaign, user, or business
155
+ * @param include - Relations to include: 'campaign', 'user', 'business'
156
+ * @returns Promise resolving to paginated campaign claims
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * // Get all claims with user details
161
+ * const { data: claims } = await getCampaignClaims({}, ['user']);
162
+ *
163
+ * // Get claims for a specific campaign
164
+ * const { data } = await getCampaignClaims({ campaignId: 'campaign-123' }, ['user', 'business']);
165
+ * ```
166
+ */
167
+ const getCampaignClaims = useCallback(async (
168
+ filters?: CampaignClaimFilters,
169
+ include?: CampaignClaimIncludeRelation[]
170
+ ): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
110
171
  if (!isInitialized || !sdk) {
111
172
  throw new Error('SDK not initialized. Call initialize() first.');
112
173
  }
113
174
 
114
175
  try {
115
- const result = await sdk.campaigns.getCampaignClaims();
176
+ const result = await sdk.campaigns.getCampaignClaims(filters, include);
116
177
  return result;
117
178
  } catch (error) {
118
179
  console.error('Failed to fetch campaign claims:', error);
@@ -120,13 +181,23 @@ export const useCampaigns = () => {
120
181
  }
121
182
  }, [sdk, isInitialized]);
122
183
 
123
- const getCampaignClaimsByUserId = useCallback(async (userId: string): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
184
+ /**
185
+ * Admin: Get campaign claims by user ID with optional include relations
186
+ *
187
+ * @param userId - The user ID
188
+ * @param include - Relations to include: 'campaign', 'user', 'business'
189
+ * @returns Promise resolving to paginated campaign claims
190
+ */
191
+ const getCampaignClaimsByUserId = useCallback(async (
192
+ userId: string,
193
+ include?: CampaignClaimIncludeRelation[]
194
+ ): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
124
195
  if (!isInitialized || !sdk) {
125
196
  throw new Error('SDK not initialized. Call initialize() first.');
126
197
  }
127
198
 
128
199
  try {
129
- const result = await sdk.campaigns.getCampaignClaimsByUserId(userId);
200
+ const result = await sdk.campaigns.getCampaignClaimsByUserId(userId, undefined, include);
130
201
  return result;
131
202
  } catch (error) {
132
203
  console.error('Failed to fetch campaign claims by user ID:', error);
@@ -134,13 +205,23 @@ export const useCampaigns = () => {
134
205
  }
135
206
  }, [sdk, isInitialized]);
136
207
 
137
- const getCampaignClaimsByBusinessId = useCallback(async (businessId: string): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
208
+ /**
209
+ * Admin: Get campaign claims by business ID with optional include relations
210
+ *
211
+ * @param businessId - The business ID
212
+ * @param include - Relations to include: 'campaign', 'user', 'business'
213
+ * @returns Promise resolving to paginated campaign claims
214
+ */
215
+ const getCampaignClaimsByBusinessId = useCallback(async (
216
+ businessId: string,
217
+ include?: CampaignClaimIncludeRelation[]
218
+ ): Promise<PaginatedResponseDTO<CampaignClaimDTO>> => {
138
219
  if (!isInitialized || !sdk) {
139
220
  throw new Error('SDK not initialized. Call initialize() first.');
140
221
  }
141
222
 
142
223
  try {
143
- const result = await sdk.campaigns.getCampaignClaimsByBusinessId(businessId);
224
+ const result = await sdk.campaigns.getCampaignClaimsByBusinessId(businessId, undefined, include);
144
225
  return result;
145
226
  } catch (error) {
146
227
  console.error('Failed to fetch campaign claims by business ID:', error);
@@ -148,6 +229,85 @@ export const useCampaigns = () => {
148
229
  }
149
230
  }, [sdk, isInitialized]);
150
231
 
232
+ // ==========================================
233
+ // TRIGGER SOURCE ASSIGNMENT (Admin)
234
+ // Note: TriggerSource CRUD is in useTriggerSources hook
235
+ // ==========================================
236
+
237
+ /**
238
+ * Admin: Assign a trigger source to a campaign
239
+ *
240
+ * Associates a trigger source (QR code, NFC tag, etc.) with a campaign.
241
+ * A campaign can have multiple trigger sources.
242
+ *
243
+ * Note: To create/update/delete trigger sources, use `useTriggerSources` hook.
244
+ *
245
+ * @param campaignId - Campaign UUID
246
+ * @param triggerSourceId - Trigger source UUID
247
+ * @returns Promise resolving to updated campaign
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const { create } = useTriggerSources();
252
+ * const { assignTriggerSource } = useCampaigns();
253
+ *
254
+ * // Create trigger source first
255
+ * const source = await create({ type: 'QR_CODE', name: 'Store QR' });
256
+ *
257
+ * // Then assign to campaign
258
+ * const updated = await assignTriggerSource('campaign-123', source.id);
259
+ * ```
260
+ */
261
+ const assignTriggerSource = useCallback(async (
262
+ campaignId: string,
263
+ triggerSourceId: string
264
+ ): Promise<CampaignDTO> => {
265
+ if (!isInitialized || !sdk) {
266
+ throw new Error('SDK not initialized. Call initialize() first.');
267
+ }
268
+ if (!isAuthenticated) {
269
+ throw new Error('SDK not authenticated. assignTriggerSource requires admin authentication.');
270
+ }
271
+
272
+ try {
273
+ const result = await sdk.campaigns.assignTriggerSource(campaignId, triggerSourceId);
274
+ return result;
275
+ } catch (error) {
276
+ console.error('Failed to assign trigger source:', error);
277
+ throw error;
278
+ }
279
+ }, [sdk, isInitialized, isAuthenticated]);
280
+
281
+ /**
282
+ * Admin: Remove a trigger source from a campaign
283
+ *
284
+ * Removes the association between a trigger source and a campaign.
285
+ * The trigger source itself is not deleted.
286
+ *
287
+ * @param campaignId - Campaign UUID
288
+ * @param triggerSourceId - Trigger source UUID
289
+ * @returns Promise resolving to updated campaign
290
+ */
291
+ const removeTriggerSource = useCallback(async (
292
+ campaignId: string,
293
+ triggerSourceId: string
294
+ ): Promise<CampaignDTO> => {
295
+ if (!isInitialized || !sdk) {
296
+ throw new Error('SDK not initialized. Call initialize() first.');
297
+ }
298
+ if (!isAuthenticated) {
299
+ throw new Error('SDK not authenticated. removeTriggerSource requires admin authentication.');
300
+ }
301
+
302
+ try {
303
+ const result = await sdk.campaigns.removeTriggerSource(campaignId, triggerSourceId);
304
+ return result;
305
+ } catch (error) {
306
+ console.error('Failed to remove trigger source:', error);
307
+ throw error;
308
+ }
309
+ }, [sdk, isInitialized, isAuthenticated]);
310
+
151
311
  return {
152
312
  getActiveCampaigns,
153
313
  getCampaignById,
@@ -158,6 +318,8 @@ export const useCampaigns = () => {
158
318
  getCampaignClaims,
159
319
  getCampaignClaimsByUserId,
160
320
  getCampaignClaimsByBusinessId,
321
+ assignTriggerSource,
322
+ removeTriggerSource,
161
323
  isAvailable: isInitialized && !!sdk?.campaigns,
162
324
  };
163
325
  };