@multiplayer-app/session-recorder-react-native 0.0.1-alpha.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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/babel.config.js +13 -0
  4. package/dist/config/masking.d.ts +30 -0
  5. package/dist/config/masking.js +1 -0
  6. package/dist/config/masking.js.map +1 -0
  7. package/dist/expo.d.ts +11 -0
  8. package/dist/expo.js +1 -0
  9. package/dist/expo.js.map +1 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/otel/helpers.d.ts +3 -0
  14. package/dist/otel/helpers.js +1 -0
  15. package/dist/otel/helpers.js.map +1 -0
  16. package/dist/otel/index.d.ts +40 -0
  17. package/dist/otel/index.js +1 -0
  18. package/dist/otel/index.js.map +1 -0
  19. package/dist/otel/instrumentations/gestureInstrumentation.d.ts +15 -0
  20. package/dist/otel/instrumentations/gestureInstrumentation.js +1 -0
  21. package/dist/otel/instrumentations/gestureInstrumentation.js.map +1 -0
  22. package/dist/otel/instrumentations/index.d.ts +5 -0
  23. package/dist/otel/instrumentations/index.js +1 -0
  24. package/dist/otel/instrumentations/index.js.map +1 -0
  25. package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +8 -0
  26. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -0
  27. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -0
  28. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +12 -0
  29. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -0
  30. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -0
  31. package/dist/recorder/gestureRecorder.d.ts +42 -0
  32. package/dist/recorder/gestureRecorder.js +1 -0
  33. package/dist/recorder/gestureRecorder.js.map +1 -0
  34. package/dist/recorder/index.d.ts +16 -0
  35. package/dist/recorder/index.js +1 -0
  36. package/dist/recorder/index.js.map +1 -0
  37. package/dist/recorder/navigationTracker.d.ts +43 -0
  38. package/dist/recorder/navigationTracker.js +1 -0
  39. package/dist/recorder/navigationTracker.js.map +1 -0
  40. package/dist/recorder/screenRecorder.d.ts +46 -0
  41. package/dist/recorder/screenRecorder.js +1 -0
  42. package/dist/recorder/screenRecorder.js.map +1 -0
  43. package/dist/services/api.service.d.ts +20 -0
  44. package/dist/services/api.service.js +1 -0
  45. package/dist/services/api.service.js.map +1 -0
  46. package/dist/services/storage.service.d.ts +23 -0
  47. package/dist/services/storage.service.js +1 -0
  48. package/dist/services/storage.service.js.map +1 -0
  49. package/dist/sessionRecorder.d.ts +54 -0
  50. package/dist/sessionRecorder.js +1 -0
  51. package/dist/sessionRecorder.js.map +1 -0
  52. package/dist/types/index.d.ts +81 -0
  53. package/dist/types/index.js +1 -0
  54. package/dist/types/index.js.map +1 -0
  55. package/dist/utils/platform.d.ts +9 -0
  56. package/dist/utils/platform.js +1 -0
  57. package/dist/utils/platform.js.map +1 -0
  58. package/dist/version.d.ts +1 -0
  59. package/dist/version.js +1 -0
  60. package/dist/version.js.map +1 -0
  61. package/examples/sample-expo-app/README.md +142 -0
  62. package/examples/sample-expo-app/app/(tabs)/_layout.tsx +60 -0
  63. package/examples/sample-expo-app/app/(tabs)/explore.tsx +110 -0
  64. package/examples/sample-expo-app/app/(tabs)/index.tsx +125 -0
  65. package/examples/sample-expo-app/app/(tabs)/posts.tsx +96 -0
  66. package/examples/sample-expo-app/app/(tabs)/users.tsx +131 -0
  67. package/examples/sample-expo-app/app/+not-found.tsx +32 -0
  68. package/examples/sample-expo-app/app/_layout.tsx +53 -0
  69. package/examples/sample-expo-app/app/post/[id].tsx +199 -0
  70. package/examples/sample-expo-app/app/user/[id].tsx +270 -0
  71. package/examples/sample-expo-app/app.json +42 -0
  72. package/examples/sample-expo-app/assets/fonts/SpaceMono-Regular.ttf +0 -0
  73. package/examples/sample-expo-app/assets/images/adaptive-icon.png +0 -0
  74. package/examples/sample-expo-app/assets/images/favicon.png +0 -0
  75. package/examples/sample-expo-app/assets/images/icon.png +0 -0
  76. package/examples/sample-expo-app/assets/images/partial-react-logo.png +0 -0
  77. package/examples/sample-expo-app/assets/images/react-logo.png +0 -0
  78. package/examples/sample-expo-app/assets/images/react-logo@2x.png +0 -0
  79. package/examples/sample-expo-app/assets/images/react-logo@3x.png +0 -0
  80. package/examples/sample-expo-app/assets/images/splash-icon.png +0 -0
  81. package/examples/sample-expo-app/components/Collapsible.tsx +45 -0
  82. package/examples/sample-expo-app/components/ErrorView.tsx +52 -0
  83. package/examples/sample-expo-app/components/ExternalLink.tsx +24 -0
  84. package/examples/sample-expo-app/components/HapticTab.tsx +18 -0
  85. package/examples/sample-expo-app/components/HelloWave.tsx +40 -0
  86. package/examples/sample-expo-app/components/LoadingSpinner.tsx +34 -0
  87. package/examples/sample-expo-app/components/ParallaxScrollView.tsx +82 -0
  88. package/examples/sample-expo-app/components/ThemedText.tsx +60 -0
  89. package/examples/sample-expo-app/components/ThemedView.tsx +14 -0
  90. package/examples/sample-expo-app/components/ui/IconSymbol.ios.tsx +32 -0
  91. package/examples/sample-expo-app/components/ui/IconSymbol.tsx +41 -0
  92. package/examples/sample-expo-app/components/ui/TabBarBackground.ios.tsx +19 -0
  93. package/examples/sample-expo-app/components/ui/TabBarBackground.tsx +6 -0
  94. package/examples/sample-expo-app/constants/Colors.ts +26 -0
  95. package/examples/sample-expo-app/eslint.config.js +10 -0
  96. package/examples/sample-expo-app/hooks/useApi.ts +41 -0
  97. package/examples/sample-expo-app/hooks/useColorScheme.ts +1 -0
  98. package/examples/sample-expo-app/hooks/useColorScheme.web.ts +21 -0
  99. package/examples/sample-expo-app/hooks/useThemeColor.ts +21 -0
  100. package/examples/sample-expo-app/metro.config.js +26 -0
  101. package/examples/sample-expo-app/package-lock.json +26296 -0
  102. package/examples/sample-expo-app/package.json +59 -0
  103. package/examples/sample-expo-app/scripts/reset-project.js +112 -0
  104. package/examples/sample-expo-app/services/api.ts +98 -0
  105. package/examples/sample-expo-app/tsconfig.json +17 -0
  106. package/examples/sample-expo-app/utils/navigation.ts +19 -0
  107. package/package.json +98 -0
  108. package/src/config/masking.ts +78 -0
  109. package/src/expo.ts +41 -0
  110. package/src/index.ts +20 -0
  111. package/src/otel/helpers.ts +21 -0
  112. package/src/otel/index.ts +348 -0
  113. package/src/otel/instrumentations/gestureInstrumentation.ts +141 -0
  114. package/src/otel/instrumentations/index.ts +86 -0
  115. package/src/otel/instrumentations/reactNativeInstrumentation.ts +164 -0
  116. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +114 -0
  117. package/src/recorder/gestureRecorder.ts +429 -0
  118. package/src/recorder/index.ts +71 -0
  119. package/src/recorder/navigationTracker.ts +447 -0
  120. package/src/recorder/screenRecorder.ts +411 -0
  121. package/src/services/api.service.ts +78 -0
  122. package/src/services/storage.service.ts +130 -0
  123. package/src/sessionRecorder.ts +367 -0
  124. package/src/types/expo.d.ts +23 -0
  125. package/src/types/index.ts +88 -0
  126. package/src/utils/platform.ts +75 -0
  127. package/src/version.ts +1 -0
  128. package/tsconfig.json +24 -0
@@ -0,0 +1,42 @@
1
+ {
2
+ "expo": {
3
+ "name": "sample-expo-app",
4
+ "slug": "sample-expo-app",
5
+ "version": "1.0.0",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/images/icon.png",
8
+ "scheme": "sampleexpoapp",
9
+ "userInterfaceStyle": "automatic",
10
+ "newArchEnabled": true,
11
+ "ios": {
12
+ "supportsTablet": true
13
+ },
14
+ "android": {
15
+ "adaptiveIcon": {
16
+ "foregroundImage": "./assets/images/adaptive-icon.png",
17
+ "backgroundColor": "#ffffff"
18
+ },
19
+ "edgeToEdgeEnabled": true
20
+ },
21
+ "web": {
22
+ "bundler": "metro",
23
+ "output": "static",
24
+ "favicon": "./assets/images/favicon.png"
25
+ },
26
+ "plugins": [
27
+ "expo-router",
28
+ [
29
+ "expo-splash-screen",
30
+ {
31
+ "image": "./assets/images/splash-icon.png",
32
+ "imageWidth": 200,
33
+ "resizeMode": "contain",
34
+ "backgroundColor": "#ffffff"
35
+ }
36
+ ]
37
+ ],
38
+ "experiments": {
39
+ "typedRoutes": true
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,45 @@
1
+ import { PropsWithChildren, useState } from 'react';
2
+ import { StyleSheet, TouchableOpacity } from 'react-native';
3
+
4
+ import { ThemedText } from '@/components/ThemedText';
5
+ import { ThemedView } from '@/components/ThemedView';
6
+ import { IconSymbol } from '@/components/ui/IconSymbol';
7
+ import { Colors } from '@/constants/Colors';
8
+ import { useColorScheme } from '@/hooks/useColorScheme';
9
+
10
+ export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
11
+ const [isOpen, setIsOpen] = useState(false);
12
+ const theme = useColorScheme() ?? 'light';
13
+
14
+ return (
15
+ <ThemedView>
16
+ <TouchableOpacity
17
+ style={styles.heading}
18
+ onPress={() => setIsOpen((value) => !value)}
19
+ activeOpacity={0.8}>
20
+ <IconSymbol
21
+ name="chevron.right"
22
+ size={18}
23
+ weight="medium"
24
+ color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
25
+ style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
26
+ />
27
+
28
+ <ThemedText type="defaultSemiBold">{title}</ThemedText>
29
+ </TouchableOpacity>
30
+ {isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
31
+ </ThemedView>
32
+ );
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ heading: {
37
+ flexDirection: 'row',
38
+ alignItems: 'center',
39
+ gap: 6,
40
+ },
41
+ content: {
42
+ marginTop: 6,
43
+ marginLeft: 24,
44
+ },
45
+ });
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import { StyleSheet, TouchableOpacity } from 'react-native'
3
+ import { ThemedView } from './ThemedView'
4
+ import { ThemedText } from './ThemedText'
5
+ import { IconSymbol } from './ui/IconSymbol'
6
+ import { Colors } from '@/constants/Colors'
7
+ import { useColorScheme } from '@/hooks/useColorScheme'
8
+
9
+ interface ErrorViewProps {
10
+ message: string
11
+ onRetry?: () => void
12
+ }
13
+
14
+ export function ErrorView({ message, onRetry }: ErrorViewProps) {
15
+ const colorScheme = useColorScheme()
16
+
17
+ return (
18
+ <ThemedView style={styles.container}>
19
+ <IconSymbol name='exclamationmark.triangle.fill' size={48} color={Colors[colorScheme ?? 'light'].tint} />
20
+ <ThemedText style={styles.message}>{message}</ThemedText>
21
+ {onRetry && (
22
+ <TouchableOpacity style={styles.retryButton} onPress={onRetry}>
23
+ <ThemedText style={styles.retryText}>Try Again</ThemedText>
24
+ </TouchableOpacity>
25
+ )}
26
+ </ThemedView>
27
+ )
28
+ }
29
+
30
+ const styles = StyleSheet.create({
31
+ container: {
32
+ flex: 1,
33
+ justifyContent: 'center',
34
+ alignItems: 'center',
35
+ padding: 20
36
+ },
37
+ message: {
38
+ marginTop: 16,
39
+ textAlign: 'center',
40
+ marginBottom: 20
41
+ },
42
+ retryButton: {
43
+ backgroundColor: Colors.light.tint,
44
+ paddingHorizontal: 20,
45
+ paddingVertical: 10,
46
+ borderRadius: 8
47
+ },
48
+ retryText: {
49
+ color: 'white',
50
+ fontWeight: '600'
51
+ }
52
+ })
@@ -0,0 +1,24 @@
1
+ import { Href, Link } from 'expo-router';
2
+ import { openBrowserAsync } from 'expo-web-browser';
3
+ import { type ComponentProps } from 'react';
4
+ import { Platform } from 'react-native';
5
+
6
+ type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string };
7
+
8
+ export function ExternalLink({ href, ...rest }: Props) {
9
+ return (
10
+ <Link
11
+ target="_blank"
12
+ {...rest}
13
+ href={href}
14
+ onPress={async (event) => {
15
+ if (Platform.OS !== 'web') {
16
+ // Prevent the default behavior of linking to the default browser on native.
17
+ event.preventDefault();
18
+ // Open the link in an in-app browser.
19
+ await openBrowserAsync(href);
20
+ }
21
+ }}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,18 @@
1
+ import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
2
+ import { PlatformPressable } from '@react-navigation/elements';
3
+ import * as Haptics from 'expo-haptics';
4
+
5
+ export function HapticTab(props: BottomTabBarButtonProps) {
6
+ return (
7
+ <PlatformPressable
8
+ {...props}
9
+ onPressIn={(ev) => {
10
+ if (process.env.EXPO_OS === 'ios') {
11
+ // Add a soft haptic feedback when pressing down on the tabs.
12
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
13
+ }
14
+ props.onPressIn?.(ev);
15
+ }}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,40 @@
1
+ import { useEffect } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withSequence,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
10
+
11
+ import { ThemedText } from '@/components/ThemedText';
12
+
13
+ export function HelloWave() {
14
+ const rotationAnimation = useSharedValue(0);
15
+
16
+ useEffect(() => {
17
+ rotationAnimation.value = withRepeat(
18
+ withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
19
+ 4 // Run the animation 4 times
20
+ );
21
+ }, [rotationAnimation]);
22
+
23
+ const animatedStyle = useAnimatedStyle(() => ({
24
+ transform: [{ rotate: `${rotationAnimation.value}deg` }],
25
+ }));
26
+
27
+ return (
28
+ <Animated.View style={animatedStyle}>
29
+ <ThemedText style={styles.text}>👋</ThemedText>
30
+ </Animated.View>
31
+ );
32
+ }
33
+
34
+ const styles = StyleSheet.create({
35
+ text: {
36
+ fontSize: 28,
37
+ lineHeight: 32,
38
+ marginTop: -6,
39
+ },
40
+ });
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+ import { ActivityIndicator, StyleSheet } from 'react-native'
3
+ import { ThemedView } from './ThemedView'
4
+ import { ThemedText } from './ThemedText'
5
+ import { Colors } from '@/constants/Colors'
6
+ import { useColorScheme } from '@/hooks/useColorScheme'
7
+
8
+ interface LoadingSpinnerProps {
9
+ message?: string
10
+ }
11
+
12
+ export function LoadingSpinner({ message = 'Loading...' }: LoadingSpinnerProps) {
13
+ const colorScheme = useColorScheme()
14
+
15
+ return (
16
+ <ThemedView style={styles.container}>
17
+ <ActivityIndicator size='large' color={Colors[colorScheme ?? 'light'].tint} />
18
+ <ThemedText style={styles.message}>{message}</ThemedText>
19
+ </ThemedView>
20
+ )
21
+ }
22
+
23
+ const styles = StyleSheet.create({
24
+ container: {
25
+ flex: 1,
26
+ justifyContent: 'center',
27
+ alignItems: 'center',
28
+ padding: 20
29
+ },
30
+ message: {
31
+ marginTop: 16,
32
+ textAlign: 'center'
33
+ }
34
+ })
@@ -0,0 +1,82 @@
1
+ import type { PropsWithChildren, ReactElement } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Animated, {
4
+ interpolate,
5
+ useAnimatedRef,
6
+ useAnimatedStyle,
7
+ useScrollViewOffset,
8
+ } from 'react-native-reanimated';
9
+
10
+ import { ThemedView } from '@/components/ThemedView';
11
+ import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
12
+ import { useColorScheme } from '@/hooks/useColorScheme';
13
+
14
+ const HEADER_HEIGHT = 250;
15
+
16
+ type Props = PropsWithChildren<{
17
+ headerImage: ReactElement;
18
+ headerBackgroundColor: { dark: string; light: string };
19
+ }>;
20
+
21
+ export default function ParallaxScrollView({
22
+ children,
23
+ headerImage,
24
+ headerBackgroundColor,
25
+ }: Props) {
26
+ const colorScheme = useColorScheme() ?? 'light';
27
+ const scrollRef = useAnimatedRef<Animated.ScrollView>();
28
+ const scrollOffset = useScrollViewOffset(scrollRef);
29
+ const bottom = useBottomTabOverflow();
30
+ const headerAnimatedStyle = useAnimatedStyle(() => {
31
+ return {
32
+ transform: [
33
+ {
34
+ translateY: interpolate(
35
+ scrollOffset.value,
36
+ [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
37
+ [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
38
+ ),
39
+ },
40
+ {
41
+ scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
42
+ },
43
+ ],
44
+ };
45
+ });
46
+
47
+ return (
48
+ <ThemedView style={styles.container}>
49
+ <Animated.ScrollView
50
+ ref={scrollRef}
51
+ scrollEventThrottle={16}
52
+ scrollIndicatorInsets={{ bottom }}
53
+ contentContainerStyle={{ paddingBottom: bottom }}>
54
+ <Animated.View
55
+ style={[
56
+ styles.header,
57
+ { backgroundColor: headerBackgroundColor[colorScheme] },
58
+ headerAnimatedStyle,
59
+ ]}>
60
+ {headerImage}
61
+ </Animated.View>
62
+ <ThemedView style={styles.content}>{children}</ThemedView>
63
+ </Animated.ScrollView>
64
+ </ThemedView>
65
+ );
66
+ }
67
+
68
+ const styles = StyleSheet.create({
69
+ container: {
70
+ flex: 1,
71
+ },
72
+ header: {
73
+ height: HEADER_HEIGHT,
74
+ overflow: 'hidden',
75
+ },
76
+ content: {
77
+ flex: 1,
78
+ padding: 32,
79
+ gap: 16,
80
+ overflow: 'hidden',
81
+ },
82
+ });
@@ -0,0 +1,60 @@
1
+ import { StyleSheet, Text, type TextProps } from 'react-native';
2
+
3
+ import { useThemeColor } from '@/hooks/useThemeColor';
4
+
5
+ export type ThemedTextProps = TextProps & {
6
+ lightColor?: string;
7
+ darkColor?: string;
8
+ type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9
+ };
10
+
11
+ export function ThemedText({
12
+ style,
13
+ lightColor,
14
+ darkColor,
15
+ type = 'default',
16
+ ...rest
17
+ }: ThemedTextProps) {
18
+ const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19
+
20
+ return (
21
+ <Text
22
+ style={[
23
+ { color },
24
+ type === 'default' ? styles.default : undefined,
25
+ type === 'title' ? styles.title : undefined,
26
+ type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
27
+ type === 'subtitle' ? styles.subtitle : undefined,
28
+ type === 'link' ? styles.link : undefined,
29
+ style,
30
+ ]}
31
+ {...rest}
32
+ />
33
+ );
34
+ }
35
+
36
+ const styles = StyleSheet.create({
37
+ default: {
38
+ fontSize: 16,
39
+ lineHeight: 24,
40
+ },
41
+ defaultSemiBold: {
42
+ fontSize: 16,
43
+ lineHeight: 24,
44
+ fontWeight: '600',
45
+ },
46
+ title: {
47
+ fontSize: 32,
48
+ fontWeight: 'bold',
49
+ lineHeight: 32,
50
+ },
51
+ subtitle: {
52
+ fontSize: 20,
53
+ fontWeight: 'bold',
54
+ },
55
+ link: {
56
+ lineHeight: 30,
57
+ fontSize: 16,
58
+ color: '#0a7ea4',
59
+ },
60
+ });
@@ -0,0 +1,14 @@
1
+ import { View, type ViewProps } from 'react-native';
2
+
3
+ import { useThemeColor } from '@/hooks/useThemeColor';
4
+
5
+ export type ThemedViewProps = ViewProps & {
6
+ lightColor?: string;
7
+ darkColor?: string;
8
+ };
9
+
10
+ export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11
+ const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12
+
13
+ return <View style={[{ backgroundColor }, style]} {...otherProps} />;
14
+ }
@@ -0,0 +1,32 @@
1
+ import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export function IconSymbol({
5
+ name,
6
+ size = 24,
7
+ color,
8
+ style,
9
+ weight = 'regular',
10
+ }: {
11
+ name: SymbolViewProps['name'];
12
+ size?: number;
13
+ color: string;
14
+ style?: StyleProp<ViewStyle>;
15
+ weight?: SymbolWeight;
16
+ }) {
17
+ return (
18
+ <SymbolView
19
+ weight={weight}
20
+ tintColor={color}
21
+ resizeMode="scaleAspectFit"
22
+ name={name}
23
+ style={[
24
+ {
25
+ width: size,
26
+ height: size,
27
+ },
28
+ style,
29
+ ]}
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,41 @@
1
+ // Fallback for using MaterialIcons on Android and web.
2
+
3
+ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
4
+ import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
5
+ import { ComponentProps } from 'react';
6
+ import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
7
+
8
+ type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
9
+ type IconSymbolName = keyof typeof MAPPING;
10
+
11
+ /**
12
+ * Add your SF Symbols to Material Icons mappings here.
13
+ * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
14
+ * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
15
+ */
16
+ const MAPPING = {
17
+ 'house.fill': 'home',
18
+ 'paperplane.fill': 'send',
19
+ 'chevron.left.forwardslash.chevron.right': 'code',
20
+ 'chevron.right': 'chevron-right',
21
+ } as IconMapping;
22
+
23
+ /**
24
+ * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
25
+ * This ensures a consistent look across platforms, and optimal resource usage.
26
+ * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
27
+ */
28
+ export function IconSymbol({
29
+ name,
30
+ size = 24,
31
+ color,
32
+ style,
33
+ }: {
34
+ name: IconSymbolName;
35
+ size?: number;
36
+ color: string | OpaqueColorValue;
37
+ style?: StyleProp<TextStyle>;
38
+ weight?: SymbolWeight;
39
+ }) {
40
+ return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
41
+ }
@@ -0,0 +1,19 @@
1
+ import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
2
+ import { BlurView } from 'expo-blur';
3
+ import { StyleSheet } from 'react-native';
4
+
5
+ export default function BlurTabBarBackground() {
6
+ return (
7
+ <BlurView
8
+ // System chrome material automatically adapts to the system's theme
9
+ // and matches the native tab bar appearance on iOS.
10
+ tint="systemChromeMaterial"
11
+ intensity={100}
12
+ style={StyleSheet.absoluteFill}
13
+ />
14
+ );
15
+ }
16
+
17
+ export function useBottomTabOverflow() {
18
+ return useBottomTabBarHeight();
19
+ }
@@ -0,0 +1,6 @@
1
+ // This is a shim for web and Android where the tab bar is generally opaque.
2
+ export default undefined;
3
+
4
+ export function useBottomTabOverflow() {
5
+ return 0;
6
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3
+ * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4
+ */
5
+
6
+ const tintColorLight = '#0a7ea4';
7
+ const tintColorDark = '#fff';
8
+
9
+ export const Colors = {
10
+ light: {
11
+ text: '#11181C',
12
+ background: '#fff',
13
+ tint: tintColorLight,
14
+ icon: '#687076',
15
+ tabIconDefault: '#687076',
16
+ tabIconSelected: tintColorLight,
17
+ },
18
+ dark: {
19
+ text: '#ECEDEE',
20
+ background: '#151718',
21
+ tint: tintColorDark,
22
+ icon: '#9BA1A6',
23
+ tabIconDefault: '#9BA1A6',
24
+ tabIconSelected: tintColorDark,
25
+ },
26
+ };
@@ -0,0 +1,10 @@
1
+ // https://docs.expo.dev/guides/using-eslint/
2
+ const { defineConfig } = require('eslint/config');
3
+ const expoConfig = require('eslint-config-expo/flat');
4
+
5
+ module.exports = defineConfig([
6
+ expoConfig,
7
+ {
8
+ ignores: ['dist/*'],
9
+ },
10
+ ]);
@@ -0,0 +1,41 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ interface ApiState<T> {
4
+ data: T | null;
5
+ loading: boolean;
6
+ error: string | null;
7
+ }
8
+
9
+ export function useApi<T>(
10
+ apiCall: () => Promise<T>,
11
+ dependencies: any[] = []
12
+ ): ApiState<T> & { refetch: () => void } {
13
+ const [state, setState] = useState<ApiState<T>>({
14
+ data: null,
15
+ loading: true,
16
+ error: null,
17
+ });
18
+
19
+ const fetchData = async () => {
20
+ setState(prev => ({ ...prev, loading: true, error: null }));
21
+ try {
22
+ const data = await apiCall();
23
+ setState({ data, loading: false, error: null });
24
+ } catch (error) {
25
+ setState({
26
+ data: null,
27
+ loading: false,
28
+ error: error instanceof Error ? error.message : 'An error occurred',
29
+ });
30
+ }
31
+ };
32
+
33
+ useEffect(() => {
34
+ fetchData();
35
+ }, dependencies);
36
+
37
+ return {
38
+ ...state,
39
+ refetch: fetchData,
40
+ };
41
+ }
@@ -0,0 +1 @@
1
+ export { useColorScheme } from 'react-native';
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useColorScheme as useRNColorScheme } from 'react-native';
3
+
4
+ /**
5
+ * To support static rendering, this value needs to be re-calculated on the client side for web
6
+ */
7
+ export function useColorScheme() {
8
+ const [hasHydrated, setHasHydrated] = useState(false);
9
+
10
+ useEffect(() => {
11
+ setHasHydrated(true);
12
+ }, []);
13
+
14
+ const colorScheme = useRNColorScheme();
15
+
16
+ if (hasHydrated) {
17
+ return colorScheme;
18
+ }
19
+
20
+ return 'light';
21
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Learn more about light and dark modes:
3
+ * https://docs.expo.dev/guides/color-schemes/
4
+ */
5
+
6
+ import { Colors } from '@/constants/Colors';
7
+ import { useColorScheme } from '@/hooks/useColorScheme';
8
+
9
+ export function useThemeColor(
10
+ props: { light?: string; dark?: string },
11
+ colorName: keyof typeof Colors.light & keyof typeof Colors.dark
12
+ ) {
13
+ const theme = useColorScheme() ?? 'light';
14
+ const colorFromProps = props[theme];
15
+
16
+ if (colorFromProps) {
17
+ return colorFromProps;
18
+ } else {
19
+ return Colors[theme][colorName];
20
+ }
21
+ }