@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,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
|
+
}
|