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