@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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SquadApiClient,
|
|
3
|
+
SessionManager,
|
|
4
|
+
SSOManager,
|
|
5
|
+
TokenStorage,
|
|
6
|
+
InMemoryStorage,
|
|
7
|
+
NexusPartnerRegistry,
|
|
8
|
+
PartnerAuthManager,
|
|
9
|
+
AnalyticsTracker,
|
|
10
|
+
Logger,
|
|
11
|
+
getApiBaseUrl,
|
|
12
|
+
isPartnerConfig,
|
|
13
|
+
} from '@squad-sports/core';
|
|
14
|
+
import type {
|
|
15
|
+
SquadSDKConfig,
|
|
16
|
+
SquadPartnerConfig,
|
|
17
|
+
SquadConfig,
|
|
18
|
+
StorageAdapter,
|
|
19
|
+
} from '@squad-sports/core';
|
|
20
|
+
import { SecureStorageAdapter } from './storage/SecureStorage';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Main entry point for the Squad Sports SDK.
|
|
24
|
+
*
|
|
25
|
+
* Two ways to initialize:
|
|
26
|
+
*
|
|
27
|
+
* **Simple (recommended):** Just provide your partner ID.
|
|
28
|
+
* Everything auto-resolves from the Nexus partner registry.
|
|
29
|
+
* ```ts
|
|
30
|
+
* await SquadSportsSDK.setup({ partnerId: 'acme-sports' });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* **With SSO:** Pass a Ticketmaster (or other) token and users are auto-authenticated.
|
|
34
|
+
* ```ts
|
|
35
|
+
* await SquadSportsSDK.setup({
|
|
36
|
+
* partnerId: 'acme-sports',
|
|
37
|
+
* ssoToken: ticketmasterAccessToken,
|
|
38
|
+
* ssoProvider: 'ticketmaster',
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* **Full control:** Provide every config value manually.
|
|
43
|
+
* ```ts
|
|
44
|
+
* SquadSportsSDK.initialize({ apiKey: '...', environment: 'production', community: { ... } });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class SquadSportsSDK {
|
|
48
|
+
private static instance: SquadSportsSDK | null = null;
|
|
49
|
+
|
|
50
|
+
readonly config: SquadSDKConfig;
|
|
51
|
+
readonly apiClient: SquadApiClient;
|
|
52
|
+
readonly sessionManager: SessionManager;
|
|
53
|
+
readonly ssoManager: SSOManager;
|
|
54
|
+
readonly partnerAuthManager: PartnerAuthManager;
|
|
55
|
+
readonly tokenStorage: TokenStorage;
|
|
56
|
+
|
|
57
|
+
private constructor(config: SquadSDKConfig) {
|
|
58
|
+
this.config = config;
|
|
59
|
+
|
|
60
|
+
const storage: StorageAdapter = config.storage ?? new SecureStorageAdapter();
|
|
61
|
+
this.tokenStorage = new TokenStorage(storage);
|
|
62
|
+
|
|
63
|
+
const baseUrl = getApiBaseUrl(config.environment, config.apiBaseUrl);
|
|
64
|
+
|
|
65
|
+
this.apiClient = new SquadApiClient({
|
|
66
|
+
baseUrl,
|
|
67
|
+
apiKey: config.apiKey,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.sessionManager = new SessionManager(this.apiClient, this.tokenStorage);
|
|
71
|
+
this.ssoManager = new SSOManager(this.apiClient, this.tokenStorage);
|
|
72
|
+
this.partnerAuthManager = new PartnerAuthManager(this.apiClient, this.tokenStorage);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Initialization paths ---
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* RECOMMENDED: One-step async setup.
|
|
79
|
+
* Accepts either a simple partnerId config or a full config.
|
|
80
|
+
* Resolves partner config from Nexus, restores session, and handles SSO.
|
|
81
|
+
*/
|
|
82
|
+
static async setup(config: SquadConfig): Promise<SquadSportsSDK> {
|
|
83
|
+
let resolvedConfig: SquadSDKConfig;
|
|
84
|
+
|
|
85
|
+
if (isPartnerConfig(config)) {
|
|
86
|
+
// Simple path — resolve everything from partner ID
|
|
87
|
+
resolvedConfig = await NexusPartnerRegistry.resolve(config);
|
|
88
|
+
} else {
|
|
89
|
+
resolvedConfig = config;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
SquadSportsSDK.reset();
|
|
93
|
+
const sdk = new SquadSportsSDK(resolvedConfig);
|
|
94
|
+
SquadSportsSDK.instance = sdk;
|
|
95
|
+
|
|
96
|
+
// Configure logger
|
|
97
|
+
Logger.shared.configure({
|
|
98
|
+
partnerId: resolvedConfig.partnerAuth?.partnerId,
|
|
99
|
+
environment: resolvedConfig.environment,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Configure analytics
|
|
103
|
+
AnalyticsTracker.shared.configure({
|
|
104
|
+
apiClient: sdk.apiClient,
|
|
105
|
+
partnerId: resolvedConfig.partnerAuth?.partnerId,
|
|
106
|
+
enabled: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Auto-restore previous session
|
|
110
|
+
const restored = await sdk.restoreSession();
|
|
111
|
+
|
|
112
|
+
if (restored) {
|
|
113
|
+
AnalyticsTracker.shared.track('session_restored');
|
|
114
|
+
const userId = await sdk.tokenStorage.getUserId();
|
|
115
|
+
if (userId) {
|
|
116
|
+
AnalyticsTracker.shared.setUserId(userId);
|
|
117
|
+
Logger.shared.setUserId(userId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If restored, check community scoping — re-auth if community changed
|
|
122
|
+
if (restored && resolvedConfig.partnerAuth?.communityId) {
|
|
123
|
+
const storedCommunityId = await sdk.tokenStorage.getCommunityId();
|
|
124
|
+
if (storedCommunityId && storedCommunityId !== resolvedConfig.partnerAuth.communityId) {
|
|
125
|
+
await sdk.tokenStorage.clearAll();
|
|
126
|
+
sdk.apiClient.clearAuthState();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Auto-authenticate via SSO if configured
|
|
131
|
+
if (resolvedConfig.sso?.accessToken && !sdk.apiClient.currentToken) {
|
|
132
|
+
await sdk.ssoManager.authenticate(resolvedConfig.sso);
|
|
133
|
+
AnalyticsTracker.shared.track('sso_login', { provider: resolvedConfig.sso.provider });
|
|
134
|
+
// Persist community/partner context after SSO
|
|
135
|
+
if (resolvedConfig.partnerAuth && sdk.apiClient.currentToken) {
|
|
136
|
+
await sdk.tokenStorage.setCommunityId(resolvedConfig.partnerAuth.communityId);
|
|
137
|
+
await sdk.tokenStorage.setPartnerId(resolvedConfig.partnerAuth.partnerId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Partner user sync — if userData provided and still no valid token
|
|
142
|
+
if (resolvedConfig.partnerAuth?.userData && !sdk.apiClient.currentToken) {
|
|
143
|
+
const syncResult = await sdk.partnerAuthManager.syncUser(
|
|
144
|
+
resolvedConfig.partnerAuth.partnerId,
|
|
145
|
+
resolvedConfig.partnerAuth.communityId,
|
|
146
|
+
resolvedConfig.partnerAuth.userData,
|
|
147
|
+
);
|
|
148
|
+
if (syncResult.success) {
|
|
149
|
+
AnalyticsTracker.shared.track('partner_sync_success', { partnerId: resolvedConfig.partnerAuth.partnerId });
|
|
150
|
+
if (syncResult.userId) {
|
|
151
|
+
AnalyticsTracker.shared.setUserId(syncResult.userId);
|
|
152
|
+
Logger.shared.setUserId(syncResult.userId);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
AnalyticsTracker.shared.track('partner_sync_failed', { partnerId: resolvedConfig.partnerAuth.partnerId });
|
|
156
|
+
Logger.shared.warn('Partner user sync failed. User will be directed to auth screen.');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Wire silent re-auth on 401 (interceptor-level, can retry original request)
|
|
161
|
+
// Supports both partner sync and SSO token refresh
|
|
162
|
+
const ssoConfig = resolvedConfig.sso;
|
|
163
|
+
const partnerAuth = resolvedConfig.partnerAuth;
|
|
164
|
+
const hasPartnerUserData = !!partnerAuth?.userData;
|
|
165
|
+
const hasSSOToken = !!ssoConfig?.accessToken;
|
|
166
|
+
|
|
167
|
+
if (hasPartnerUserData || hasSSOToken) {
|
|
168
|
+
sdk.apiClient.setSilentReAuth(async () => {
|
|
169
|
+
// Try partner re-auth first (most reliable — server-side user sync)
|
|
170
|
+
if (hasPartnerUserData && partnerAuth?.userData) {
|
|
171
|
+
Logger.shared.info('Attempting silent partner re-auth');
|
|
172
|
+
const reSync = await sdk.partnerAuthManager.syncUser(
|
|
173
|
+
partnerAuth.partnerId,
|
|
174
|
+
partnerAuth.communityId,
|
|
175
|
+
partnerAuth.userData,
|
|
176
|
+
);
|
|
177
|
+
if (reSync.success) {
|
|
178
|
+
Logger.shared.info('Silent partner re-auth succeeded');
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Try SSO token re-exchange (works if the external SSO token is still valid)
|
|
184
|
+
if (hasSSOToken && ssoConfig) {
|
|
185
|
+
Logger.shared.info('Attempting silent SSO re-auth');
|
|
186
|
+
try {
|
|
187
|
+
const result = await sdk.ssoManager.authenticate(ssoConfig);
|
|
188
|
+
if (result?.success) {
|
|
189
|
+
Logger.shared.info('Silent SSO re-auth succeeded');
|
|
190
|
+
// Persist community/partner context after SSO refresh
|
|
191
|
+
if (partnerAuth && sdk.apiClient.currentToken) {
|
|
192
|
+
await sdk.tokenStorage.setCommunityId(partnerAuth.communityId);
|
|
193
|
+
await sdk.tokenStorage.setPartnerId(partnerAuth.partnerId);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
Logger.shared.warn('Silent SSO re-auth failed');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Logger.shared.warn('All silent re-auth attempts failed');
|
|
203
|
+
return false;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
AnalyticsTracker.shared.track('sdk_initialized', {
|
|
208
|
+
partnerId: resolvedConfig.partnerAuth?.partnerId,
|
|
209
|
+
environment: resolvedConfig.environment,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return sdk;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Synchronous initialization with full config (no auto-resolution).
|
|
217
|
+
* Use this when you already have all config values.
|
|
218
|
+
*/
|
|
219
|
+
static initialize(config: SquadSDKConfig): SquadSportsSDK {
|
|
220
|
+
if (SquadSportsSDK.instance) {
|
|
221
|
+
console.warn('[SquadSportsSDK] Already initialized. Call reset() first to reinitialize.');
|
|
222
|
+
return SquadSportsSDK.instance;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
SquadSportsSDK.instance = new SquadSportsSDK(config);
|
|
226
|
+
return SquadSportsSDK.instance;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get the current SDK instance. Throws if not initialized.
|
|
231
|
+
*/
|
|
232
|
+
static get shared(): SquadSportsSDK {
|
|
233
|
+
if (!SquadSportsSDK.instance) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
'[SquadSportsSDK] Not initialized. Call SquadSportsSDK.setup() or SquadSportsSDK.initialize() first.',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return SquadSportsSDK.instance;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static get isInitialized(): boolean {
|
|
242
|
+
return SquadSportsSDK.instance !== null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static reset(): void {
|
|
246
|
+
if (SquadSportsSDK.instance) {
|
|
247
|
+
SquadSportsSDK.instance.apiClient.cancelAllRequests();
|
|
248
|
+
SquadSportsSDK.instance.apiClient.clearAllCaches();
|
|
249
|
+
AnalyticsTracker.shared.destroy();
|
|
250
|
+
}
|
|
251
|
+
SquadSportsSDK.instance = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Session ---
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Restore a previous session from stored tokens.
|
|
258
|
+
*/
|
|
259
|
+
async restoreSession(): Promise<boolean> {
|
|
260
|
+
const token = await this.tokenStorage.getAccessToken();
|
|
261
|
+
if (!token) return false;
|
|
262
|
+
|
|
263
|
+
this.apiClient.updateAccessToken(token);
|
|
264
|
+
const isValid = await this.sessionManager.validateSession();
|
|
265
|
+
|
|
266
|
+
if (!isValid) {
|
|
267
|
+
await this.tokenStorage.clearAll();
|
|
268
|
+
this.apiClient.clearAuthState();
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Authenticate via SSO with an external token.
|
|
277
|
+
* Convenience method for post-initialization SSO.
|
|
278
|
+
*/
|
|
279
|
+
async authenticateWithSSO(
|
|
280
|
+
provider: 'ticketmaster' | 'oauth2' | 'custom',
|
|
281
|
+
token: string,
|
|
282
|
+
): Promise<boolean> {
|
|
283
|
+
const result = await this.ssoManager.authenticateWithToken(provider, token);
|
|
284
|
+
return result.success;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
jest.mock('react-native', () => ({
|
|
2
|
+
Linking: {
|
|
3
|
+
addEventListener: jest.fn(),
|
|
4
|
+
getInitialURL: jest.fn(),
|
|
5
|
+
openURL: jest.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { DeepLinkHandler } from '../realtime/DeepLinkHandler';
|
|
10
|
+
|
|
11
|
+
describe('DeepLinkHandler', () => {
|
|
12
|
+
describe('parseUrl', () => {
|
|
13
|
+
// Squad scheme
|
|
14
|
+
test('parses squad://invite/CODE', () => {
|
|
15
|
+
const route = DeepLinkHandler.parseUrl('squad://invite/ABC123');
|
|
16
|
+
expect(route).toEqual({ screen: 'Invite', params: { code: 'ABC123' } });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('parses squad://profile/USER_ID', () => {
|
|
20
|
+
const route = DeepLinkHandler.parseUrl('squad://profile/user-456');
|
|
21
|
+
expect(route).toEqual({ screen: 'Profile', params: { userId: 'user-456' } });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('parses squad://message/CONNECTION_ID', () => {
|
|
25
|
+
const route = DeepLinkHandler.parseUrl('squad://message/conn-789');
|
|
26
|
+
expect(route).toEqual({ screen: 'Messaging', params: { connectionId: 'conn-789' } });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('parses squad://poll/POLL_ID', () => {
|
|
30
|
+
const route = DeepLinkHandler.parseUrl('squad://poll/poll-101');
|
|
31
|
+
expect(route).toEqual({ screen: 'PollResponse', params: { pollId: 'poll-101' } });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('parses squad://events', () => {
|
|
35
|
+
const route = DeepLinkHandler.parseUrl('squad://events');
|
|
36
|
+
expect(route).toEqual({ screen: 'Events' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('parses squad://wallet', () => {
|
|
40
|
+
const route = DeepLinkHandler.parseUrl('squad://wallet');
|
|
41
|
+
expect(route).toEqual({ screen: 'Wallet' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('parses squad://freestyle', () => {
|
|
45
|
+
const route = DeepLinkHandler.parseUrl('squad://freestyle');
|
|
46
|
+
expect(route).toEqual({ screen: 'FreestyleCreate' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Universal links
|
|
50
|
+
test('parses https://app.withyoursquad.com/invite/CODE', () => {
|
|
51
|
+
const route = DeepLinkHandler.parseUrl('https://app.withyoursquad.com/invite/XYZ789');
|
|
52
|
+
expect(route).toEqual({ screen: 'Invite', params: { code: 'XYZ789' } });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('parses https://app.withyoursquad.com/profile/USER_ID', () => {
|
|
56
|
+
const route = DeepLinkHandler.parseUrl('https://app.withyoursquad.com/profile/user-123');
|
|
57
|
+
expect(route).toEqual({ screen: 'Profile', params: { userId: 'user-123' } });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Edge cases
|
|
61
|
+
test('returns null for unknown scheme', () => {
|
|
62
|
+
expect(DeepLinkHandler.parseUrl('https://random.com/path')).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('returns Home for unknown squad path', () => {
|
|
66
|
+
const route = DeepLinkHandler.parseUrl('squad://unknown');
|
|
67
|
+
expect(route).toEqual({ screen: 'Home' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('returns null for empty URL', () => {
|
|
71
|
+
expect(DeepLinkHandler.parseUrl('')).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('parses invite without code', () => {
|
|
75
|
+
const route = DeepLinkHandler.parseUrl('squad://invite');
|
|
76
|
+
expect(route).toEqual({ screen: 'Invite' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('profile without userId returns null', () => {
|
|
80
|
+
const route = DeepLinkHandler.parseUrl('squad://profile');
|
|
81
|
+
expect(route).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('linkingConfig', () => {
|
|
86
|
+
test('has correct prefixes', () => {
|
|
87
|
+
const config = DeepLinkHandler.linkingConfig;
|
|
88
|
+
expect(config.prefixes).toContain('squad://');
|
|
89
|
+
expect(config.prefixes).toContain('https://app.withyoursquad.com');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('has screen mappings', () => {
|
|
93
|
+
const screens = DeepLinkHandler.linkingConfig.config.screens;
|
|
94
|
+
expect(screens).toHaveProperty('Home');
|
|
95
|
+
expect(screens).toHaveProperty('Invite');
|
|
96
|
+
expect(screens).toHaveProperty('Profile');
|
|
97
|
+
expect(screens).toHaveProperty('Messaging');
|
|
98
|
+
expect(screens).toHaveProperty('PollResponse');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// Mock react-native with proper function components
|
|
4
|
+
jest.mock('react-native', () => {
|
|
5
|
+
const React = require('react');
|
|
6
|
+
return {
|
|
7
|
+
View: (props: any) => React.createElement('View', props),
|
|
8
|
+
Text: (props: any) => React.createElement('Text', props),
|
|
9
|
+
Pressable: (props: any) => React.createElement('Pressable', props),
|
|
10
|
+
StyleSheet: {
|
|
11
|
+
create: (styles: Record<string, any>) => styles,
|
|
12
|
+
},
|
|
13
|
+
Platform: { OS: 'ios' },
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Mock the theme import
|
|
18
|
+
jest.mock('../theme/ThemeContext', () => ({
|
|
19
|
+
Colors: {
|
|
20
|
+
gray1: '#111',
|
|
21
|
+
gray6: '#666',
|
|
22
|
+
gray9: '#999',
|
|
23
|
+
white: '#fff',
|
|
24
|
+
orange1: '#e9785c',
|
|
25
|
+
purple1: '#7c3aed',
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { create, act } from 'react-test-renderer';
|
|
30
|
+
import type { ReactTestRenderer } from 'react-test-renderer';
|
|
31
|
+
import { ScreenErrorBoundary } from '../components/ErrorBoundary';
|
|
32
|
+
|
|
33
|
+
// Suppress console.error noise from intentional errors
|
|
34
|
+
const originalConsoleError = console.error;
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
console.error = jest.fn();
|
|
37
|
+
});
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
console.error = originalConsoleError;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// A component that throws on render
|
|
43
|
+
function ThrowingChild() {
|
|
44
|
+
throw new Error('Test render crash');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('ScreenErrorBoundary', () => {
|
|
48
|
+
test('renders children normally when no error', async () => {
|
|
49
|
+
let tree: ReactTestRenderer;
|
|
50
|
+
await act(async () => {
|
|
51
|
+
tree = create(
|
|
52
|
+
React.createElement(
|
|
53
|
+
ScreenErrorBoundary,
|
|
54
|
+
null,
|
|
55
|
+
React.createElement('Text', null, 'Hello'),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
const json = JSON.stringify(tree!.toJSON());
|
|
60
|
+
expect(json).toContain('Hello');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('catches render error and shows default fallback', async () => {
|
|
64
|
+
let tree: ReactTestRenderer;
|
|
65
|
+
await act(async () => {
|
|
66
|
+
tree = create(
|
|
67
|
+
React.createElement(
|
|
68
|
+
ScreenErrorBoundary,
|
|
69
|
+
null,
|
|
70
|
+
React.createElement(ThrowingChild),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
const json = JSON.stringify(tree!.toJSON());
|
|
75
|
+
expect(json).toContain('Something went wrong');
|
|
76
|
+
expect(json).toContain('Try Again');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('screenName appears in error message', async () => {
|
|
80
|
+
let tree: ReactTestRenderer;
|
|
81
|
+
await act(async () => {
|
|
82
|
+
tree = create(
|
|
83
|
+
React.createElement(
|
|
84
|
+
ScreenErrorBoundary,
|
|
85
|
+
{ screenName: 'Home' },
|
|
86
|
+
React.createElement(ThrowingChild),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
const json = JSON.stringify(tree!.toJSON());
|
|
91
|
+
expect(json).toContain('Home');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('onError callback fires', async () => {
|
|
95
|
+
const onError = jest.fn();
|
|
96
|
+
await act(async () => {
|
|
97
|
+
create(
|
|
98
|
+
React.createElement(
|
|
99
|
+
ScreenErrorBoundary,
|
|
100
|
+
{ onError },
|
|
101
|
+
React.createElement(ThrowingChild),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
expect(onError).toHaveBeenCalledWith(
|
|
106
|
+
expect.any(Error),
|
|
107
|
+
expect.objectContaining({ componentStack: expect.any(String) }),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('custom fallback prop is rendered', async () => {
|
|
112
|
+
let tree: ReactTestRenderer;
|
|
113
|
+
await act(async () => {
|
|
114
|
+
tree = create(
|
|
115
|
+
React.createElement(
|
|
116
|
+
ScreenErrorBoundary,
|
|
117
|
+
{ fallback: React.createElement('Text', null, 'Custom Fallback') },
|
|
118
|
+
React.createElement(ThrowingChild),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
const json = JSON.stringify(tree!.toJSON());
|
|
123
|
+
expect(json).toContain('Custom Fallback');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('"Try Again" button resets error state', async () => {
|
|
127
|
+
let shouldThrow = true;
|
|
128
|
+
function ConditionalThrower() {
|
|
129
|
+
if (shouldThrow) throw new Error('boom');
|
|
130
|
+
return React.createElement('Text', null, 'Recovered');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let tree: ReactTestRenderer;
|
|
134
|
+
await act(async () => {
|
|
135
|
+
tree = create(
|
|
136
|
+
React.createElement(
|
|
137
|
+
ScreenErrorBoundary,
|
|
138
|
+
null,
|
|
139
|
+
React.createElement(ConditionalThrower),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Verify we're in error state
|
|
145
|
+
const instance = tree!.root;
|
|
146
|
+
const retryButton = instance.findByProps({ accessibilityLabel: 'Try again' });
|
|
147
|
+
expect(retryButton).toBeTruthy();
|
|
148
|
+
|
|
149
|
+
// Fix the child so it won't throw on re-render
|
|
150
|
+
shouldThrow = false;
|
|
151
|
+
|
|
152
|
+
// Press "Try Again"
|
|
153
|
+
await act(async () => {
|
|
154
|
+
retryButton.props.onPress();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Should now show recovered content
|
|
158
|
+
const json = JSON.stringify(tree!.toJSON());
|
|
159
|
+
expect(json).toContain('Recovered');
|
|
160
|
+
});
|
|
161
|
+
});
|