@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,59 @@
1
+ /**
2
+ * Image optimization hook — resizes and compresses images before upload.
3
+ * Ported from squad-demo/src/hooks/useImageOptimization.ts + useImagePreloader.ts + useImageListPreloader.ts.
4
+ */
5
+ import { useCallback } from 'react';
6
+
7
+ interface OptimizedImage {
8
+ uri: string;
9
+ width: number;
10
+ height: number;
11
+ base64?: string;
12
+ }
13
+
14
+ export function useImageOptimization() {
15
+ const optimizeImage = useCallback(async (
16
+ uri: string,
17
+ maxWidth: number = 800,
18
+ quality: number = 0.8,
19
+ ): Promise<OptimizedImage> => {
20
+ try {
21
+ const ImageManipulator = await import('expo-image-manipulator');
22
+ const result = await ImageManipulator.manipulateAsync(
23
+ uri,
24
+ [{ resize: { width: maxWidth } }],
25
+ { compress: quality, format: ImageManipulator.SaveFormat.JPEG },
26
+ );
27
+ return { uri: result.uri, width: result.width, height: result.height };
28
+ } catch {
29
+ return { uri, width: 0, height: 0 };
30
+ }
31
+ }, []);
32
+
33
+ return { optimizeImage };
34
+ }
35
+
36
+ export function useImagePreloader() {
37
+ const preloadImage = useCallback(async (uri: string): Promise<boolean> => {
38
+ try {
39
+ const { Image } = await import('expo-image');
40
+ await Image.prefetch(uri);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }, []);
46
+
47
+ return { preloadImage };
48
+ }
49
+
50
+ export function useImageListPreloader() {
51
+ const preloadImages = useCallback(async (uris: string[]): Promise<void> => {
52
+ try {
53
+ const { Image } = await import('expo-image');
54
+ await Promise.allSettled(uris.map(uri => Image.prefetch(uri)));
55
+ } catch {}
56
+ }, []);
57
+
58
+ return { preloadImages };
59
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Onboarding step guard — ensures users can't skip onboarding steps.
3
+ * Ported from squad-demo/src/hooks/useOnboardingStepGuard.ts.
4
+ */
5
+ import { useEffect } from 'react';
6
+ import { useRecoilValue } from 'recoil';
7
+ import { useNavigation } from '@react-navigation/native';
8
+ import { reActiveAccessToken, reActiveUserId } from '../state/session';
9
+ import { reUserCache } from '../state/user';
10
+
11
+ export function useOnboardingStepGuard(requiredStep: 'token' | 'displayName' | 'community') {
12
+ const navigation = useNavigation();
13
+ const token = useRecoilValue(reActiveAccessToken);
14
+ const userId = useRecoilValue(reActiveUserId);
15
+ const user = useRecoilValue(reUserCache);
16
+
17
+ useEffect(() => {
18
+ switch (requiredStep) {
19
+ case 'token':
20
+ if (!token) {
21
+ navigation.reset({ index: 0, routes: [{ name: 'Landing' as never }] });
22
+ }
23
+ break;
24
+ case 'displayName':
25
+ if (!token || !user?.displayName) {
26
+ navigation.reset({ index: 0, routes: [{ name: 'OnboardingWelcome' as never }] });
27
+ }
28
+ break;
29
+ case 'community':
30
+ if (!token || !user?.community) {
31
+ navigation.reset({ index: 0, routes: [{ name: 'OnboardingTeamSelect' as never }] });
32
+ }
33
+ break;
34
+ }
35
+ }, [token, userId, user, requiredStep, navigation]);
36
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Pending navigation hook — handles post-onboarding redirects.
3
+ * Ported from squad-demo/src/hooks/usePendingNavigation.ts.
4
+ */
5
+ import { useCallback } from 'react';
6
+ import { useRecoilState } from 'recoil';
7
+ import { rePendingNavigation } from '../state/session';
8
+ import { useNavigation } from '@react-navigation/native';
9
+
10
+ export function usePendingNavigation() {
11
+ const [pending, setPending] = useRecoilState(rePendingNavigation);
12
+ const navigation = useNavigation();
13
+
14
+ const executePendingNavigation = useCallback(() => {
15
+ if (pending.hasPendingInviter && pending.inviterId) {
16
+ (navigation as any).navigate('Profile', { userId: pending.inviterId });
17
+ setPending({ hasPendingInviter: false });
18
+ }
19
+ }, [pending, setPending, navigation]);
20
+
21
+ const setPendingInviter = useCallback((inviterId: string) => {
22
+ setPending({ hasPendingInviter: true, inviterId });
23
+ }, [setPending]);
24
+
25
+ return { pending, executePendingNavigation, setPendingInviter };
26
+ }
@@ -0,0 +1,84 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { useApiClient } from '../SquadProvider';
3
+ import type { User, Connection, Feed } from '@squad-sports/core';
4
+
5
+ /**
6
+ * Hook to load and manage the logged-in user's data.
7
+ */
8
+ export function useCurrentUser() {
9
+ const apiClient = useApiClient();
10
+ const [user, setUser] = useState<User | null>(null);
11
+ const [loading, setLoading] = useState(true);
12
+
13
+ const refresh = useCallback(async () => {
14
+ setLoading(true);
15
+ try {
16
+ const userData = await apiClient.getLoggedInUser();
17
+ setUser(userData);
18
+ } catch (error) {
19
+ console.error('[useCurrentUser] Error:', error);
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ }, [apiClient]);
24
+
25
+ useEffect(() => {
26
+ refresh();
27
+ }, [refresh]);
28
+
29
+ return { user, loading, refresh };
30
+ }
31
+
32
+ /**
33
+ * Hook to load and manage squad connections.
34
+ */
35
+ export function useSquadConnections() {
36
+ const apiClient = useApiClient();
37
+ const [connections, setConnections] = useState<Connection[]>([]);
38
+ const [loading, setLoading] = useState(true);
39
+
40
+ const refresh = useCallback(async (forceFresh = false) => {
41
+ setLoading(true);
42
+ try {
43
+ const squad = await apiClient.getUserConnections(forceFresh);
44
+ setConnections(squad?.connections ?? []);
45
+ } catch (error) {
46
+ console.error('[useSquadConnections] Error:', error);
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ }, [apiClient]);
51
+
52
+ useEffect(() => {
53
+ refresh();
54
+ }, [refresh]);
55
+
56
+ return { connections, loading, refresh };
57
+ }
58
+
59
+ /**
60
+ * Hook to load the freestyle feed.
61
+ */
62
+ export function useFeed() {
63
+ const apiClient = useApiClient();
64
+ const [feed, setFeed] = useState<Feed | null>(null);
65
+ const [loading, setLoading] = useState(true);
66
+
67
+ const refresh = useCallback(async (forceFresh = false) => {
68
+ setLoading(true);
69
+ try {
70
+ const data = await apiClient.getFeed(1, 20, forceFresh);
71
+ setFeed(data);
72
+ } catch (error) {
73
+ console.error('[useFeed] Error:', error);
74
+ } finally {
75
+ setLoading(false);
76
+ }
77
+ }, [apiClient]);
78
+
79
+ useEffect(() => {
80
+ refresh();
81
+ }, [refresh]);
82
+
83
+ return { feed, loading, refresh };
84
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * User created hook — tracks when a new user is created during onboarding.
3
+ * Ported from squad-demo/src/hooks/useUserCreated.ts.
4
+ */
5
+ import { useCallback } from 'react';
6
+ import { useSetRecoilState } from 'recoil';
7
+ import { reUserCache, reLoggedInUserLoaded } from '../state/user';
8
+ import { reActiveAccessToken, reActiveUserId } from '../state/session';
9
+ import type { User } from '@squad-sports/core';
10
+
11
+ export function useUserCreated() {
12
+ const setUserCache = useSetRecoilState(reUserCache);
13
+ const setUserLoaded = useSetRecoilState(reLoggedInUserLoaded);
14
+ const setAccessToken = useSetRecoilState(reActiveAccessToken);
15
+ const setUserId = useSetRecoilState(reActiveUserId);
16
+
17
+ const onUserCreated = useCallback((user: User, accessToken: string) => {
18
+ setUserCache(user);
19
+ setUserLoaded(true);
20
+ if (user.id) setUserId(user.id);
21
+ setAccessToken(accessToken);
22
+ }, [setUserCache, setUserLoaded, setAccessToken, setUserId]);
23
+
24
+ return { onUserCreated };
25
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * User update hook — updates user profile and syncs state.
3
+ * Ported from squad-demo/src/hooks/useUserUpdate.ts.
4
+ */
5
+ import { useCallback } from 'react';
6
+ import { useSetRecoilState } from 'recoil';
7
+ import { useApiClient } from '../SquadProvider';
8
+ import { reUserCache } from '../state/user';
9
+ import { updateUser } from '../services/UserUpdateService';
10
+ import type { User } from '@squad-sports/core';
11
+
12
+ export function useUserUpdate() {
13
+ const apiClient = useApiClient();
14
+ const setUserCache = useSetRecoilState(reUserCache);
15
+
16
+ const update = useCallback(async (user: User): Promise<User | null> => {
17
+ const result = await updateUser(apiClient, user);
18
+ if (result) {
19
+ setUserCache(result);
20
+ }
21
+ return result;
22
+ }, [apiClient, setUserCache]);
23
+
24
+ return { updateUser: update };
25
+ }
@@ -0,0 +1,40 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ /**
4
+ * Tracks how long a component has been in the viewport.
5
+ * Fires the onImpression callback once after the threshold is met.
6
+ *
7
+ * @param placementId - Unique placement identifier
8
+ * @param thresholdMs - Minimum viewport time to count as impression (1000 for cards, 5000 for banners)
9
+ * @param onImpression - Called once when threshold is met
10
+ * @param active - Whether tracking is active (e.g., component is visible)
11
+ */
12
+ export function useViewabilityTracker(
13
+ placementId: string,
14
+ thresholdMs: number,
15
+ onImpression: (placementId: string, durationMs: number) => void,
16
+ active: boolean = true,
17
+ ): void {
18
+ const firedRef = useRef(false);
19
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
20
+
21
+ useEffect(() => {
22
+ if (!active || firedRef.current) return;
23
+
24
+ timerRef.current = setTimeout(() => {
25
+ if (!firedRef.current) {
26
+ firedRef.current = true;
27
+ onImpression(placementId, thresholdMs);
28
+ }
29
+ }, thresholdMs);
30
+
31
+ return () => {
32
+ if (timerRef.current) clearTimeout(timerRef.current);
33
+ };
34
+ }, [placementId, thresholdMs, onImpression, active]);
35
+
36
+ // Reset when placement changes
37
+ useEffect(() => {
38
+ firedRef.current = false;
39
+ }, [placementId]);
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ // SDK Entry
2
+ export { SquadSportsSDK } from './SquadSportsSDK';
3
+
4
+ // Provider & Hooks
5
+ export { SquadProvider, useSquadSDK, useApiClient } from './SquadProvider';
6
+
7
+ // Experience (drop-in component)
8
+ export { SquadExperience } from './SquadExperience';
9
+
10
+ // Navigation
11
+ export { SquadNavigator } from './navigation/SquadNavigator';
12
+ export type { RootStackParamList } from './navigation/SquadNavigator';
13
+
14
+ // Theme
15
+ export { ThemeProvider, useTheme, Colors, defaultTheme, buildCommunityTheme } from './theme/ThemeContext';
16
+ export type { Theme, ColorScheme } from './theme/ThemeContext';
17
+
18
+ // UX Components
19
+ export {
20
+ Button,
21
+ XButton,
22
+ TextInput,
23
+ PhoneNumberInput,
24
+ CodeInput,
25
+ Screen,
26
+ ScreenHeader,
27
+ LoadingOverlay,
28
+ AvoidKeyboardScreen,
29
+ Toast,
30
+ TitleLarge,
31
+ TitleRegular,
32
+ TitleMedium,
33
+ TitleSmall,
34
+ TitleTiny,
35
+ SubtitleSmall,
36
+ BodyRegular,
37
+ BodyMedium,
38
+ BodySmall,
39
+ ButtonLarge,
40
+ ButtonSmall,
41
+ ErrorHint,
42
+ UserImage,
43
+ BackButton,
44
+ } from './components';
45
+ export type { ButtonProps, ToastType } from './components';
46
+
47
+ // Feed Components
48
+ export { default as FreestyleCard } from './components/feed/FreestyleCard';
49
+ export { default as PollCard } from './components/feed/PollCard';
50
+ export { default as MessageCard } from './components/message/MessageCard';
51
+ export { AudioPlayerRow } from './components/audio/AudioPlayerRow';
52
+
53
+ // Auth Screens
54
+ export { LandingScreen } from './screens/auth/LandingScreen';
55
+ export { EnterEmailScreen } from './screens/auth/EnterEmailScreen';
56
+ export { EnterCodeScreen } from './screens/auth/EnterCodeScreen';
57
+ export { EmailVerificationScreen } from './screens/auth/EmailVerificationScreen';
58
+
59
+ // Onboarding Screens
60
+ export { OnboardingWelcomeScreen } from './screens/onboarding/OnboardingWelcomeScreen';
61
+ export { OnboardingTeamSelectScreen } from './screens/onboarding/OnboardingTeamSelectScreen';
62
+ export { OnboardingAccountSetupScreen } from './screens/onboarding/OnboardingAccountSetupScreen';
63
+
64
+ // Main Screens
65
+ export { HomeScreen } from './screens/home/HomeScreen';
66
+ export { ProfileScreen } from './screens/profile/ProfileScreen';
67
+ export { SettingsScreen } from './screens/settings/SettingsScreen';
68
+ export { EditProfileScreen } from './screens/settings/EditProfileScreen';
69
+ export { BlockedUsersScreen } from './screens/settings/BlockedUsersScreen';
70
+ export { DeleteAccountScreen } from './screens/settings/DeleteAccountScreen';
71
+ export { MessagingScreen } from './screens/messaging/MessagingScreen';
72
+ export { FreestyleCreationScreen } from './screens/freestyle/FreestyleCreationScreen';
73
+ export { PollResponseScreen } from './screens/polls/PollResponseScreen';
74
+ export { InviteScreen } from './screens/invite/InviteScreen';
75
+
76
+ // Squad Line (Voice Calling)
77
+ export { AddCallTitleScreen } from './screens/squad-line/AddCallTitleScreen';
78
+ export { SquadLineClient } from './squad-line/SquadLineClient';
79
+ export type { CallState, CallInfo } from './squad-line/SquadLineClient';
80
+ export { useSquadLine } from './squad-line/useSquadLine';
81
+ export { CallScreen } from './squad-line/CallScreen';
82
+ export { IncomingCallOverlay } from './squad-line/IncomingCallOverlay';
83
+
84
+ // Real-time & Offline
85
+ export { EventProcessor } from './realtime/EventProcessor';
86
+ export type { ConnectionQuality } from './realtime/EventProcessor';
87
+ export { useRealtimeSync, useRealtimeEvent, useMessageUpdates, useSquadUpdates } from './realtime/useRealtimeSync';
88
+ export { OfflineQueue } from './realtime/OfflineQueue';
89
+ export type { OfflineAction, OfflineActionType } from './realtime/OfflineQueue';
90
+ export { NetworkMonitor, useNetworkStatus } from './realtime/NetworkMonitor';
91
+ export { PushNotificationHandler } from './realtime/PushNotificationHandler';
92
+ export type { PushNotificationPayload, NotificationType, NotificationRouteAction } from './realtime/PushNotificationHandler';
93
+
94
+ // Data Hooks
95
+ export { useCurrentUser, useSquadConnections, useFeed } from './hooks/useSquadData';
96
+
97
+ // Re-export core types
98
+ export type {
99
+ SquadSDKConfig,
100
+ SquadPartnerConfig,
101
+ SquadConfig,
102
+ StorageAdapter,
103
+ CommunityConfig,
104
+ FeatureFlags,
105
+ Environment,
106
+ SSOConfig,
107
+ SSOProvider,
108
+ NexusPartnerConfig,
109
+ } from '@squad-sports/core';
@@ -0,0 +1,262 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+ import { createNativeStackNavigator } from '@react-navigation/native-stack';
4
+
5
+ import { User } from '@squad-sports/core';
6
+ import { useSquadSDK } from '../SquadProvider';
7
+ import { Colors } from '../theme/ThemeContext';
8
+ import { ScreenErrorBoundary } from '../components/ErrorBoundary';
9
+
10
+ // Auth Screens
11
+ import { LandingScreen } from '../screens/auth/LandingScreen';
12
+ import { EnterEmailScreen } from '../screens/auth/EnterEmailScreen';
13
+ import { EnterCodeScreen } from '../screens/auth/EnterCodeScreen';
14
+ import { EmailVerificationScreen } from '../screens/auth/EmailVerificationScreen';
15
+
16
+ // Onboarding Screens
17
+ import { OnboardingWelcomeScreen } from '../screens/onboarding/OnboardingWelcomeScreen';
18
+ import { OnboardingTeamSelectScreen } from '../screens/onboarding/OnboardingTeamSelectScreen';
19
+ import { OnboardingAccountSetupScreen } from '../screens/onboarding/OnboardingAccountSetupScreen';
20
+
21
+ // Main Screens
22
+ import { HomeScreen } from '../screens/home/HomeScreen';
23
+ import { ProfileScreen } from '../screens/profile/ProfileScreen';
24
+ import { SettingsScreen } from '../screens/settings/SettingsScreen';
25
+ import { MessagingScreen } from '../screens/messaging/MessagingScreen';
26
+ import { FreestyleCreationScreen } from '../screens/freestyle/FreestyleCreationScreen';
27
+ import { PollResponseScreen } from '../screens/polls/PollResponseScreen';
28
+ import { InviteScreen } from '../screens/invite/InviteScreen';
29
+
30
+ // Settings sub-screens
31
+ import { EditProfileScreen } from '../screens/settings/EditProfileScreen';
32
+ import { BlockedUsersScreen } from '../screens/settings/BlockedUsersScreen';
33
+ import { DeleteAccountScreen } from '../screens/settings/DeleteAccountScreen';
34
+
35
+ // Events & Wallet
36
+ import { EventScreen } from '../screens/events/EventScreen';
37
+ import { WalletScreen } from '../screens/wallet/WalletScreen';
38
+
39
+ // Freestyle sub-screens
40
+ import { FreestyleScreen } from '../screens/freestyle/FreestyleScreen';
41
+ import { FreestyleListensScreen } from '../screens/freestyle/FreestyleListensScreen';
42
+ import { FreestyleReactionsScreen } from '../screens/freestyle/FreestyleReactionsScreen';
43
+ import { CommunityFreestyleListensScreen, CommunityFreestyleReactionsScreen } from '../screens/freestyle/CommunityFreestyleScreen';
44
+
45
+ // Poll sub-screens
46
+ import { PollSummationScreen } from '../screens/polls/PollSummationScreen';
47
+
48
+ // Auth sub-screens
49
+ import { LoginScreen } from '../screens/auth/LoginScreen';
50
+
51
+ // Invite sub-screens
52
+ import { InvitationQrCodeScreen } from '../screens/invite/InvitationQrCodeScreen';
53
+
54
+ // Settings sub-screens
55
+ import { NetworkStatusScreen } from '../screens/settings/NetworkStatusScreen';
56
+
57
+ // Squad Line
58
+ import { AddCallTitleScreen } from '../screens/squad-line/AddCallTitleScreen';
59
+ import { CallScreen } from '../squad-line/CallScreen';
60
+ import { IncomingCallOverlay } from '../squad-line/IncomingCallOverlay';
61
+
62
+ function withErrorBoundary<P extends object>(
63
+ Component: React.ComponentType<P>,
64
+ screenName: string,
65
+ ): React.ComponentType<P> {
66
+ return function WrappedScreen(props: P) {
67
+ return (
68
+ <ScreenErrorBoundary screenName={screenName}>
69
+ <Component {...props} />
70
+ </ScreenErrorBoundary>
71
+ );
72
+ };
73
+ }
74
+
75
+ export type RootStackParamList = {
76
+ // Auth
77
+ Landing: undefined;
78
+ EnterEmail: { email?: string } | undefined;
79
+ EnterCode: { phone?: string; email?: string };
80
+ EmailVerification: undefined;
81
+ SignUp: undefined;
82
+
83
+ // Onboarding
84
+ OnboardingWelcome: undefined;
85
+ OnboardingTeamSelect: undefined;
86
+ OnboardingAccountSetup: undefined;
87
+
88
+ // Main
89
+ Home: undefined;
90
+ Profile: { userId: string };
91
+ Settings: undefined;
92
+ EditProfile: undefined;
93
+ BlockedUsers: undefined;
94
+ DeleteAccount: undefined;
95
+ Messaging: { connectionId: string };
96
+ FreestyleCreate: undefined;
97
+ PollResponse: { pollId: string };
98
+ Invite: undefined;
99
+
100
+ // Squad Line
101
+ AddCallTitle: { connectionId: string };
102
+ ActiveCall: { connectionId: string; title: string };
103
+ IncomingCall: { callerId: string; callerName: string };
104
+
105
+ // Events & Wallet
106
+ Events: undefined;
107
+ Wallet: undefined;
108
+
109
+ // Freestyle sub-screens
110
+ Freestyle: { freestyleId: string };
111
+ FreestyleListens: { freestyleId: string };
112
+ FreestyleReactions: { freestyleId: string };
113
+ CommunityFreestyleListens: { freestyleId: string };
114
+ CommunityFreestyleReactions: { freestyleId: string; emoji?: string };
115
+
116
+ // Poll sub-screens
117
+ PollSummation: { pollId: string };
118
+
119
+ // Auth sub-screens
120
+ Login: undefined;
121
+
122
+ // Invite sub-screens
123
+ InvitationQrCode: undefined;
124
+
125
+ // Settings sub-screens
126
+ NetworkStatus: undefined;
127
+ };
128
+
129
+ const Stack = createNativeStackNavigator<RootStackParamList>();
130
+
131
+ type NavigationState = 'loading' | 'auth' | 'onboarding' | 'main';
132
+
133
+ export function SquadNavigator() {
134
+ const sdk = useSquadSDK();
135
+ const [navState, setNavState] = useState<NavigationState>('loading');
136
+
137
+ useEffect(() => {
138
+ const determineRoute = async () => {
139
+ try {
140
+ const token = await sdk.tokenStorage.getAccessToken();
141
+ if (!token) {
142
+ setNavState('auth');
143
+ return;
144
+ }
145
+
146
+ const user = await sdk.apiClient.getLoggedInUser();
147
+ if (!user) {
148
+ setNavState('auth');
149
+ return;
150
+ }
151
+
152
+ const partnerAuth = sdk.config.partnerAuth;
153
+ const hasDisplayName = !!(user.displayName?.trim());
154
+ const hasCommunity = !!(user.community?.trim());
155
+
156
+ // Partner flow: auto-fill missing profile data and skip onboarding
157
+ if (partnerAuth?.communityId) {
158
+ if (!hasCommunity) {
159
+ await sdk.apiClient.updateUserCommunity({ id: partnerAuth.communityId }).catch(() => {});
160
+ }
161
+ if (!hasDisplayName && partnerAuth.userData?.displayName) {
162
+ await sdk.apiClient.updateLoggedInUser(
163
+ new User({ displayName: partnerAuth.userData.displayName }),
164
+ ).catch(() => {});
165
+ }
166
+ setNavState('main');
167
+ return;
168
+ }
169
+
170
+ if (!hasDisplayName || !hasCommunity) {
171
+ setNavState('onboarding');
172
+ return;
173
+ }
174
+
175
+ setNavState('main');
176
+ } catch {
177
+ setNavState('auth');
178
+ }
179
+ };
180
+
181
+ determineRoute();
182
+
183
+ // Listen for auth changes
184
+ const onAuthInvalid = () => setNavState('auth');
185
+ sdk.apiClient.emitter.on('auth:invalid', onAuthInvalid);
186
+ return () => {
187
+ sdk.apiClient.emitter.off('auth:invalid', onAuthInvalid);
188
+ };
189
+ }, [sdk]);
190
+
191
+ if (navState === 'loading') {
192
+ return (
193
+ <View style={{ flex: 1, backgroundColor: Colors.gray9, justifyContent: 'center', alignItems: 'center' }}>
194
+ <ActivityIndicator size="large" color={Colors.white} />
195
+ </View>
196
+ );
197
+ }
198
+
199
+ return (
200
+ <>
201
+ <Stack.Navigator
202
+ screenOptions={{
203
+ headerShown: false,
204
+ animation: 'slide_from_right',
205
+ contentStyle: { backgroundColor: Colors.gray9 },
206
+ }}
207
+ >
208
+ {navState === 'auth' && (
209
+ <>
210
+ <Stack.Screen name="Landing" component={withErrorBoundary(LandingScreen, 'Landing')} />
211
+ <Stack.Screen name="EnterEmail" component={withErrorBoundary(EnterEmailScreen, 'EnterEmail')} />
212
+ <Stack.Screen name="EnterCode" component={withErrorBoundary(EnterCodeScreen, 'EnterCode')} />
213
+ <Stack.Screen name="EmailVerification" component={withErrorBoundary(EmailVerificationScreen, 'EmailVerification')} />
214
+ <Stack.Screen name="Login" component={withErrorBoundary(LoginScreen, 'Login')} />
215
+ </>
216
+ )}
217
+
218
+ {navState === 'onboarding' && (
219
+ <>
220
+ <Stack.Screen name="OnboardingWelcome" component={withErrorBoundary(OnboardingWelcomeScreen, 'OnboardingWelcome')} />
221
+ <Stack.Screen name="OnboardingTeamSelect" component={withErrorBoundary(OnboardingTeamSelectScreen, 'OnboardingTeamSelect')} />
222
+ <Stack.Screen name="OnboardingAccountSetup" component={withErrorBoundary(OnboardingAccountSetupScreen, 'OnboardingAccountSetup')} />
223
+ </>
224
+ )}
225
+
226
+ {navState === 'main' && (
227
+ <>
228
+ <Stack.Screen name="Home" component={withErrorBoundary(HomeScreen, 'Home')} />
229
+ <Stack.Screen name="Profile" component={withErrorBoundary(ProfileScreen, 'Profile')} />
230
+ <Stack.Screen name="Settings" component={withErrorBoundary(SettingsScreen, 'Settings')} />
231
+ <Stack.Screen name="EditProfile" component={withErrorBoundary(EditProfileScreen, 'EditProfile')} />
232
+ <Stack.Screen name="BlockedUsers" component={withErrorBoundary(BlockedUsersScreen, 'BlockedUsers')} />
233
+ <Stack.Screen name="DeleteAccount" component={withErrorBoundary(DeleteAccountScreen, 'DeleteAccount')} />
234
+ <Stack.Screen name="Events" component={withErrorBoundary(EventScreen, 'Events')} />
235
+ <Stack.Screen name="Wallet" component={withErrorBoundary(WalletScreen, 'Wallet')} />
236
+ <Stack.Screen name="Messaging" component={withErrorBoundary(MessagingScreen, 'Messaging')} />
237
+ <Stack.Screen name="FreestyleCreate" component={withErrorBoundary(FreestyleCreationScreen, 'FreestyleCreate')} />
238
+ <Stack.Screen name="PollResponse" component={withErrorBoundary(PollResponseScreen, 'PollResponse')} />
239
+ <Stack.Screen name="Invite" component={withErrorBoundary(InviteScreen, 'Invite')} />
240
+ <Stack.Screen name="AddCallTitle" component={withErrorBoundary(AddCallTitleScreen, 'AddCallTitle')} />
241
+ <Stack.Screen name="Freestyle" component={withErrorBoundary(FreestyleScreen, 'Freestyle')} />
242
+ <Stack.Screen name="FreestyleListens" component={withErrorBoundary(FreestyleListensScreen, 'FreestyleListens')} />
243
+ <Stack.Screen name="FreestyleReactions" component={withErrorBoundary(FreestyleReactionsScreen, 'FreestyleReactions')} />
244
+ <Stack.Screen name="CommunityFreestyleListens" component={withErrorBoundary(CommunityFreestyleListensScreen, 'CommunityFreestyleListens')} />
245
+ <Stack.Screen name="CommunityFreestyleReactions" component={withErrorBoundary(CommunityFreestyleReactionsScreen, 'CommunityFreestyleReactions')} />
246
+ <Stack.Screen name="PollSummation" component={withErrorBoundary(PollSummationScreen, 'PollSummation')} />
247
+ <Stack.Screen name="InvitationQrCode" component={withErrorBoundary(InvitationQrCodeScreen, 'InvitationQrCode')} />
248
+ <Stack.Screen name="NetworkStatus" component={withErrorBoundary(NetworkStatusScreen, 'NetworkStatus')} />
249
+ <Stack.Screen
250
+ name="ActiveCall"
251
+ component={withErrorBoundary(CallScreen, 'ActiveCall')}
252
+ options={{ animation: 'slide_from_bottom', gestureEnabled: false }}
253
+ />
254
+ </>
255
+ )}
256
+ </Stack.Navigator>
257
+
258
+ {/* Global overlay for incoming calls */}
259
+ <IncomingCallOverlay />
260
+ </>
261
+ );
262
+ }