be-components 7.4.3 → 7.4.4

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.
Files changed (45) hide show
  1. package/lib/commonjs/ApiOverrides/index.js +0 -2
  2. package/lib/commonjs/ApiOverrides/index.js.map +1 -1
  3. package/lib/commonjs/NotificationManager/api/index.js +448 -0
  4. package/lib/commonjs/NotificationManager/api/index.js.map +1 -0
  5. package/lib/commonjs/NotificationManager/index.js +1159 -0
  6. package/lib/commonjs/NotificationManager/index.js.map +1 -0
  7. package/lib/commonjs/index.js +20 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/types.d.js.map +1 -1
  10. package/lib/module/ApiOverrides/index.js +0 -2
  11. package/lib/module/ApiOverrides/index.js.map +1 -1
  12. package/lib/module/NotificationManager/api/index.js +441 -0
  13. package/lib/module/NotificationManager/api/index.js.map +1 -0
  14. package/lib/module/NotificationManager/index.js +1140 -0
  15. package/lib/module/NotificationManager/index.js.map +1 -0
  16. package/lib/module/index.js +3 -1
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/types.d.js.map +1 -1
  19. package/lib/typescript/lib/commonjs/ApiOverrides/index.d.ts.map +1 -1
  20. package/lib/typescript/lib/commonjs/NotificationManager/api/index.d.ts +804 -0
  21. package/lib/typescript/lib/commonjs/NotificationManager/api/index.d.ts.map +1 -0
  22. package/lib/typescript/lib/commonjs/NotificationManager/index.d.ts +815 -0
  23. package/lib/typescript/lib/commonjs/NotificationManager/index.d.ts.map +1 -0
  24. package/lib/typescript/lib/commonjs/index.d.ts +803 -0
  25. package/lib/typescript/lib/commonjs/index.d.ts.map +1 -1
  26. package/lib/typescript/lib/module/ApiOverrides/index.d.ts.map +1 -1
  27. package/lib/typescript/lib/module/NotificationManager/api/index.d.ts +803 -0
  28. package/lib/typescript/lib/module/NotificationManager/api/index.d.ts.map +1 -0
  29. package/lib/typescript/lib/module/NotificationManager/index.d.ts +16 -0
  30. package/lib/typescript/lib/module/NotificationManager/index.d.ts.map +1 -0
  31. package/lib/typescript/lib/module/index.d.ts +4 -1
  32. package/lib/typescript/lib/module/index.d.ts.map +1 -1
  33. package/lib/typescript/src/ApiOverrides/index.d.ts.map +1 -1
  34. package/lib/typescript/src/NotificationManager/api/index.d.ts +822 -0
  35. package/lib/typescript/src/NotificationManager/api/index.d.ts.map +1 -0
  36. package/lib/typescript/src/NotificationManager/index.d.ts +17 -0
  37. package/lib/typescript/src/NotificationManager/index.d.ts.map +1 -0
  38. package/lib/typescript/src/index.d.ts +3 -1
  39. package/lib/typescript/src/index.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/ApiOverrides/index.ts +0 -2
  42. package/src/NotificationManager/api/index.ts +134 -0
  43. package/src/NotificationManager/index.tsx +894 -0
  44. package/src/index.tsx +6 -1
  45. package/src/types.d.ts +70 -0
@@ -0,0 +1,894 @@
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
+ import { FlatList, TouchableOpacity, ActivityIndicator } from 'react-native';
3
+ import { View, Text, Button, TextInput } from '../Components/Themed';
4
+ import { useColors } from '../constants/useColors';
5
+ import { NotificationApi, NotificationHelpers } from './api';
6
+ import type { PlayerNotificationProps, MyPlayerProps, NotificationGroupProps, FocusPositionProps } from '../types';
7
+ import { Icons } from '../Components';
8
+ import DropDown from '../Components/Dropdown';
9
+ import { showConfirmAlert } from '../Components/ConfirmAlert';
10
+
11
+ type NotificationManagerProps = {
12
+ player_ids: string[];
13
+ me: MyPlayerProps;
14
+ notification_type?: 'order_notifications' | 'competition_notifications' | 'social_notifications';
15
+ default_path_name?: string;
16
+ onFocusPosition?:(pos:FocusPositionProps) => void,
17
+ default_params?: Record<string, string>;
18
+ onComplete?: (notification: PlayerNotificationProps) => void;
19
+ onClose?: () => void;
20
+ };
21
+
22
+ const notification_types = [
23
+ { label: 'Order Notifications', value: 'order_notifications' },
24
+ { label: 'Competition Notifications', value: 'competition_notifications' },
25
+ { label: 'Social Notifications', value: 'social_notifications' }
26
+ ];
27
+
28
+ const sections = ['header','top_row', 'preview', 'second_row']
29
+
30
+ const NotificationManager = ({
31
+ player_ids,
32
+ me,
33
+ notification_type,
34
+ default_path_name,
35
+ onFocusPosition,
36
+ default_params,
37
+ onComplete,
38
+ onClose
39
+ }: NotificationManagerProps) => {
40
+ const Colors = useColors();
41
+ const [loading, setLoading] = useState(false);
42
+ const [loadingGroups, setLoadingGroups] = useState(false);
43
+ const [notificationGroups, setNotificationGroups] = useState<NotificationGroupProps[]>([]);
44
+ const [selectedGroup, setSelectedGroup] = useState<NotificationGroupProps | null>(null);
45
+ const [showGroupsModal, setShowGroupsModal] = useState(false);
46
+ const [groupMemberIds, setGroupMemberIds] = useState<string[]>([]);
47
+ const [showEditCustomGroupModal, setShowEditCustomGroupModal] = useState(false);
48
+ const [customGroupCsv, setCustomGroupCsv] = useState('');
49
+ const [sendingProgress, setSendingProgress] = useState<{ current: number; total: number } | null>(null);
50
+
51
+ // Create a custom group from initial player_ids
52
+ const customGroup: NotificationGroupProps | null = player_ids.length > 0 ? {
53
+ notification_group_id: 'custom_group',
54
+ name: 'Custom Recipients',
55
+ description: `${player_ids.length} selected recipients`,
56
+ status: 'active',
57
+ grouping_function:'',
58
+ player_count: player_ids.length,
59
+ create_datetime: new Date().toISOString(),
60
+ last_update_datetime: new Date().toISOString()
61
+ } : null;
62
+
63
+ // Initialize selectedPath based on default_path_name if provided
64
+ const initialPath = default_path_name
65
+ ? NotificationHelpers.app_paths.find(p => p.path_name === default_path_name) || NotificationHelpers.app_paths[0]
66
+ : NotificationHelpers.app_paths[0];
67
+
68
+ const [selectedPath, setSelectedPath] = useState<typeof NotificationHelpers.app_paths[0] | undefined>(initialPath);
69
+ const [customPathName, setCustomPathName] = useState('');
70
+ const [pathParams, setPathParams] = useState<Record<string, string>>(default_params || {});
71
+ const [pathSearch, setPathSearch] = useState('');
72
+ const [notification, setNotification] = useState<Partial<PlayerNotificationProps>>({
73
+ type: notification_type || 'order_notifications',
74
+ notify_body_type: 'custom',
75
+ title: '',
76
+ body: '',
77
+ status: 'pending',
78
+ expire_time: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days from now
79
+ });
80
+
81
+ useEffect(() => {
82
+ startUp();
83
+ // Auto-select custom group if initial player_ids provided
84
+ if (customGroup) {
85
+ setSelectedGroup(customGroup);
86
+ setGroupMemberIds(player_ids);
87
+ }
88
+ }, []);
89
+
90
+ useEffect(() => {
91
+ // Initialize pathParams when selectedPath changes, merging with default_params
92
+ const mergedParams = { ...selectedPath?.params, ...(default_params || {}) };
93
+ setPathParams(mergedParams as any);
94
+ }, [selectedPath]);
95
+
96
+ const startUp = () => {
97
+ NotificationApi.setEnvironment();
98
+ };
99
+
100
+ const loadNotificationGroups = async () => {
101
+ setLoadingGroups(true);
102
+ const groups = await NotificationApi.getActiveNotificationGroups();
103
+ setNotificationGroups(groups);
104
+ setLoadingGroups(false);
105
+ };
106
+
107
+ const handleSelectGroup = async (group_id: string) => {
108
+ setLoadingGroups(true);
109
+
110
+ // Check if it's the custom group
111
+ if (group_id === 'custom_group' && customGroup) {
112
+ setSelectedGroup(customGroup);
113
+ setGroupMemberIds(player_ids);
114
+ setLoadingGroups(false);
115
+ setShowGroupsModal(false);
116
+ return;
117
+ }
118
+
119
+ const group = notificationGroups.find(g => g.notification_group_id === group_id);
120
+ setSelectedGroup(group || null);
121
+
122
+ // Fetch all members for this group
123
+ const members = await NotificationApi.getNotificationGroupMembersByGroup(group_id);
124
+
125
+ // Flatten all player_ids arrays from all member objects
126
+ const memberPlayerIds = members.flatMap(m => m.player_ids);
127
+ setGroupMemberIds(memberPlayerIds);
128
+
129
+ setLoadingGroups(false);
130
+ setShowGroupsModal(false);
131
+ };
132
+
133
+ const handleRemoveGroup = () => {
134
+ setSelectedGroup(null);
135
+ setGroupMemberIds([]);
136
+ };
137
+
138
+ const handleOpenEditCustomGroup = () => {
139
+ // Populate CSV with current groupMemberIds
140
+ setCustomGroupCsv(groupMemberIds.join(','));
141
+ setShowEditCustomGroupModal(true);
142
+ };
143
+
144
+ const handleSaveCustomGroup = () => {
145
+ // Parse CSV and update groupMemberIds
146
+ const ids = customGroupCsv
147
+ .split(',')
148
+ .map(id => id.trim())
149
+ .filter(id => id.length > 0);
150
+
151
+ setGroupMemberIds(ids);
152
+
153
+ // Update the custom group description
154
+ if (customGroup && selectedGroup?.notification_group_id === 'custom_group') {
155
+ const updatedGroup = {
156
+ ...customGroup,
157
+ description: `${ids.length} selected recipients`,
158
+ player_count: ids.length
159
+ };
160
+ setSelectedGroup(updatedGroup);
161
+ }
162
+
163
+ setShowEditCustomGroupModal(false);
164
+ };
165
+
166
+ // Filter and sort paths: primary first (alphabetically), then others (alphabetically), limited by search
167
+ const filteredPaths = useMemo(() => {
168
+ let filtered = NotificationHelpers.app_paths;
169
+
170
+ // Apply search filter
171
+ if (pathSearch) {
172
+ filtered = filtered.filter(path =>
173
+ path.label.toLowerCase().includes(pathSearch.toLowerCase()) ||
174
+ path.path_name.toLowerCase().includes(pathSearch.toLowerCase())
175
+ );
176
+ }
177
+
178
+ // Sort: primary paths first (alphabetically), then non-primary (alphabetically)
179
+ const sorted = [...filtered].sort((a, b) => {
180
+ if (a.primary && !b.primary) return -1;
181
+ if (!a.primary && b.primary) return 1;
182
+ return a.label.localeCompare(b.label);
183
+ });
184
+
185
+ // Limit to 15 results
186
+ return sorted.slice(0, 15);
187
+ }, [pathSearch]);
188
+
189
+ const isValid = (): string[] => {
190
+ const errors: string[] = [];
191
+
192
+ if (!notification.title || notification.title.trim() === '') {
193
+ errors.push('Notification title is required');
194
+ }
195
+
196
+ if (!notification.body || notification.body.trim() === '') {
197
+ errors.push('Notification body is required');
198
+ }
199
+
200
+ if (groupMemberIds.length === 0) {
201
+ errors.push('At least one recipient is required');
202
+ }
203
+
204
+ // Check required params
205
+ if (selectedPath?.requiredParams && selectedPath.requiredParams.length > 0) {
206
+ selectedPath.requiredParams.forEach(paramKey => {
207
+ if (!pathParams[paramKey] || pathParams[paramKey].trim() === '') {
208
+ errors.push(`Required parameter "${paramKey}" is missing`);
209
+ }
210
+ });
211
+ }
212
+
213
+ // Check custom path name
214
+ if (selectedPath?.path_name === 'custom' && (!customPathName || customPathName.trim() === '')) {
215
+ errors.push('Custom path name is required');
216
+ }
217
+
218
+ return errors;
219
+ };
220
+
221
+ const confirmSendNotification = (options?:{ test:boolean }) => {
222
+ const errors = isValid();
223
+
224
+ if (errors.length > 0) {
225
+ const errorMessage = 'Please fix the following errors:\n\n' + errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n');
226
+ alert(errorMessage);
227
+ return;
228
+ }
229
+
230
+ showConfirmAlert('Are you sure?', options?.test ? 'This will send a test notification to you only':'This will send a notification to all users selected',
231
+ () => handleSendNotification(options),
232
+ () => console.log('Done')
233
+ )
234
+ };
235
+
236
+ const handleSendNotification = async (options?:{ test:boolean }) => {
237
+ // Use only group member IDs (no individual player_ids)
238
+ let player_ids_to_send = [...groupMemberIds];
239
+
240
+ if(options?.test){
241
+ //Change to the admin
242
+ player_ids_to_send = [me.player_id]
243
+ }
244
+
245
+ setLoading(true);
246
+ try {
247
+ // Determine final path_name
248
+ const finalPathName = selectedPath?.path_name === 'custom' ? customPathName : selectedPath?.path_name;
249
+
250
+ // Construct the complete notification object with all required fields
251
+ const completeNotification: PlayerNotificationProps = {
252
+ player_notification_id: '', // Will be generated by backend
253
+ player_id: me.player_id,
254
+ type: notification.type as 'order_notifications' | 'competition_notifications' | 'social_notifications',
255
+ notify_body_type: notification.notify_body_type || 'custom',
256
+ title: notification.title!,
257
+ body: notification.body!,
258
+ options: {
259
+ id: Date.now().toString(),
260
+ body: notification.body!,
261
+ icon: 'default',
262
+ type: notification.type || 'order_notifications',
263
+ data: {
264
+ id: Date.now().toString(),
265
+ player_id: me.player_id,
266
+ pageStack: 'MainStack',
267
+ page: 'Notifications',
268
+ pageParams: {},
269
+ url: notification.link_override,
270
+ path_name: finalPathName,
271
+ params: pathParams
272
+ }
273
+ },
274
+ status: 'unread',
275
+ link_override: notification.link_override,
276
+ expire_time: 0,
277
+ create_datetime: new Date().toISOString(),
278
+ last_update_datetime: new Date().toISOString(),
279
+ };
280
+
281
+ // Split into batches of 500
282
+ const BATCH_SIZE = 500;
283
+ const batches: string[][] = [];
284
+ for (let i = 0; i < player_ids_to_send.length; i += BATCH_SIZE) {
285
+ batches.push(player_ids_to_send.slice(i, i + BATCH_SIZE));
286
+ }
287
+
288
+ // Send notifications in batches
289
+ for (let i = 0; i < batches.length; i++) {
290
+ const batch = batches[i];
291
+ if (!batch) continue;
292
+
293
+ setSendingProgress({ current: i + 1, total: batches.length });
294
+ await NotificationApi.broadcastNotifications(
295
+ completeNotification,
296
+ batch
297
+ );
298
+ }
299
+
300
+ setSendingProgress(null);
301
+
302
+ if (onComplete) {
303
+ onComplete(completeNotification);
304
+ }
305
+
306
+ // Only reset form if not a test send
307
+ if (!options?.test) {
308
+ setNotification({
309
+ type: 'order_notifications',
310
+ notify_body_type: 'custom',
311
+ title: '',
312
+ body: '',
313
+ status: 'pending',
314
+ expire_time: Date.now() + (7 * 24 * 60 * 60 * 1000),
315
+ });
316
+ }
317
+
318
+ const recipientCount = options?.test ? 1 : groupMemberIds.length;
319
+ const recipientMessage = options?.test ? 'Test notification sent to you!' : `Notification sent successfully to ${recipientCount} user${recipientCount !== 1 ? 's' : ''}!`;
320
+ alert(recipientMessage);
321
+ } catch (error) {
322
+ console.error('Error sending notification:', error);
323
+ alert('Failed to send notification. Please try again.');
324
+ }
325
+ setLoading(false);
326
+ };
327
+
328
+
329
+ const renderSections = (data:{ item:string, index:number }) => {
330
+ switch(data.item){
331
+ case 'header':
332
+ const headerTotalRecipients = groupMemberIds.length;
333
+ return (
334
+ <View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
335
+ <View transparent style={{ flex:1 }}>
336
+ <Text theme='h1'>Send Notification</Text>
337
+ <Text theme='description' style={{ marginTop:3 }}>Sending to {headerTotalRecipients} users</Text>
338
+ </View>
339
+ {onClose && (
340
+ <Button transparent onPress={onClose}>
341
+ <Icons.CloseIcon size={16} color={Colors.text.h1} />
342
+ </Button>
343
+ )}
344
+ </View>
345
+ )
346
+ case 'preview':
347
+ return (
348
+ <View float style={{ margin: 5 }}>
349
+ <View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
350
+ <View transparent style={{ flex:1 }}>
351
+ <Text theme='h1'>Preview</Text>
352
+ <Text theme='description' style={{ marginTop:3 }}>What will this notification look like?</Text>
353
+ </View>
354
+ </View>
355
+ <View
356
+ float
357
+ style={{
358
+ padding: 15,
359
+ margin:10,
360
+ borderRadius: 8,
361
+ borderWidth: 1,
362
+ borderColor: Colors.borders.light,
363
+ }}
364
+ >
365
+ <View transparent style={{ flexDirection: 'row', alignItems: 'center' }}>
366
+ <View
367
+ style={{
368
+ width: 40,
369
+ height: 40,
370
+ borderRadius: 8,
371
+ justifyContent: 'center',
372
+ alignItems: 'center',
373
+ marginRight: 12,
374
+ }}
375
+ >
376
+ <Icons.NotificationIcon size={20} color={Colors.text.h1} />
377
+ </View>
378
+ <View transparent style={{ flex: 1 }}>
379
+ <Text theme="h1">
380
+ {notification.title || 'Notification Title'}
381
+ </Text>
382
+ <Text theme="description" style={{ marginTop: 4 }}>
383
+ {notification.body || 'Notification message will appear here'}
384
+ </Text>
385
+ </View>
386
+ </View>
387
+ </View>
388
+ </View>
389
+ )
390
+ case 'top_row':
391
+ const totalRecipients = groupMemberIds.length;
392
+ return (
393
+ <View style={{ flexDirection:'row', flexWrap:'wrap' }}>
394
+ <View
395
+ float
396
+ style={{
397
+ flexGrow:1,
398
+ minWidth:300,
399
+ padding:10,
400
+ margin:5
401
+ }}
402
+ >
403
+ <View transparent style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
404
+ <View transparent style={{ flex:1 }}>
405
+ <Text theme="h2">Recipients ({totalRecipients})</Text>
406
+ <Text theme="description" style={{ marginTop: 4 }}>
407
+ {totalRecipients === 0 ? 'No recipients selected' : 'Selected group'}
408
+ </Text>
409
+ </View>
410
+ <Button
411
+ type="action"
412
+ onPress={() => {
413
+ loadNotificationGroups();
414
+ setShowGroupsModal(true);
415
+ }}
416
+ style={{ padding: 8 }}
417
+ >
418
+ <Text theme="h1" color={Colors.text.white}>
419
+ Select Group
420
+ </Text>
421
+ </Button>
422
+ </View>
423
+
424
+ {/* Display Selected Group */}
425
+ {selectedGroup && (
426
+ <View
427
+ float
428
+ style={{
429
+ flexDirection: 'row',
430
+ alignItems: 'center',
431
+ justifyContent: 'space-between',
432
+ padding: 12,
433
+ borderRadius: 8,
434
+ marginTop: 5
435
+ }}
436
+ >
437
+ <View transparent style={{ flex: 1 }}>
438
+ <Text theme="h1">{selectedGroup.name}</Text>
439
+ <Text theme="description" style={{ marginTop: 2 }}>
440
+ {groupMemberIds.length} members
441
+ </Text>
442
+ </View>
443
+ <View transparent style={{ flexDirection: 'row' }}>
444
+ {selectedGroup.notification_group_id === 'custom_group' && (
445
+ <Button
446
+ type="action"
447
+ onPress={handleOpenEditCustomGroup}
448
+ style={{ padding: 8, marginRight: 8 }}
449
+ >
450
+ <Icons.EditIcon size={16} color={Colors.text.white} />
451
+ </Button>
452
+ )}
453
+ <Button
454
+ type="error"
455
+ onPress={handleRemoveGroup}
456
+ style={{ padding: 8 }}
457
+ >
458
+ <Icons.CloseIcon size={16} color={Colors.text.white} />
459
+ </Button>
460
+ </View>
461
+ </View>
462
+ )}
463
+ </View>
464
+ </View>
465
+ )
466
+
467
+ case 'second_row':
468
+ return (
469
+ <View style={{ flexDirection:'row', flexWrap:'wrap' }}>
470
+ <View float style={{ minWidth:300, flexGrow :1, margin:5 }}>
471
+ <View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
472
+ <View transparent style={{ flex:1 }}>
473
+ <Text theme='h1'>Notification Message</Text>
474
+ <Text theme='description' style={{ marginTop:3 }}>Update notification message below</Text>
475
+ </View>
476
+ </View>
477
+ <View transparent type='body'>
478
+ <View transparent style={{ padding:10 }}>
479
+ <Text theme="h2" style={{ marginBottom:10 }}>
480
+ Notification Title
481
+ </Text>
482
+ <TextInput
483
+ value={notification.title}
484
+ onFocusPosition={onFocusPosition}
485
+ placeholder="Enter notification title"
486
+ onChangeText={(title) => setNotification({ ...notification, title })}
487
+
488
+ />
489
+ </View>
490
+ <View transparent style={{ padding:10 }}>
491
+ <Text theme="h2" style={{ marginBottom:10 }}>
492
+ Notification Body
493
+ </Text>
494
+ <TextInput
495
+ value={notification.body}
496
+ onFocusPosition={onFocusPosition}
497
+ placeholder="Enter notification message"
498
+ onChangeText={(body) => setNotification({ ...notification, body })}
499
+ multiline
500
+ numberOfLines={6}
501
+ style={{ padding: 12, borderRadius: 8, minHeight: 120 }}
502
+ />
503
+ </View>
504
+ </View>
505
+ </View>
506
+ <View float style={{ flexGrow:1, minWidth:300, margin:5 }}>
507
+ <View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
508
+ <View transparent style={{ flex:1 }}>
509
+ <Text theme='h1'>Notification Settings</Text>
510
+ <Text theme='description' style={{ marginTop:3 }}>Manage settigns below</Text>
511
+ </View>
512
+ </View>
513
+ <View transparent type='body'>
514
+ <View type='row' style={{ padding:10 }}>
515
+ <Text theme="h2" style={{ flex:1 }}>
516
+ Notification Type
517
+ </Text>
518
+ <DropDown
519
+ selected_value={
520
+ notification_types.find((t) => t.value === notification.type)?.label ?? ''
521
+ }
522
+ dropdown_options={[
523
+ { value: 'type', eligible_options: notification_types.map((t) => t.label) },
524
+ ]}
525
+ onOptionSelect={(selected) => {
526
+ const type = notification_types.find((t) => t.label === selected)?.value;
527
+ setNotification({ ...notification, type: type as any });
528
+ }}
529
+ />
530
+ </View>
531
+ <View transparent style={{ padding:10 }}>
532
+ <Text theme="h2" style={{ marginBottom:10 }}>
533
+ Link Override (Optional)
534
+ </Text>
535
+ <TextInput
536
+ value={notification.link_override || ''}
537
+ onFocusPosition={onFocusPosition}
538
+ placeholder="https://example.com"
539
+ onChangeText={(link_override) =>
540
+ setNotification({ ...notification, link_override })
541
+ }
542
+ style={{ padding: 12, borderRadius: 8 }}
543
+ />
544
+ </View>
545
+ <View transparent style={{ padding:10 }}>
546
+ <Text theme="h2" style={{ marginBottom: 10 }}>
547
+ Deep Link Path
548
+ </Text>
549
+
550
+ {/* Selected Path Display */}
551
+ <View
552
+ type="header"
553
+ style={{
554
+ padding: 12,
555
+ borderRadius: 8,
556
+ marginBottom: 8,
557
+ flexDirection: 'row',
558
+ alignItems: 'center',
559
+ justifyContent: 'space-between'
560
+ }}
561
+ >
562
+ <View transparent style={{ flex: 1 }}>
563
+ <Text theme="h1" style={{ marginBottom: 2 }}>
564
+ {selectedPath?.label}
565
+ </Text>
566
+ <Text theme="description" style={{ fontSize: 12 }}>
567
+ {selectedPath?.path_name}
568
+ </Text>
569
+ </View>
570
+ {selectedPath?.primary && (
571
+ <View transparent style={{ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4 }}>
572
+ <Text theme="description" color={Colors.text.white} style={{ fontSize: 10 }}>
573
+ Primary
574
+ </Text>
575
+ </View>
576
+ )}
577
+ </View>
578
+
579
+ {/* Search Box */}
580
+ <TextInput
581
+ value={pathSearch}
582
+ placeholder="Search paths to change..."
583
+ onFocusPosition={onFocusPosition}
584
+ onChangeText={setPathSearch}
585
+ style={{ padding: 12, borderRadius: 8, marginBottom: 8 }}
586
+ />
587
+
588
+ {/* Path Results List */}
589
+ {pathSearch && (
590
+ <View
591
+ float
592
+ style={{
593
+ maxHeight: 300,
594
+ borderRadius: 8,
595
+ borderWidth: 1,
596
+ borderColor: Colors.borders.light,
597
+ }}
598
+ >
599
+ <FlatList
600
+ data={filteredPaths}
601
+ keyExtractor={(item) => item.path_name}
602
+ renderItem={({ item }) => (
603
+ <TouchableOpacity
604
+ onPress={() => {
605
+ setSelectedPath(item);
606
+ setPathSearch('');
607
+ }}
608
+ style={{
609
+ padding: 12,
610
+ borderBottomWidth: 1,
611
+ borderColor: Colors.borders.light,
612
+ }}
613
+ >
614
+ <View transparent style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
615
+ <View transparent style={{ flex: 1 }}>
616
+ <Text theme="h2" style={{ marginBottom: 2 }}>
617
+ {item.label}
618
+ </Text>
619
+ <Text theme="description" style={{ fontSize: 11 }}>
620
+ {item.path_name}
621
+ </Text>
622
+ </View>
623
+ {item.primary && (
624
+ <View transparent style={{ paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, marginLeft: 8 }}>
625
+ <Text theme="description" color={Colors.text.white} style={{ fontSize: 9 }}>
626
+ Primary
627
+ </Text>
628
+ </View>
629
+ )}
630
+ </View>
631
+ </TouchableOpacity>
632
+ )}
633
+ ListEmptyComponent={
634
+ <View style={{ padding: 20, alignItems: 'center' }}>
635
+ <Text theme="description">No paths found</Text>
636
+ </View>
637
+ }
638
+ />
639
+ </View>
640
+ )}
641
+ </View>
642
+ {Object.keys(selectedPath?.params ?? {}).length > 0 && (
643
+ <View style={{ padding:10 }}>
644
+ <Text theme="h2" style={{ marginBottom: 8 }}>
645
+ Path Parameters
646
+ </Text>
647
+ <View type="header" style={{ padding: 12, borderRadius: 8 }}>
648
+ {Object.keys(selectedPath?.params ?? {}).map((paramKey) => {
649
+ const isRequired = selectedPath?.requiredParams.includes(paramKey);
650
+ return (
651
+ <View key={paramKey} transparent style={{ marginBottom: 12 }}>
652
+ <View transparent style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
653
+ <Text theme="description">
654
+ {paramKey}
655
+ </Text>
656
+ {isRequired ? (
657
+ <View transparent style={{ marginLeft: 4, paddingHorizontal: 6, paddingVertical: 2, backgroundColor: Colors.text.error, borderRadius: 4 }}>
658
+ <Text theme="description" color={Colors.text.white} style={{ fontSize: 10 }}>
659
+ Required
660
+ </Text>
661
+ </View>
662
+ ) : (
663
+ <View transparent style={{ marginLeft: 4, paddingHorizontal: 6, paddingVertical: 2, backgroundColor: Colors.borders.light, borderRadius: 4 }}>
664
+ <Text theme="description" style={{ fontSize: 10 }}>
665
+ Optional
666
+ </Text>
667
+ </View>
668
+ )}
669
+ </View>
670
+ <TextInput
671
+ value={pathParams[paramKey] || ''}
672
+ onFocusPosition={onFocusPosition}
673
+ placeholder={`Enter ${paramKey}${isRequired ? ' (required)' : ' (optional)'}`}
674
+ onChangeText={(value) =>
675
+ setPathParams({ ...pathParams, [paramKey]: value })
676
+ }
677
+ style={{
678
+ padding: 10,
679
+ borderRadius: 8,
680
+ borderWidth: isRequired ? 2 : 1,
681
+ borderColor: isRequired && !pathParams[paramKey] ? Colors.text.error : Colors.borders.light
682
+ }}
683
+ />
684
+ </View>
685
+ );
686
+ })}
687
+ </View>
688
+ </View>
689
+ )}
690
+ </View>
691
+ {selectedPath?.path_name === 'custom' && (
692
+ <View style={{ marginBottom: 20 }}>
693
+ <Text theme="h2" style={{ marginBottom: 8 }}>
694
+ Custom Path Name
695
+ </Text>
696
+ <TextInput
697
+ value={customPathName}
698
+ onFocusPosition={onFocusPosition}
699
+ placeholder="/your/custom/path"
700
+ onChangeText={setCustomPathName}
701
+ style={{ padding: 12, borderRadius: 8 }}
702
+ />
703
+ </View>
704
+ )}
705
+ </View>
706
+
707
+ </View>
708
+ )
709
+ default: return <></>
710
+ }
711
+ }
712
+
713
+ return (
714
+ <View
715
+ style={{ flex: 1 }}
716
+ >
717
+ <View transparent style={{ flex:1 }}>
718
+ <FlatList
719
+ data={sections}
720
+ keyExtractor={item => item}
721
+ key={'notification_manager_list'}
722
+ renderItem={renderSections}
723
+ />
724
+ </View>
725
+
726
+ {/* Footer with Send Button */}
727
+ <View type='footer' style={{ flexDirection:'row', alignItems:'center', padding:10, borderBottomRightRadius:8, borderBottomLeftRadius:8 }}>
728
+ <Button
729
+ type="action"
730
+ disabled={loading}
731
+ loading={loading}
732
+ onPress={() => confirmSendNotification({ test: true })}
733
+ style={{
734
+ flex:1,
735
+ opacity: loading || isValid().length > 0 ? 0.5 : 1,
736
+ }}
737
+ >
738
+ <Text textAlign='center' theme="h1" color={Colors.text.white}>
739
+ {loading
740
+ ? (sendingProgress ? `Sending ${sendingProgress.current}/${sendingProgress.total}` : 'Sending...')
741
+ : 'Send Test'}
742
+ </Text>
743
+ </Button>
744
+ <Button
745
+ type="success"
746
+ disabled={loading}
747
+ loading={loading}
748
+ onPress={() => confirmSendNotification()}
749
+ style={{
750
+ flex:2,
751
+ marginLeft:5,
752
+ opacity: loading || isValid().length > 0 ? 0.5 : 1,
753
+ }}
754
+ >
755
+ <Text textAlign='center' theme="h1" color={Colors.text.white}>
756
+ {loading
757
+ ? (sendingProgress ? `Sending batch ${sendingProgress.current} of ${sendingProgress.total}` : 'Sending...')
758
+ : 'Send Notification'}
759
+ </Text>
760
+ </Button>
761
+ </View>
762
+
763
+ {/* Notification Groups Modal */}
764
+ {showGroupsModal && (
765
+ <View type='blur' style={{ position:'absolute', top:0, left:0, right:0, bottom:0, padding:20 }}>
766
+ <View float style={{ flex:1 }}>
767
+ <View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
768
+ <View transparent style={{ flex:1 }}>
769
+ <Text theme='h1'>Notification Groups</Text>
770
+ <Text theme='description' style={{ marginTop:3 }}>Select a group to send to</Text>
771
+ </View>
772
+ </View>
773
+ <View style={{flex:1}}>
774
+ {loadingGroups ? (
775
+ <View style={{ padding: 40, alignItems: 'center' }}>
776
+ <ActivityIndicator size="large" color={Colors.text.action} />
777
+ </View>
778
+ ) : (
779
+ <FlatList
780
+ data={customGroup ? [customGroup, ...notificationGroups] : notificationGroups}
781
+ keyExtractor={(item) => item.notification_group_id}
782
+ renderItem={({ item }) => (
783
+ <TouchableOpacity
784
+ onPress={() => handleSelectGroup(item.notification_group_id)}
785
+ style={{
786
+ padding: 15,
787
+ borderBottomWidth: 1,
788
+ borderColor: Colors.borders.light,
789
+ backgroundColor: item.notification_group_id === 'custom_group' ? Colors.views.header : 'transparent'
790
+ }}
791
+ >
792
+ <View transparent>
793
+ <View transparent style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
794
+ <View transparent style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
795
+ <Text theme="h1">{item.name}</Text>
796
+ {item.notification_group_id === 'custom_group' && (
797
+ <View transparent style={{ marginLeft: 8, paddingHorizontal: 6, paddingVertical: 2, backgroundColor: Colors.buttons.background.action, borderRadius: 4 }}>
798
+ <Text theme="description" color={Colors.text.white} style={{ fontSize: 10 }}>
799
+ CUSTOM
800
+ </Text>
801
+ </View>
802
+ )}
803
+ </View>
804
+ {item.player_count !== undefined && (
805
+ <View transparent style={{ marginLeft: 8, paddingHorizontal: 8, paddingVertical: 4, backgroundColor: Colors.views.header, borderRadius: 4 }}>
806
+ <Text theme="description" style={{ fontSize: 12, fontWeight: '600' }}>
807
+ {item.player_count.toLocaleString()} users
808
+ </Text>
809
+ </View>
810
+ )}
811
+ </View>
812
+ {item.description && (
813
+ <Text theme="description" style={{ marginTop: 4 }}>
814
+ {item.description}
815
+ </Text>
816
+ )}
817
+ </View>
818
+ </TouchableOpacity>
819
+ )}
820
+ ListEmptyComponent={
821
+ <View style={{ padding: 40, alignItems: 'center' }}>
822
+ <Text theme="description">No groups found</Text>
823
+ </View>
824
+ }
825
+ />
826
+ )}
827
+ </View>
828
+ <View type='footer' style={{ flexDirection:'row', alignItems:'center', padding:10, borderBottomRightRadius:8, borderBottomLeftRadius:8 }}>
829
+ <Button
830
+ style={{ flex:1 }}
831
+ type='close'
832
+ title='CLOSE'
833
+ onPress={() => setShowGroupsModal(false)}
834
+ />
835
+ </View>
836
+ </View>
837
+ </View>
838
+ )}
839
+
840
+ {/* Edit Custom Group Modal */}
841
+ {showEditCustomGroupModal && (
842
+ <View type='blur' style={{ position:'absolute', top:0, left:0, right:0, bottom:0, padding:20 }}>
843
+ <View float style={{ maxHeight: '80%', minHeight: 400 }}>
844
+ <View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
845
+ <View transparent style={{ flex:1 }}>
846
+ <Text theme='h1'>Edit Custom Recipients</Text>
847
+ <Text theme='description' style={{ marginTop:3 }}>Paste comma-separated player IDs</Text>
848
+ </View>
849
+ </View>
850
+ <View style={{ flex: 1, padding: 15 }}>
851
+ <Text theme="h2" style={{ marginBottom: 10 }}>
852
+ Player IDs (CSV format)
853
+ </Text>
854
+ <TextInput
855
+ value={customGroupCsv}
856
+ onFocusPosition={onFocusPosition}
857
+ placeholder="98,100,2485,etc"
858
+ onChangeText={setCustomGroupCsv}
859
+ multiline
860
+ numberOfLines={10}
861
+ style={{
862
+ padding: 12,
863
+ borderRadius: 8,
864
+ minHeight: 200,
865
+ textAlignVertical: 'top'
866
+ }}
867
+ />
868
+ <Text theme="description" style={{ marginTop: 8 }}>
869
+ Current count: {customGroupCsv.split(',').filter(id => id.trim().length > 0).length} IDs
870
+ </Text>
871
+ </View>
872
+ <View type='footer' style={{ flexDirection:'row', alignItems:'center', padding:10, borderBottomRightRadius:8, borderBottomLeftRadius:8 }}>
873
+ <Button
874
+ style={{ flex:1, marginRight: 5 }}
875
+ type='close'
876
+ title='CANCEL'
877
+ onPress={() => setShowEditCustomGroupModal(false)}
878
+ />
879
+ <Button
880
+ style={{ flex:1 }}
881
+ type='success'
882
+ title='SAVE'
883
+ onPress={handleSaveCustomGroup}
884
+ />
885
+ </View>
886
+ </View>
887
+ </View>
888
+ )}
889
+ </View>
890
+ );
891
+ };
892
+
893
+ export default NotificationManager;
894
+ export { NotificationApi, NotificationHelpers };