@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Squad For Sports, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Squad Sports SDK for React Native
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@squad-sports/react-native.svg)](https://www.npmjs.com/package/@squad-sports/react-native)
4
+ [![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20Android-lightgrey.svg)](https://docs.squadforsports.com)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ Add fan engagement features to your sports app in minutes. Drop in a single component and get messaging, polls, freestyles, voice calling, sponsorship inventory, and real-time updates.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @squad-sports/react-native @squad-sports/core
13
+ ```
14
+
15
+ ### Peer Dependencies
16
+
17
+ ```bash
18
+ npm install react-native-gesture-handler react-native-safe-area-context \
19
+ @react-navigation/native @react-navigation/native-stack \
20
+ @react-native-async-storage/async-storage \
21
+ react-native-sse expo-secure-store expo-av expo-image expo-image-picker
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```tsx
27
+ import { SquadExperience } from '@squad-sports/react-native';
28
+
29
+ export default function App() {
30
+ return (
31
+ <SquadExperience
32
+ partnerId="your-partner-id"
33
+ apiKey="your-api-key"
34
+ />
35
+ );
36
+ }
37
+ ```
38
+
39
+ That's it. The SDK auto-resolves your community config, theming, and features from your partner ID.
40
+
41
+ ### With Ticketmaster SSO
42
+
43
+ ```tsx
44
+ <SquadExperience
45
+ partnerId="your-partner-id"
46
+ apiKey="your-api-key"
47
+ ssoToken={ticketmasterAccessToken}
48
+ />
49
+ ```
50
+
51
+ ### With Seamless Partner Auth (No Login Screen)
52
+
53
+ ```tsx
54
+ <SquadExperience
55
+ partnerId="your-partner-id"
56
+ apiKey="your-api-key"
57
+ userData={{
58
+ email: user.email,
59
+ displayName: user.name,
60
+ externalUserId: user.id,
61
+ }}
62
+ />
63
+ ```
64
+
65
+ ## Features
66
+
67
+ - **Messaging** — 1:1 messaging with audio messages and reactions
68
+ - **Polls** — Interactive polls with live results and branded sponsor polls
69
+ - **Freestyles** — Audio posts with community-wide sharing
70
+ - **Squad Line** — Real-time voice calls via Twilio
71
+ - **Events** — Event attendance and check-ins
72
+ - **Wallet** — Rewards, coupons, and sponsor promotions
73
+ - **Sponsorship** — In-app sponsorship inventory (branded polls, content cards, banners, interstitials)
74
+ - **Real-Time** — Server-sent events with offline queueing
75
+ - **Analytics** — 28 event types with custom adapter support (Mixpanel, Amplitude)
76
+ - **SSO** — Ticketmaster, OAuth2, SAML, and custom providers
77
+
78
+ ## Documentation
79
+
80
+ Full integration guide, API reference, and configuration options:
81
+
82
+ **[docs.squadforsports.com](https://docs.squadforsports.com)**
83
+
84
+ ## Requirements
85
+
86
+ - React Native 0.73+
87
+ - React 18+
88
+ - iOS 15+ / Android SDK 24+ (Android 7.0)
89
+ - Expo SDK 50+ (if using Expo)
90
+
91
+ ## Get Your Partner ID
92
+
93
+ Register at **[partners.squadforsports.com](https://partners.squadforsports.com)** to get your partner ID and API key.
94
+
95
+ ## License
96
+
97
+ MIT. See [LICENSE](LICENSE).
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@squad-sports/react-native",
3
+ "version": "1.3.1",
4
+ "description": "Squad Sports Experience SDK for React Native",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": ["src"],
8
+ "scripts": {
9
+ "build": "echo 'RN package uses source directly'",
10
+ "typecheck": "tsc --noEmit",
11
+ "test": "jest --passWithNoTests",
12
+ "clean": "echo 'nothing to clean'"
13
+ },
14
+ "dependencies": {
15
+ "@squad-sports/core": "1.3.1"
16
+ },
17
+ "peerDependencies": {
18
+ "react": ">=18.0.0",
19
+ "react-native": ">=0.72.0",
20
+ "@react-native-async-storage/async-storage": ">=1.20.0",
21
+ "@react-navigation/native": ">=6.0.0",
22
+ "@react-navigation/native-stack": ">=6.0.0",
23
+ "@react-navigation/bottom-tabs": ">=6.0.0",
24
+ "@gorhom/bottom-sheet": ">=5.0.0",
25
+ "react-native-gesture-handler": ">=2.0.0",
26
+ "react-native-safe-area-context": ">=4.0.0",
27
+ "react-native-screens": ">=3.0.0",
28
+ "react-native-reanimated": ">=3.0.0",
29
+ "recoil": ">=0.7.0",
30
+ "expo-av": ">=15.0.0",
31
+ "expo-image": ">=2.0.0",
32
+ "@twilio/voice-react-native-sdk": ">=1.0.0",
33
+ "expo-secure-store": ">=14.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "expo-secure-store": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/withyoursquad/squad-sports-react-native.git"
44
+ },
45
+ "homepage": "https://docs.squadforsports.com"
46
+ }
@@ -0,0 +1,200 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { ActivityIndicator, Pressable, Text, View } from 'react-native';
3
+ import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
4
+ import type { NavigationContainerRef } from '@react-navigation/native';
5
+ import { AnalyticsTracker } from '@squad-sports/core';
6
+ import type { SquadSDKConfig, SquadConfig, SSOProvider, PartnerUserData } from '@squad-sports/core';
7
+
8
+ import { SquadProvider } from './SquadProvider';
9
+ import { SquadNavigator } from './navigation/SquadNavigator';
10
+ import { SquadSportsSDK } from './SquadSportsSDK';
11
+ import { useTheme } from './theme/ThemeContext';
12
+ import { ScreenErrorBoundary } from './components/ErrorBoundary';
13
+
14
+ function SquadNavigationContainer() {
15
+ const { theme } = useTheme();
16
+ const navigationRef = useRef<NavigationContainerRef<any>>(null);
17
+ const routeNameRef = useRef<string | undefined>(undefined);
18
+
19
+ const navigationTheme = {
20
+ ...DefaultTheme,
21
+ colors: {
22
+ ...DefaultTheme.colors,
23
+ background: theme.screenBackground,
24
+ },
25
+ };
26
+
27
+ const onReady = useCallback(() => {
28
+ routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name;
29
+ }, []);
30
+
31
+ const onStateChange = useCallback(() => {
32
+ const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
33
+ if (currentRouteName && currentRouteName !== routeNameRef.current) {
34
+ AnalyticsTracker.shared.trackScreen(currentRouteName);
35
+ routeNameRef.current = currentRouteName;
36
+ }
37
+ }, []);
38
+
39
+ return (
40
+ <NavigationContainer
41
+ ref={navigationRef}
42
+ theme={navigationTheme}
43
+ {...{ independent: true } as any}
44
+ onReady={onReady}
45
+ onStateChange={onStateChange}
46
+ >
47
+ <SquadNavigator />
48
+ </NavigationContainer>
49
+ );
50
+ }
51
+
52
+ interface SquadExperienceProps {
53
+ // --- Simple path (recommended) ---
54
+
55
+ /** Your Nexus partner ID. Everything else auto-resolves. */
56
+ partnerId?: string;
57
+
58
+ /** Your API key from partners.squadforsports.com. Required when using partnerId. */
59
+ apiKey?: string;
60
+
61
+ /** Pre-authenticated SSO token (Ticketmaster, OAuth, etc.) */
62
+ ssoToken?: string;
63
+
64
+ /** SSO provider (defaults to 'ticketmaster' if ssoToken is provided) */
65
+ ssoProvider?: SSOProvider;
66
+
67
+ /** User data from host app for seamless partner auth (no login required) */
68
+ userData?: PartnerUserData;
69
+
70
+ // --- Full config path ---
71
+
72
+ /** Full SDK configuration (overrides partnerId if both provided) */
73
+ config?: SquadConfig;
74
+
75
+ // --- Callbacks ---
76
+
77
+ /** Called when SDK initialization completes */
78
+ onReady?: () => void;
79
+
80
+ /** Called if SDK initialization fails */
81
+ onError?: (error: Error) => void;
82
+ }
83
+
84
+ /**
85
+ * Self-contained Squad Sports experience.
86
+ *
87
+ * Drop this into any React Native app. Three integration levels:
88
+ *
89
+ * **Simplest (Nexus partner):**
90
+ * ```tsx
91
+ * <SquadExperience partnerId="acme-sports" />
92
+ * ```
93
+ *
94
+ * **With Ticketmaster SSO:**
95
+ * ```tsx
96
+ * <SquadExperience partnerId="acme-sports" ssoToken={tmToken} />
97
+ * ```
98
+ *
99
+ * **Full control:**
100
+ * ```tsx
101
+ * <SquadExperience config={{ apiKey: '...', environment: 'production', community: { ... } }} />
102
+ * ```
103
+ */
104
+ export function SquadExperience({
105
+ partnerId,
106
+ apiKey,
107
+ ssoToken,
108
+ ssoProvider,
109
+ userData,
110
+ config: configProp,
111
+ onReady,
112
+ onError,
113
+ }: SquadExperienceProps) {
114
+ const [resolvedConfig, setResolvedConfig] = useState<SquadSDKConfig | null>(null);
115
+ const [error, setError] = useState<Error | null>(null);
116
+ const [retryCount, setRetryCount] = useState(0);
117
+
118
+ useEffect(() => {
119
+ const init = async () => {
120
+ setError(null);
121
+
122
+ try {
123
+ let sdkConfig: SquadConfig;
124
+
125
+ if (configProp) {
126
+ // Full config provided
127
+ sdkConfig = configProp;
128
+ } else if (partnerId) {
129
+ if (!apiKey) {
130
+ throw new Error(
131
+ '[SquadExperience] apiKey is required when using partnerId. Get your key at partners.squadforsports.com',
132
+ );
133
+ }
134
+ // Simple partner path
135
+ sdkConfig = {
136
+ partnerId,
137
+ apiKey,
138
+ ssoToken,
139
+ ssoProvider: ssoProvider ?? (ssoToken ? 'ticketmaster' : undefined),
140
+ userData,
141
+ };
142
+ } else {
143
+ throw new Error(
144
+ '[SquadExperience] Either partnerId or config is required.',
145
+ );
146
+ }
147
+
148
+ const sdk = await SquadSportsSDK.setup(sdkConfig);
149
+ setResolvedConfig(sdk.config);
150
+ onReady?.();
151
+ } catch (err) {
152
+ const error = err instanceof Error ? err : new Error(String(err));
153
+ setError(error);
154
+ onError?.(error);
155
+ }
156
+ };
157
+
158
+ init();
159
+ }, [partnerId, ssoToken, ssoProvider, userData, retryCount]);
160
+
161
+ if (error) {
162
+ return (
163
+ <View style={{ flex: 1, backgroundColor: '#111111', justifyContent: 'center', alignItems: 'center', padding: 24 }}>
164
+ <View style={{ backgroundColor: '#1D1D1D', borderRadius: 12, padding: 24, maxWidth: 320 }}>
165
+ <View style={{ width: 48, height: 48, borderRadius: 24, backgroundColor: '#FF4478', justifyContent: 'center', alignItems: 'center', alignSelf: 'center', marginBottom: 16 }}>
166
+ <Text style={{ color: '#fff', fontSize: 24, fontWeight: '700' }}>!</Text>
167
+ </View>
168
+ <Text style={{ color: '#FFFFFF', fontSize: 16, fontWeight: '600', textAlign: 'center', marginBottom: 8 }}>
169
+ Unable to load Squad
170
+ </Text>
171
+ <Text style={{ color: '#8A8A8A', fontSize: 13, textAlign: 'center', marginBottom: 20 }}>
172
+ {error.message}
173
+ </Text>
174
+ <Pressable
175
+ onPress={() => setRetryCount(c => c + 1)}
176
+ style={{ backgroundColor: '#6E82E7', borderRadius: 8, paddingVertical: 12, paddingHorizontal: 24, alignSelf: 'center' }}
177
+ >
178
+ <Text style={{ color: '#FFFFFF', fontSize: 14, fontWeight: '600' }}>Try Again</Text>
179
+ </Pressable>
180
+ </View>
181
+ </View>
182
+ );
183
+ }
184
+
185
+ if (!resolvedConfig) {
186
+ return (
187
+ <View style={{ flex: 1, backgroundColor: '#111111', justifyContent: 'center', alignItems: 'center' }}>
188
+ <ActivityIndicator size="large" color="#6E82E7" />
189
+ </View>
190
+ );
191
+ }
192
+
193
+ return (
194
+ <ScreenErrorBoundary screenName="SquadExperience">
195
+ <SquadProvider config={resolvedConfig}>
196
+ <SquadNavigationContainer />
197
+ </SquadProvider>
198
+ </ScreenErrorBoundary>
199
+ );
200
+ }
@@ -0,0 +1,232 @@
1
+ import React, { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
2
+ import { ActivityIndicator, AppState, View } from 'react-native';
3
+ import type { AppStateStatus } from 'react-native';
4
+ import { RecoilRoot } from 'recoil';
5
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
6
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
7
+
8
+ import { AnalyticsTracker } from '@squad-sports/core';
9
+ import type { SquadApiClient } from '@squad-sports/core';
10
+ import type { SquadSDKConfig, SquadConfig } from '@squad-sports/core';
11
+
12
+ import { SquadSportsSDK } from './SquadSportsSDK';
13
+ import { ThemeProvider } from './theme/ThemeContext';
14
+ import { EventProcessor, type ConnectionQuality } from './realtime/EventProcessor';
15
+ import { NetworkMonitor } from './realtime/NetworkMonitor';
16
+ import { OfflineQueue } from './realtime/OfflineQueue';
17
+ import { NetworkBanner } from './components/ux/layout/NetworkBanner';
18
+
19
+ // --- Contexts ---
20
+
21
+ const SDKContext = createContext<SquadSportsSDK | null>(null);
22
+ const ApiClientContext = createContext<SquadApiClient | null>(null);
23
+
24
+ export function useSquadSDK(): SquadSportsSDK {
25
+ const sdk = useContext(SDKContext);
26
+ if (!sdk) {
27
+ throw new Error('useSquadSDK must be used within <SquadProvider>');
28
+ }
29
+ return sdk;
30
+ }
31
+
32
+ export function useApiClient(): SquadApiClient {
33
+ const client = useContext(ApiClientContext);
34
+ if (!client) {
35
+ throw new Error('useApiClient must be used within <SquadProvider>');
36
+ }
37
+ return client;
38
+ }
39
+
40
+ // --- Internal: auto-connects real-time sync when authenticated ---
41
+
42
+ function RealtimeSyncSentinel({ apiClient }: { apiClient: SquadApiClient }) {
43
+ const [isOnline, setIsOnline] = useState(true);
44
+ const [sseQuality, setSSEQuality] = useState<ConnectionQuality>('disconnected');
45
+
46
+ useEffect(() => {
47
+ // Wire up EventProcessor
48
+ const processor = EventProcessor.shared;
49
+ processor.setApiClient(apiClient);
50
+
51
+ // Only connect SSE if we have a token
52
+ if (apiClient.currentToken) {
53
+ processor.setShouldAllowEvents(true);
54
+ processor.connect();
55
+ }
56
+
57
+ const onQuality = (q: ConnectionQuality) => setSSEQuality(q);
58
+ processor.emitter.on('connection:quality', onQuality);
59
+
60
+ // Wire up network monitoring
61
+ const monitor = NetworkMonitor.shared;
62
+ monitor.startMonitoring();
63
+
64
+ const onNetworkChange = (online: boolean) => {
65
+ setIsOnline(online);
66
+
67
+ if (online) {
68
+ // Reconnect SSE when back online
69
+ if (apiClient.currentToken) {
70
+ processor.setShouldAllowEvents(true);
71
+ processor.connect();
72
+ }
73
+
74
+ // Flush offline queue
75
+ const offlineQueue = new OfflineQueue();
76
+ offlineQueue.processQueue(async (action) => {
77
+ // Route actions to appropriate API calls
78
+ try {
79
+ switch (action.type) {
80
+ case 'message:create': {
81
+ const { Message } = await import('@squad-sports/core');
82
+ const msg = new Message(action.payload as any);
83
+ await apiClient.createMessage(msg);
84
+ return true;
85
+ }
86
+ case 'freestyle:reaction': {
87
+ const { FreestyleReaction } = await import('@squad-sports/core');
88
+ const payload = action.payload as any;
89
+ const reaction = new FreestyleReaction(payload.reaction);
90
+ await apiClient.createFreestyleReaction(reaction, payload.freestyleId);
91
+ return true;
92
+ }
93
+ case 'message:reaction': {
94
+ const { MessageReaction } = await import('@squad-sports/core');
95
+ const payload = action.payload as any;
96
+ const reaction = new MessageReaction(payload.reaction);
97
+ await apiClient.createMessageReaction(reaction, payload.messageId);
98
+ return true;
99
+ }
100
+ default:
101
+ return false;
102
+ }
103
+ } catch {
104
+ return false;
105
+ }
106
+ });
107
+ } else {
108
+ processor.setShouldAllowEvents(false);
109
+ }
110
+ };
111
+
112
+ monitor.addListener(onNetworkChange);
113
+
114
+ // Listen for auth invalidation — disconnect SSE
115
+ const onAuthInvalid = () => {
116
+ processor.setShouldAllowEvents(false);
117
+ processor.disconnect();
118
+ };
119
+ apiClient.emitter.on('auth:invalid', onAuthInvalid);
120
+
121
+ return () => {
122
+ processor.emitter.off('connection:quality', onQuality);
123
+ monitor.removeListener(onNetworkChange);
124
+ apiClient.emitter.off('auth:invalid', onAuthInvalid);
125
+ };
126
+ }, [apiClient]);
127
+
128
+ // Return banner visibility — only show when offline
129
+ return <NetworkBanner isOnline={isOnline} />;
130
+ }
131
+
132
+ // --- Provider ---
133
+
134
+ interface SquadProviderProps {
135
+ children: React.ReactNode;
136
+ /** Full or partner config. If omitted, uses SquadSportsSDK.shared. */
137
+ config?: SquadConfig;
138
+ }
139
+
140
+ /**
141
+ * Top-level provider that wraps the SDK experience.
142
+ * Automatically sets up:
143
+ * - RecoilRoot (state management)
144
+ * - SafeAreaProvider
145
+ * - GestureHandler
146
+ * - Theme (from community config)
147
+ * - API client context
148
+ * - Real-time SSE sync (auto-connects when authenticated)
149
+ * - Network monitoring + offline queue
150
+ * - Offline banner
151
+ */
152
+ export function SquadProvider({ children, config }: SquadProviderProps) {
153
+ const [sdk, setSDK] = useState<SquadSportsSDK | null>(null);
154
+ const [ready, setReady] = useState(false);
155
+
156
+ useEffect(() => {
157
+ const init = async () => {
158
+ try {
159
+ let instance: SquadSportsSDK;
160
+
161
+ if (config) {
162
+ instance = await SquadSportsSDK.setup(config);
163
+ } else {
164
+ instance = SquadSportsSDK.shared;
165
+ }
166
+
167
+ setSDK(instance);
168
+ } catch (error) {
169
+ console.error('[SquadProvider] Initialization error:', error);
170
+ } finally {
171
+ setReady(true);
172
+ }
173
+ };
174
+
175
+ init();
176
+ }, []);
177
+
178
+ // App lifecycle — flush analytics on background, reconnect on foreground
179
+ useEffect(() => {
180
+ if (!sdk) return;
181
+
182
+ const handleAppStateChange = (nextState: AppStateStatus) => {
183
+ if (nextState === 'background' || nextState === 'inactive') {
184
+ AnalyticsTracker.shared.flush();
185
+ EventProcessor.shared.setShouldAllowEvents(false);
186
+ } else if (nextState === 'active') {
187
+ if (sdk.apiClient.currentToken) {
188
+ EventProcessor.shared.setShouldAllowEvents(true);
189
+ EventProcessor.shared.connect();
190
+ sdk.apiClient.validateToken().then((valid: boolean) => {
191
+ if (!valid) {
192
+ sdk.apiClient.emitter.emit('auth:invalid', { status: 401, url: '' });
193
+ }
194
+ });
195
+ }
196
+ }
197
+ };
198
+
199
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
200
+ return () => subscription.remove();
201
+ }, [sdk]);
202
+
203
+ if (!ready || !sdk) {
204
+ return (
205
+ <View style={{ flex: 1, backgroundColor: '#111111', justifyContent: 'center', alignItems: 'center' }}>
206
+ <ActivityIndicator size="large" color="#ffffff" />
207
+ </View>
208
+ );
209
+ }
210
+
211
+ const communityConfig = sdk.config.community;
212
+
213
+ return (
214
+ <SDKContext.Provider value={sdk}>
215
+ <ApiClientContext.Provider value={sdk.apiClient}>
216
+ <RecoilRoot>
217
+ <SafeAreaProvider>
218
+ <GestureHandlerRootView style={{ flex: 1 }}>
219
+ <ThemeProvider
220
+ communityColor={communityConfig.primaryColor}
221
+ communitySecondaryColor={communityConfig.secondaryColor}
222
+ >
223
+ <RealtimeSyncSentinel apiClient={sdk.apiClient} />
224
+ {children}
225
+ </ThemeProvider>
226
+ </GestureHandlerRootView>
227
+ </SafeAreaProvider>
228
+ </RecoilRoot>
229
+ </ApiClientContext.Provider>
230
+ </SDKContext.Provider>
231
+ );
232
+ }