@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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +46 -0
- package/src/SquadExperience.tsx +200 -0
- package/src/SquadProvider.tsx +232 -0
- package/src/SquadSportsSDK.ts +286 -0
- package/src/__tests__/DeepLinkHandler.test.ts +101 -0
- package/src/__tests__/ErrorBoundary.test.tsx +161 -0
- package/src/__tests__/EventProcessor.test.ts +241 -0
- package/src/__tests__/PushNotificationHandler.test.ts +91 -0
- package/src/__tests__/SecureStorage.test.ts +62 -0
- package/src/__tests__/SquadSportsSDK.test.ts +278 -0
- package/src/__tests__/VerificationCooldown.test.ts +153 -0
- package/src/components/ErrorBoundary.tsx +129 -0
- package/src/components/SOTD/SOTDComponents.tsx +101 -0
- package/src/components/audio/AudioPlayerRow.tsx +189 -0
- package/src/components/audio/recording/AudioRecording.tsx +232 -0
- package/src/components/communities/CommunityComponents.tsx +78 -0
- package/src/components/dialogs/AllDialogs.tsx +123 -0
- package/src/components/dialogs/ConfirmDialog.tsx +77 -0
- package/src/components/dialogs/PermissionDialog.tsx +132 -0
- package/src/components/dragAndDrop/DragAndDrop.tsx +56 -0
- package/src/components/events/EventComponents.tsx +93 -0
- package/src/components/feed/ChatBannerCard.tsx +94 -0
- package/src/components/feed/FeedLoadingSkeleton.tsx +43 -0
- package/src/components/feed/FreestyleCard.tsx +119 -0
- package/src/components/feed/InterstitialOverlay.tsx +190 -0
- package/src/components/feed/PollCard.tsx +158 -0
- package/src/components/feed/SponsoredContentCard.tsx +118 -0
- package/src/components/freestyle/FreestyleComponents.tsx +148 -0
- package/src/components/index.ts +42 -0
- package/src/components/message/MessageCard.tsx +166 -0
- package/src/components/message/MessageComponents.tsx +143 -0
- package/src/components/poll/PollComponents.tsx +226 -0
- package/src/components/sentinels/Sentinels.tsx +175 -0
- package/src/components/squad/FeedSquad.tsx +54 -0
- package/src/components/toasts/Toasts.tsx +88 -0
- package/src/components/ux/RootUXComponents.tsx +157 -0
- package/src/components/ux/action-sheet/ActionSheet.tsx +67 -0
- package/src/components/ux/bottom-sheet/BottomSheetComponents.tsx +93 -0
- package/src/components/ux/bottom-sheet/SquadBottomSheet.tsx +119 -0
- package/src/components/ux/buttons/Button.tsx +37 -0
- package/src/components/ux/buttons/InfoButton.tsx +24 -0
- package/src/components/ux/buttons/XButton.tsx +27 -0
- package/src/components/ux/carousel/Carousel.tsx +134 -0
- package/src/components/ux/emoji-picker/EmojiReactionPicker.tsx +139 -0
- package/src/components/ux/errors/ErrorHint.tsx +46 -0
- package/src/components/ux/inputs/CodeInput.tsx +121 -0
- package/src/components/ux/inputs/DatePicker.tsx +76 -0
- package/src/components/ux/inputs/MaskedTextInput.tsx +95 -0
- package/src/components/ux/inputs/PhoneNumberInput.tsx +90 -0
- package/src/components/ux/inputs/SearchTextInput.tsx +70 -0
- package/src/components/ux/inputs/TextInput.tsx +58 -0
- package/src/components/ux/layout/AvoidKeyboardScreen.tsx +58 -0
- package/src/components/ux/layout/BlurOverlay.tsx +26 -0
- package/src/components/ux/layout/CrossFade.tsx +30 -0
- package/src/components/ux/layout/DismissKeyboardOnBlur.tsx +14 -0
- package/src/components/ux/layout/LoadingOverlay.tsx +60 -0
- package/src/components/ux/layout/NetworkBanner.tsx +64 -0
- package/src/components/ux/layout/PermissionsCTAContent.tsx +54 -0
- package/src/components/ux/layout/RefreshControl.tsx +7 -0
- package/src/components/ux/layout/Screen.tsx +31 -0
- package/src/components/ux/layout/ScreenHeader.tsx +89 -0
- package/src/components/ux/layout/TabBar.tsx +39 -0
- package/src/components/ux/layout/Toast.tsx +116 -0
- package/src/components/ux/layout/TransparentOverlay.tsx +21 -0
- package/src/components/ux/navigation/BackButton.tsx +29 -0
- package/src/components/ux/navigation/LinkButton.tsx +21 -0
- package/src/components/ux/navigation/UrlButton.tsx +25 -0
- package/src/components/ux/scroll/RefreshableFlatList.tsx +35 -0
- package/src/components/ux/shapes/Shapes.tsx +23 -0
- package/src/components/ux/text/Typography.tsx +28 -0
- package/src/components/ux/user-image/UserImage.tsx +75 -0
- package/src/components/ux/user-image/UserImageVariants.tsx +104 -0
- package/src/components/wallet/WalletComponents.tsx +116 -0
- package/src/contexts/AuthContext.tsx +45 -0
- package/src/contexts/EventProcessorContext.tsx +41 -0
- package/src/contexts/PlayerQueueContext.tsx +95 -0
- package/src/hooks/useAuth.ts +23 -0
- package/src/hooks/useDataRefresh.ts +30 -0
- package/src/hooks/useEventProcessor.ts +6 -0
- package/src/hooks/useImageOptimization.ts +59 -0
- package/src/hooks/useOnboardingStepGuard.ts +36 -0
- package/src/hooks/usePendingNavigation.ts +26 -0
- package/src/hooks/useSquadData.ts +84 -0
- package/src/hooks/useUserCreated.ts +25 -0
- package/src/hooks/useUserUpdate.ts +25 -0
- package/src/hooks/useViewabilityTracker.ts +40 -0
- package/src/index.ts +109 -0
- package/src/navigation/SquadNavigator.tsx +262 -0
- package/src/realtime/DeepLinkHandler.ts +113 -0
- package/src/realtime/EventProcessor.ts +313 -0
- package/src/realtime/NetworkMonitor.ts +84 -0
- package/src/realtime/OfflineQueue.ts +133 -0
- package/src/realtime/PushNotificationHandler.ts +125 -0
- package/src/realtime/useRealtimeSync.ts +84 -0
- package/src/screens/auth/EmailVerificationScreen.tsx +201 -0
- package/src/screens/auth/EnterCodeScreen.tsx +253 -0
- package/src/screens/auth/EnterEmailScreen.tsx +234 -0
- package/src/screens/auth/LandingScreen.tsx +90 -0
- package/src/screens/auth/LoginScreen.tsx +126 -0
- package/src/screens/events/EventScreen.tsx +163 -0
- package/src/screens/freestyle/CommunityFreestyleScreen.tsx +82 -0
- package/src/screens/freestyle/FreestyleCreationScreen.tsx +255 -0
- package/src/screens/freestyle/FreestyleListensScreen.tsx +44 -0
- package/src/screens/freestyle/FreestyleReactionsScreen.tsx +44 -0
- package/src/screens/freestyle/FreestyleScreen.tsx +64 -0
- package/src/screens/home/HomeScreen.tsx +365 -0
- package/src/screens/home/slivers/SquadCircle.tsx +77 -0
- package/src/screens/invite/InvitationQrCodeScreen.tsx +114 -0
- package/src/screens/invite/InviteScreen.tsx +175 -0
- package/src/screens/messaging/MessagingScreen.tsx +360 -0
- package/src/screens/onboarding/OnboardingAccountSetupScreen.tsx +221 -0
- package/src/screens/onboarding/OnboardingTeamSelectScreen.tsx +215 -0
- package/src/screens/onboarding/OnboardingWelcomeScreen.tsx +93 -0
- package/src/screens/polls/PollResponseScreen.tsx +229 -0
- package/src/screens/polls/PollSummationScreen.tsx +78 -0
- package/src/screens/profile/ProfileScreen.tsx +234 -0
- package/src/screens/settings/BlockedUsersScreen.tsx +139 -0
- package/src/screens/settings/DeleteAccountScreen.tsx +182 -0
- package/src/screens/settings/EditProfileScreen.tsx +154 -0
- package/src/screens/settings/NetworkStatusScreen.tsx +59 -0
- package/src/screens/settings/SettingsScreen.tsx +194 -0
- package/src/screens/squad-line/AddCallTitleScreen.tsx +293 -0
- package/src/screens/wallet/WalletScreen.tsx +174 -0
- package/src/services/AuthStateManager.ts +93 -0
- package/src/services/NavigationService.ts +40 -0
- package/src/services/UserDataManager.ts +59 -0
- package/src/services/UserUpdateService.ts +31 -0
- package/src/services/VerificationStateManager.ts +41 -0
- package/src/squad-line/CallScreen.tsx +158 -0
- package/src/squad-line/IncomingCallOverlay.tsx +113 -0
- package/src/squad-line/SquadLineClient.ts +327 -0
- package/src/squad-line/useSquadLine.ts +80 -0
- package/src/state/audio.ts +38 -0
- package/src/state/client.ts +26 -0
- package/src/state/communities.ts +45 -0
- package/src/state/contacts.ts +28 -0
- package/src/state/device-info.ts +22 -0
- package/src/state/events.ts +16 -0
- package/src/state/features.ts +57 -0
- package/src/state/index.ts +121 -0
- package/src/state/invitations.ts +16 -0
- package/src/state/modal-keys.ts +63 -0
- package/src/state/modal-queue.ts +104 -0
- package/src/state/navigation.ts +34 -0
- package/src/state/permissions.ts +43 -0
- package/src/state/session.ts +223 -0
- package/src/state/squaddie-of-the-day.ts +21 -0
- package/src/state/sync/crdt.ts +70 -0
- package/src/state/sync/dependable.ts +213 -0
- package/src/state/sync/feed-v2.ts +42 -0
- package/src/state/sync/messages.ts +44 -0
- package/src/state/sync/offline-support.ts +42 -0
- package/src/state/sync/polls.ts +37 -0
- package/src/state/sync/refresh.ts +24 -0
- package/src/state/sync/squad-v2.ts +25 -0
- package/src/state/ui.ts +36 -0
- package/src/state/user.ts +46 -0
- package/src/state/wallet.ts +26 -0
- package/src/storage/SecureStorage.ts +77 -0
- package/src/theme/ThemeContext.tsx +159 -0
- 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
|
+
[](https://www.npmjs.com/package/@squad-sports/react-native)
|
|
4
|
+
[](https://docs.squadforsports.com)
|
|
5
|
+
[](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
|
+
}
|