@squad-sports/react-native 1.3.1

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +46 -0
  4. package/src/SquadExperience.tsx +200 -0
  5. package/src/SquadProvider.tsx +232 -0
  6. package/src/SquadSportsSDK.ts +286 -0
  7. package/src/__tests__/DeepLinkHandler.test.ts +101 -0
  8. package/src/__tests__/ErrorBoundary.test.tsx +161 -0
  9. package/src/__tests__/EventProcessor.test.ts +241 -0
  10. package/src/__tests__/PushNotificationHandler.test.ts +91 -0
  11. package/src/__tests__/SecureStorage.test.ts +62 -0
  12. package/src/__tests__/SquadSportsSDK.test.ts +278 -0
  13. package/src/__tests__/VerificationCooldown.test.ts +153 -0
  14. package/src/components/ErrorBoundary.tsx +129 -0
  15. package/src/components/SOTD/SOTDComponents.tsx +101 -0
  16. package/src/components/audio/AudioPlayerRow.tsx +189 -0
  17. package/src/components/audio/recording/AudioRecording.tsx +232 -0
  18. package/src/components/communities/CommunityComponents.tsx +78 -0
  19. package/src/components/dialogs/AllDialogs.tsx +123 -0
  20. package/src/components/dialogs/ConfirmDialog.tsx +77 -0
  21. package/src/components/dialogs/PermissionDialog.tsx +132 -0
  22. package/src/components/dragAndDrop/DragAndDrop.tsx +56 -0
  23. package/src/components/events/EventComponents.tsx +93 -0
  24. package/src/components/feed/ChatBannerCard.tsx +94 -0
  25. package/src/components/feed/FeedLoadingSkeleton.tsx +43 -0
  26. package/src/components/feed/FreestyleCard.tsx +119 -0
  27. package/src/components/feed/InterstitialOverlay.tsx +190 -0
  28. package/src/components/feed/PollCard.tsx +158 -0
  29. package/src/components/feed/SponsoredContentCard.tsx +118 -0
  30. package/src/components/freestyle/FreestyleComponents.tsx +148 -0
  31. package/src/components/index.ts +42 -0
  32. package/src/components/message/MessageCard.tsx +166 -0
  33. package/src/components/message/MessageComponents.tsx +143 -0
  34. package/src/components/poll/PollComponents.tsx +226 -0
  35. package/src/components/sentinels/Sentinels.tsx +175 -0
  36. package/src/components/squad/FeedSquad.tsx +54 -0
  37. package/src/components/toasts/Toasts.tsx +88 -0
  38. package/src/components/ux/RootUXComponents.tsx +157 -0
  39. package/src/components/ux/action-sheet/ActionSheet.tsx +67 -0
  40. package/src/components/ux/bottom-sheet/BottomSheetComponents.tsx +93 -0
  41. package/src/components/ux/bottom-sheet/SquadBottomSheet.tsx +119 -0
  42. package/src/components/ux/buttons/Button.tsx +37 -0
  43. package/src/components/ux/buttons/InfoButton.tsx +24 -0
  44. package/src/components/ux/buttons/XButton.tsx +27 -0
  45. package/src/components/ux/carousel/Carousel.tsx +134 -0
  46. package/src/components/ux/emoji-picker/EmojiReactionPicker.tsx +139 -0
  47. package/src/components/ux/errors/ErrorHint.tsx +46 -0
  48. package/src/components/ux/inputs/CodeInput.tsx +121 -0
  49. package/src/components/ux/inputs/DatePicker.tsx +76 -0
  50. package/src/components/ux/inputs/MaskedTextInput.tsx +95 -0
  51. package/src/components/ux/inputs/PhoneNumberInput.tsx +90 -0
  52. package/src/components/ux/inputs/SearchTextInput.tsx +70 -0
  53. package/src/components/ux/inputs/TextInput.tsx +58 -0
  54. package/src/components/ux/layout/AvoidKeyboardScreen.tsx +58 -0
  55. package/src/components/ux/layout/BlurOverlay.tsx +26 -0
  56. package/src/components/ux/layout/CrossFade.tsx +30 -0
  57. package/src/components/ux/layout/DismissKeyboardOnBlur.tsx +14 -0
  58. package/src/components/ux/layout/LoadingOverlay.tsx +60 -0
  59. package/src/components/ux/layout/NetworkBanner.tsx +64 -0
  60. package/src/components/ux/layout/PermissionsCTAContent.tsx +54 -0
  61. package/src/components/ux/layout/RefreshControl.tsx +7 -0
  62. package/src/components/ux/layout/Screen.tsx +31 -0
  63. package/src/components/ux/layout/ScreenHeader.tsx +89 -0
  64. package/src/components/ux/layout/TabBar.tsx +39 -0
  65. package/src/components/ux/layout/Toast.tsx +116 -0
  66. package/src/components/ux/layout/TransparentOverlay.tsx +21 -0
  67. package/src/components/ux/navigation/BackButton.tsx +29 -0
  68. package/src/components/ux/navigation/LinkButton.tsx +21 -0
  69. package/src/components/ux/navigation/UrlButton.tsx +25 -0
  70. package/src/components/ux/scroll/RefreshableFlatList.tsx +35 -0
  71. package/src/components/ux/shapes/Shapes.tsx +23 -0
  72. package/src/components/ux/text/Typography.tsx +28 -0
  73. package/src/components/ux/user-image/UserImage.tsx +75 -0
  74. package/src/components/ux/user-image/UserImageVariants.tsx +104 -0
  75. package/src/components/wallet/WalletComponents.tsx +116 -0
  76. package/src/contexts/AuthContext.tsx +45 -0
  77. package/src/contexts/EventProcessorContext.tsx +41 -0
  78. package/src/contexts/PlayerQueueContext.tsx +95 -0
  79. package/src/hooks/useAuth.ts +23 -0
  80. package/src/hooks/useDataRefresh.ts +30 -0
  81. package/src/hooks/useEventProcessor.ts +6 -0
  82. package/src/hooks/useImageOptimization.ts +59 -0
  83. package/src/hooks/useOnboardingStepGuard.ts +36 -0
  84. package/src/hooks/usePendingNavigation.ts +26 -0
  85. package/src/hooks/useSquadData.ts +84 -0
  86. package/src/hooks/useUserCreated.ts +25 -0
  87. package/src/hooks/useUserUpdate.ts +25 -0
  88. package/src/hooks/useViewabilityTracker.ts +40 -0
  89. package/src/index.ts +109 -0
  90. package/src/navigation/SquadNavigator.tsx +262 -0
  91. package/src/realtime/DeepLinkHandler.ts +113 -0
  92. package/src/realtime/EventProcessor.ts +313 -0
  93. package/src/realtime/NetworkMonitor.ts +84 -0
  94. package/src/realtime/OfflineQueue.ts +133 -0
  95. package/src/realtime/PushNotificationHandler.ts +125 -0
  96. package/src/realtime/useRealtimeSync.ts +84 -0
  97. package/src/screens/auth/EmailVerificationScreen.tsx +201 -0
  98. package/src/screens/auth/EnterCodeScreen.tsx +253 -0
  99. package/src/screens/auth/EnterEmailScreen.tsx +234 -0
  100. package/src/screens/auth/LandingScreen.tsx +90 -0
  101. package/src/screens/auth/LoginScreen.tsx +126 -0
  102. package/src/screens/events/EventScreen.tsx +163 -0
  103. package/src/screens/freestyle/CommunityFreestyleScreen.tsx +82 -0
  104. package/src/screens/freestyle/FreestyleCreationScreen.tsx +255 -0
  105. package/src/screens/freestyle/FreestyleListensScreen.tsx +44 -0
  106. package/src/screens/freestyle/FreestyleReactionsScreen.tsx +44 -0
  107. package/src/screens/freestyle/FreestyleScreen.tsx +64 -0
  108. package/src/screens/home/HomeScreen.tsx +365 -0
  109. package/src/screens/home/slivers/SquadCircle.tsx +77 -0
  110. package/src/screens/invite/InvitationQrCodeScreen.tsx +114 -0
  111. package/src/screens/invite/InviteScreen.tsx +175 -0
  112. package/src/screens/messaging/MessagingScreen.tsx +360 -0
  113. package/src/screens/onboarding/OnboardingAccountSetupScreen.tsx +221 -0
  114. package/src/screens/onboarding/OnboardingTeamSelectScreen.tsx +215 -0
  115. package/src/screens/onboarding/OnboardingWelcomeScreen.tsx +93 -0
  116. package/src/screens/polls/PollResponseScreen.tsx +229 -0
  117. package/src/screens/polls/PollSummationScreen.tsx +78 -0
  118. package/src/screens/profile/ProfileScreen.tsx +234 -0
  119. package/src/screens/settings/BlockedUsersScreen.tsx +139 -0
  120. package/src/screens/settings/DeleteAccountScreen.tsx +182 -0
  121. package/src/screens/settings/EditProfileScreen.tsx +154 -0
  122. package/src/screens/settings/NetworkStatusScreen.tsx +59 -0
  123. package/src/screens/settings/SettingsScreen.tsx +194 -0
  124. package/src/screens/squad-line/AddCallTitleScreen.tsx +293 -0
  125. package/src/screens/wallet/WalletScreen.tsx +174 -0
  126. package/src/services/AuthStateManager.ts +93 -0
  127. package/src/services/NavigationService.ts +40 -0
  128. package/src/services/UserDataManager.ts +59 -0
  129. package/src/services/UserUpdateService.ts +31 -0
  130. package/src/services/VerificationStateManager.ts +41 -0
  131. package/src/squad-line/CallScreen.tsx +158 -0
  132. package/src/squad-line/IncomingCallOverlay.tsx +113 -0
  133. package/src/squad-line/SquadLineClient.ts +327 -0
  134. package/src/squad-line/useSquadLine.ts +80 -0
  135. package/src/state/audio.ts +38 -0
  136. package/src/state/client.ts +26 -0
  137. package/src/state/communities.ts +45 -0
  138. package/src/state/contacts.ts +28 -0
  139. package/src/state/device-info.ts +22 -0
  140. package/src/state/events.ts +16 -0
  141. package/src/state/features.ts +57 -0
  142. package/src/state/index.ts +121 -0
  143. package/src/state/invitations.ts +16 -0
  144. package/src/state/modal-keys.ts +63 -0
  145. package/src/state/modal-queue.ts +104 -0
  146. package/src/state/navigation.ts +34 -0
  147. package/src/state/permissions.ts +43 -0
  148. package/src/state/session.ts +223 -0
  149. package/src/state/squaddie-of-the-day.ts +21 -0
  150. package/src/state/sync/crdt.ts +70 -0
  151. package/src/state/sync/dependable.ts +213 -0
  152. package/src/state/sync/feed-v2.ts +42 -0
  153. package/src/state/sync/messages.ts +44 -0
  154. package/src/state/sync/offline-support.ts +42 -0
  155. package/src/state/sync/polls.ts +37 -0
  156. package/src/state/sync/refresh.ts +24 -0
  157. package/src/state/sync/squad-v2.ts +25 -0
  158. package/src/state/ui.ts +36 -0
  159. package/src/state/user.ts +46 -0
  160. package/src/state/wallet.ts +26 -0
  161. package/src/storage/SecureStorage.ts +77 -0
  162. package/src/theme/ThemeContext.tsx +159 -0
  163. package/src/types/modules.d.ts +165 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Root-level UX components from squad-demo/src/components/ux/.
3
+ * Ported: ToolTip, TopToast, TopDialog, TopBottomSheet, SettingsCaret,
4
+ * UserLabelTag, UserImageBorder, QrCode, QrScan, WaitForLoadable,
5
+ * OfflineBanner, HexagonImageMask, UserImageWithCommunityTag.
6
+ */
7
+ import React, { useEffect, useState } from 'react';
8
+ import { View, Text, StyleSheet, Pressable, Animated, Dimensions } from 'react-native';
9
+ import { Colors } from '../../theme/ThemeContext';
10
+
11
+ // --- ToolTip ---
12
+ export function ToolTip({ text, visible, onDismiss, position = 'bottom' }: {
13
+ text: string; visible: boolean; onDismiss?: () => void; position?: 'top' | 'bottom';
14
+ }) {
15
+ if (!visible) return null;
16
+ return (
17
+ <Pressable style={[styles.tooltip, position === 'top' ? styles.tooltipTop : styles.tooltipBottom]} onPress={onDismiss}>
18
+ <Text style={styles.tooltipText}>{text}</Text>
19
+ <View style={[styles.tooltipArrow, position === 'top' ? styles.arrowBottom : styles.arrowTop]} />
20
+ </Pressable>
21
+ );
22
+ }
23
+
24
+ // --- TopToast ---
25
+ export function TopToast({ visible, message, onDismiss }: { visible: boolean; message: string; onDismiss?: () => void }) {
26
+ const [anim] = useState(new Animated.Value(visible ? 1 : 0));
27
+ useEffect(() => {
28
+ Animated.timing(anim, { toValue: visible ? 1 : 0, duration: 300, useNativeDriver: true }).start();
29
+ if (visible) { const t = setTimeout(() => onDismiss?.(), 3000); return () => clearTimeout(t); }
30
+ }, [visible]);
31
+ if (!visible) return null;
32
+ return (
33
+ <Animated.View style={[styles.topToast, { opacity: anim }]}>
34
+ <Text style={styles.topToastText}>{message}</Text>
35
+ </Animated.View>
36
+ );
37
+ }
38
+
39
+ // --- TopDialog ---
40
+ export function TopDialog({ visible, title, message, onConfirm, onCancel }: {
41
+ visible: boolean; title: string; message: string; onConfirm: () => void; onCancel: () => void;
42
+ }) {
43
+ if (!visible) return null;
44
+ return (
45
+ <View style={styles.dialogOverlay}>
46
+ <View style={styles.dialogBox}>
47
+ <Text style={styles.dialogTitle}>{title}</Text>
48
+ <Text style={styles.dialogMessage}>{message}</Text>
49
+ <View style={styles.dialogButtons}>
50
+ <Pressable style={styles.dialogButton} onPress={onCancel}><Text style={styles.dialogButtonText}>Cancel</Text></Pressable>
51
+ <Pressable style={[styles.dialogButton, styles.dialogButtonPrimary]} onPress={onConfirm}><Text style={styles.dialogButtonTextPrimary}>Confirm</Text></Pressable>
52
+ </View>
53
+ </View>
54
+ </View>
55
+ );
56
+ }
57
+
58
+ // --- SettingsCaret ---
59
+ export function SettingsCaret() {
60
+ return <Text style={styles.caret}>{'>'}</Text>;
61
+ }
62
+
63
+ // --- UserLabelTag ---
64
+ export function UserLabelTag({ label, color = Colors.purple1 }: { label: string; color?: string }) {
65
+ return (
66
+ <View style={[styles.labelTag, { backgroundColor: color }]}>
67
+ <Text style={styles.labelTagText}>{label}</Text>
68
+ </View>
69
+ );
70
+ }
71
+
72
+ // --- UserImageBorder ---
73
+ export function UserImageBorder({ color = Colors.purple1, size = 52, children }: { color?: string; size?: number; children: React.ReactNode }) {
74
+ return (
75
+ <View style={[styles.imageBorder, { width: size, height: size, borderRadius: size / 2, borderColor: color }]}>
76
+ {children}
77
+ </View>
78
+ );
79
+ }
80
+
81
+ // --- WaitForLoadable ---
82
+ export function WaitForLoadable({ loadable, fallback, children }: {
83
+ loadable: { state: string; contents: unknown }; fallback: React.ReactNode; children: (data: unknown) => React.ReactNode;
84
+ }) {
85
+ if (loadable.state === 'hasValue') return <>{children(loadable.contents)}</>;
86
+ if (loadable.state === 'hasError') return null;
87
+ return <>{fallback}</>;
88
+ }
89
+
90
+ // --- OfflineBanner ---
91
+ export function OfflineBanner({ isOnline }: { isOnline: boolean }) {
92
+ if (isOnline) return null;
93
+ return (
94
+ <View style={styles.offlineBanner}><Text style={styles.offlineBannerText}>No internet connection</Text></View>
95
+ );
96
+ }
97
+
98
+ // --- UserImageWithCommunityTag ---
99
+ export function UserImageWithCommunityTag({ imageUrl, displayName, communityName, communityColor, size = 48 }: {
100
+ imageUrl?: string; displayName?: string; communityName?: string; communityColor?: string; size?: number;
101
+ }) {
102
+ const UserImage = require('./user-image/UserImage').default;
103
+ return (
104
+ <View>
105
+ <UserImage imageUrl={imageUrl} displayName={displayName} size={size} />
106
+ {communityName && (
107
+ <View style={[styles.communityTag, { backgroundColor: communityColor ?? Colors.purple1 }]}>
108
+ <Text style={styles.communityTagText}>{communityName.substring(0, 3).toUpperCase()}</Text>
109
+ </View>
110
+ )}
111
+ </View>
112
+ );
113
+ }
114
+
115
+ const styles = StyleSheet.create({
116
+ // ToolTip
117
+ tooltip: { position: 'absolute', backgroundColor: Colors.gray2, borderRadius: 8, padding: 12, maxWidth: 250, zIndex: 100, shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 8 },
118
+ tooltipTop: { top: -60 },
119
+ tooltipBottom: { bottom: -60 },
120
+ tooltipText: { color: Colors.white, fontSize: 13 },
121
+ tooltipArrow: { position: 'absolute', width: 12, height: 12, backgroundColor: Colors.gray2, transform: [{ rotate: '45deg' }], alignSelf: 'center' },
122
+ arrowTop: { top: -6 },
123
+ arrowBottom: { bottom: -6 },
124
+
125
+ // TopToast
126
+ topToast: { position: 'absolute', top: 60, left: 16, right: 16, backgroundColor: Colors.gray2, borderRadius: 12, padding: 16, zIndex: 10000, shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 8, elevation: 8 },
127
+ topToastText: { color: Colors.white, fontSize: 14 },
128
+
129
+ // TopDialog
130
+ dialogOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, padding: 32 },
131
+ dialogBox: { backgroundColor: Colors.gray2, borderRadius: 20, padding: 24, width: '100%', maxWidth: 320 },
132
+ dialogTitle: { color: Colors.white, fontSize: 18, fontWeight: '600', marginBottom: 8, textAlign: 'center' },
133
+ dialogMessage: { color: Colors.gray6, fontSize: 14, textAlign: 'center', marginBottom: 20 },
134
+ dialogButtons: { flexDirection: 'row', gap: 8 },
135
+ dialogButton: { flex: 1, paddingVertical: 12, borderRadius: 20, alignItems: 'center' },
136
+ dialogButtonPrimary: { backgroundColor: Colors.purple1 },
137
+ dialogButtonText: { color: Colors.gray6, fontSize: 15 },
138
+ dialogButtonTextPrimary: { color: Colors.gray1, fontSize: 15, fontWeight: '600' },
139
+
140
+ // SettingsCaret
141
+ caret: { color: Colors.gray6, fontSize: 14 },
142
+
143
+ // UserLabelTag
144
+ labelTag: { borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2 },
145
+ labelTagText: { color: Colors.white, fontSize: 10, fontWeight: '700' },
146
+
147
+ // UserImageBorder
148
+ imageBorder: { borderWidth: 2, justifyContent: 'center', alignItems: 'center' },
149
+
150
+ // OfflineBanner
151
+ offlineBanner: { backgroundColor: Colors.orange2, paddingVertical: 8, paddingHorizontal: 16, alignItems: 'center' },
152
+ offlineBannerText: { color: Colors.white, fontSize: 13, fontWeight: '600' },
153
+
154
+ // CommunityTag
155
+ communityTag: { position: 'absolute', bottom: -2, right: -4, borderRadius: 6, paddingHorizontal: 4, paddingVertical: 1, borderWidth: 2, borderColor: Colors.gray9 },
156
+ communityTagText: { color: Colors.white, fontSize: 7, fontWeight: '800' },
157
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Action sheet components.
3
+ * Ported from squad-demo/src/components/ux/action-sheet/.
4
+ */
5
+ import React from 'react';
6
+ import { View, Text, StyleSheet, Pressable, Modal } from 'react-native';
7
+ import { Colors } from '../../../theme/ThemeContext';
8
+
9
+ interface ActionSheetItemProps {
10
+ label: string;
11
+ onPress: () => void;
12
+ destructive?: boolean;
13
+ icon?: string;
14
+ }
15
+
16
+ export function ActionSheetItem({ label, onPress, destructive, icon }: ActionSheetItemProps) {
17
+ return (
18
+ <Pressable
19
+ style={styles.item}
20
+ onPress={onPress}
21
+ accessibilityRole="menuitem"
22
+ >
23
+ {icon && <Text style={styles.itemIcon}>{icon}</Text>}
24
+ <Text style={[styles.itemText, destructive && styles.itemTextDestructive]}>
25
+ {label}
26
+ </Text>
27
+ </Pressable>
28
+ );
29
+ }
30
+
31
+ interface ActionSheetProps {
32
+ visible: boolean;
33
+ onDismiss: () => void;
34
+ title?: string;
35
+ items: ActionSheetItemProps[];
36
+ }
37
+
38
+ export default function ActionSheet({ visible, onDismiss, title, items }: ActionSheetProps) {
39
+ return (
40
+ <Modal visible={visible} transparent animationType="slide" onRequestClose={onDismiss}>
41
+ <Pressable style={styles.backdrop} onPress={onDismiss} />
42
+ <View style={styles.container}>
43
+ {title && <Text style={styles.title}>{title}</Text>}
44
+ {items.map((item, i) => (
45
+ <ActionSheetItem key={i} {...item} onPress={() => { item.onPress(); onDismiss(); }} />
46
+ ))}
47
+ <View style={styles.separator} />
48
+ <Pressable style={styles.cancelItem} onPress={onDismiss}>
49
+ <Text style={styles.cancelText}>Cancel</Text>
50
+ </Pressable>
51
+ </View>
52
+ </Modal>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' },
58
+ container: { backgroundColor: Colors.gray2, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingBottom: 32 },
59
+ title: { color: Colors.gray6, fontSize: 13, textAlign: 'center', paddingVertical: 12 },
60
+ item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16, paddingHorizontal: 20, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: Colors.gray3 },
61
+ itemIcon: { fontSize: 20, marginRight: 12, color: Colors.white },
62
+ itemText: { color: Colors.white, fontSize: 16 },
63
+ itemTextDestructive: { color: Colors.red },
64
+ separator: { height: 8, backgroundColor: Colors.gray3 },
65
+ cancelItem: { paddingVertical: 16, alignItems: 'center' },
66
+ cancelText: { color: Colors.white, fontSize: 16, fontWeight: '600' },
67
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Bottom sheet sub-components.
3
+ * Ported from squad-demo/src/components/ux/bottom-sheet/components/ + factory + registry.
4
+ */
5
+ import React from 'react';
6
+ import { View, StyleSheet } from 'react-native';
7
+ import { Colors } from '../../../theme/ThemeContext';
8
+
9
+ // --- BottomSheetHandle ---
10
+ export function BottomSheetHandle() {
11
+ return (
12
+ <View style={styles.handleContainer}>
13
+ <View style={styles.handle} />
14
+ </View>
15
+ );
16
+ }
17
+
18
+ // --- BottomSheetBackdrop ---
19
+ export function BottomSheetBackdrop({ onPress }: { onPress?: () => void }) {
20
+ return (
21
+ <View style={styles.backdrop}>
22
+ {onPress && <View style={StyleSheet.absoluteFill} onTouchEnd={onPress} />}
23
+ </View>
24
+ );
25
+ }
26
+
27
+ // --- ErrorInfoBottomSheet ---
28
+ export function ErrorInfoBottomSheet({
29
+ error,
30
+ onDismiss,
31
+ }: { error: { name: string; message: string; code?: string; nativeErrorMessage?: string }; onDismiss: () => void }) {
32
+ const [showDetails, setShowDetails] = React.useState(false);
33
+ const { Text, Pressable } = require('react-native');
34
+
35
+ return (
36
+ <View style={styles.errorSheet}>
37
+ <Text style={styles.errorTitle}>{error.name}</Text>
38
+ <Text style={styles.errorMessage}>{error.message}</Text>
39
+ {error.nativeErrorMessage && (
40
+ <Pressable onPress={() => setShowDetails(!showDetails)}>
41
+ <Text style={styles.errorDetailsToggle}>{showDetails ? 'Hide' : 'More'} details</Text>
42
+ </Pressable>
43
+ )}
44
+ {showDetails && error.nativeErrorMessage && (
45
+ <Text style={styles.errorDetails}>{error.nativeErrorMessage}</Text>
46
+ )}
47
+ <Pressable style={styles.errorDismiss} onPress={onDismiss}>
48
+ <Text style={styles.errorDismissText}>Dismiss</Text>
49
+ </Pressable>
50
+ </View>
51
+ );
52
+ }
53
+
54
+ // --- WithTab (tabbed bottom sheet content) ---
55
+ export function WithTab({
56
+ tabs,
57
+ activeTab,
58
+ onTabChange,
59
+ children,
60
+ }: { tabs: string[]; activeTab: string; onTabChange: (tab: string) => void; children: React.ReactNode }) {
61
+ const { Text, Pressable } = require('react-native');
62
+
63
+ return (
64
+ <View>
65
+ <View style={styles.tabBar}>
66
+ {tabs.map(tab => (
67
+ <Pressable key={tab} style={[styles.tab, tab === activeTab && styles.tabActive]} onPress={() => onTabChange(tab)}>
68
+ <Text style={[styles.tabText, tab === activeTab && styles.tabTextActive]}>{tab}</Text>
69
+ </Pressable>
70
+ ))}
71
+ </View>
72
+ {children}
73
+ </View>
74
+ );
75
+ }
76
+
77
+ const styles = StyleSheet.create({
78
+ handleContainer: { alignItems: 'center', paddingVertical: 12 },
79
+ handle: { width: 40, height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.14)' },
80
+ backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)' },
81
+ errorSheet: { padding: 24 },
82
+ errorTitle: { color: Colors.white, fontSize: 18, fontWeight: '600', marginBottom: 8 },
83
+ errorMessage: { color: Colors.gray6, fontSize: 14, marginBottom: 16 },
84
+ errorDetailsToggle: { color: Colors.purple1, fontSize: 14, marginBottom: 8 },
85
+ errorDetails: { color: Colors.gray6, fontSize: 12, fontFamily: 'monospace', marginBottom: 16 },
86
+ errorDismiss: { paddingVertical: 12, borderRadius: 20, backgroundColor: Colors.purple1, alignItems: 'center' },
87
+ errorDismissText: { color: Colors.gray1, fontWeight: '600' },
88
+ tabBar: { flexDirection: 'row', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: Colors.gray3 },
89
+ tab: { flex: 1, paddingVertical: 12, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: 'transparent' },
90
+ tabActive: { borderBottomColor: Colors.purple1 },
91
+ tabText: { color: Colors.gray6, fontSize: 14 },
92
+ tabTextActive: { color: Colors.white, fontWeight: '500' },
93
+ });
@@ -0,0 +1,119 @@
1
+ import React, { useCallback, useMemo, useRef, forwardRef, useImperativeHandle } from 'react';
2
+ import { View, Text, StyleSheet, Pressable } from 'react-native';
3
+ import BottomSheet, {
4
+ BottomSheetBackdrop,
5
+ BottomSheetView,
6
+ type BottomSheetBackdropProps,
7
+ } from '@gorhom/bottom-sheet';
8
+ import { Colors } from '../../../theme/ThemeContext';
9
+
10
+ export interface SquadBottomSheetRef {
11
+ open: () => void;
12
+ close: () => void;
13
+ }
14
+
15
+ interface SquadBottomSheetProps {
16
+ children: React.ReactNode;
17
+ snapPoints?: (string | number)[];
18
+ title?: string;
19
+ onClose?: () => void;
20
+ enableDynamicSizing?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Themed bottom sheet wrapper around @gorhom/bottom-sheet.
25
+ * Provides consistent styling, backdrop, and handle across the SDK.
26
+ */
27
+ export const SquadBottomSheet = forwardRef<SquadBottomSheetRef, SquadBottomSheetProps>(
28
+ function SquadBottomSheet({ children, snapPoints: customSnapPoints, title, onClose, enableDynamicSizing }, ref) {
29
+ const bottomSheetRef = useRef<BottomSheet>(null);
30
+
31
+ const snapPoints = useMemo(
32
+ () => customSnapPoints ?? ['50%', '80%'],
33
+ [customSnapPoints],
34
+ );
35
+
36
+ useImperativeHandle(ref, () => ({
37
+ open: () => bottomSheetRef.current?.snapToIndex(0),
38
+ close: () => bottomSheetRef.current?.close(),
39
+ }));
40
+
41
+ const handleClose = useCallback(() => {
42
+ onClose?.();
43
+ }, [onClose]);
44
+
45
+ const renderBackdrop = useCallback(
46
+ (props: BottomSheetBackdropProps) => (
47
+ <BottomSheetBackdrop
48
+ {...props}
49
+ disappearsOnIndex={-1}
50
+ appearsOnIndex={0}
51
+ opacity={0.6}
52
+ />
53
+ ),
54
+ [],
55
+ );
56
+
57
+ return (
58
+ <BottomSheet
59
+ ref={bottomSheetRef}
60
+ index={-1}
61
+ snapPoints={snapPoints}
62
+ enablePanDownToClose
63
+ onClose={handleClose}
64
+ backdropComponent={renderBackdrop}
65
+ enableDynamicSizing={enableDynamicSizing}
66
+ backgroundStyle={styles.background}
67
+ handleIndicatorStyle={styles.handleIndicator}
68
+ >
69
+ {title && (
70
+ <View style={styles.header}>
71
+ <Text style={styles.title}>{title}</Text>
72
+ <Pressable onPress={() => bottomSheetRef.current?.close()} hitSlop={8}>
73
+ <Text style={styles.closeButton}>x</Text>
74
+ </Pressable>
75
+ </View>
76
+ )}
77
+ <BottomSheetView style={styles.content}>
78
+ {children}
79
+ </BottomSheetView>
80
+ </BottomSheet>
81
+ );
82
+ },
83
+ );
84
+
85
+ const styles = StyleSheet.create({
86
+ background: {
87
+ backgroundColor: Colors.gray2,
88
+ borderTopLeftRadius: 20,
89
+ borderTopRightRadius: 20,
90
+ },
91
+ handleIndicator: {
92
+ backgroundColor: 'rgba(255, 255, 255, 0.14)',
93
+ width: 40,
94
+ },
95
+ header: {
96
+ flexDirection: 'row',
97
+ justifyContent: 'space-between',
98
+ alignItems: 'center',
99
+ paddingHorizontal: 20,
100
+ paddingTop: 8,
101
+ paddingBottom: 16,
102
+ borderBottomWidth: StyleSheet.hairlineWidth,
103
+ borderBottomColor: Colors.gray3,
104
+ },
105
+ title: {
106
+ color: Colors.white,
107
+ fontSize: 18,
108
+ fontWeight: '600',
109
+ },
110
+ closeButton: {
111
+ color: Colors.gray6,
112
+ fontSize: 22,
113
+ fontWeight: '300',
114
+ },
115
+ content: {
116
+ flex: 1,
117
+ padding: 20,
118
+ },
119
+ });
@@ -0,0 +1,37 @@
1
+ import React, { useCallback } from 'react';
2
+ import { Pressable, StyleProp, ViewStyle, PressableProps } from 'react-native';
3
+
4
+ export type ButtonProps = PressableProps & {
5
+ disabledStyle?: StyleProp<ViewStyle>;
6
+ feedback?: boolean;
7
+ };
8
+
9
+ export default function Button({
10
+ disabledStyle = {},
11
+ feedback = true,
12
+ style,
13
+ ...props
14
+ }: ButtonProps) {
15
+ const styleWithFeedback = useCallback(
16
+ ({ pressed }: { pressed: boolean }) => {
17
+ const stylesToApply: StyleProp<ViewStyle>[] = [
18
+ feedback ? { opacity: pressed ? 0.6 : 1 } : {},
19
+ style as StyleProp<ViewStyle>,
20
+ props.disabled ? disabledStyle : {},
21
+ ].filter(Boolean);
22
+ return stylesToApply;
23
+ },
24
+ [style, feedback, props.disabled, disabledStyle],
25
+ );
26
+
27
+ return (
28
+ <Pressable
29
+ {...props}
30
+ style={styleWithFeedback}
31
+ accessibilityRole={props.accessibilityRole ?? 'button'}
32
+ accessibilityState={{ disabled: props.disabled ?? undefined }}
33
+ >
34
+ {props.children}
35
+ </Pressable>
36
+ );
37
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { Text, StyleSheet, View } from 'react-native';
3
+ import Button from './Button';
4
+ import { Colors } from '../../../theme/ThemeContext';
5
+
6
+ interface InfoButtonProps {
7
+ onPress: () => void;
8
+ size?: number;
9
+ }
10
+
11
+ export default function InfoButton({ onPress, size = 20 }: InfoButtonProps) {
12
+ return (
13
+ <Button onPress={onPress} accessibilityLabel="More information">
14
+ <View style={[styles.container, { width: size, height: size, borderRadius: size / 2 }]}>
15
+ <Text style={[styles.text, { fontSize: size * 0.6 }]}>i</Text>
16
+ </View>
17
+ </Button>
18
+ );
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ container: { borderWidth: 1.5, borderColor: Colors.gray6, justifyContent: 'center', alignItems: 'center' },
23
+ text: { color: Colors.gray6, fontWeight: '600', fontStyle: 'italic' },
24
+ });
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { Text, StyleSheet } from 'react-native';
3
+ import Button from './Button';
4
+ import { Colors } from '../../../theme/ThemeContext';
5
+
6
+ interface XButtonProps {
7
+ onPress: () => void;
8
+ color?: string;
9
+ size?: number;
10
+ }
11
+
12
+ export default function XButton({ onPress, color = Colors.white, size = 24 }: XButtonProps) {
13
+ return (
14
+ <Button onPress={onPress} style={styles.button}>
15
+ <Text style={[styles.text, { color, fontSize: size }]}>x</Text>
16
+ </Button>
17
+ );
18
+ }
19
+
20
+ const styles = StyleSheet.create({
21
+ button: {
22
+ padding: 8,
23
+ },
24
+ text: {
25
+ fontWeight: '300',
26
+ },
27
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Carousel components.
3
+ * Ported from squad-demo/src/components/ux/carousel/.
4
+ */
5
+ import React, { useCallback, useRef, useState } from 'react';
6
+ import {
7
+ View, FlatList, StyleSheet, Dimensions, Pressable, Text,
8
+ type NativeSyntheticEvent, type NativeScrollEvent,
9
+ } from 'react-native';
10
+ import { Colors } from '../../../theme/ThemeContext';
11
+
12
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
13
+
14
+ // --- Pagination Dot ---
15
+
16
+ export function PaginationDot({ active, color = Colors.white }: { active: boolean; color?: string }) {
17
+ return (
18
+ <View style={[styles.dot, active ? { backgroundColor: color } : styles.dotInactive]} />
19
+ );
20
+ }
21
+
22
+ // --- Carousel Pagination ---
23
+
24
+ export function CarouselPagination({ count, activeIndex, color }: { count: number; activeIndex: number; color?: string }) {
25
+ return (
26
+ <View style={styles.pagination} accessibilityRole="tablist">
27
+ {Array.from({ length: count }, (_, i) => (
28
+ <PaginationDot key={i} active={i === activeIndex} color={color} />
29
+ ))}
30
+ </View>
31
+ );
32
+ }
33
+
34
+ // --- Carousel Button ---
35
+
36
+ export function CarouselButton({ direction, onPress }: { direction: 'left' | 'right'; onPress: () => void }) {
37
+ return (
38
+ <Pressable
39
+ style={[styles.navButton, direction === 'left' ? styles.navLeft : styles.navRight]}
40
+ onPress={onPress}
41
+ accessibilityRole="button"
42
+ accessibilityLabel={direction === 'left' ? 'Previous' : 'Next'}
43
+ >
44
+ <Text style={styles.navText}>{direction === 'left' ? '<' : '>'}</Text>
45
+ </Pressable>
46
+ );
47
+ }
48
+
49
+ // --- Carousel Item ---
50
+
51
+ export function CarouselItem({ children, width }: { children: React.ReactNode; width?: number }) {
52
+ return <View style={[styles.item, { width: width ?? SCREEN_WIDTH - 48 }]}>{children}</View>;
53
+ }
54
+
55
+ // --- Main Carousel ---
56
+
57
+ interface CarouselProps {
58
+ data: unknown[];
59
+ renderItem: (item: unknown, index: number) => React.ReactNode;
60
+ itemWidth?: number;
61
+ showPagination?: boolean;
62
+ showNavButtons?: boolean;
63
+ paginationColor?: string;
64
+ }
65
+
66
+ export default function Carousel({
67
+ data,
68
+ renderItem,
69
+ itemWidth = SCREEN_WIDTH - 48,
70
+ showPagination = true,
71
+ showNavButtons = false,
72
+ paginationColor,
73
+ }: CarouselProps) {
74
+ const [activeIndex, setActiveIndex] = useState(0);
75
+ const flatListRef = useRef<FlatList>(null);
76
+
77
+ const handleScroll = useCallback(
78
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
79
+ const offset = event.nativeEvent.contentOffset.x;
80
+ const index = Math.round(offset / itemWidth);
81
+ setActiveIndex(index);
82
+ },
83
+ [itemWidth],
84
+ );
85
+
86
+ const scrollTo = useCallback(
87
+ (index: number) => {
88
+ const clamped = Math.max(0, Math.min(index, data.length - 1));
89
+ flatListRef.current?.scrollToOffset({ offset: clamped * itemWidth, animated: true });
90
+ setActiveIndex(clamped);
91
+ },
92
+ [data.length, itemWidth],
93
+ );
94
+
95
+ return (
96
+ <View>
97
+ <FlatList
98
+ ref={flatListRef}
99
+ data={data}
100
+ horizontal
101
+ pagingEnabled
102
+ showsHorizontalScrollIndicator={false}
103
+ onScroll={handleScroll}
104
+ scrollEventThrottle={16}
105
+ snapToInterval={itemWidth}
106
+ decelerationRate="fast"
107
+ keyExtractor={(_, i) => String(i)}
108
+ renderItem={({ item, index }) => (
109
+ <View style={{ width: itemWidth }}>{renderItem(item, index)}</View>
110
+ )}
111
+ />
112
+ {showNavButtons && data.length > 1 && (
113
+ <>
114
+ <CarouselButton direction="left" onPress={() => scrollTo(activeIndex - 1)} />
115
+ <CarouselButton direction="right" onPress={() => scrollTo(activeIndex + 1)} />
116
+ </>
117
+ )}
118
+ {showPagination && data.length > 1 && (
119
+ <CarouselPagination count={data.length} activeIndex={activeIndex} color={paginationColor} />
120
+ )}
121
+ </View>
122
+ );
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ dot: { width: 8, height: 8, borderRadius: 4, marginHorizontal: 3 },
127
+ dotInactive: { backgroundColor: 'rgba(255,255,255,0.3)' },
128
+ pagination: { flexDirection: 'row', justifyContent: 'center', paddingVertical: 12 },
129
+ navButton: { position: 'absolute', top: '40%', width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center', zIndex: 10 },
130
+ navLeft: { left: 4 },
131
+ navRight: { right: 4 },
132
+ navText: { color: Colors.white, fontSize: 18, fontWeight: '600' },
133
+ item: { justifyContent: 'center', alignItems: 'center' },
134
+ });