@squad-sports/react-native 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +46 -0
  4. package/src/SquadExperience.tsx +200 -0
  5. package/src/SquadProvider.tsx +232 -0
  6. package/src/SquadSportsSDK.ts +286 -0
  7. package/src/__tests__/DeepLinkHandler.test.ts +101 -0
  8. package/src/__tests__/ErrorBoundary.test.tsx +161 -0
  9. package/src/__tests__/EventProcessor.test.ts +241 -0
  10. package/src/__tests__/PushNotificationHandler.test.ts +91 -0
  11. package/src/__tests__/SecureStorage.test.ts +62 -0
  12. package/src/__tests__/SquadSportsSDK.test.ts +278 -0
  13. package/src/__tests__/VerificationCooldown.test.ts +153 -0
  14. package/src/components/ErrorBoundary.tsx +129 -0
  15. package/src/components/SOTD/SOTDComponents.tsx +101 -0
  16. package/src/components/audio/AudioPlayerRow.tsx +189 -0
  17. package/src/components/audio/recording/AudioRecording.tsx +232 -0
  18. package/src/components/communities/CommunityComponents.tsx +78 -0
  19. package/src/components/dialogs/AllDialogs.tsx +123 -0
  20. package/src/components/dialogs/ConfirmDialog.tsx +77 -0
  21. package/src/components/dialogs/PermissionDialog.tsx +132 -0
  22. package/src/components/dragAndDrop/DragAndDrop.tsx +56 -0
  23. package/src/components/events/EventComponents.tsx +93 -0
  24. package/src/components/feed/ChatBannerCard.tsx +94 -0
  25. package/src/components/feed/FeedLoadingSkeleton.tsx +43 -0
  26. package/src/components/feed/FreestyleCard.tsx +119 -0
  27. package/src/components/feed/InterstitialOverlay.tsx +190 -0
  28. package/src/components/feed/PollCard.tsx +158 -0
  29. package/src/components/feed/SponsoredContentCard.tsx +118 -0
  30. package/src/components/freestyle/FreestyleComponents.tsx +148 -0
  31. package/src/components/index.ts +42 -0
  32. package/src/components/message/MessageCard.tsx +166 -0
  33. package/src/components/message/MessageComponents.tsx +143 -0
  34. package/src/components/poll/PollComponents.tsx +226 -0
  35. package/src/components/sentinels/Sentinels.tsx +175 -0
  36. package/src/components/squad/FeedSquad.tsx +54 -0
  37. package/src/components/toasts/Toasts.tsx +88 -0
  38. package/src/components/ux/RootUXComponents.tsx +157 -0
  39. package/src/components/ux/action-sheet/ActionSheet.tsx +67 -0
  40. package/src/components/ux/bottom-sheet/BottomSheetComponents.tsx +93 -0
  41. package/src/components/ux/bottom-sheet/SquadBottomSheet.tsx +119 -0
  42. package/src/components/ux/buttons/Button.tsx +37 -0
  43. package/src/components/ux/buttons/InfoButton.tsx +24 -0
  44. package/src/components/ux/buttons/XButton.tsx +27 -0
  45. package/src/components/ux/carousel/Carousel.tsx +134 -0
  46. package/src/components/ux/emoji-picker/EmojiReactionPicker.tsx +139 -0
  47. package/src/components/ux/errors/ErrorHint.tsx +46 -0
  48. package/src/components/ux/inputs/CodeInput.tsx +121 -0
  49. package/src/components/ux/inputs/DatePicker.tsx +76 -0
  50. package/src/components/ux/inputs/MaskedTextInput.tsx +95 -0
  51. package/src/components/ux/inputs/PhoneNumberInput.tsx +90 -0
  52. package/src/components/ux/inputs/SearchTextInput.tsx +70 -0
  53. package/src/components/ux/inputs/TextInput.tsx +58 -0
  54. package/src/components/ux/layout/AvoidKeyboardScreen.tsx +58 -0
  55. package/src/components/ux/layout/BlurOverlay.tsx +26 -0
  56. package/src/components/ux/layout/CrossFade.tsx +30 -0
  57. package/src/components/ux/layout/DismissKeyboardOnBlur.tsx +14 -0
  58. package/src/components/ux/layout/LoadingOverlay.tsx +60 -0
  59. package/src/components/ux/layout/NetworkBanner.tsx +64 -0
  60. package/src/components/ux/layout/PermissionsCTAContent.tsx +54 -0
  61. package/src/components/ux/layout/RefreshControl.tsx +7 -0
  62. package/src/components/ux/layout/Screen.tsx +31 -0
  63. package/src/components/ux/layout/ScreenHeader.tsx +89 -0
  64. package/src/components/ux/layout/TabBar.tsx +39 -0
  65. package/src/components/ux/layout/Toast.tsx +116 -0
  66. package/src/components/ux/layout/TransparentOverlay.tsx +21 -0
  67. package/src/components/ux/navigation/BackButton.tsx +29 -0
  68. package/src/components/ux/navigation/LinkButton.tsx +21 -0
  69. package/src/components/ux/navigation/UrlButton.tsx +25 -0
  70. package/src/components/ux/scroll/RefreshableFlatList.tsx +35 -0
  71. package/src/components/ux/shapes/Shapes.tsx +23 -0
  72. package/src/components/ux/text/Typography.tsx +28 -0
  73. package/src/components/ux/user-image/UserImage.tsx +75 -0
  74. package/src/components/ux/user-image/UserImageVariants.tsx +104 -0
  75. package/src/components/wallet/WalletComponents.tsx +116 -0
  76. package/src/contexts/AuthContext.tsx +45 -0
  77. package/src/contexts/EventProcessorContext.tsx +41 -0
  78. package/src/contexts/PlayerQueueContext.tsx +95 -0
  79. package/src/hooks/useAuth.ts +23 -0
  80. package/src/hooks/useDataRefresh.ts +30 -0
  81. package/src/hooks/useEventProcessor.ts +6 -0
  82. package/src/hooks/useImageOptimization.ts +59 -0
  83. package/src/hooks/useOnboardingStepGuard.ts +36 -0
  84. package/src/hooks/usePendingNavigation.ts +26 -0
  85. package/src/hooks/useSquadData.ts +84 -0
  86. package/src/hooks/useUserCreated.ts +25 -0
  87. package/src/hooks/useUserUpdate.ts +25 -0
  88. package/src/hooks/useViewabilityTracker.ts +40 -0
  89. package/src/index.ts +109 -0
  90. package/src/navigation/SquadNavigator.tsx +262 -0
  91. package/src/realtime/DeepLinkHandler.ts +113 -0
  92. package/src/realtime/EventProcessor.ts +313 -0
  93. package/src/realtime/NetworkMonitor.ts +84 -0
  94. package/src/realtime/OfflineQueue.ts +133 -0
  95. package/src/realtime/PushNotificationHandler.ts +125 -0
  96. package/src/realtime/useRealtimeSync.ts +84 -0
  97. package/src/screens/auth/EmailVerificationScreen.tsx +201 -0
  98. package/src/screens/auth/EnterCodeScreen.tsx +253 -0
  99. package/src/screens/auth/EnterEmailScreen.tsx +234 -0
  100. package/src/screens/auth/LandingScreen.tsx +90 -0
  101. package/src/screens/auth/LoginScreen.tsx +126 -0
  102. package/src/screens/events/EventScreen.tsx +163 -0
  103. package/src/screens/freestyle/CommunityFreestyleScreen.tsx +82 -0
  104. package/src/screens/freestyle/FreestyleCreationScreen.tsx +255 -0
  105. package/src/screens/freestyle/FreestyleListensScreen.tsx +44 -0
  106. package/src/screens/freestyle/FreestyleReactionsScreen.tsx +44 -0
  107. package/src/screens/freestyle/FreestyleScreen.tsx +64 -0
  108. package/src/screens/home/HomeScreen.tsx +365 -0
  109. package/src/screens/home/slivers/SquadCircle.tsx +77 -0
  110. package/src/screens/invite/InvitationQrCodeScreen.tsx +114 -0
  111. package/src/screens/invite/InviteScreen.tsx +175 -0
  112. package/src/screens/messaging/MessagingScreen.tsx +360 -0
  113. package/src/screens/onboarding/OnboardingAccountSetupScreen.tsx +221 -0
  114. package/src/screens/onboarding/OnboardingTeamSelectScreen.tsx +215 -0
  115. package/src/screens/onboarding/OnboardingWelcomeScreen.tsx +93 -0
  116. package/src/screens/polls/PollResponseScreen.tsx +229 -0
  117. package/src/screens/polls/PollSummationScreen.tsx +78 -0
  118. package/src/screens/profile/ProfileScreen.tsx +234 -0
  119. package/src/screens/settings/BlockedUsersScreen.tsx +139 -0
  120. package/src/screens/settings/DeleteAccountScreen.tsx +182 -0
  121. package/src/screens/settings/EditProfileScreen.tsx +154 -0
  122. package/src/screens/settings/NetworkStatusScreen.tsx +59 -0
  123. package/src/screens/settings/SettingsScreen.tsx +194 -0
  124. package/src/screens/squad-line/AddCallTitleScreen.tsx +293 -0
  125. package/src/screens/wallet/WalletScreen.tsx +174 -0
  126. package/src/services/AuthStateManager.ts +93 -0
  127. package/src/services/NavigationService.ts +40 -0
  128. package/src/services/UserDataManager.ts +59 -0
  129. package/src/services/UserUpdateService.ts +31 -0
  130. package/src/services/VerificationStateManager.ts +41 -0
  131. package/src/squad-line/CallScreen.tsx +158 -0
  132. package/src/squad-line/IncomingCallOverlay.tsx +113 -0
  133. package/src/squad-line/SquadLineClient.ts +327 -0
  134. package/src/squad-line/useSquadLine.ts +80 -0
  135. package/src/state/audio.ts +38 -0
  136. package/src/state/client.ts +26 -0
  137. package/src/state/communities.ts +45 -0
  138. package/src/state/contacts.ts +28 -0
  139. package/src/state/device-info.ts +22 -0
  140. package/src/state/events.ts +16 -0
  141. package/src/state/features.ts +57 -0
  142. package/src/state/index.ts +121 -0
  143. package/src/state/invitations.ts +16 -0
  144. package/src/state/modal-keys.ts +63 -0
  145. package/src/state/modal-queue.ts +104 -0
  146. package/src/state/navigation.ts +34 -0
  147. package/src/state/permissions.ts +43 -0
  148. package/src/state/session.ts +223 -0
  149. package/src/state/squaddie-of-the-day.ts +21 -0
  150. package/src/state/sync/crdt.ts +70 -0
  151. package/src/state/sync/dependable.ts +213 -0
  152. package/src/state/sync/feed-v2.ts +42 -0
  153. package/src/state/sync/messages.ts +44 -0
  154. package/src/state/sync/offline-support.ts +42 -0
  155. package/src/state/sync/polls.ts +37 -0
  156. package/src/state/sync/refresh.ts +24 -0
  157. package/src/state/sync/squad-v2.ts +25 -0
  158. package/src/state/ui.ts +36 -0
  159. package/src/state/user.ts +46 -0
  160. package/src/state/wallet.ts +26 -0
  161. package/src/storage/SecureStorage.ts +77 -0
  162. package/src/theme/ThemeContext.tsx +159 -0
  163. package/src/types/modules.d.ts +165 -0
@@ -0,0 +1,58 @@
1
+ import React, { ReactNode } from 'react';
2
+ import {
3
+ KeyboardAvoidingView,
4
+ Platform,
5
+ StyleSheet,
6
+ ScrollView,
7
+ StyleProp,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import { SafeAreaView } from 'react-native-safe-area-context';
11
+ import { Colors } from '../../../theme/ThemeContext';
12
+
13
+ interface AvoidKeyboardScreenProps {
14
+ children: ReactNode;
15
+ screenStyle?: StyleProp<ViewStyle>;
16
+ scrollEnabled?: boolean;
17
+ }
18
+
19
+ export default function AvoidKeyboardScreen({
20
+ children,
21
+ screenStyle,
22
+ scrollEnabled = false,
23
+ }: AvoidKeyboardScreenProps) {
24
+ return (
25
+ <SafeAreaView style={[styles.container, screenStyle]}>
26
+ <KeyboardAvoidingView
27
+ style={styles.keyboardView}
28
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
29
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
30
+ >
31
+ {scrollEnabled ? (
32
+ <ScrollView
33
+ contentContainerStyle={styles.scrollContent}
34
+ keyboardShouldPersistTaps="handled"
35
+ bounces={false}
36
+ >
37
+ {children}
38
+ </ScrollView>
39
+ ) : (
40
+ children
41
+ )}
42
+ </KeyboardAvoidingView>
43
+ </SafeAreaView>
44
+ );
45
+ }
46
+
47
+ const styles = StyleSheet.create({
48
+ container: {
49
+ flex: 1,
50
+ backgroundColor: Colors.black,
51
+ },
52
+ keyboardView: {
53
+ flex: 1,
54
+ },
55
+ scrollContent: {
56
+ flexGrow: 1,
57
+ },
58
+ });
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ interface BlurOverlayProps {
5
+ visible: boolean;
6
+ children?: React.ReactNode;
7
+ }
8
+
9
+ export default function BlurOverlay({ visible, children }: BlurOverlayProps) {
10
+ if (!visible) return null;
11
+ return (
12
+ <View style={styles.overlay}>
13
+ {children}
14
+ </View>
15
+ );
16
+ }
17
+
18
+ const styles = StyleSheet.create({
19
+ overlay: {
20
+ ...StyleSheet.absoluteFillObject,
21
+ backgroundColor: 'rgba(0,0,0,0.6)',
22
+ justifyContent: 'center',
23
+ alignItems: 'center',
24
+ zIndex: 9998,
25
+ },
26
+ });
@@ -0,0 +1,30 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, StyleSheet, View } from 'react-native';
3
+
4
+ interface CrossFadeProps {
5
+ children: React.ReactNode;
6
+ visible: boolean;
7
+ duration?: number;
8
+ }
9
+
10
+ export default function CrossFade({ children, visible, duration = 300 }: CrossFadeProps) {
11
+ const opacity = useRef(new Animated.Value(visible ? 1 : 0)).current;
12
+
13
+ useEffect(() => {
14
+ Animated.timing(opacity, {
15
+ toValue: visible ? 1 : 0,
16
+ duration,
17
+ useNativeDriver: true,
18
+ }).start();
19
+ }, [visible, duration, opacity]);
20
+
21
+ return (
22
+ <Animated.View style={[styles.container, { opacity }]} pointerEvents={visible ? 'auto' : 'none'}>
23
+ {children}
24
+ </Animated.View>
25
+ );
26
+ }
27
+
28
+ const styles = StyleSheet.create({
29
+ container: { ...StyleSheet.absoluteFillObject },
30
+ });
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { Keyboard, Pressable, StyleSheet } from 'react-native';
3
+
4
+ export default function DismissKeyboardOnBlur({ children }: { children: React.ReactNode }) {
5
+ return (
6
+ <Pressable style={styles.container} onPress={Keyboard.dismiss}>
7
+ {children}
8
+ </Pressable>
9
+ );
10
+ }
11
+
12
+ const styles = StyleSheet.create({
13
+ container: { flex: 1 },
14
+ });
@@ -0,0 +1,60 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { ActivityIndicator, StyleSheet, Animated, Text } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+
5
+ interface LoadingOverlayProps {
6
+ visible: boolean;
7
+ text?: string;
8
+ }
9
+
10
+ export default function LoadingOverlay({ visible, text }: LoadingOverlayProps) {
11
+ const [fadeAnim] = useState(new Animated.Value(0));
12
+ const [shouldRender, setShouldRender] = useState(false);
13
+
14
+ useEffect(() => {
15
+ if (visible) {
16
+ setShouldRender(true);
17
+ Animated.timing(fadeAnim, {
18
+ toValue: 1,
19
+ duration: 400,
20
+ useNativeDriver: true,
21
+ }).start();
22
+ } else {
23
+ Animated.timing(fadeAnim, {
24
+ toValue: 0,
25
+ duration: 600,
26
+ useNativeDriver: true,
27
+ }).start(() => {
28
+ setShouldRender(false);
29
+ });
30
+ }
31
+ }, [visible, fadeAnim]);
32
+
33
+ if (!shouldRender) return null;
34
+
35
+ return (
36
+ <Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
37
+ <ActivityIndicator size="large" color={Colors.white} />
38
+ {text && <Text style={styles.text}>{text}</Text>}
39
+ </Animated.View>
40
+ );
41
+ }
42
+
43
+ const styles = StyleSheet.create({
44
+ overlay: {
45
+ alignItems: 'center',
46
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
47
+ bottom: 0,
48
+ justifyContent: 'center',
49
+ left: 0,
50
+ position: 'absolute',
51
+ right: 0,
52
+ top: 0,
53
+ zIndex: 9999,
54
+ },
55
+ text: {
56
+ color: Colors.white,
57
+ marginTop: 16,
58
+ fontSize: 14,
59
+ },
60
+ });
@@ -0,0 +1,64 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Animated, StyleSheet, Text } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+
5
+ interface NetworkBannerProps {
6
+ isOnline: boolean;
7
+ }
8
+
9
+ /**
10
+ * Animated banner that slides down when the device goes offline.
11
+ * Automatically wired into SquadProvider — integrators don't need to add this.
12
+ */
13
+ export function NetworkBanner({ isOnline }: NetworkBannerProps) {
14
+ const [slideAnim] = useState(new Animated.Value(-50));
15
+ const [shouldRender, setShouldRender] = useState(false);
16
+
17
+ useEffect(() => {
18
+ if (!isOnline) {
19
+ setShouldRender(true);
20
+ Animated.spring(slideAnim, {
21
+ toValue: 0,
22
+ useNativeDriver: true,
23
+ tension: 80,
24
+ friction: 10,
25
+ }).start();
26
+ } else {
27
+ Animated.timing(slideAnim, {
28
+ toValue: -50,
29
+ duration: 300,
30
+ useNativeDriver: true,
31
+ }).start(() => setShouldRender(false));
32
+ }
33
+ }, [isOnline, slideAnim]);
34
+
35
+ if (!shouldRender) return null;
36
+
37
+ return (
38
+ <Animated.View
39
+ style={[styles.banner, { transform: [{ translateY: slideAnim }] }]}
40
+ >
41
+ <Text style={styles.text}>No internet connection</Text>
42
+ </Animated.View>
43
+ );
44
+ }
45
+
46
+ const styles = StyleSheet.create({
47
+ banner: {
48
+ position: 'absolute',
49
+ top: 0,
50
+ left: 0,
51
+ right: 0,
52
+ backgroundColor: Colors.orange2,
53
+ paddingTop: 50,
54
+ paddingBottom: 8,
55
+ paddingHorizontal: 16,
56
+ zIndex: 9998,
57
+ alignItems: 'center',
58
+ },
59
+ text: {
60
+ color: Colors.white,
61
+ fontSize: 13,
62
+ fontWeight: '600',
63
+ },
64
+ });
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, Linking } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+ import Button from '../buttons/Button';
5
+ import { TitleSmall, BodyRegular } from '../text/Typography';
6
+
7
+ interface PermissionsCTAContentProps {
8
+ icon?: string;
9
+ title: string;
10
+ description: string;
11
+ buttonLabel?: string;
12
+ onAllow?: () => void;
13
+ isDenied?: boolean;
14
+ }
15
+
16
+ export default function PermissionsCTAContent({
17
+ icon,
18
+ title,
19
+ description,
20
+ buttonLabel = 'Allow',
21
+ onAllow,
22
+ isDenied = false,
23
+ }: PermissionsCTAContentProps) {
24
+ return (
25
+ <View style={styles.container}>
26
+ {icon && (
27
+ <View style={styles.iconContainer}>
28
+ <Text style={styles.icon}>{icon}</Text>
29
+ </View>
30
+ )}
31
+ <TitleSmall style={styles.title}>{title}</TitleSmall>
32
+ <BodyRegular style={styles.description}>{description}</BodyRegular>
33
+ <Button
34
+ style={styles.button}
35
+ onPress={isDenied ? () => Linking.openSettings() : onAllow}
36
+ accessibilityLabel={isDenied ? 'Open Settings' : buttonLabel}
37
+ >
38
+ <Text style={styles.buttonText}>
39
+ {isDenied ? 'Open Settings' : buttonLabel}
40
+ </Text>
41
+ </Button>
42
+ </View>
43
+ );
44
+ }
45
+
46
+ const styles = StyleSheet.create({
47
+ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32 },
48
+ iconContainer: { width: 64, height: 64, borderRadius: 32, backgroundColor: Colors.purple1, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
49
+ icon: { color: Colors.white, fontSize: 28, fontWeight: '700' },
50
+ title: { color: Colors.white, marginBottom: 8, textAlign: 'center' },
51
+ description: { color: Colors.gray6, textAlign: 'center', marginBottom: 24 },
52
+ button: { paddingVertical: 14, paddingHorizontal: 32, borderRadius: 24, backgroundColor: Colors.purple1 },
53
+ buttonText: { color: Colors.gray1, fontWeight: '600', fontSize: 15 },
54
+ });
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { RefreshControl as RNRefreshControl, type RefreshControlProps } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+
5
+ export default function RefreshControl(props: RefreshControlProps) {
6
+ return <RNRefreshControl tintColor={Colors.white} colors={[Colors.purple1]} {...props} />;
7
+ }
@@ -0,0 +1,31 @@
1
+ import React, { ReactNode, useMemo, memo } from 'react';
2
+ import { StyleSheet, ViewStyle, Dimensions } from 'react-native';
3
+ import { SafeAreaView } from 'react-native-safe-area-context';
4
+ import { useTheme } from '../../../theme/ThemeContext';
5
+
6
+ const { width, height } = Dimensions.get('window');
7
+
8
+ function Screen({ children, style }: { children: ReactNode; style?: ViewStyle }) {
9
+ const { theme } = useTheme();
10
+
11
+ const screenStyle = useMemo(
12
+ () => [
13
+ styles.screen,
14
+ style,
15
+ { backgroundColor: theme.screenBackground },
16
+ ],
17
+ [style, theme.screenBackground],
18
+ );
19
+
20
+ return <SafeAreaView style={screenStyle}>{children}</SafeAreaView>;
21
+ }
22
+
23
+ export default memo(Screen);
24
+
25
+ const styles = StyleSheet.create({
26
+ screen: {
27
+ flex: 1,
28
+ width,
29
+ height,
30
+ },
31
+ });
@@ -0,0 +1,89 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { StyleSheet, View, Text, Pressable, Dimensions } from 'react-native';
3
+ import { useNavigation } from '@react-navigation/native';
4
+ import { useTheme, Colors } from '../../../theme/ThemeContext';
5
+
6
+ const { width } = Dimensions.get('window');
7
+
8
+ interface ScreenHeaderProps {
9
+ right?: ReactNode;
10
+ title: string;
11
+ showBack?: boolean;
12
+ onBack?: () => void;
13
+ }
14
+
15
+ export default function ScreenHeader({
16
+ right,
17
+ title,
18
+ showBack = true,
19
+ onBack,
20
+ }: ScreenHeaderProps) {
21
+ const { theme } = useTheme();
22
+ const navigation = useNavigation();
23
+
24
+ const handleBack = () => {
25
+ if (onBack) {
26
+ onBack();
27
+ } else {
28
+ navigation.goBack();
29
+ }
30
+ };
31
+
32
+ const textColor = theme.isDarkMode ? Colors.white : Colors.gray1;
33
+
34
+ return (
35
+ <View style={styles.screenHeader} accessibilityRole="header">
36
+ <View style={styles.sideContainer}>
37
+ {showBack && (
38
+ <Pressable
39
+ onPress={handleBack}
40
+ hitSlop={8}
41
+ accessibilityRole="button"
42
+ accessibilityLabel="Go back"
43
+ >
44
+ <Text style={[styles.backText, { color: textColor }]}>{'<'}</Text>
45
+ </Pressable>
46
+ )}
47
+ </View>
48
+ <View style={styles.titleContainer}>
49
+ <Text
50
+ style={[styles.screenHeaderText, { color: textColor }]}
51
+ numberOfLines={1}
52
+ accessibilityRole="header"
53
+ >
54
+ {title}
55
+ </Text>
56
+ </View>
57
+ <View style={styles.sideContainer}>{right}</View>
58
+ </View>
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ screenHeader: {
64
+ alignItems: 'center',
65
+ justifyContent: 'space-between',
66
+ flexDirection: 'row',
67
+ height: 56,
68
+ paddingHorizontal: 16,
69
+ width,
70
+ },
71
+ sideContainer: {
72
+ width: 40,
73
+ alignItems: 'center',
74
+ },
75
+ titleContainer: {
76
+ flexGrow: 1,
77
+ alignItems: 'center',
78
+ justifyContent: 'center',
79
+ },
80
+ screenHeaderText: {
81
+ textAlign: 'center',
82
+ fontSize: 18,
83
+ fontWeight: '600',
84
+ },
85
+ backText: {
86
+ fontSize: 22,
87
+ fontWeight: '600',
88
+ },
89
+ });
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, Pressable } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+
5
+ interface TabBarProps {
6
+ tabs: Array<{ key: string; label: string }>;
7
+ activeTab: string;
8
+ onTabPress: (key: string) => void;
9
+ activeColor?: string;
10
+ }
11
+
12
+ export default function TabBar({ tabs, activeTab, onTabPress, activeColor = Colors.purple1 }: TabBarProps) {
13
+ return (
14
+ <View style={styles.container} accessibilityRole="tablist">
15
+ {tabs.map(tab => {
16
+ const isActive = tab.key === activeTab;
17
+ return (
18
+ <Pressable
19
+ key={tab.key}
20
+ style={[styles.tab, isActive && { borderBottomColor: activeColor }]}
21
+ onPress={() => onTabPress(tab.key)}
22
+ accessibilityRole="tab"
23
+ accessibilityState={{ selected: isActive }}
24
+ >
25
+ <Text style={[styles.tabText, isActive && { color: Colors.white }]}>
26
+ {tab.label}
27
+ </Text>
28
+ </Pressable>
29
+ );
30
+ })}
31
+ </View>
32
+ );
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ container: { flexDirection: 'row', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: Colors.gray3 },
37
+ tab: { flex: 1, paddingVertical: 12, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: 'transparent' },
38
+ tabText: { color: Colors.gray6, fontSize: 14, fontWeight: '500' },
39
+ });
@@ -0,0 +1,116 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Animated, StyleSheet, Text, View } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+ import Button from '../buttons/Button';
5
+
6
+ export type ToastType = 'success' | 'error' | 'info';
7
+
8
+ interface ToastProps {
9
+ visible: boolean;
10
+ message: string;
11
+ type?: ToastType;
12
+ duration?: number;
13
+ onDismiss?: () => void;
14
+ }
15
+
16
+ const TOAST_COLORS: Record<ToastType, string> = {
17
+ success: Colors.green,
18
+ error: Colors.red,
19
+ info: Colors.purple1,
20
+ };
21
+
22
+ export function Toast({
23
+ visible,
24
+ message,
25
+ type = 'info',
26
+ duration = 3000,
27
+ onDismiss,
28
+ }: ToastProps) {
29
+ const [fadeAnim] = useState(new Animated.Value(0));
30
+ const [shouldRender, setShouldRender] = useState(false);
31
+
32
+ useEffect(() => {
33
+ if (visible) {
34
+ setShouldRender(true);
35
+ Animated.timing(fadeAnim, {
36
+ toValue: 1,
37
+ duration: 300,
38
+ useNativeDriver: true,
39
+ }).start();
40
+
41
+ if (duration > 0) {
42
+ const timer = setTimeout(() => {
43
+ Animated.timing(fadeAnim, {
44
+ toValue: 0,
45
+ duration: 300,
46
+ useNativeDriver: true,
47
+ }).start(() => {
48
+ setShouldRender(false);
49
+ onDismiss?.();
50
+ });
51
+ }, duration);
52
+ return () => clearTimeout(timer);
53
+ }
54
+ } else {
55
+ Animated.timing(fadeAnim, {
56
+ toValue: 0,
57
+ duration: 300,
58
+ useNativeDriver: true,
59
+ }).start(() => setShouldRender(false));
60
+ }
61
+ }, [visible, duration, fadeAnim, onDismiss]);
62
+
63
+ if (!shouldRender) return null;
64
+
65
+ return (
66
+ <Animated.View
67
+ style={[
68
+ styles.container,
69
+ { opacity: fadeAnim, transform: [{ translateY: fadeAnim.interpolate({ inputRange: [0, 1], outputRange: [-20, 0] }) }] },
70
+ ]}
71
+ >
72
+ <View style={[styles.indicator, { backgroundColor: TOAST_COLORS[type] }]} />
73
+ <Text style={styles.message} numberOfLines={2}>{message}</Text>
74
+ <Button onPress={onDismiss}>
75
+ <Text style={styles.dismiss}>x</Text>
76
+ </Button>
77
+ </Animated.View>
78
+ );
79
+ }
80
+
81
+ const styles = StyleSheet.create({
82
+ container: {
83
+ position: 'absolute',
84
+ top: 60,
85
+ left: 16,
86
+ right: 16,
87
+ backgroundColor: Colors.gray2,
88
+ borderRadius: 12,
89
+ flexDirection: 'row',
90
+ alignItems: 'center',
91
+ paddingVertical: 12,
92
+ paddingHorizontal: 16,
93
+ zIndex: 10000,
94
+ shadowColor: '#000',
95
+ shadowOffset: { width: 0, height: 4 },
96
+ shadowOpacity: 0.3,
97
+ shadowRadius: 8,
98
+ elevation: 8,
99
+ },
100
+ indicator: {
101
+ width: 4,
102
+ height: 24,
103
+ borderRadius: 2,
104
+ marginRight: 12,
105
+ },
106
+ message: {
107
+ flex: 1,
108
+ color: Colors.white,
109
+ fontSize: 14,
110
+ },
111
+ dismiss: {
112
+ color: Colors.gray6,
113
+ fontSize: 18,
114
+ paddingLeft: 12,
115
+ },
116
+ });
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet } from 'react-native';
3
+
4
+ interface TransparentOverlayProps {
5
+ visible: boolean;
6
+ onPress?: () => void;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export default function TransparentOverlay({ visible, onPress, children }: TransparentOverlayProps) {
11
+ if (!visible) return null;
12
+ return (
13
+ <Pressable style={styles.overlay} onPress={onPress}>
14
+ {children}
15
+ </Pressable>
16
+ );
17
+ }
18
+
19
+ const styles = StyleSheet.create({
20
+ overlay: { ...StyleSheet.absoluteFillObject, zIndex: 9997 },
21
+ });
@@ -0,0 +1,29 @@
1
+ import React, { useCallback } from 'react';
2
+ import { Text, StyleSheet } from 'react-native';
3
+ import { useNavigation } from '@react-navigation/native';
4
+ import { useTheme, Colors } from '../../../theme/ThemeContext';
5
+ import Button from '../buttons/Button';
6
+
7
+ export default function BackButton() {
8
+ const navigation = useNavigation();
9
+ const { theme } = useTheme();
10
+ const handlePress = useCallback(() => navigation.goBack(), [navigation]);
11
+
12
+ const color = theme.isDarkMode ? Colors.white : Colors.gray1;
13
+
14
+ return (
15
+ <Button onPress={handlePress} style={styles.button}>
16
+ <Text style={[styles.chevron, { color }]}>{'<'}</Text>
17
+ </Button>
18
+ );
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ button: {
23
+ padding: 4,
24
+ },
25
+ chevron: {
26
+ fontSize: 22,
27
+ fontWeight: '600',
28
+ },
29
+ });
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Text, StyleSheet } from 'react-native';
3
+ import { useNavigation } from '@react-navigation/native';
4
+ import Button from '../buttons/Button';
5
+ import { Colors } from '../../../theme/ThemeContext';
6
+
7
+ interface LinkButtonProps {
8
+ screen: string;
9
+ params?: Record<string, unknown>;
10
+ children: React.ReactNode;
11
+ style?: object;
12
+ }
13
+
14
+ export default function LinkButton({ screen, params, children, style }: LinkButtonProps) {
15
+ const navigation = useNavigation();
16
+ return (
17
+ <Button onPress={() => (navigation as any).navigate(screen, params)} style={style} accessibilityRole="link">
18
+ {children}
19
+ </Button>
20
+ );
21
+ }