@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,232 @@
1
+ /**
2
+ * Audio recording components — full recording stack.
3
+ * Ported from squad-demo/src/components/audio/recording-footer/.
4
+ * Components: RecordButton, PlaybackButton, SendButton, RecordingTimer, RecordingFooter.
5
+ */
6
+ import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
7
+ import { View, Text, StyleSheet, Pressable, Animated } from 'react-native';
8
+ import { Audio } from 'expo-av';
9
+ import { Colors } from '../../../theme/ThemeContext';
10
+
11
+ // --- RecordButton ---
12
+ export function RecordButton({
13
+ isRecording,
14
+ onPressIn,
15
+ onPressOut,
16
+ color = Colors.purple1,
17
+ }: { isRecording: boolean; onPressIn: () => void; onPressOut: () => void; color?: string }) {
18
+ const pulseAnim = useRef(new Animated.Value(1)).current;
19
+
20
+ useEffect(() => {
21
+ if (isRecording) {
22
+ Animated.loop(Animated.sequence([
23
+ Animated.timing(pulseAnim, { toValue: 1.15, duration: 800, useNativeDriver: true }),
24
+ Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true }),
25
+ ])).start();
26
+ } else {
27
+ pulseAnim.stopAnimation();
28
+ pulseAnim.setValue(1);
29
+ }
30
+ }, [isRecording, pulseAnim]);
31
+
32
+ return (
33
+ <Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
34
+ <Pressable
35
+ onPressIn={onPressIn}
36
+ onPressOut={onPressOut}
37
+ style={[styles.recordButton, isRecording && styles.recordButtonActive, { borderColor: isRecording ? Colors.red : color }]}
38
+ accessibilityRole="button"
39
+ accessibilityLabel={isRecording ? 'Stop recording' : 'Start recording'}
40
+ >
41
+ {isRecording ? (
42
+ <View style={styles.stopIcon} />
43
+ ) : (
44
+ <View style={[styles.micIcon, { backgroundColor: color }]} />
45
+ )}
46
+ </Pressable>
47
+ </Animated.View>
48
+ );
49
+ }
50
+
51
+ // --- PlaybackButton ---
52
+ export function PlaybackButton({
53
+ isPlaying,
54
+ onPress,
55
+ }: { isPlaying: boolean; onPress: () => void }) {
56
+ return (
57
+ <Pressable style={styles.playbackButton} onPress={onPress} accessibilityRole="button" accessibilityLabel={isPlaying ? 'Pause' : 'Play'}>
58
+ {isPlaying ? (
59
+ <View style={styles.pauseIcon}>
60
+ <View style={styles.pauseBar} />
61
+ <View style={styles.pauseBar} />
62
+ </View>
63
+ ) : (
64
+ <View style={styles.playIcon} />
65
+ )}
66
+ </Pressable>
67
+ );
68
+ }
69
+
70
+ // --- SendButton ---
71
+ export function SendButton({ onPress, disabled, color = Colors.purple1 }: { onPress: () => void; disabled?: boolean; color?: string }) {
72
+ return (
73
+ <Pressable
74
+ style={[styles.sendButton, { backgroundColor: disabled ? Colors.gray3 : color }]}
75
+ onPress={onPress}
76
+ disabled={disabled}
77
+ accessibilityRole="button"
78
+ accessibilityLabel="Send"
79
+ >
80
+ <Text style={[styles.sendIcon, { color: disabled ? Colors.gray6 : Colors.gray1 }]}>{'>'}</Text>
81
+ </Pressable>
82
+ );
83
+ }
84
+
85
+ // --- RecordingTimer ---
86
+ export function RecordingTimer({ seconds }: { seconds: number }) {
87
+ const m = Math.floor(seconds / 60);
88
+ const s = seconds % 60;
89
+ return (
90
+ <Text style={styles.timer} accessibilityLabel={`Recording: ${m} minutes ${s} seconds`}>
91
+ {m}:{s.toString().padStart(2, '0')}
92
+ </Text>
93
+ );
94
+ }
95
+
96
+ // --- Waveform ---
97
+ export function Waveform({ levels, progress = 0, color = Colors.white }: { levels?: number[]; progress?: number; color?: string }) {
98
+ const bars = levels ?? Array.from({ length: 30 }, () => Math.random());
99
+ const filledBars = Math.floor(bars.length * progress);
100
+
101
+ return (
102
+ <View style={styles.waveformContainer}>
103
+ {bars.map((level, i) => (
104
+ <View
105
+ key={i}
106
+ style={[
107
+ styles.waveformBar,
108
+ { height: 4 + level * 20, backgroundColor: i < filledBars ? color : Colors.gray5 },
109
+ ]}
110
+ />
111
+ ))}
112
+ </View>
113
+ );
114
+ }
115
+
116
+ // --- RecordingFooter ---
117
+ export interface RecordingFooterProps {
118
+ onRecordingComplete: (uri: string, durationMs: number) => void;
119
+ onCancel?: () => void;
120
+ primaryColor?: string;
121
+ disabled?: boolean;
122
+ }
123
+
124
+ export function RecordingFooter({ onRecordingComplete, onCancel, primaryColor = Colors.purple1, disabled }: RecordingFooterProps) {
125
+ const [state, setState] = useState<'idle' | 'recording' | 'preview'>('idle');
126
+ const [seconds, setSeconds] = useState(0);
127
+ const [recordingUri, setRecordingUri] = useState<string | null>(null);
128
+ const recordingRef = useRef<Audio.Recording | null>(null);
129
+ const timerRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
130
+
131
+ const startRecording = useCallback(async () => {
132
+ try {
133
+ const { status } = await Audio.requestPermissionsAsync();
134
+ if (status !== 'granted') return;
135
+ await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
136
+ const recording = new Audio.Recording();
137
+ await recording.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
138
+ await recording.startAsync();
139
+ recordingRef.current = recording;
140
+ setState('recording');
141
+ setSeconds(0);
142
+ timerRef.current = setInterval(() => setSeconds(s => s + 1), 1000);
143
+ } catch {}
144
+ }, []);
145
+
146
+ const stopRecording = useCallback(async () => {
147
+ if (timerRef.current) clearInterval(timerRef.current);
148
+ try {
149
+ if (recordingRef.current) {
150
+ await recordingRef.current.stopAndUnloadAsync();
151
+ const uri = recordingRef.current.getURI();
152
+ setRecordingUri(uri);
153
+ recordingRef.current = null;
154
+ setState('preview');
155
+ }
156
+ } catch { setState('idle'); }
157
+ }, []);
158
+
159
+ const handleSend = useCallback(() => {
160
+ if (recordingUri) {
161
+ onRecordingComplete(recordingUri, seconds * 1000);
162
+ setRecordingUri(null);
163
+ setState('idle');
164
+ setSeconds(0);
165
+ }
166
+ }, [recordingUri, seconds, onRecordingComplete]);
167
+
168
+ const handleCancel = useCallback(() => {
169
+ setRecordingUri(null);
170
+ setState('idle');
171
+ setSeconds(0);
172
+ onCancel?.();
173
+ }, [onCancel]);
174
+
175
+ if (disabled) return null;
176
+
177
+ return (
178
+ <View style={styles.footer}>
179
+ {state === 'idle' && (
180
+ <RecordButton isRecording={false} onPressIn={startRecording} onPressOut={() => {}} color={primaryColor} />
181
+ )}
182
+ {state === 'recording' && (
183
+ <View style={styles.recordingRow}>
184
+ <Pressable onPress={handleCancel} style={styles.cancelButton}>
185
+ <Text style={styles.cancelText}>x</Text>
186
+ </Pressable>
187
+ <View style={styles.recordingCenter}>
188
+ <View style={styles.recordingDot} />
189
+ <RecordingTimer seconds={seconds} />
190
+ </View>
191
+ <Pressable onPress={stopRecording} style={[styles.stopButton, { backgroundColor: primaryColor }]}>
192
+ <View style={styles.stopIconSmall} />
193
+ </Pressable>
194
+ </View>
195
+ )}
196
+ {state === 'preview' && (
197
+ <View style={styles.previewRow}>
198
+ <Pressable onPress={handleCancel} style={styles.cancelButton}>
199
+ <Text style={styles.cancelText}>x</Text>
200
+ </Pressable>
201
+ <RecordingTimer seconds={seconds} />
202
+ <SendButton onPress={handleSend} color={primaryColor} />
203
+ </View>
204
+ )}
205
+ </View>
206
+ );
207
+ }
208
+
209
+ const styles = StyleSheet.create({
210
+ recordButton: { width: 64, height: 64, borderRadius: 32, borderWidth: 3, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(110,130,231,0.1)' },
211
+ recordButtonActive: { backgroundColor: 'rgba(255,68,120,0.1)' },
212
+ micIcon: { width: 18, height: 28, borderRadius: 9 },
213
+ stopIcon: { width: 20, height: 20, borderRadius: 4, backgroundColor: Colors.red },
214
+ playbackButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: Colors.white, justifyContent: 'center', alignItems: 'center' },
215
+ pauseIcon: { flexDirection: 'row', gap: 3 },
216
+ pauseBar: { width: 3, height: 14, backgroundColor: Colors.gray1, borderRadius: 1 },
217
+ playIcon: { width: 0, height: 0, borderLeftWidth: 10, borderTopWidth: 6, borderBottomWidth: 6, borderLeftColor: Colors.gray1, borderTopColor: 'transparent', borderBottomColor: 'transparent', marginLeft: 2 },
218
+ sendButton: { width: 44, height: 44, borderRadius: 22, justifyContent: 'center', alignItems: 'center' },
219
+ sendIcon: { fontSize: 18, fontWeight: '700' },
220
+ timer: { color: Colors.white, fontSize: 16, fontVariant: ['tabular-nums'], fontWeight: '300' },
221
+ waveformContainer: { flexDirection: 'row', alignItems: 'center', gap: 2, height: 24 },
222
+ waveformBar: { width: 3, borderRadius: 1.5 },
223
+ footer: { padding: 16, alignItems: 'center' },
224
+ recordingRow: { flexDirection: 'row', alignItems: 'center', gap: 16, width: '100%' },
225
+ recordingCenter: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, justifyContent: 'center' },
226
+ recordingDot: { width: 10, height: 10, borderRadius: 5, backgroundColor: Colors.red },
227
+ cancelButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: Colors.gray3, justifyContent: 'center', alignItems: 'center' },
228
+ cancelText: { color: Colors.white, fontSize: 18, fontWeight: '300' },
229
+ stopButton: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center' },
230
+ stopIconSmall: { width: 14, height: 14, borderRadius: 2, backgroundColor: Colors.white },
231
+ previewRow: { flexDirection: 'row', alignItems: 'center', gap: 16, width: '100%', justifyContent: 'space-between' },
232
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Community components.
3
+ * Ported from squad-demo/src/components/communities/*.
4
+ */
5
+ import React from 'react';
6
+ import { View, Text, StyleSheet, Pressable, FlatList } from 'react-native';
7
+ import { Colors } from '../../theme/ThemeContext';
8
+ import { BodyRegular, BodySmall, SubtitleSmall } from '../ux/text/Typography';
9
+
10
+ // --- CommunityRow ---
11
+ export function CommunityRow({
12
+ id, name, color, isSelected, onPress,
13
+ }: { id: string; name: string; color?: string; isSelected?: boolean; onPress?: () => void }) {
14
+ const dotColor = color ?? Colors.purple1;
15
+ return (
16
+ <Pressable style={[styles.row, isSelected && [styles.rowSelected, { borderColor: dotColor }]]} onPress={onPress}>
17
+ <View style={[styles.dot, { backgroundColor: dotColor }]} />
18
+ <BodyRegular style={styles.rowName}>{name}</BodyRegular>
19
+ {isSelected && <Text style={[styles.check, { color: dotColor }]}>v</Text>}
20
+ </Pressable>
21
+ );
22
+ }
23
+
24
+ // --- CommunityTag ---
25
+ export function CommunityTag({ name, color = Colors.purple1 }: { name: string; color?: string }) {
26
+ return (
27
+ <View style={[styles.tag, { backgroundColor: color }]}>
28
+ <Text style={styles.tagText}>{name.substring(0, 3).toUpperCase()}</Text>
29
+ </View>
30
+ );
31
+ }
32
+
33
+ // --- CommunityBottomSheet ---
34
+ export function CommunityBottomSheet({
35
+ communities, selectedId, onSelect,
36
+ }: { communities: Array<{ id: string; name: string; color?: string }>; selectedId?: string; onSelect: (id: string) => void }) {
37
+ return (
38
+ <FlatList
39
+ data={communities}
40
+ keyExtractor={item => item.id}
41
+ renderItem={({ item }) => (
42
+ <CommunityRow
43
+ id={item.id}
44
+ name={item.name}
45
+ color={item.color}
46
+ isSelected={item.id === selectedId}
47
+ onPress={() => onSelect(item.id)}
48
+ />
49
+ )}
50
+ contentContainerStyle={styles.list}
51
+ />
52
+ );
53
+ }
54
+
55
+ // --- OnboardingCommunitySelector ---
56
+ export function OnboardingCommunitySelector({
57
+ communities, selectedId, onSelect,
58
+ }: { communities: Array<{ id: string; name: string; color?: string }>; selectedId?: string; onSelect: (id: string) => void }) {
59
+ return (
60
+ <View style={styles.selectorContainer}>
61
+ {communities.map(c => (
62
+ <CommunityRow key={c.id} {...c} isSelected={c.id === selectedId} onPress={() => onSelect(c.id)} />
63
+ ))}
64
+ </View>
65
+ );
66
+ }
67
+
68
+ const styles = StyleSheet.create({
69
+ row: { flexDirection: 'row', alignItems: 'center', padding: 16, backgroundColor: Colors.gray2, borderRadius: 12, borderWidth: 2, borderColor: 'transparent', marginBottom: 8 },
70
+ rowSelected: { backgroundColor: 'rgba(110,130,231,0.1)' },
71
+ dot: { width: 32, height: 32, borderRadius: 16, marginRight: 12 },
72
+ rowName: { color: Colors.white, flex: 1, fontWeight: '600' },
73
+ check: { fontSize: 18, fontWeight: '700' },
74
+ tag: { borderRadius: 6, paddingHorizontal: 6, paddingVertical: 2 },
75
+ tagText: { color: Colors.white, fontSize: 8, fontWeight: '800' },
76
+ list: { padding: 16 },
77
+ selectorContainer: { gap: 8 },
78
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * All dialog components.
3
+ * Ported from squad-demo/src/components/dialogs/*.
4
+ * 17 dialog types: permission dialogs, confirmations, introductions.
5
+ */
6
+ import React from 'react';
7
+ import { View, Text, StyleSheet, Pressable, Linking } from 'react-native';
8
+ import { Colors } from '../../theme/ThemeContext';
9
+ import Button from '../ux/buttons/Button';
10
+ import { TitleSmall, BodyRegular } from '../ux/text/Typography';
11
+
12
+ // --- Base Dialog ---
13
+ interface BaseDialogProps {
14
+ visible: boolean;
15
+ title: string;
16
+ message: string;
17
+ icon?: string;
18
+ confirmLabel?: string;
19
+ cancelLabel?: string;
20
+ destructive?: boolean;
21
+ onConfirm: () => void;
22
+ onCancel: () => void;
23
+ }
24
+
25
+ function BaseDialog({ visible, title, message, icon, confirmLabel = 'OK', cancelLabel = 'Cancel', destructive, onConfirm, onCancel }: BaseDialogProps) {
26
+ if (!visible) return null;
27
+ return (
28
+ <View style={styles.overlay} accessibilityRole="alert">
29
+ <View style={styles.dialog}>
30
+ {icon && <View style={styles.iconCircle}><Text style={styles.icon}>{icon}</Text></View>}
31
+ <TitleSmall style={styles.title}>{title}</TitleSmall>
32
+ <BodyRegular style={styles.message}>{message}</BodyRegular>
33
+ <View style={styles.buttons}>
34
+ <Button style={[styles.confirmBtn, destructive && styles.destructiveBtn]} onPress={onConfirm}>
35
+ <Text style={[styles.confirmText, destructive && styles.destructiveText]}>{confirmLabel}</Text>
36
+ </Button>
37
+ <Button style={styles.cancelBtn} onPress={onCancel}>
38
+ <Text style={styles.cancelText}>{cancelLabel}</Text>
39
+ </Button>
40
+ </View>
41
+ </View>
42
+ </View>
43
+ );
44
+ }
45
+
46
+ // --- Permission Dialogs ---
47
+ export function MicrophonePermissionDialog({ visible, onAllow, onDeny }: { visible: boolean; onAllow: () => void; onDeny: () => void }) {
48
+ return <BaseDialog visible={visible} title="Microphone Access" message="Squad needs microphone access to record freestyles and voice messages." icon="M" confirmLabel="Allow" onConfirm={onAllow} onCancel={onDeny} />;
49
+ }
50
+
51
+ export function CameraPermissionDialog({ visible, onAllow, onDeny }: { visible: boolean; onAllow: () => void; onDeny: () => void }) {
52
+ return <BaseDialog visible={visible} title="Camera Access" message="Squad needs camera access to take profile photos." icon="C" confirmLabel="Allow" onConfirm={onAllow} onCancel={onDeny} />;
53
+ }
54
+
55
+ export function ImagesPermissionDialog({ visible, onAllow, onDeny }: { visible: boolean; onAllow: () => void; onDeny: () => void }) {
56
+ return <BaseDialog visible={visible} title="Photo Library" message="Squad needs access to your photos for your profile picture." icon="I" confirmLabel="Allow" onConfirm={onAllow} onCancel={onDeny} />;
57
+ }
58
+
59
+ export function ContactPermissionDialog({ visible, onAllow, onDeny }: { visible: boolean; onAllow: () => void; onDeny: () => void }) {
60
+ return <BaseDialog visible={visible} title="Contacts Access" message="Squad can find your friends who are already on the app." icon="P" confirmLabel="Allow" onConfirm={onAllow} onCancel={onDeny} />;
61
+ }
62
+
63
+ export function NotificationsPermissionDialog({ visible, onAllow, onDeny }: { visible: boolean; onAllow: () => void; onDeny: () => void }) {
64
+ return <BaseDialog visible={visible} title="Notifications" message="Get notified when you receive messages, calls, and squad updates." icon="N" confirmLabel="Allow" onConfirm={onAllow} onCancel={onDeny} />;
65
+ }
66
+
67
+ // --- Confirmation Dialogs ---
68
+ export function DeleteConfirmationDialog({ visible, onConfirm, onCancel }: { visible: boolean; onConfirm: () => void; onCancel: () => void }) {
69
+ return <BaseDialog visible={visible} title="Delete" message="Are you sure? This action cannot be undone." confirmLabel="Delete" destructive onConfirm={onConfirm} onCancel={onCancel} />;
70
+ }
71
+
72
+ export function BlockConfirmationDialog({ visible, userName, onConfirm, onCancel }: { visible: boolean; userName: string; onConfirm: () => void; onCancel: () => void }) {
73
+ return <BaseDialog visible={visible} title="Block User" message={`Are you sure you want to block ${userName}? They won't be able to contact you.`} confirmLabel="Block" destructive onConfirm={onConfirm} onCancel={onCancel} />;
74
+ }
75
+
76
+ export function UnblockConfirmationDialog({ visible, userName, onConfirm, onCancel }: { visible: boolean; userName: string; onConfirm: () => void; onCancel: () => void }) {
77
+ return <BaseDialog visible={visible} title="Unblock User" message={`Unblock ${userName}?`} confirmLabel="Unblock" onConfirm={onConfirm} onCancel={onCancel} />;
78
+ }
79
+
80
+ export function FlagConfirmationDialog({ visible, onConfirm, onCancel }: { visible: boolean; onConfirm: () => void; onCancel: () => void }) {
81
+ return <BaseDialog visible={visible} title="Report Content" message="Are you sure you want to report this content? Our team will review it." confirmLabel="Report" destructive onConfirm={onConfirm} onCancel={onCancel} />;
82
+ }
83
+
84
+ export function RemoveFromSquadDialog({ visible, userName, onConfirm, onCancel }: { visible: boolean; userName: string; onConfirm: () => void; onCancel: () => void }) {
85
+ return <BaseDialog visible={visible} title="Remove from Squad" message={`Remove ${userName} from your squad?`} confirmLabel="Remove" destructive onConfirm={onConfirm} onCancel={onCancel} />;
86
+ }
87
+
88
+ export function NoConnectionDialog({ visible, onDismiss }: { visible: boolean; onDismiss: () => void }) {
89
+ return <BaseDialog visible={visible} title="No Connection" message="Please check your internet connection and try again." confirmLabel="OK" onConfirm={onDismiss} onCancel={onDismiss} />;
90
+ }
91
+
92
+ // --- Special Dialogs ---
93
+ export function VersionUpgradeDialog({ visible, onUpdate, onDismiss, hardLock = false }: { visible: boolean; onUpdate: () => void; onDismiss: () => void; hardLock?: boolean }) {
94
+ return <BaseDialog visible={visible} title="Update Available" message={hardLock ? "A required update is available. Please update to continue." : "A new version is available with improvements."} confirmLabel="Update" cancelLabel={hardLock ? "" : "Later"} onConfirm={onUpdate} onCancel={onDismiss} />;
95
+ }
96
+
97
+ export function CollectEmailDialog({ visible, onSubmit, onDismiss }: { visible: boolean; onSubmit: (email: string) => void; onDismiss: () => void }) {
98
+ return <BaseDialog visible={visible} title="Stay Connected" message="Enter your email to receive important updates about your squad." confirmLabel="Submit" onConfirm={() => onSubmit('')} onCancel={onDismiss} />;
99
+ }
100
+
101
+ export function ProgressCongratulationDialog({ visible, title, message, onDismiss }: { visible: boolean; title: string; message: string; onDismiss: () => void }) {
102
+ return <BaseDialog visible={visible} title={title} message={message} confirmLabel="Awesome!" onConfirm={onDismiss} onCancel={onDismiss} />;
103
+ }
104
+
105
+ export function SquadLineInvitationDialog({ visible, callerName, onAccept, onDecline }: { visible: boolean; callerName: string; onAccept: () => void; onDecline: () => void }) {
106
+ return <BaseDialog visible={visible} title="Squad Line" message={`${callerName} is inviting you to a call`} confirmLabel="Accept" cancelLabel="Decline" onConfirm={onAccept} onCancel={onDecline} />;
107
+ }
108
+
109
+ const styles = StyleSheet.create({
110
+ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, padding: 32 },
111
+ dialog: { backgroundColor: Colors.gray2, borderRadius: 20, padding: 32, width: '100%', maxWidth: 340, alignItems: 'center' },
112
+ iconCircle: { width: 56, height: 56, borderRadius: 28, backgroundColor: Colors.purple1, justifyContent: 'center', alignItems: 'center', marginBottom: 16 },
113
+ icon: { color: Colors.white, fontSize: 24, fontWeight: '700' },
114
+ title: { color: Colors.white, marginBottom: 8, textAlign: 'center' },
115
+ message: { color: Colors.gray6, textAlign: 'center', marginBottom: 24 },
116
+ buttons: { width: '100%', gap: 8 },
117
+ confirmBtn: { height: 48, borderRadius: 24, backgroundColor: Colors.purple1, justifyContent: 'center', alignItems: 'center' },
118
+ confirmText: { color: Colors.gray1, fontSize: 15, fontWeight: '600' },
119
+ destructiveBtn: { backgroundColor: Colors.red },
120
+ destructiveText: { color: Colors.white },
121
+ cancelBtn: { height: 48, justifyContent: 'center', alignItems: 'center' },
122
+ cancelText: { color: Colors.gray6, fontSize: 15 },
123
+ });
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { Colors } from '../../theme/ThemeContext';
4
+ import Button from '../ux/buttons/Button';
5
+ import { TitleSmall, BodyRegular } from '../ux/text/Typography';
6
+
7
+ interface ConfirmDialogProps {
8
+ visible: boolean;
9
+ title: string;
10
+ message: string;
11
+ confirmLabel?: string;
12
+ cancelLabel?: string;
13
+ destructive?: boolean;
14
+ onConfirm: () => void;
15
+ onCancel: () => void;
16
+ }
17
+
18
+ export function ConfirmDialog({
19
+ visible,
20
+ title,
21
+ message,
22
+ confirmLabel = 'Confirm',
23
+ cancelLabel = 'Cancel',
24
+ destructive = false,
25
+ onConfirm,
26
+ onCancel,
27
+ }: ConfirmDialogProps) {
28
+ if (!visible) return null;
29
+
30
+ return (
31
+ <View style={styles.overlay}>
32
+ <View style={styles.dialog}>
33
+ <TitleSmall style={styles.title}>{title}</TitleSmall>
34
+ <BodyRegular style={styles.message}>{message}</BodyRegular>
35
+
36
+ <View style={styles.buttons}>
37
+ <Button
38
+ style={[styles.confirmButton, destructive && styles.destructiveButton]}
39
+ onPress={onConfirm}
40
+ >
41
+ <Text style={[styles.confirmText, destructive && styles.destructiveText]}>
42
+ {confirmLabel}
43
+ </Text>
44
+ </Button>
45
+ <Button style={styles.cancelButton} onPress={onCancel}>
46
+ <Text style={styles.cancelText}>{cancelLabel}</Text>
47
+ </Button>
48
+ </View>
49
+ </View>
50
+ </View>
51
+ );
52
+ }
53
+
54
+ const styles = StyleSheet.create({
55
+ overlay: {
56
+ ...StyleSheet.absoluteFillObject,
57
+ backgroundColor: 'rgba(0,0,0,0.7)',
58
+ justifyContent: 'center', alignItems: 'center',
59
+ zIndex: 9999, padding: 32,
60
+ },
61
+ dialog: {
62
+ backgroundColor: Colors.gray2, borderRadius: 20,
63
+ padding: 32, width: '100%', maxWidth: 340, alignItems: 'center',
64
+ },
65
+ title: { color: Colors.white, marginBottom: 8, textAlign: 'center' },
66
+ message: { color: Colors.gray6, textAlign: 'center', marginBottom: 24 },
67
+ buttons: { width: '100%', gap: 8 },
68
+ confirmButton: {
69
+ height: 48, borderRadius: 24, backgroundColor: Colors.purple1,
70
+ justifyContent: 'center', alignItems: 'center',
71
+ },
72
+ confirmText: { color: Colors.gray1, fontSize: 15, fontWeight: '600' },
73
+ destructiveButton: { backgroundColor: Colors.red },
74
+ destructiveText: { color: Colors.white },
75
+ cancelButton: { height: 48, justifyContent: 'center', alignItems: 'center' },
76
+ cancelText: { color: Colors.gray6, fontSize: 15 },
77
+ });
@@ -0,0 +1,132 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, Linking, Platform } from 'react-native';
3
+ import { Colors } from '../../theme/ThemeContext';
4
+ import Button from '../ux/buttons/Button';
5
+ import { TitleSmall, BodyRegular } from '../ux/text/Typography';
6
+
7
+ export type PermissionType = 'microphone' | 'camera' | 'contacts' | 'notifications' | 'photos';
8
+
9
+ interface PermissionDialogProps {
10
+ type: PermissionType;
11
+ visible: boolean;
12
+ onAllow: () => void;
13
+ onDeny: () => void;
14
+ isDenied?: boolean;
15
+ }
16
+
17
+ const PERMISSION_CONFIG: Record<PermissionType, { icon: string; title: string; description: string }> = {
18
+ microphone: {
19
+ icon: 'M',
20
+ title: 'Microphone Access',
21
+ description: 'Squad needs microphone access to record freestyles and voice messages.',
22
+ },
23
+ camera: {
24
+ icon: 'C',
25
+ title: 'Camera Access',
26
+ description: 'Squad needs camera access to take profile photos.',
27
+ },
28
+ contacts: {
29
+ icon: 'P',
30
+ title: 'Contacts Access',
31
+ description: 'Squad can find your friends who are already on the app.',
32
+ },
33
+ notifications: {
34
+ icon: 'N',
35
+ title: 'Notifications',
36
+ description: 'Get notified when you receive messages, calls, and squad updates.',
37
+ },
38
+ photos: {
39
+ icon: 'I',
40
+ title: 'Photo Library',
41
+ description: 'Squad needs access to your photos to set your profile picture.',
42
+ },
43
+ };
44
+
45
+ export function PermissionDialog({
46
+ type,
47
+ visible,
48
+ onAllow,
49
+ onDeny,
50
+ isDenied = false,
51
+ }: PermissionDialogProps) {
52
+ if (!visible) return null;
53
+
54
+ const config = PERMISSION_CONFIG[type];
55
+
56
+ return (
57
+ <View style={styles.overlay}>
58
+ <View style={styles.dialog}>
59
+ <View style={styles.iconContainer}>
60
+ <Text style={styles.icon}>{config.icon}</Text>
61
+ </View>
62
+
63
+ <TitleSmall style={styles.title}>{config.title}</TitleSmall>
64
+ <BodyRegular style={styles.description}>{config.description}</BodyRegular>
65
+
66
+ <View style={styles.buttons}>
67
+ {isDenied ? (
68
+ <>
69
+ <Button style={styles.settingsButton} onPress={() => Linking.openSettings()}>
70
+ <Text style={styles.settingsText}>Open Settings</Text>
71
+ </Button>
72
+ <Button style={styles.laterButton} onPress={onDeny}>
73
+ <Text style={styles.laterText}>Not Now</Text>
74
+ </Button>
75
+ </>
76
+ ) : (
77
+ <>
78
+ <Button style={styles.allowButton} onPress={onAllow}>
79
+ <Text style={styles.allowText}>Allow</Text>
80
+ </Button>
81
+ <Button style={styles.laterButton} onPress={onDeny}>
82
+ <Text style={styles.laterText}>Not Now</Text>
83
+ </Button>
84
+ </>
85
+ )}
86
+ </View>
87
+ </View>
88
+ </View>
89
+ );
90
+ }
91
+
92
+ const styles = StyleSheet.create({
93
+ overlay: {
94
+ ...StyleSheet.absoluteFillObject,
95
+ backgroundColor: 'rgba(0,0,0,0.7)',
96
+ justifyContent: 'center',
97
+ alignItems: 'center',
98
+ zIndex: 9999,
99
+ padding: 32,
100
+ },
101
+ dialog: {
102
+ backgroundColor: Colors.gray2,
103
+ borderRadius: 20,
104
+ padding: 32,
105
+ width: '100%',
106
+ maxWidth: 340,
107
+ alignItems: 'center',
108
+ },
109
+ iconContainer: {
110
+ width: 64, height: 64, borderRadius: 32,
111
+ backgroundColor: Colors.purple1, justifyContent: 'center', alignItems: 'center',
112
+ marginBottom: 20,
113
+ },
114
+ icon: { color: Colors.white, fontSize: 28, fontWeight: '700' },
115
+ title: { color: Colors.white, marginBottom: 8, textAlign: 'center' },
116
+ description: { color: Colors.gray6, textAlign: 'center', marginBottom: 24 },
117
+ buttons: { width: '100%', gap: 8 },
118
+ allowButton: {
119
+ height: 48, borderRadius: 24, backgroundColor: Colors.purple1,
120
+ justifyContent: 'center', alignItems: 'center',
121
+ },
122
+ allowText: { color: Colors.gray1, fontSize: 15, fontWeight: '600' },
123
+ settingsButton: {
124
+ height: 48, borderRadius: 24, backgroundColor: Colors.purple1,
125
+ justifyContent: 'center', alignItems: 'center',
126
+ },
127
+ settingsText: { color: Colors.gray1, fontSize: 15, fontWeight: '600' },
128
+ laterButton: {
129
+ height: 48, justifyContent: 'center', alignItems: 'center',
130
+ },
131
+ laterText: { color: Colors.gray6, fontSize: 15 },
132
+ });