@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,234 @@
1
+ import React, { useCallback, useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ StyleSheet,
7
+ Pressable,
8
+ Linking,
9
+ useWindowDimensions,
10
+ } from 'react-native';
11
+ import { useNavigation } from '@react-navigation/native';
12
+ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
13
+
14
+ import type { RootStackParamList } from '../../navigation/SquadNavigator';
15
+ import { useSquadSDK } from '../../SquadProvider';
16
+ import { Colors } from '../../theme/ThemeContext';
17
+ import { ErrorHint } from '../../components/ux/errors/ErrorHint';
18
+ import Button from '../../components/ux/buttons/Button';
19
+ import AvoidKeyboardScreen from '../../components/ux/layout/AvoidKeyboardScreen';
20
+ import { TitleRegular, BodyMedium, SubtitleSmall } from '../../components/ux/text/Typography';
21
+
22
+ type Nav = NativeStackNavigationProp<RootStackParamList, 'EnterEmail'>;
23
+
24
+ function validateEmail(email: string): boolean {
25
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
26
+ }
27
+
28
+ export function EnterEmailScreen() {
29
+ const navigation = useNavigation<Nav>();
30
+ const sdk = useSquadSDK();
31
+ const { height } = useWindowDimensions();
32
+ const isShort = height < 700;
33
+
34
+ const [email, setEmail] = useState('');
35
+ const [firstName, setFirstName] = useState('');
36
+ const [error, setError] = useState<string | null>(null);
37
+ const [isLoading, setIsLoading] = useState(false);
38
+ const inputRef = useRef<TextInput>(null);
39
+
40
+ const buttonDisabled = !email || !firstName || isLoading;
41
+
42
+ const onChangeEmail = useCallback((value: string) => {
43
+ setError(null);
44
+ setEmail(value);
45
+ }, []);
46
+
47
+ const onSubmit = useCallback(async () => {
48
+ if (!validateEmail(email)) {
49
+ setError('Please enter a valid email address');
50
+ return;
51
+ }
52
+
53
+ setIsLoading(true);
54
+ setError(null);
55
+
56
+ try {
57
+ const result = await sdk.sessionManager.createSession({
58
+ email,
59
+ firstName,
60
+ });
61
+
62
+ if (result.status === 'pending') {
63
+ navigation.navigate('EnterCode', { email });
64
+ } else {
65
+ setError(result.error ?? 'Failed to send verification code');
66
+ }
67
+ } catch (err) {
68
+ setError('An error occurred. Please try again.');
69
+ } finally {
70
+ setIsLoading(false);
71
+ }
72
+ }, [email, firstName, sdk, navigation]);
73
+
74
+ return (
75
+ <AvoidKeyboardScreen>
76
+ <View style={[styles.headerContainer, isShort && styles.headerContainerShort]}>
77
+ <Pressable style={styles.backButton} onPress={() => navigation.goBack()}>
78
+ <Text style={styles.backButtonText}>{'<'}</Text>
79
+ </Pressable>
80
+ </View>
81
+
82
+ <View style={[styles.container, isShort && styles.containerShort]}>
83
+ <TitleRegular style={styles.title}>Create Your Account</TitleRegular>
84
+
85
+ <View style={styles.formContainer}>
86
+ <View style={styles.inputContainer}>
87
+ <TextInput
88
+ value={firstName}
89
+ onChangeText={setFirstName}
90
+ placeholder="First name"
91
+ placeholderTextColor={Colors.gray6}
92
+ style={styles.textInput}
93
+ autoCapitalize="words"
94
+ returnKeyType="next"
95
+ onSubmitEditing={() => inputRef.current?.focus()}
96
+ />
97
+ </View>
98
+ <View style={styles.inputContainer}>
99
+ <TextInput
100
+ ref={inputRef}
101
+ value={email}
102
+ onChangeText={onChangeEmail}
103
+ onSubmitEditing={onSubmit}
104
+ placeholder="Email address"
105
+ placeholderTextColor={Colors.gray6}
106
+ style={styles.textInput}
107
+ keyboardType="email-address"
108
+ autoCapitalize="none"
109
+ autoCorrect={false}
110
+ returnKeyType="go"
111
+ />
112
+ </View>
113
+ <ErrorHint hint={error} />
114
+ </View>
115
+
116
+ <View style={styles.footerContainer}>
117
+ <Button
118
+ style={[styles.button, buttonDisabled && styles.buttonDisabled]}
119
+ onPress={onSubmit}
120
+ disabled={buttonDisabled}
121
+ >
122
+ <Text style={[styles.buttonText, buttonDisabled && styles.buttonDisabledText]}>
123
+ {isLoading ? 'Sending...' : 'Send Code'}
124
+ </Text>
125
+ </Button>
126
+
127
+ <BodyMedium style={styles.privacyText}>
128
+ By tapping "Send Code" you agree to the{' '}
129
+ <Text
130
+ style={styles.privacyLink}
131
+ onPress={() => Linking.openURL('https://www.withyoursquad.com/terms')}
132
+ >
133
+ Terms & Conditions
134
+ </Text>
135
+ {' '}and{' '}
136
+ <Text
137
+ style={styles.privacyLink}
138
+ onPress={() => Linking.openURL('https://www.withyoursquad.com/privacy')}
139
+ >
140
+ Privacy Policy
141
+ </Text>
142
+ </BodyMedium>
143
+ </View>
144
+ </View>
145
+ </AvoidKeyboardScreen>
146
+ );
147
+ }
148
+
149
+ const styles = StyleSheet.create({
150
+ headerContainer: {
151
+ flexDirection: 'row',
152
+ alignItems: 'center',
153
+ paddingHorizontal: 20,
154
+ paddingTop: 20,
155
+ marginBottom: 60,
156
+ marginTop: 36,
157
+ },
158
+ headerContainerShort: {
159
+ marginBottom: 40,
160
+ marginTop: 24,
161
+ },
162
+ backButton: {
163
+ padding: 10,
164
+ },
165
+ backButtonText: {
166
+ fontSize: 24,
167
+ color: Colors.white,
168
+ fontWeight: '600',
169
+ },
170
+ container: {
171
+ flex: 1,
172
+ paddingBottom: 16,
173
+ paddingHorizontal: 24,
174
+ },
175
+ containerShort: {
176
+ paddingBottom: 8,
177
+ },
178
+ title: {
179
+ color: Colors.white,
180
+ marginBottom: 24,
181
+ },
182
+ formContainer: {
183
+ flex: 1,
184
+ width: '100%',
185
+ maxWidth: 400,
186
+ alignSelf: 'center',
187
+ },
188
+ inputContainer: {
189
+ marginBottom: 16,
190
+ },
191
+ textInput: {
192
+ color: Colors.white,
193
+ fontSize: 16,
194
+ fontWeight: '500',
195
+ paddingVertical: 16,
196
+ paddingHorizontal: 16,
197
+ borderWidth: 1,
198
+ borderColor: Colors.gray5,
199
+ borderRadius: 8,
200
+ backgroundColor: Colors.black,
201
+ },
202
+ footerContainer: {
203
+ width: '100%',
204
+ maxWidth: 400,
205
+ alignSelf: 'center',
206
+ },
207
+ button: {
208
+ alignItems: 'center',
209
+ backgroundColor: Colors.purple1,
210
+ borderRadius: 8,
211
+ height: 56,
212
+ justifyContent: 'center',
213
+ },
214
+ buttonDisabled: {
215
+ backgroundColor: Colors.gray2,
216
+ },
217
+ buttonText: {
218
+ fontSize: 16,
219
+ color: Colors.gray1,
220
+ fontWeight: '600',
221
+ },
222
+ buttonDisabledText: {
223
+ color: Colors.gray6,
224
+ },
225
+ privacyText: {
226
+ color: Colors.gray6,
227
+ marginTop: 16,
228
+ textAlign: 'center',
229
+ },
230
+ privacyLink: {
231
+ color: Colors.purple1,
232
+ fontWeight: '500',
233
+ },
234
+ });
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
3
+ import { useNavigation } from '@react-navigation/native';
4
+ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
5
+
6
+ import type { RootStackParamList } from '../../navigation/SquadNavigator';
7
+ import { useTheme } from '../../theme/ThemeContext';
8
+
9
+ type LandingNavProp = NativeStackNavigationProp<RootStackParamList, 'Landing'>;
10
+
11
+ export function LandingScreen() {
12
+ const navigation = useNavigation<LandingNavProp>();
13
+ const { theme, colors } = useTheme();
14
+
15
+ return (
16
+ <View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
17
+ <View style={styles.content}>
18
+ <Text style={[styles.title, { color: colors.white }]}>
19
+ Squad Sports
20
+ </Text>
21
+ <Text style={[styles.subtitle, { color: colors.gray6 }]}>
22
+ Connect with your squad
23
+ </Text>
24
+ </View>
25
+
26
+ <View style={styles.buttons}>
27
+ <TouchableOpacity
28
+ style={[styles.button, { backgroundColor: theme.buttonColor }]}
29
+ onPress={() => navigation.navigate('EnterEmail')}
30
+ >
31
+ <Text style={[styles.buttonText, { color: theme.buttonText }]}>
32
+ Get Started
33
+ </Text>
34
+ </TouchableOpacity>
35
+
36
+ <TouchableOpacity
37
+ style={[styles.secondaryButton]}
38
+ onPress={() => navigation.navigate('EnterEmail')}
39
+ >
40
+ <Text style={[styles.secondaryButtonText, { color: colors.white }]}>
41
+ I already have an account
42
+ </Text>
43
+ </TouchableOpacity>
44
+ </View>
45
+ </View>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ container: {
51
+ flex: 1,
52
+ justifyContent: 'space-between',
53
+ paddingHorizontal: 24,
54
+ paddingBottom: 48,
55
+ },
56
+ content: {
57
+ flex: 1,
58
+ justifyContent: 'center',
59
+ alignItems: 'center',
60
+ },
61
+ title: {
62
+ fontSize: 32,
63
+ fontWeight: '700',
64
+ marginBottom: 8,
65
+ },
66
+ subtitle: {
67
+ fontSize: 16,
68
+ },
69
+ buttons: {
70
+ gap: 12,
71
+ },
72
+ button: {
73
+ height: 52,
74
+ borderRadius: 26,
75
+ justifyContent: 'center',
76
+ alignItems: 'center',
77
+ },
78
+ buttonText: {
79
+ fontSize: 16,
80
+ fontWeight: '600',
81
+ },
82
+ secondaryButton: {
83
+ height: 52,
84
+ justifyContent: 'center',
85
+ alignItems: 'center',
86
+ },
87
+ secondaryButtonText: {
88
+ fontSize: 16,
89
+ },
90
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Phone-based login screen.
3
+ * Ported from squad-demo/src/screens/LoginScreen.tsx.
4
+ */
5
+ import React, { useCallback, useState, useRef } from 'react';
6
+ import { View, Text, TextInput, StyleSheet, Pressable, Linking, useWindowDimensions } from 'react-native';
7
+ import { useNavigation } from '@react-navigation/native';
8
+ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
9
+ import type { RootStackParamList } from '../../navigation/SquadNavigator';
10
+ import { useSquadSDK } from '../../SquadProvider';
11
+ import { Colors } from '../../theme/ThemeContext';
12
+ import Button from '../../components/ux/buttons/Button';
13
+ import { ErrorHint } from '../../components/ux/errors/ErrorHint';
14
+ import AvoidKeyboardScreen from '../../components/ux/layout/AvoidKeyboardScreen';
15
+ import { TitleRegular, BodyMedium, SubtitleSmall } from '../../components/ux/text/Typography';
16
+
17
+ type Nav = NativeStackNavigationProp<RootStackParamList, 'Login'>;
18
+
19
+ export function LoginScreen() {
20
+ const navigation = useNavigation<Nav>();
21
+ const sdk = useSquadSDK();
22
+ const { height } = useWindowDimensions();
23
+ const isShort = height < 700;
24
+
25
+ const [phone, setPhone] = useState('');
26
+ const [error, setError] = useState<string | null>(null);
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const inputRef = useRef<TextInput>(null);
29
+
30
+ const isValid = phone.replace(/\D/g, '').length >= 10;
31
+
32
+ const handleSubmit = useCallback(async () => {
33
+ if (!isValid || isLoading) return;
34
+ setIsLoading(true);
35
+ setError(null);
36
+
37
+ try {
38
+ const result = await sdk.sessionManager.createSession({ phone });
39
+ if (result.status === 'pending') {
40
+ navigation.navigate('EnterCode', { phone });
41
+ } else {
42
+ setError('Failed to send verification code.');
43
+ }
44
+ } catch {
45
+ setError('An error occurred. Please try again.');
46
+ } finally {
47
+ setIsLoading(false);
48
+ }
49
+ }, [phone, isValid, isLoading, sdk, navigation]);
50
+
51
+ return (
52
+ <AvoidKeyboardScreen>
53
+ <View style={[styles.headerContainer, isShort && styles.headerShort]}>
54
+ <Pressable style={styles.backButton} onPress={() => navigation.goBack()}>
55
+ <Text style={styles.backText}>{'<'}</Text>
56
+ </Pressable>
57
+ </View>
58
+
59
+ <View style={styles.container}>
60
+ <TitleRegular style={styles.title}>Enter Your Phone Number</TitleRegular>
61
+
62
+ <View style={styles.inputRow}>
63
+ <View style={styles.countryCode}>
64
+ <Text style={styles.countryCodeText}>+1</Text>
65
+ </View>
66
+ <TextInput
67
+ ref={inputRef}
68
+ value={phone}
69
+ onChangeText={(t) => { setPhone(t); setError(null); }}
70
+ onSubmitEditing={handleSubmit}
71
+ placeholder="Phone number"
72
+ placeholderTextColor={Colors.gray6}
73
+ keyboardType="phone-pad"
74
+ style={styles.input}
75
+ maxLength={15}
76
+ accessibilityLabel="Phone number"
77
+ />
78
+ </View>
79
+
80
+ <ErrorHint hint={error} />
81
+
82
+ <View style={styles.footer}>
83
+ <Button
84
+ style={[styles.button, !isValid && styles.buttonDisabled]}
85
+ onPress={handleSubmit}
86
+ disabled={!isValid || isLoading}
87
+ >
88
+ <Text style={[styles.buttonText, !isValid && styles.buttonTextDisabled]}>
89
+ {isLoading ? 'Sending...' : 'Send Code'}
90
+ </Text>
91
+ </Button>
92
+
93
+ <BodyMedium style={styles.legal}>
94
+ By tapping "Send Code" you agree to the{' '}
95
+ <Text style={styles.legalLink} onPress={() => Linking.openURL('https://www.withyoursquad.com/terms')}>
96
+ Terms & Conditions
97
+ </Text>{' '}and{' '}
98
+ <Text style={styles.legalLink} onPress={() => Linking.openURL('https://www.withyoursquad.com/privacy')}>
99
+ Privacy Policy
100
+ </Text>
101
+ </BodyMedium>
102
+ </View>
103
+ </View>
104
+ </AvoidKeyboardScreen>
105
+ );
106
+ }
107
+
108
+ const styles = StyleSheet.create({
109
+ headerContainer: { paddingHorizontal: 20, paddingTop: 20, marginBottom: 60, marginTop: 36 },
110
+ headerShort: { marginBottom: 40, marginTop: 24 },
111
+ backButton: { padding: 10 },
112
+ backText: { fontSize: 24, color: Colors.white, fontWeight: '600' },
113
+ container: { flex: 1, paddingHorizontal: 24, paddingBottom: 16 },
114
+ title: { color: Colors.white, marginBottom: 24 },
115
+ inputRow: { flexDirection: 'row', borderWidth: 1, borderColor: Colors.gray5, borderRadius: 8, backgroundColor: Colors.black, overflow: 'hidden', marginBottom: 8 },
116
+ countryCode: { paddingHorizontal: 16, paddingVertical: 16, borderRightWidth: 1, borderRightColor: Colors.gray5 },
117
+ countryCodeText: { color: Colors.white, fontSize: 16, fontWeight: '500' },
118
+ input: { flex: 1, paddingHorizontal: 16, paddingVertical: 16, color: Colors.white, fontSize: 16, fontWeight: '500' },
119
+ footer: { marginTop: 'auto' },
120
+ button: { height: 56, borderRadius: 8, backgroundColor: Colors.purple1, justifyContent: 'center', alignItems: 'center' },
121
+ buttonDisabled: { backgroundColor: Colors.gray2 },
122
+ buttonText: { color: Colors.gray1, fontSize: 16, fontWeight: '600' },
123
+ buttonTextDisabled: { color: Colors.gray6 },
124
+ legal: { color: Colors.gray6, marginTop: 16, textAlign: 'center' },
125
+ legalLink: { color: Colors.purple1, fontWeight: '500' },
126
+ });
@@ -0,0 +1,163 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ FlatList,
7
+ Pressable,
8
+ } from 'react-native';
9
+
10
+ import { useApiClient } from '../../SquadProvider';
11
+ import { useTheme, Colors } from '../../theme/ThemeContext';
12
+ import ScreenHeader from '../../components/ux/layout/ScreenHeader';
13
+ import Button from '../../components/ux/buttons/Button';
14
+ import UserImage from '../../components/ux/user-image/UserImage';
15
+ import { TitleSmall, TitleMedium, BodyRegular, BodyMedium, BodySmall } from '../../components/ux/text/Typography';
16
+
17
+ interface EventItem {
18
+ id: string;
19
+ title: string;
20
+ description?: string;
21
+ date?: string;
22
+ location?: string;
23
+ imageUrl?: string;
24
+ attendeeCount: number;
25
+ isAttending: boolean;
26
+ }
27
+
28
+ export function EventScreen() {
29
+ const apiClient = useApiClient();
30
+ const { theme } = useTheme();
31
+
32
+ const [events, setEvents] = useState<EventItem[]>([]);
33
+ const [loading, setLoading] = useState(true);
34
+
35
+ useEffect(() => {
36
+ const load = async () => {
37
+ try {
38
+ const data = await apiClient.getEvents();
39
+ if (data?.events) {
40
+ setEvents(data.events.map((e: any) => ({
41
+ id: e.id,
42
+ title: e.title ?? '',
43
+ description: e.description,
44
+ date: e.startDate ?? e.date,
45
+ location: e.location,
46
+ imageUrl: e.imageUrl,
47
+ attendeeCount: e.attendeeCount ?? 0,
48
+ isAttending: e.isAttending ?? false,
49
+ })));
50
+ }
51
+ } catch {}
52
+ setLoading(false);
53
+ };
54
+ load();
55
+ }, [apiClient]);
56
+
57
+ const toggleAttendance = useCallback(async (eventId: string, isAttending: boolean) => {
58
+ try {
59
+ if (isAttending) {
60
+ await apiClient.removeAttendee(eventId);
61
+ } else {
62
+ await apiClient.setAttendee(eventId);
63
+ }
64
+ setEvents(prev => prev.map(e =>
65
+ e.id === eventId
66
+ ? { ...e, isAttending: !isAttending, attendeeCount: e.attendeeCount + (isAttending ? -1 : 1) }
67
+ : e
68
+ ));
69
+ } catch {}
70
+ }, [apiClient]);
71
+
72
+ const formatDate = (dateStr?: string) => {
73
+ if (!dateStr) return '';
74
+ const d = new Date(dateStr);
75
+ return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
76
+ };
77
+
78
+ return (
79
+ <View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
80
+ <ScreenHeader title="Events" />
81
+
82
+ <FlatList
83
+ data={events}
84
+ keyExtractor={item => item.id}
85
+ contentContainerStyle={styles.list}
86
+ renderItem={({ item }) => (
87
+ <View style={styles.card}>
88
+ {item.imageUrl && (
89
+ <View style={styles.cardImagePlaceholder}>
90
+ <BodySmall style={styles.cardImageText}>Event Image</BodySmall>
91
+ </View>
92
+ )}
93
+ <View style={styles.cardContent}>
94
+ <TitleSmall style={styles.eventTitle}>{item.title}</TitleSmall>
95
+ {item.date && (
96
+ <BodyMedium style={styles.eventDate}>{formatDate(item.date)}</BodyMedium>
97
+ )}
98
+ {item.location && (
99
+ <BodySmall style={styles.eventLocation}>{item.location}</BodySmall>
100
+ )}
101
+ {item.description && (
102
+ <BodyRegular style={styles.eventDesc} numberOfLines={2}>{item.description}</BodyRegular>
103
+ )}
104
+
105
+ <View style={styles.cardFooter}>
106
+ <BodySmall style={styles.attendeeCount}>
107
+ {item.attendeeCount} attending
108
+ </BodySmall>
109
+ <Button
110
+ style={[
111
+ styles.attendButton,
112
+ item.isAttending
113
+ ? styles.attendButtonActive
114
+ : { backgroundColor: theme.buttonColor },
115
+ ]}
116
+ onPress={() => toggleAttendance(item.id, item.isAttending)}
117
+ >
118
+ <Text style={[
119
+ styles.attendButtonText,
120
+ item.isAttending ? styles.attendButtonTextActive : { color: theme.buttonText },
121
+ ]}>
122
+ {item.isAttending ? 'Going' : 'Attend'}
123
+ </Text>
124
+ </Button>
125
+ </View>
126
+ </View>
127
+ </View>
128
+ )}
129
+ ListEmptyComponent={
130
+ !loading ? (
131
+ <View style={styles.empty}>
132
+ <BodyRegular style={styles.emptyText}>No upcoming events</BodyRegular>
133
+ </View>
134
+ ) : null
135
+ }
136
+ />
137
+ </View>
138
+ );
139
+ }
140
+
141
+ const styles = StyleSheet.create({
142
+ container: { flex: 1 },
143
+ list: { padding: 24, gap: 16 },
144
+ card: { backgroundColor: Colors.gray2, borderRadius: 16, overflow: 'hidden' },
145
+ cardImagePlaceholder: {
146
+ height: 140, backgroundColor: Colors.gray3,
147
+ justifyContent: 'center', alignItems: 'center',
148
+ },
149
+ cardImageText: { color: Colors.gray6 },
150
+ cardContent: { padding: 16 },
151
+ eventTitle: { color: Colors.white, marginBottom: 4 },
152
+ eventDate: { color: Colors.purple1, marginBottom: 2 },
153
+ eventLocation: { color: Colors.gray6, marginBottom: 8 },
154
+ eventDesc: { color: Colors.gray6, marginBottom: 16 },
155
+ cardFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
156
+ attendeeCount: { color: Colors.gray6 },
157
+ attendButton: { paddingVertical: 8, paddingHorizontal: 20, borderRadius: 16 },
158
+ attendButtonActive: { backgroundColor: 'transparent', borderWidth: 1, borderColor: Colors.green },
159
+ attendButtonText: { fontSize: 13, fontWeight: '600' },
160
+ attendButtonTextActive: { color: Colors.green },
161
+ empty: { alignItems: 'center', paddingTop: 48 },
162
+ emptyText: { color: Colors.gray6 },
163
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Community freestyle listens/reactions screens.
3
+ * Ported from squad-demo/src/screens/CommunityFreestyleListens.tsx + CommunityFreestyleReaction.tsx.
4
+ */
5
+ import React, { useEffect, useState } from 'react';
6
+ import { View, StyleSheet, FlatList } from 'react-native';
7
+ import { useRoute } from '@react-navigation/native';
8
+ import { useApiClient } from '../../SquadProvider';
9
+ import { useTheme, Colors } from '../../theme/ThemeContext';
10
+ import ScreenHeader from '../../components/ux/layout/ScreenHeader';
11
+ import { FreestyleUserListen, FreestyleUserReaction } from '../../components/freestyle/FreestyleComponents';
12
+ import { BodyRegular } from '../../components/ux/text/Typography';
13
+
14
+ export function CommunityFreestyleListensScreen() {
15
+ const route = useRoute<any>();
16
+ const apiClient = useApiClient();
17
+ const { theme } = useTheme();
18
+ const freestyleId = route.params?.freestyleId;
19
+ const [listens, setListens] = useState<Array<{ userName: string; userImageUrl?: string; listenedAt?: string }>>([]);
20
+
21
+ useEffect(() => {
22
+ // Load community-wide listens for this freestyle
23
+ }, [freestyleId, apiClient]);
24
+
25
+ return (
26
+ <View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
27
+ <ScreenHeader title="Community Listens" />
28
+ <FlatList
29
+ data={listens}
30
+ keyExtractor={(_, i) => String(i)}
31
+ renderItem={({ item }) => <FreestyleUserListen {...item} />}
32
+ contentContainerStyle={styles.list}
33
+ ListEmptyComponent={<View style={styles.empty}><BodyRegular style={styles.emptyText}>No community listens yet</BodyRegular></View>}
34
+ />
35
+ </View>
36
+ );
37
+ }
38
+
39
+ export function CommunityFreestyleReactionsScreen() {
40
+ const route = useRoute<any>();
41
+ const apiClient = useApiClient();
42
+ const { theme } = useTheme();
43
+ const freestyleId = route.params?.freestyleId;
44
+ const emoji = route.params?.emoji;
45
+ const [reactions, setReactions] = useState<Array<{ userName: string; userImageUrl?: string; emoji: string }>>([]);
46
+
47
+ useEffect(() => {
48
+ const load = async () => {
49
+ try {
50
+ const result = await apiClient.getFreestyleReactionsForCommunity(freestyleId, emoji ?? '');
51
+ if (result?.reactions) {
52
+ setReactions(result.reactions.map((r: any) => ({
53
+ userName: r.creator?.displayName ?? '',
54
+ userImageUrl: r.creator?.imageUrl,
55
+ emoji: r.emojiLabel ?? emoji ?? '',
56
+ })));
57
+ }
58
+ } catch {}
59
+ };
60
+ if (freestyleId) load();
61
+ }, [freestyleId, emoji, apiClient]);
62
+
63
+ return (
64
+ <View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
65
+ <ScreenHeader title="Community Reactions" />
66
+ <FlatList
67
+ data={reactions}
68
+ keyExtractor={(_, i) => String(i)}
69
+ renderItem={({ item }) => <FreestyleUserReaction {...item} />}
70
+ contentContainerStyle={styles.list}
71
+ ListEmptyComponent={<View style={styles.empty}><BodyRegular style={styles.emptyText}>No community reactions yet</BodyRegular></View>}
72
+ />
73
+ </View>
74
+ );
75
+ }
76
+
77
+ const styles = StyleSheet.create({
78
+ container: { flex: 1 },
79
+ list: { paddingBottom: 48 },
80
+ empty: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 48 },
81
+ emptyText: { color: Colors.gray6 },
82
+ });