├── .gitignore
├── DUMMY.env
├── README.md
├── app.json
├── app
├── (authenticated)
│ ├── (modals)
│ │ ├── account.tsx
│ │ └── lock.tsx
│ ├── (tabs)
│ │ ├── _layout.tsx
│ │ ├── crypto.tsx
│ │ ├── home.tsx
│ │ ├── invest.tsx
│ │ ├── lifestyle.tsx
│ │ └── transfers.tsx
│ └── crypto
│ │ └── [id].tsx
├── _layout.tsx
├── api
│ ├── info+api.ts
│ ├── listings+api.ts
│ └── tickers+api.ts
├── help.tsx
├── index.tsx
├── login.tsx
├── signup.tsx
└── verify
│ └── [phone].tsx
├── assets
├── fonts
│ ├── SpaceMono-Regular.ttf
│ └── fonts.d.ts
├── images
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── germany.png
│ ├── icon-dark.png
│ ├── icon-vivid.png
│ ├── icon.png
│ └── splash.png
└── videos
│ ├── intro.mp4
│ └── intro2.mp4
├── babel.config.js
├── banner.png
├── commands.sh
├── components
├── CustomHeader.tsx
├── Dropdown.tsx
├── RoundBtn.tsx
└── SortableList
│ ├── Config.tsx
│ ├── Item.tsx
│ ├── SortableList.tsx
│ ├── Tile.tsx
│ └── WidgetList.tsx
├── constants
├── Colors.ts
└── Styles.ts
├── context
└── UserInactivity.tsx
├── interfaces
└── crypto.ts
├── package-lock.json
├── package.json
├── screenshots
├── 1.png
├── 10.png
├── 11.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── charts.gif
├── icon.gif
├── lockscreen.gif
├── login.gif
└── state.gif
├── store
├── balanceStore.ts
└── mmkv-storage.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | android/
38 | ios/
39 | .env
40 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
41 | # The following patterns were generated by expo-cli
42 |
43 | expo-env.d.ts
44 | # @end expo-cli
--------------------------------------------------------------------------------
/DUMMY.env:
--------------------------------------------------------------------------------
1 | EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=
2 | CRYPTO_API_KEY=
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native FinTech Clone with Clerk
2 |
3 | This is a React Native FinTech clone using [Clerk](https://go.clerk.com/tQXLCe8) for user authentication with OTP. This app was inspired by the [Revolut](https://www.revolut.com/) app.
4 |
5 | Additional features:
6 |
7 | - [Expo Router](https://docs.expo.dev/routing/introduction/) file-based navigation and API Routes
8 | - [SMS OTP](https://clerk.com/docs/custom-flows/email-sms-otp?utm_source=sponsorship&utm_medium=github&utm_campaign=simong&utm_content=rn-fintech) Auth with Clerk
9 | - [Reanimated](https://docs.swmansion.com/react-native-reanimated/) 3 for animations
10 | - [Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/) for gestures
11 | - [Zustand](https://zustand-demo.pmnd.rs/) and [MMKV](https://github.com/mrousavy/react-native-mmkv) for state management
12 | - [Victory Native XL](https://commerce.nearform.com/open-source/victory-native) for charts
13 | - [Zeego](https://zeego.dev/start) for native menus
14 | - [CoinMarketCap API](https://coinmarketcap.com/api/documentation/v1/) for crypto prices
15 |
16 | ## Screenshots
17 |
18 |
19 |

20 |

21 |

22 |

23 |

24 |

25 |

26 |

27 |

28 |

29 |

30 |
31 |
32 |
33 | ## Demo
34 |
35 |
36 |

37 |

38 |

39 |

40 |

41 |
42 |
43 |
44 | ## 🚀 More
45 |
46 | **Take a shortcut from web developer to mobile development fluency with guided learning**
47 |
48 | Enjoyed this project? Learn to use React Native to build production-ready, native mobile apps for both iOS and Android based on your existing web development skills.
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "fintech",
4 | "slug": "fintech",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "assetBundlePatterns": ["**/*"],
16 | "ios": {
17 | "supportsTablet": true,
18 | "bundleIdentifier": "com.galaxies.fintech"
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/images/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | },
25 | "package": "com.supersimon.fintech"
26 | },
27 | "web": {
28 | "bundler": "metro",
29 | "output": "server",
30 | "favicon": "./assets/images/favicon.png"
31 | },
32 | "plugins": [
33 | [
34 | "expo-router",
35 | {
36 | "origin": "https://galaxies.dev"
37 | }
38 | ],
39 | "expo-secure-store",
40 | [
41 | "expo-local-authentication",
42 | {
43 | "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID."
44 | }
45 | ],
46 | [
47 | "expo-dynamic-app-icon",
48 | {
49 | "default": {
50 | "image": "./assets/images/icon.png",
51 | "prerendered": true
52 | },
53 | "dark": {
54 | "image": "./assets/images/icon-dark.png",
55 | "prerendered": true
56 | },
57 | "vivid": {
58 | "image": "./assets/images/icon-vivid.png",
59 | "prerendered": true
60 | }
61 | }
62 | ]
63 | ],
64 | "experiments": {
65 | "typedRoutes": true
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/(authenticated)/(modals)/account.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth, useUser } from '@clerk/clerk-expo';
2 | import { useEffect, useState } from 'react';
3 | import { View, Text, StyleSheet, TouchableOpacity, Image, TextInput } from 'react-native';
4 | import { BlurView } from 'expo-blur';
5 | import Colors from '@/constants/Colors';
6 | import { Ionicons } from '@expo/vector-icons';
7 | import * as ImagePicker from 'expo-image-picker';
8 | import { getAppIcon, setAppIcon } from 'expo-dynamic-app-icon';
9 |
10 | const ICONS = [
11 | {
12 | name: 'Default',
13 | icon: require('@/assets/images/icon.png'),
14 | },
15 | {
16 | name: 'Dark',
17 | icon: require('@/assets/images/icon-dark.png'),
18 | },
19 | {
20 | name: 'Vivid',
21 | icon: require('@/assets/images/icon-vivid.png'),
22 | },
23 | ];
24 |
25 | const Page = () => {
26 | const { user } = useUser();
27 | const { signOut } = useAuth();
28 | const [firstName, setFirstName] = useState(user?.firstName);
29 | const [lastName, setLastName] = useState(user?.lastName);
30 | const [edit, setEdit] = useState(false);
31 |
32 | const [activeIcon, setActiveIcon] = useState('Default');
33 |
34 | useEffect(() => {
35 | const loadCurrentIconPref = async () => {
36 | const icon = await getAppIcon();
37 | console.log('🚀 ~ loadCurrentIconPref ~ icon:', icon);
38 | setActiveIcon(icon);
39 | };
40 | loadCurrentIconPref();
41 | }, []);
42 |
43 | const onSaveUser = async () => {
44 | try {
45 | await user?.update({ firstName: firstName!, lastName: lastName! });
46 | setEdit(false);
47 | } catch (error) {
48 | console.error(error);
49 | } finally {
50 | setEdit(false);
51 | }
52 | };
53 |
54 | const onCaptureImage = async () => {
55 | let result = await ImagePicker.launchImageLibraryAsync({
56 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
57 | allowsEditing: true,
58 | aspect: [4, 3],
59 | quality: 0.75,
60 | base64: true,
61 | });
62 |
63 | if (!result.canceled) {
64 | const base64 = `data:image/png;base64,${result.assets[0].base64}`;
65 | console.log(base64);
66 |
67 | user?.setProfileImage({
68 | file: base64,
69 | });
70 | }
71 | };
72 |
73 | const onChangeAppIcon = async (icon: string) => {
74 | await setAppIcon(icon.toLowerCase());
75 | setActiveIcon(icon);
76 | };
77 |
78 | return (
79 |
83 |
84 |
85 | {user?.imageUrl && }
86 |
87 |
88 |
89 | {!edit && (
90 |
91 |
92 | {firstName} {lastName}
93 |
94 | setEdit(true)}>
95 |
96 |
97 |
98 | )}
99 | {edit && (
100 |
101 |
107 |
113 |
114 |
115 |
116 |
117 | )}
118 |
119 |
120 |
121 |
122 | signOut()}>
123 |
124 | Log out
125 |
126 |
127 |
128 | Account
129 |
130 |
131 |
132 | Learn
133 |
134 |
135 |
136 | Inbox
137 |
144 | 14
145 |
146 |
147 |
148 |
149 |
150 | {ICONS.map((icon) => (
151 | onChangeAppIcon(icon.name)}>
155 |
156 | {icon.name}
157 | {activeIcon.toLowerCase() === icon.name.toLowerCase() && (
158 |
159 | )}
160 |
161 | ))}
162 |
163 |
164 | );
165 | };
166 |
167 | const styles = StyleSheet.create({
168 | editRow: {
169 | flex: 1,
170 | flexDirection: 'row',
171 | gap: 12,
172 | alignItems: 'center',
173 | justifyContent: 'center',
174 | marginTop: 20,
175 | },
176 | avatar: {
177 | width: 100,
178 | height: 100,
179 | borderRadius: 50,
180 | backgroundColor: Colors.gray,
181 | },
182 | captureBtn: {
183 | width: 100,
184 | height: 100,
185 | borderRadius: 50,
186 | backgroundColor: Colors.gray,
187 | justifyContent: 'center',
188 | alignItems: 'center',
189 | },
190 | inputField: {
191 | width: 140,
192 | height: 44,
193 | borderWidth: 1,
194 | borderColor: Colors.gray,
195 | borderRadius: 8,
196 | padding: 10,
197 | backgroundColor: '#fff',
198 | },
199 | actions: {
200 | backgroundColor: 'rgba(256, 256, 256, 0.1)',
201 | borderRadius: 16,
202 | gap: 0,
203 | margin: 20,
204 | },
205 | btn: {
206 | padding: 14,
207 | flexDirection: 'row',
208 | gap: 20,
209 | },
210 | });
211 | export default Page;
212 |
--------------------------------------------------------------------------------
/app/(authenticated)/(modals)/lock.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { useUser } from '@clerk/clerk-expo';
3 | import { useEffect, useState } from 'react';
4 | import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
5 | import { SafeAreaView } from 'react-native-safe-area-context';
6 | import * as Haptics from 'expo-haptics';
7 | import { MaterialCommunityIcons } from '@expo/vector-icons';
8 | import { useRouter } from 'expo-router';
9 | import * as LocalAuthentication from 'expo-local-authentication';
10 | import Animated, {
11 | useAnimatedStyle,
12 | useSharedValue,
13 | withRepeat,
14 | withSequence,
15 | withTiming,
16 | } from 'react-native-reanimated';
17 |
18 | const Page = () => {
19 | const { user } = useUser();
20 | const [firstName, setFirstName] = useState(user?.firstName);
21 | const [code, setCode] = useState([]);
22 | const codeLength = Array(6).fill(0);
23 | const router = useRouter();
24 |
25 | const offset = useSharedValue(0);
26 |
27 | const style = useAnimatedStyle(() => {
28 | return {
29 | transform: [{ translateX: offset.value }],
30 | };
31 | });
32 |
33 | const OFFSET = 20;
34 | const TIME = 80;
35 |
36 | useEffect(() => {
37 | if (code.length === 6) {
38 | if (code.join('') === '111111') {
39 | router.replace('/(authenticated)/(tabs)/home');
40 | setCode([]);
41 | } else {
42 | offset.value = withSequence(
43 | withTiming(-OFFSET, { duration: TIME / 2 }),
44 | withRepeat(withTiming(OFFSET, { duration: TIME }), 4, true),
45 | withTiming(0, { duration: TIME / 2 })
46 | );
47 | Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
48 | setCode([]);
49 | }
50 | }
51 | }, [code]);
52 |
53 | const onNumberPress = (number: number) => {
54 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
55 | setCode([...code, number]);
56 | };
57 |
58 | const numberBackspace = () => {
59 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
60 | setCode(code.slice(0, -1));
61 | };
62 |
63 | const onBiometricAuthPress = async () => {
64 | const { success } = await LocalAuthentication.authenticateAsync();
65 | if (success) {
66 | router.replace('/(authenticated)/(tabs)/home');
67 | } else {
68 | Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
69 | }
70 | };
71 |
72 | return (
73 |
74 | Welcome back, {firstName}
75 |
76 |
77 | {codeLength.map((_, index) => (
78 |
87 | ))}
88 |
89 |
90 |
91 |
92 | {[1, 2, 3].map((number) => (
93 | onNumberPress(number)}>
94 | {number}
95 |
96 | ))}
97 |
98 |
99 |
100 | {[4, 5, 6].map((number) => (
101 | onNumberPress(number)}>
102 | {number}
103 |
104 | ))}
105 |
106 |
107 | {[7, 8, 9].map((number) => (
108 | onNumberPress(number)}>
109 | {number}
110 |
111 | ))}
112 |
113 |
115 |
116 |
117 |
118 |
119 | onNumberPress(0)}>
120 | 0
121 |
122 |
123 |
124 | {code.length > 0 && (
125 |
126 |
127 |
128 |
129 |
130 | )}
131 |
132 |
133 |
140 | Forgot your passcode?
141 |
142 |
143 |
144 | );
145 | };
146 |
147 | const styles = StyleSheet.create({
148 | greeting: {
149 | fontSize: 24,
150 | fontWeight: 'bold',
151 | marginTop: 80,
152 | alignSelf: 'center',
153 | },
154 | codeView: {
155 | flexDirection: 'row',
156 | justifyContent: 'center',
157 | alignItems: 'center',
158 | gap: 20,
159 | marginVertical: 100,
160 | },
161 | codeEmpty: {
162 | width: 20,
163 | height: 20,
164 | borderRadius: 10,
165 | },
166 | numbersView: {
167 | marginHorizontal: 80,
168 | gap: 60,
169 | },
170 | number: {
171 | fontSize: 32,
172 | },
173 | });
174 | export default Page;
175 |
--------------------------------------------------------------------------------
/app/(authenticated)/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { FontAwesome } from '@expo/vector-icons';
3 | import { Tabs } from 'expo-router';
4 | import { BlurView } from 'expo-blur';
5 | import CustomHeader from '@/components/CustomHeader';
6 |
7 | const Layout = () => {
8 | return (
9 | (
13 |
21 | ),
22 | tabBarStyle: {
23 | backgroundColor: 'transparent',
24 | position: 'absolute',
25 | bottom: 0,
26 | left: 0,
27 | right: 0,
28 | elevation: 0,
29 | borderTopWidth: 0,
30 | },
31 | }}>
32 | (
37 |
38 | ),
39 | header: () => ,
40 | headerTransparent: true,
41 | }}
42 | />
43 | (
48 |
49 | ),
50 | }}
51 | />
52 | (
57 |
58 | ),
59 | }}
60 | />
61 | ,
66 | header: () => ,
67 | headerTransparent: true,
68 | }}
69 | />
70 | ,
75 | }}
76 | />
77 |
78 | );
79 | };
80 | export default Layout;
81 |
--------------------------------------------------------------------------------
/app/(authenticated)/(tabs)/crypto.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, Image, TouchableOpacity, ScrollView } from 'react-native';
2 | import { useQuery } from '@tanstack/react-query';
3 | import { Currency } from '@/interfaces/crypto';
4 | import { Link } from 'expo-router';
5 | import { useHeaderHeight } from '@react-navigation/elements';
6 | import Colors from '@/constants/Colors';
7 | import { defaultStyles } from '@/constants/Styles';
8 | import { Ionicons } from '@expo/vector-icons';
9 |
10 | const Page = () => {
11 | const headerHeight = useHeaderHeight();
12 |
13 | const currencies = useQuery({
14 | queryKey: ['listings'],
15 | queryFn: () => fetch('/api/listings').then((res) => res.json()),
16 | });
17 |
18 | const ids = currencies.data?.map((currency: Currency) => currency.id).join(',');
19 |
20 | const { data } = useQuery({
21 | queryKey: ['info', ids],
22 | queryFn: () => fetch(`/api/info?ids=${ids}`).then((res) => res.json()),
23 | enabled: !!ids,
24 | });
25 |
26 | return (
27 |
30 | Latest Crypot
31 |
32 | {currencies.data?.map((currency: Currency) => (
33 |
34 |
35 |
36 |
37 | {currency.name}
38 | {currency.symbol}
39 |
40 |
41 | {currency.quote.EUR.price.toFixed(2)} €
42 |
43 | 0 ? 'caret-up' : 'caret-down'}
45 | size={16}
46 | color={currency.quote.EUR.percent_change_1h > 0 ? 'green' : 'red'}
47 | />
48 | 0 ? 'green' : 'red' }}>
50 | {currency.quote.EUR.percent_change_1h.toFixed(2)} %
51 |
52 |
53 |
54 |
55 |
56 | ))}
57 |
58 |
59 | );
60 | };
61 | export default Page;
62 |
--------------------------------------------------------------------------------
/app/(authenticated)/(tabs)/home.tsx:
--------------------------------------------------------------------------------
1 | import Dropdown from '@/components/Dropdown';
2 | import RoundBtn from '@/components/RoundBtn';
3 | import WidgetList from '@/components/SortableList/WidgetList';
4 | import Colors from '@/constants/Colors';
5 | import { defaultStyles } from '@/constants/Styles';
6 | import { useBalanceStore } from '@/store/balanceStore';
7 | import { Ionicons } from '@expo/vector-icons';
8 | import { View, Text, ScrollView, StyleSheet, Button, TouchableOpacity } from 'react-native';
9 | import { useHeaderHeight } from '@react-navigation/elements';
10 |
11 | const Page = () => {
12 | const { balance, runTransaction, transactions, clearTransactions } = useBalanceStore();
13 | const headerHeight = useHeaderHeight();
14 |
15 | const onAddMoney = () => {
16 | runTransaction({
17 | id: Math.random().toString(),
18 | amount: Math.floor(Math.random() * 1000) * (Math.random() > 0.5 ? 1 : -1),
19 | date: new Date(),
20 | title: 'Added money',
21 | });
22 | };
23 |
24 | return (
25 |
30 |
31 |
32 | {balance()}
33 | €
34 |
35 |
40 | Accounts
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Transactions
52 |
53 | {transactions.length === 0 && (
54 | No transactions yet
55 | )}
56 | {transactions.map((transaction) => (
57 |
60 |
61 | 0 ? 'add' : 'remove'}
63 | size={24}
64 | color={Colors.dark}
65 | />
66 |
67 |
68 |
69 | {transaction.title}
70 |
71 | {transaction.date.toLocaleString()}
72 |
73 |
74 | {transaction.amount}€
75 |
76 | ))}
77 |
78 | Widgets
79 |
80 |
81 | );
82 | };
83 | const styles = StyleSheet.create({
84 | account: {
85 | margin: 80,
86 | alignItems: 'center',
87 | },
88 | row: {
89 | flexDirection: 'row',
90 | alignItems: 'flex-end',
91 | justifyContent: 'center',
92 | gap: 10,
93 | },
94 | balance: {
95 | fontSize: 50,
96 | fontWeight: 'bold',
97 | },
98 | currency: {
99 | fontSize: 20,
100 | fontWeight: '500',
101 | },
102 | actionRow: {
103 | flexDirection: 'row',
104 | justifyContent: 'space-between',
105 | padding: 20,
106 | },
107 | transactions: {
108 | marginHorizontal: 20,
109 | padding: 14,
110 | backgroundColor: '#fff',
111 | borderRadius: 16,
112 | gap: 20,
113 | },
114 | circle: {
115 | width: 40,
116 | height: 40,
117 | borderRadius: 20,
118 | backgroundColor: Colors.lightGray,
119 | justifyContent: 'center',
120 | alignItems: 'center',
121 | },
122 | });
123 | export default Page;
124 |
--------------------------------------------------------------------------------
/app/(authenticated)/(tabs)/invest.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | const Page = () => {
3 | return (
4 |
5 | Page
6 |
7 | );
8 | };
9 | export default Page;
10 |
--------------------------------------------------------------------------------
/app/(authenticated)/(tabs)/lifestyle.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | const Page = () => {
3 | return (
4 |
5 | Page
6 |
7 | );
8 | };
9 | export default Page;
10 |
--------------------------------------------------------------------------------
/app/(authenticated)/(tabs)/transfers.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | const Page = () => {
3 | return (
4 |
5 | Page
6 |
7 | );
8 | };
9 | export default Page;
10 |
--------------------------------------------------------------------------------
/app/(authenticated)/crypto/[id].tsx:
--------------------------------------------------------------------------------
1 | import { Stack, useLocalSearchParams } from 'expo-router';
2 | import {
3 | View,
4 | Text,
5 | SectionList,
6 | StyleSheet,
7 | Image,
8 | TouchableOpacity,
9 | ScrollView,
10 | TextInput,
11 | } from 'react-native';
12 | import { useHeaderHeight } from '@react-navigation/elements';
13 | import { defaultStyles } from '@/constants/Styles';
14 | import Colors from '@/constants/Colors';
15 | import { useQuery } from '@tanstack/react-query';
16 | import { Ionicons } from '@expo/vector-icons';
17 | import { useEffect, useState } from 'react';
18 | const categories = ['Overview', 'News', 'Orders', 'Transactions'];
19 | import { CartesianChart, Line, useChartPressState } from 'victory-native';
20 | import { Circle, useFont } from '@shopify/react-native-skia';
21 | import { format } from 'date-fns';
22 | import * as Haptics from 'expo-haptics';
23 | import Animated, { SharedValue, useAnimatedProps } from 'react-native-reanimated';
24 |
25 | Animated.addWhitelistedNativeProps({ text: true });
26 | const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
27 |
28 | function ToolTip({ x, y }: { x: SharedValue; y: SharedValue }) {
29 | return ;
30 | }
31 |
32 | const Page = () => {
33 | const { id } = useLocalSearchParams();
34 | const headerHeight = useHeaderHeight();
35 | const [activeIndex, setActiveIndex] = useState(0);
36 | const font = useFont(require('@/assets/fonts/SpaceMono-Regular.ttf'), 12);
37 | const { state, isActive } = useChartPressState({ x: 0, y: { price: 0 } });
38 |
39 | useEffect(() => {
40 | console.log(isActive);
41 | if (isActive) Haptics.selectionAsync();
42 | }, [isActive]);
43 |
44 | const { data } = useQuery({
45 | queryKey: ['info', id],
46 | queryFn: async () => {
47 | const info = await fetch(`/api/info?ids=${id}`).then((res) => res.json());
48 | return info[+id];
49 | },
50 | });
51 |
52 | const { data: tickers } = useQuery({
53 | queryKey: ['tickers'],
54 | queryFn: async (): Promise => fetch(`/api/tickers`).then((res) => res.json()),
55 | });
56 |
57 | const animatedText = useAnimatedProps(() => {
58 | return {
59 | text: `${state.y.price.value.value.toFixed(2)} €`,
60 | defaultValue: '',
61 | };
62 | });
63 |
64 | const animatedDateText = useAnimatedProps(() => {
65 | const date = new Date(state.x.value.value);
66 | return {
67 | text: `${date.toLocaleDateString()}`,
68 | defaultValue: '',
69 | };
70 | });
71 |
72 | return (
73 | <>
74 |
75 | i.title}
80 | sections={[{ data: [{ title: 'Chart' }] }]}
81 | renderSectionHeader={() => (
82 |
95 | {categories.map((item, index) => (
96 | setActiveIndex(index)}
99 | style={activeIndex === index ? styles.categoriesBtnActive : styles.categoriesBtn}>
100 |
102 | {item}
103 |
104 |
105 | ))}
106 |
107 | )}
108 | ListHeaderComponent={() => (
109 | <>
110 |
117 | {data?.symbol}
118 |
119 |
120 |
121 |
122 |
127 |
128 | Buy
129 |
130 |
135 |
136 | Receive
137 |
138 |
139 | >
140 | )}
141 | renderItem={({ item }) => (
142 | <>
143 |
144 | {tickers && (
145 | <>
146 | {!isActive && (
147 |
148 |
149 | {tickers[tickers.length - 1].price.toFixed(2)} €
150 |
151 | Today
152 |
153 | )}
154 | {isActive && (
155 |
156 |
161 |
166 |
167 | )}
168 | `${v} €`,
176 | formatXLabel: (ms) => format(new Date(ms), 'MM/yy'),
177 | }}
178 | data={tickers!}
179 | xKey="timestamp"
180 | yKeys={['price']}>
181 | {({ points }) => (
182 | <>
183 |
184 | {isActive && }
185 | >
186 | )}
187 |
188 | >
189 | )}
190 |
191 |
192 | Overview
193 |
194 | Bitcoin is a decentralized digital currency, without a central bank or single
195 | administrator, that can be sent from user to user on the peer-to-peer bitcoin
196 | network without the need for intermediaries. Transactions are verified by network
197 | nodes through cryptography and recorded in a public distributed ledger called a
198 | blockchain.
199 |
200 |
201 | >
202 | )}>
203 | >
204 | );
205 | };
206 | const styles = StyleSheet.create({
207 | subtitle: {
208 | fontSize: 20,
209 | fontWeight: 'bold',
210 | marginBottom: 20,
211 | color: Colors.gray,
212 | },
213 | categoryText: {
214 | fontSize: 14,
215 | color: Colors.gray,
216 | },
217 | categoryTextActive: {
218 | fontSize: 14,
219 | color: '#000',
220 | },
221 | categoriesBtn: {
222 | padding: 10,
223 | paddingHorizontal: 14,
224 | alignItems: 'center',
225 | justifyContent: 'center',
226 | borderRadius: 20,
227 | },
228 | categoriesBtnActive: {
229 | padding: 10,
230 | paddingHorizontal: 14,
231 |
232 | alignItems: 'center',
233 | justifyContent: 'center',
234 | backgroundColor: '#fff',
235 | borderRadius: 20,
236 | },
237 | });
238 | export default Page;
239 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { ClerkProvider, useAuth } from '@clerk/clerk-expo';
3 | import { Ionicons } from '@expo/vector-icons';
4 | import FontAwesome from '@expo/vector-icons/FontAwesome';
5 | import { useFonts } from 'expo-font';
6 | import { Link, Stack, useRouter, useSegments } from 'expo-router';
7 | import * as SplashScreen from 'expo-splash-screen';
8 | import { StatusBar } from 'expo-status-bar';
9 | import { useEffect } from 'react';
10 | import { TouchableOpacity, Text, View, ActivityIndicator } from 'react-native';
11 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
12 | const CLERK_PUBLISHABLE_KEY = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
13 | import * as SecureStore from 'expo-secure-store';
14 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
15 | import { UserInactivityProvider } from '@/context/UserInactivity';
16 | const queryClient = new QueryClient();
17 |
18 | // Cache the Clerk JWT
19 | const tokenCache = {
20 | async getToken(key: string) {
21 | try {
22 | return SecureStore.getItemAsync(key);
23 | } catch (err) {
24 | return null;
25 | }
26 | },
27 | async saveToken(key: string, value: string) {
28 | try {
29 | return SecureStore.setItemAsync(key, value);
30 | } catch (err) {
31 | return;
32 | }
33 | },
34 | };
35 |
36 | export {
37 | // Catch any errors thrown by the Layout component.
38 | ErrorBoundary,
39 | } from 'expo-router';
40 |
41 | // Prevent the splash screen from auto-hiding before asset loading is complete.
42 | SplashScreen.preventAutoHideAsync();
43 |
44 | const InitialLayout = () => {
45 | const [loaded, error] = useFonts({
46 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
47 | ...FontAwesome.font,
48 | });
49 | const router = useRouter();
50 | const { isLoaded, isSignedIn } = useAuth();
51 | const segments = useSegments();
52 |
53 | // Expo Router uses Error Boundaries to catch errors in the navigation tree.
54 | useEffect(() => {
55 | if (error) throw error;
56 | }, [error]);
57 |
58 | useEffect(() => {
59 | if (loaded) {
60 | SplashScreen.hideAsync();
61 | }
62 | }, [loaded]);
63 |
64 | useEffect(() => {
65 | if (!isLoaded) return;
66 |
67 | const inAuthGroup = segments[0] === '(authenticated)';
68 |
69 | if (isSignedIn && !inAuthGroup) {
70 | router.replace('/(authenticated)/(tabs)/home');
71 | } else if (!isSignedIn) {
72 | router.replace('/');
73 | }
74 | }, [isSignedIn]);
75 |
76 | if (!loaded || !isLoaded) {
77 | return (
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | return (
85 |
86 |
87 | (
95 |
96 |
97 |
98 | ),
99 | }}
100 | />
101 |
102 | (
110 |
111 |
112 |
113 | ),
114 | headerRight: () => (
115 |
116 |
117 |
118 |
119 |
120 | ),
121 | }}
122 | />
123 |
124 |
125 |
126 | (
134 |
135 |
136 |
137 | ),
138 | }}
139 | />
140 |
141 | (
146 |
147 |
148 |
149 | ),
150 | headerLargeTitle: true,
151 | headerTransparent: true,
152 | headerRight: () => (
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | ),
162 | }}
163 | />
164 |
168 | (
176 |
177 |
178 |
179 | ),
180 | }}
181 | />
182 |
183 | );
184 | };
185 |
186 | const RootLayoutNav = () => {
187 | return (
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | );
199 | };
200 |
201 | export default RootLayoutNav;
202 |
--------------------------------------------------------------------------------
/app/api/info+api.ts:
--------------------------------------------------------------------------------
1 | const API_KEY = process.env.CRYPTO_API_KEY;
2 |
3 | export async function GET(request: Request) {
4 | const url = new URL(request.url);
5 | const ids = url.searchParams.get("ids") || "";
6 |
7 | const response = await fetch(
8 | `https://pro-api.coinmarketcap.com/v2/cryptocurrency/info?id=${ids}`,
9 | {
10 | headers: {
11 | "X-CMC_PRO_API_KEY": API_KEY!,
12 | },
13 | }
14 | );
15 |
16 | const res = await response.json();
17 | return Response.json(res.data);
18 | // return Response.json(data);
19 | }
20 |
21 | const data = {
22 | "1": {
23 | id: 1,
24 | name: "Bitcoin",
25 | symbol: "BTC",
26 | category: "coin",
27 | description:
28 | "Bitcoin (BTC) is a cryptocurrency launched in 2010. Users are able to generate BTC through the process of mining. Bitcoin has a current supply of 19,645,193. The last known price of Bitcoin is 66,750.48093803 USD and is up 2.35 over the last 24 hours. It is currently trading on 10848 active market(s) with $75,693,606,050.91 traded over the last 24 hours. More information can be found at https://bitcoin.org/.",
29 | slug: "bitcoin",
30 | logo: "https://s2.coinmarketcap.com/static/img/coins/64x64/1.png",
31 | subreddit: "bitcoin",
32 | notice: "",
33 | tags: [],
34 | "tag-names": [],
35 | "tag-groups": [],
36 | urls: {},
37 | platform: null,
38 | date_added: "2010-07-13T00:00:00.000Z",
39 | twitter_username: "",
40 | is_hidden: 0,
41 | date_launched: "2010-07-13T00:00:00.000Z",
42 | contract_address: [],
43 | self_reported_circulating_supply: null,
44 | self_reported_tags: null,
45 | self_reported_market_cap: null,
46 | infinite_supply: false,
47 | },
48 | "825": {
49 | id: 825,
50 | name: "Tether USDt",
51 | symbol: "USDT",
52 | category: "token",
53 | description:
54 | "Tether USDt (USDT) is a cryptocurrency and operates on the Ethereum platform. Tether USDt has a current supply of 103,800,078,701.87814 with 100,044,694,548.97124 in circulation. The last known price of Tether USDt is 1.00048841 USD and is down -0.01 over the last 24 hours. It is currently trading on 76924 active market(s) with $138,946,065,853.46 traded over the last 24 hours. More information can be found at https://tether.to.",
55 | slug: "tether",
56 | logo: "https://s2.coinmarketcap.com/static/img/coins/64x64/825.png",
57 | subreddit: "",
58 | notice: "",
59 | tags: [
60 | "payments",
61 | "stablecoin",
62 | "asset-backed-stablecoin",
63 | "avalanche-ecosystem",
64 | "solana-ecosystem",
65 | "arbitrum-ecosytem",
66 | "moonriver-ecosystem",
67 | "injective-ecosystem",
68 | "bnb-chain",
69 | "usd-stablecoin",
70 | "optimism-ecosystem",
71 | ],
72 | "tag-names": [
73 | "Payments",
74 | "Stablecoin",
75 | "Asset-Backed Stablecoin",
76 | "Avalanche Ecosystem",
77 | "Solana Ecosystem",
78 | "Arbitrum Ecosystem",
79 | "Moonriver Ecosystem",
80 | "Injective Ecosystem",
81 | "BNB Chain",
82 | "USD Stablecoin",
83 | "Optimism Ecosystem",
84 | ],
85 | "tag-groups": [
86 | "INDUSTRY",
87 | "CATEGORY",
88 | "CATEGORY",
89 | "PLATFORM",
90 | "PLATFORM",
91 | "PLATFORM",
92 | "PLATFORM",
93 | "PLATFORM",
94 | "PLATFORM",
95 | "CATEGORY",
96 | "PLATFORM",
97 | ],
98 | urls: {},
99 | platform: {
100 | id: "1027",
101 | name: "Ethereum",
102 | slug: "ethereum",
103 | symbol: "ETH",
104 | token_address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
105 | },
106 | date_added: "2015-02-25T00:00:00.000Z",
107 | twitter_username: "tether_to",
108 | is_hidden: 0,
109 | date_launched: null,
110 | contract_address: [],
111 | self_reported_circulating_supply: null,
112 | self_reported_tags: null,
113 | self_reported_market_cap: null,
114 | infinite_supply: true,
115 | },
116 | "1027": {
117 | id: 1027,
118 | name: "Ethereum",
119 | symbol: "ETH",
120 | category: "coin",
121 | description:
122 | "Ethereum (ETH) is a cryptocurrency . Ethereum has a current supply of 120,127,131.78995213. The last known price of Ethereum is 3,698.38075861 USD and is up 4.89 over the last 24 hours. It is currently trading on 8497 active market(s) with $31,574,788,707.07 traded over the last 24 hours. More information can be found at https://www.ethereum.org/.",
123 | slug: "ethereum",
124 | logo: "https://s2.coinmarketcap.com/static/img/coins/64x64/1027.png",
125 | subreddit: "ethereum",
126 | notice: "",
127 | tags: [],
128 | "tag-names": [],
129 | "tag-groups": [],
130 | urls: {},
131 | platform: null,
132 | date_added: "2015-08-07T00:00:00.000Z",
133 | twitter_username: "ethereum",
134 | is_hidden: 0,
135 | date_launched: null,
136 | contract_address: [],
137 | self_reported_circulating_supply: null,
138 | self_reported_tags: null,
139 | self_reported_market_cap: null,
140 | infinite_supply: true,
141 | },
142 | "1839": {
143 | id: 1839,
144 | name: "BNB",
145 | symbol: "BNB",
146 | category: "coin",
147 | description:
148 | "BNB (BNB) is a cryptocurrency . BNB has a current supply of 149,541,397.38261488. The last known price of BNB is 419.66183716 USD and is down -0.67 over the last 24 hours. It is currently trading on 2081 active market(s) with $2,547,806,853.73 traded over the last 24 hours. More information can be found at https://bnbchain.org/en.",
149 | slug: "bnb",
150 | logo: "https://s2.coinmarketcap.com/static/img/coins/64x64/1839.png",
151 | subreddit: "bnbchainofficial",
152 | notice: "",
153 | tags: [],
154 | "tag-names": [],
155 | "tag-groups": [],
156 | urls: {
157 | website: ["https://bnbchain.org/en"],
158 | twitter: ["https://twitter.com/bnbchain"],
159 | message_board: [],
160 | chat: ["https://t.me/BNBchaincommunity", "https://t.me/bnbchain"],
161 | facebook: [],
162 | explorer: [
163 | "https://explorer.bnbchain.org/",
164 | "https://bsctrace.com/",
165 | "https://bscscan.com/token/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
166 | "https://www.oklink.com/bsc",
167 | ],
168 | reddit: ["https://reddit.com/r/bnbchainofficial"],
169 | technical_doc: [],
170 | source_code: ["https://github.com/bnb-chain"],
171 | announcement: [],
172 | },
173 | platform: null,
174 | date_added: "2017-07-25T00:00:00.000Z",
175 | twitter_username: "bnbchain",
176 | is_hidden: 0,
177 | date_launched: null,
178 | contract_address: [],
179 | self_reported_circulating_supply: null,
180 | self_reported_tags: null,
181 | self_reported_market_cap: null,
182 | infinite_supply: false,
183 | },
184 | "5426": {
185 | id: 5426,
186 | name: "Solana",
187 | symbol: "SOL",
188 | category: "coin",
189 | description:
190 | "Solana (SOL) is a cryptocurrency launched in 2020. Solana has a current supply of 571,041,563.3089167 with 442,315,505.4744836 in circulation. The last known price of Solana is 130.62033647 USD and is down -1.43 over the last 24 hours. It is currently trading on 631 active market(s) with $4,914,597,503.56 traded over the last 24 hours. More information can be found at https://solana.com.",
191 | slug: "solana",
192 | logo: "https://s2.coinmarketcap.com/static/img/coins/64x64/5426.png",
193 | subreddit: "solana",
194 | notice: "",
195 | tags: [],
196 | "tag-names": [],
197 | "tag-groups": [],
198 | urls: {},
199 | platform: null,
200 | date_added: "2020-04-10T00:00:00.000Z",
201 | twitter_username: "solana",
202 | is_hidden: 0,
203 | date_launched: "2020-03-16T00:00:00.000Z",
204 | contract_address: [],
205 | self_reported_circulating_supply: null,
206 | self_reported_tags: null,
207 | self_reported_market_cap: null,
208 | infinite_supply: true,
209 | },
210 | };
211 |
--------------------------------------------------------------------------------
/app/api/listings+api.ts:
--------------------------------------------------------------------------------
1 | const API_KEY = process.env.CRYPTO_API_KEY;
2 |
3 | export async function GET(request: Request) {
4 | const url = new URL(request.url);
5 | const limit = url.searchParams.get("limit") || 5;
6 |
7 | const response = await fetch(
8 | `https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?start=1&limit=${limit}&convert=EUR`,
9 | {
10 | headers: {
11 | "X-CMC_PRO_API_KEY": API_KEY!,
12 | },
13 | }
14 | );
15 |
16 | const res = await response.json();
17 | return Response.json(res.data);
18 | // return Response.json(data);
19 | }
20 |
21 | const data = [
22 | {
23 | id: 1,
24 | name: "Bitcoin",
25 | symbol: "BTC",
26 | slug: "bitcoin",
27 | num_market_pairs: 10848,
28 | date_added: "2010-07-13T00:00:00.000Z",
29 | tags: [
30 | "mineable",
31 | "pow",
32 | "sha-256",
33 | "store-of-value",
34 | "state-channel",
35 | "coinbase-ventures-portfolio",
36 | "three-arrows-capital-portfolio",
37 | "polychain-capital-portfolio",
38 | "binance-labs-portfolio",
39 | "blockchain-capital-portfolio",
40 | "boostvc-portfolio",
41 | "cms-holdings-portfolio",
42 | "dcg-portfolio",
43 | "dragonfly-capital-portfolio",
44 | "electric-capital-portfolio",
45 | "fabric-ventures-portfolio",
46 | "framework-ventures-portfolio",
47 | "galaxy-digital-portfolio",
48 | "huobi-capital-portfolio",
49 | "alameda-research-portfolio",
50 | "a16z-portfolio",
51 | "1confirmation-portfolio",
52 | "winklevoss-capital-portfolio",
53 | "usv-portfolio",
54 | "placeholder-ventures-portfolio",
55 | "pantera-capital-portfolio",
56 | "multicoin-capital-portfolio",
57 | "paradigm-portfolio",
58 | "bitcoin-ecosystem",
59 | "ftx-bankruptcy-estate",
60 | ],
61 | max_supply: 21000000,
62 | circulating_supply: 19645193,
63 | total_supply: 19645193,
64 | infinite_supply: false,
65 | platform: null,
66 | cmc_rank: 1,
67 | self_reported_circulating_supply: null,
68 | self_reported_market_cap: null,
69 | tvl_ratio: null,
70 | last_updated: "2024-03-05T09:45:00.000Z",
71 | quote: {
72 | EUR: {
73 | price: 61172.17695743709,
74 | volume_24h: 69278106372.44798,
75 | volume_change_24h: 80.6251,
76 | percent_change_1h: -0.48405293,
77 | percent_change_24h: 1.68774533,
78 | percent_change_7d: 17.06519156,
79 | percent_change_30d: 54.71398276,
80 | percent_change_60d: 50.57066489,
81 | percent_change_90d: 51.40362093,
82 | market_cap: 1201739222559.0044,
83 | market_cap_dominance: 52.3011,
84 | fully_diluted_market_cap: 1284615716106.177,
85 | tvl: null,
86 | last_updated: "2024-03-05T09:45:04.000Z",
87 | },
88 | },
89 | },
90 | {
91 | id: 1027,
92 | name: "Ethereum",
93 | symbol: "ETH",
94 | slug: "ethereum",
95 | num_market_pairs: 8497,
96 | date_added: "2015-08-07T00:00:00.000Z",
97 | tags: [
98 | "pos",
99 | "smart-contracts",
100 | "ethereum-ecosystem",
101 | "coinbase-ventures-portfolio",
102 | "three-arrows-capital-portfolio",
103 | "polychain-capital-portfolio",
104 | "binance-labs-portfolio",
105 | "blockchain-capital-portfolio",
106 | "boostvc-portfolio",
107 | "cms-holdings-portfolio",
108 | "dcg-portfolio",
109 | "dragonfly-capital-portfolio",
110 | "electric-capital-portfolio",
111 | "fabric-ventures-portfolio",
112 | "framework-ventures-portfolio",
113 | "hashkey-capital-portfolio",
114 | "kenetic-capital-portfolio",
115 | "huobi-capital-portfolio",
116 | "alameda-research-portfolio",
117 | "a16z-portfolio",
118 | "1confirmation-portfolio",
119 | "winklevoss-capital-portfolio",
120 | "usv-portfolio",
121 | "placeholder-ventures-portfolio",
122 | "pantera-capital-portfolio",
123 | "multicoin-capital-portfolio",
124 | "paradigm-portfolio",
125 | "injective-ecosystem",
126 | "layer-1",
127 | "ftx-bankruptcy-estate",
128 | ],
129 | max_supply: null,
130 | circulating_supply: 120127131.78995213,
131 | total_supply: 120127131.78995213,
132 | infinite_supply: true,
133 | platform: null,
134 | cmc_rank: 2,
135 | self_reported_circulating_supply: null,
136 | self_reported_market_cap: null,
137 | tvl_ratio: null,
138 | last_updated: "2024-03-05T09:45:00.000Z",
139 | quote: {
140 | EUR: {
141 | price: 3397.272518256182,
142 | volume_24h: 28951505589.23718,
143 | volume_change_24h: 81.3652,
144 | percent_change_1h: -0.27798853,
145 | percent_change_24h: 4.42728608,
146 | percent_change_7d: 13.33097334,
147 | percent_change_30d: 60.35701006,
148 | percent_change_60d: 62.89696558,
149 | percent_change_90d: 61.68737585,
150 | market_cap: 408104603526.94293,
151 | market_cap_dominance: 17.7721,
152 | fully_diluted_market_cap: 408104603526.9387,
153 | tvl: null,
154 | last_updated: "2024-03-05T09:45:04.000Z",
155 | },
156 | },
157 | },
158 | {
159 | id: 825,
160 | name: "Tether USDt",
161 | symbol: "USDT",
162 | slug: "tether",
163 | num_market_pairs: 76927,
164 | date_added: "2015-02-25T00:00:00.000Z",
165 | tags: [
166 | "payments",
167 | "stablecoin",
168 | "asset-backed-stablecoin",
169 | "avalanche-ecosystem",
170 | "solana-ecosystem",
171 | "arbitrum-ecosytem",
172 | "moonriver-ecosystem",
173 | "injective-ecosystem",
174 | "bnb-chain",
175 | "usd-stablecoin",
176 | "optimism-ecosystem",
177 | ],
178 | max_supply: null,
179 | circulating_supply: 100044694548.97124,
180 | total_supply: 103800078701.87814,
181 | platform: {
182 | id: 1027,
183 | name: "Ethereum",
184 | symbol: "ETH",
185 | slug: "ethereum",
186 | token_address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
187 | },
188 | infinite_supply: true,
189 | cmc_rank: 3,
190 | self_reported_circulating_supply: null,
191 | self_reported_market_cap: null,
192 | tvl_ratio: null,
193 | last_updated: "2024-03-05T09:44:00.000Z",
194 | quote: {
195 | EUR: {
196 | price: 0.9218759200172967,
197 | volume_24h: 127574629087.67787,
198 | volume_change_24h: 63.8698,
199 | percent_change_1h: -0.00975918,
200 | percent_change_24h: -0.0079453,
201 | percent_change_7d: 0.00805054,
202 | percent_change_30d: 0.09012731,
203 | percent_change_60d: -0.06996444,
204 | percent_change_90d: 0.05387782,
205 | market_cap: 92228794830.18228,
206 | market_cap_dominance: 4.0164,
207 | fully_diluted_market_cap: 95690793051.16272,
208 | tvl: null,
209 | last_updated: "2024-03-05T09:45:04.000Z",
210 | },
211 | },
212 | },
213 | {
214 | id: 1839,
215 | name: "BNB",
216 | symbol: "BNB",
217 | slug: "bnb",
218 | num_market_pairs: 2081,
219 | date_added: "2017-07-25T00:00:00.000Z",
220 | tags: [
221 | "marketplace",
222 | "centralized-exchange",
223 | "payments",
224 | "smart-contracts",
225 | "alameda-research-portfolio",
226 | "multicoin-capital-portfolio",
227 | "bnb-chain",
228 | "layer-1",
229 | "sec-security-token",
230 | "alleged-sec-securities",
231 | "celsius-bankruptcy-estate",
232 | ],
233 | max_supply: null,
234 | circulating_supply: 149541397.38261488,
235 | total_supply: 149541397.38261488,
236 | infinite_supply: false,
237 | platform: null,
238 | cmc_rank: 4,
239 | self_reported_circulating_supply: null,
240 | self_reported_market_cap: null,
241 | tvl_ratio: null,
242 | last_updated: "2024-03-05T09:44:00.000Z",
243 | quote: {
244 | EUR: {
245 | price: 385.90384494527785,
246 | volume_24h: 2341285560.455857,
247 | volume_change_24h: 39.8193,
248 | percent_change_1h: -0.02460092,
249 | percent_change_24h: -0.9529521,
250 | percent_change_7d: 4.99520205,
251 | percent_change_30d: 39.39965358,
252 | percent_change_60d: 30.52969375,
253 | percent_change_90d: 78.57628595,
254 | market_cap: 57708600228.44079,
255 | market_cap_dominance: 2.5115,
256 | fully_diluted_market_cap: 57708600228.44533,
257 | tvl: null,
258 | last_updated: "2024-03-05T09:45:04.000Z",
259 | },
260 | },
261 | },
262 | {
263 | id: 5426,
264 | name: "Solana",
265 | symbol: "SOL",
266 | slug: "solana",
267 | num_market_pairs: 631,
268 | date_added: "2020-04-10T00:00:00.000Z",
269 | tags: [
270 | "pos",
271 | "platform",
272 | "solana-ecosystem",
273 | "cms-holdings-portfolio",
274 | "kenetic-capital-portfolio",
275 | "alameda-research-portfolio",
276 | "multicoin-capital-portfolio",
277 | "okex-blockdream-ventures-portfolio",
278 | "layer-1",
279 | "ftx-bankruptcy-estate",
280 | "sec-security-token",
281 | "alleged-sec-securities",
282 | ],
283 | max_supply: null,
284 | circulating_supply: 442315505.4744836,
285 | total_supply: 571041563.3089167,
286 | infinite_supply: true,
287 | platform: null,
288 | cmc_rank: 5,
289 | self_reported_circulating_supply: null,
290 | self_reported_market_cap: null,
291 | tvl_ratio: null,
292 | last_updated: "2024-03-05T09:45:00.000Z",
293 | quote: {
294 | EUR: {
295 | price: 119.63987139843265,
296 | volume_24h: 4498107313.186403,
297 | volume_change_24h: 63.4076,
298 | percent_change_1h: -0.07141547,
299 | percent_change_24h: -2.70892074,
300 | percent_change_7d: 16.58951585,
301 | percent_change_30d: 33.46755042,
302 | percent_change_60d: 26.84646008,
303 | percent_change_90d: 97.93597163,
304 | market_cap: 52918570192.49995,
305 | market_cap_dominance: 2.3031,
306 | fully_diluted_market_cap: 68319339197.442345,
307 | tvl: null,
308 | last_updated: "2024-03-05T09:45:04.000Z",
309 | },
310 | },
311 | },
312 | ];
313 |
--------------------------------------------------------------------------------
/app/api/tickers+api.ts:
--------------------------------------------------------------------------------
1 | import { ExpoRequest, ExpoResponse } from 'expo-router/server';
2 |
3 | export async function GET(request: ExpoRequest) {
4 | // const response = await fetch(
5 | // `https://api.coinpaprika.com/v1/tickers/btc-bitcoin/historical?start=2024-01-01&interval=1d`
6 | // );
7 |
8 | // const res = await response.json();
9 | // return ExpoResponse.json(res.data);
10 | return ExpoResponse.json(data);
11 | }
12 |
13 | const data = [
14 | {
15 | timestamp: '2024-01-01T00:00:00Z',
16 | price: 42850.26,
17 | volume_24h: 12058361624,
18 | market_cap: 839292148428,
19 | },
20 | {
21 | timestamp: '2024-01-02T00:00:00Z',
22 | price: 45285.22,
23 | volume_24h: 26322994437,
24 | market_cap: 887025221780,
25 | },
26 | {
27 | timestamp: '2024-01-03T00:00:00Z',
28 | price: 43976.55,
29 | volume_24h: 29942388903,
30 | market_cap: 861431009601,
31 | },
32 | {
33 | timestamp: '2024-01-04T00:00:00Z',
34 | price: 43532.22,
35 | volume_24h: 29754873104,
36 | market_cap: 852771850034,
37 | },
38 | {
39 | timestamp: '2024-01-05T00:00:00Z',
40 | price: 43841.79,
41 | volume_24h: 24271454099,
42 | market_cap: 858879240517,
43 | },
44 | {
45 | timestamp: '2024-01-06T00:00:00Z',
46 | price: 43908.65,
47 | volume_24h: 18248808441,
48 | market_cap: 860227941216,
49 | },
50 | {
51 | timestamp: '2024-01-07T00:00:00Z',
52 | price: 44136.6,
53 | volume_24h: 11455711529,
54 | market_cap: 864729684233,
55 | },
56 | {
57 | timestamp: '2024-01-08T00:00:00Z',
58 | price: 44897.76,
59 | volume_24h: 21758384796,
60 | market_cap: 879675185474,
61 | },
62 | {
63 | timestamp: '2024-01-09T00:00:00Z',
64 | price: 46685.2,
65 | volume_24h: 34144879041,
66 | market_cap: 914736088666,
67 | },
68 | {
69 | timestamp: '2024-01-10T00:00:00Z',
70 | price: 45853.39,
71 | volume_24h: 31770588758,
72 | market_cap: 898478024808,
73 | },
74 | {
75 | timestamp: '2024-01-11T00:00:00Z',
76 | price: 46596.17,
77 | volume_24h: 46021582608,
78 | market_cap: 913078868554,
79 | },
80 | {
81 | timestamp: '2024-01-12T00:00:00Z',
82 | price: 45127.31,
83 | volume_24h: 35655156245,
84 | market_cap: 884340355982,
85 | },
86 | {
87 | timestamp: '2024-01-13T00:00:00Z',
88 | price: 42899.19,
89 | volume_24h: 32912511370,
90 | market_cap: 840721784354,
91 | },
92 | {
93 | timestamp: '2024-01-14T00:00:00Z',
94 | price: 42811.04,
95 | volume_24h: 12765033700,
96 | market_cap: 839032860956,
97 | },
98 | {
99 | timestamp: '2024-01-15T00:00:00Z',
100 | price: 42653.06,
101 | volume_24h: 16966173676,
102 | market_cap: 835969337163,
103 | },
104 | {
105 | timestamp: '2024-01-16T00:00:00Z',
106 | price: 42973.84,
107 | volume_24h: 18175623271,
108 | market_cap: 842291564775,
109 | },
110 | {
111 | timestamp: '2024-01-17T00:00:00Z',
112 | price: 42759.43,
113 | volume_24h: 18685910072,
114 | market_cap: 838118506486,
115 | },
116 | {
117 | timestamp: '2024-01-18T00:00:00Z',
118 | price: 42273.12,
119 | volume_24h: 16741348168,
120 | market_cap: 828620567137,
121 | },
122 | {
123 | timestamp: '2024-01-19T00:00:00Z',
124 | price: 41315.59,
125 | volume_24h: 19963650769,
126 | market_cap: 809889152645,
127 | },
128 | {
129 | timestamp: '2024-01-20T00:00:00Z',
130 | price: 41657.69,
131 | volume_24h: 14515463486,
132 | market_cap: 816631375273,
133 | },
134 | {
135 | timestamp: '2024-01-21T00:00:00Z',
136 | price: 41750.04,
137 | volume_24h: 6685208959,
138 | market_cap: 818477388802,
139 | },
140 | {
141 | timestamp: '2024-01-22T00:00:00Z',
142 | price: 40774.92,
143 | volume_24h: 12974348357,
144 | market_cap: 799398998012,
145 | },
146 | {
147 | timestamp: '2024-01-23T00:00:00Z',
148 | price: 39450.13,
149 | volume_24h: 22098758345,
150 | market_cap: 773465605710,
151 | },
152 | {
153 | timestamp: '2024-01-24T00:00:00Z',
154 | price: 39941.79,
155 | volume_24h: 21321888504,
156 | market_cap: 783142014099,
157 | },
158 | {
159 | timestamp: '2024-01-25T00:00:00Z',
160 | price: 39972.74,
161 | volume_24h: 16924638069,
162 | market_cap: 783791225090,
163 | },
164 | {
165 | timestamp: '2024-01-26T00:00:00Z',
166 | price: 40953.08,
167 | volume_24h: 18194723196,
168 | market_cap: 803055378306,
169 | },
170 | {
171 | timestamp: '2024-01-27T00:00:00Z',
172 | price: 41842.32,
173 | volume_24h: 17251853011,
174 | market_cap: 820534883639,
175 | },
176 | {
177 | timestamp: '2024-01-28T00:00:00Z',
178 | price: 42249.79,
179 | volume_24h: 12087864476,
180 | market_cap: 828568727732,
181 | },
182 | {
183 | timestamp: '2024-01-29T00:00:00Z',
184 | price: 42483.84,
185 | volume_24h: 14196966845,
186 | market_cap: 833202137696,
187 | },
188 | {
189 | timestamp: '2024-01-30T00:00:00Z',
190 | price: 43427.48,
191 | volume_24h: 19892943249,
192 | market_cap: 851749640553,
193 | },
194 | {
195 | timestamp: '2024-01-31T00:00:00Z',
196 | price: 42932.46,
197 | volume_24h: 20269377121,
198 | market_cap: 842077065885,
199 | },
200 | {
201 | timestamp: '2024-02-01T00:00:00Z',
202 | price: 42462.84,
203 | volume_24h: 21095877892,
204 | market_cap: 832902642750,
205 | },
206 | {
207 | timestamp: '2024-02-02T00:00:00Z',
208 | price: 43074.97,
209 | volume_24h: 17004038259,
210 | market_cap: 844956366996,
211 | },
212 | {
213 | timestamp: '2024-02-03T00:00:00Z',
214 | price: 43119.55,
215 | volume_24h: 11943154537,
216 | market_cap: 845877384607,
217 | },
218 | {
219 | timestamp: '2024-02-04T00:00:00Z',
220 | price: 42928.1,
221 | volume_24h: 7935435046,
222 | market_cap: 842162939955,
223 | },
224 | {
225 | timestamp: '2024-02-05T00:00:00Z',
226 | price: 42801.74,
227 | volume_24h: 13126499866,
228 | market_cap: 839725812678,
229 | },
230 | {
231 | timestamp: '2024-02-06T00:00:00Z',
232 | price: 42954.87,
233 | volume_24h: 15510434059,
234 | market_cap: 842772455302,
235 | },
236 | {
237 | timestamp: '2024-02-07T00:00:00Z',
238 | price: 43267.78,
239 | volume_24h: 15112954527,
240 | market_cap: 848956449785,
241 | },
242 | {
243 | timestamp: '2024-02-08T00:00:00Z',
244 | price: 44880.71,
245 | volume_24h: 22684941210,
246 | market_cap: 880646467456,
247 | },
248 | {
249 | timestamp: '2024-02-09T00:00:00Z',
250 | price: 46769.61,
251 | volume_24h: 28424692985,
252 | market_cap: 917754539372,
253 | },
254 | {
255 | timestamp: '2024-02-10T00:00:00Z',
256 | price: 47365.65,
257 | volume_24h: 25293454828,
258 | market_cap: 929497130649,
259 | },
260 | {
261 | timestamp: '2024-02-11T00:00:00Z',
262 | price: 48150.61,
263 | volume_24h: 16793665536,
264 | market_cap: 944944706107,
265 | },
266 | {
267 | timestamp: '2024-02-12T00:00:00Z',
268 | price: 48777.79,
269 | volume_24h: 21781976970,
270 | market_cap: 957302696346,
271 | },
272 | {
273 | timestamp: '2024-02-13T00:00:00Z',
274 | price: 49637.42,
275 | volume_24h: 30917159837,
276 | market_cap: 974220708841,
277 | },
278 | {
279 | timestamp: '2024-02-14T00:00:00Z',
280 | price: 50858.76,
281 | volume_24h: 28645209724,
282 | market_cap: 998235637168,
283 | },
284 | {
285 | timestamp: '2024-02-15T00:00:00Z',
286 | price: 52096.34,
287 | volume_24h: 31385312931,
288 | market_cap: 1022575737668,
289 | },
290 | {
291 | timestamp: '2024-02-16T00:00:00Z',
292 | price: 52055.94,
293 | volume_24h: 28292856358,
294 | market_cap: 1021828804743,
295 | },
296 | {
297 | timestamp: '2024-02-17T00:00:00Z',
298 | price: 51665.52,
299 | volume_24h: 18506511430,
300 | market_cap: 1013501646162,
301 | },
302 | {
303 | timestamp: '2024-02-18T00:00:00Z',
304 | price: 51780.55,
305 | volume_24h: 15933733925,
306 | market_cap: 1016512308288,
307 | },
308 | {
309 | timestamp: '2024-02-19T00:00:00Z',
310 | price: 52155.6,
311 | volume_24h: 17189537561,
312 | market_cap: 1023917687978,
313 | },
314 | {
315 | timestamp: '2024-02-20T00:00:00Z',
316 | price: 51918.59,
317 | volume_24h: 23465686169,
318 | market_cap: 1019308399023,
319 | },
320 | {
321 | timestamp: '2024-02-21T00:00:00Z',
322 | price: 51518.94,
323 | volume_24h: 27704601251,
324 | market_cap: 1011508294877,
325 | },
326 | {
327 | timestamp: '2024-02-22T00:00:00Z',
328 | price: 51606.6,
329 | volume_24h: 25079609089,
330 | market_cap: 1013272679613,
331 | },
332 | {
333 | timestamp: '2024-02-23T00:00:00Z',
334 | price: 51125.41,
335 | volume_24h: 20956829269,
336 | market_cap: 1003871577747,
337 | },
338 | {
339 | timestamp: '2024-02-24T00:00:00Z',
340 | price: 51210.14,
341 | volume_24h: 15246612933,
342 | market_cap: 1005580456875,
343 | },
344 | {
345 | timestamp: '2024-02-25T00:00:00Z',
346 | price: 51697.2,
347 | volume_24h: 11667250313,
348 | market_cap: 1015193405672,
349 | },
350 | {
351 | timestamp: '2024-02-26T00:00:00Z',
352 | price: 52360.57,
353 | volume_24h: 17510163637,
354 | market_cap: 1028270731889,
355 | },
356 | {
357 | timestamp: '2024-02-27T00:00:00Z',
358 | price: 56499.49,
359 | volume_24h: 38475364003,
360 | market_cap: 1109598908583,
361 | },
362 | {
363 | timestamp: '2024-02-28T00:00:00Z',
364 | price: 59302.38,
365 | volume_24h: 40170552872,
366 | market_cap: 1164697526078,
367 | },
368 | {
369 | timestamp: '2024-02-29T00:00:00Z',
370 | price: 62101.45,
371 | volume_24h: 65757133115,
372 | market_cap: 1219723680357,
373 | },
374 | {
375 | timestamp: '2024-03-01T00:00:00Z',
376 | price: 61904.78,
377 | volume_24h: 41813422665,
378 | market_cap: 1215917487798,
379 | },
380 | {
381 | timestamp: '2024-03-02T00:00:00Z',
382 | price: 62055.01,
383 | volume_24h: 27540459281,
384 | market_cap: 1218922657833,
385 | },
386 | {
387 | timestamp: '2024-03-03T00:00:00Z',
388 | price: 62278.41,
389 | volume_24h: 16630916714,
390 | market_cap: 1223367166324,
391 | },
392 | {
393 | timestamp: '2024-03-04T00:00:00Z',
394 | price: 65381.54,
395 | volume_24h: 39816706055,
396 | market_cap: 1284379118156,
397 | },
398 | {
399 | timestamp: '2024-03-05T00:00:00Z',
400 | price: 66231.42,
401 | volume_24h: 71646839317,
402 | market_cap: 1301136217279,
403 | },
404 | {
405 | timestamp: '2024-03-06T00:00:00Z',
406 | price: 65967.28,
407 | volume_24h: 83579811918,
408 | market_cap: 1295140803705,
409 | },
410 | {
411 | timestamp: '2024-03-07T00:00:00Z',
412 | price: 66863,
413 | volume_24h: 49265715529,
414 | market_cap: 1313675687104,
415 | },
416 | {
417 | timestamp: '2024-03-08T00:00:00Z',
418 | price: 67779.98,
419 | volume_24h: 43090760490,
420 | market_cap: 1331760711857,
421 | },
422 | {
423 | timestamp: '2024-03-09T00:00:00Z',
424 | price: 68413.63,
425 | volume_24h: 44516079008,
426 | market_cap: 1344280727872,
427 | },
428 | {
429 | timestamp: '2024-03-10T00:00:00Z',
430 | price: 69414.36,
431 | volume_24h: 25786485181,
432 | market_cap: 1364008786938,
433 | },
434 | {
435 | timestamp: '2024-03-11T00:00:00Z',
436 | price: 71012.88,
437 | volume_24h: 45768614399,
438 | market_cap: 1395492712945,
439 | },
440 | {
441 | timestamp: '2024-03-12T00:00:00Z',
442 | price: 72016.38,
443 | volume_24h: 56528925733,
444 | market_cap: 1415263222742,
445 | },
446 | ];
447 |
--------------------------------------------------------------------------------
/app/help.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | const Page = () => {
3 | return (
4 |
5 | Page
6 |
7 | );
8 | };
9 | export default Page;
10 |
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { defaultStyles } from '@/constants/Styles';
3 | import { useAssets } from 'expo-asset';
4 | import { ResizeMode, Video } from 'expo-av';
5 | import { Link } from 'expo-router';
6 | import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
7 |
8 | const Page = () => {
9 | const [assets] = useAssets([require('@/assets/videos/intro.mp4')]);
10 |
11 | return (
12 |
13 | {assets && (
14 |
22 | )}
23 |
24 | Ready to change the way you money?
25 |
26 |
27 |
28 |
32 |
33 | Log in
34 |
35 |
36 |
40 |
41 | Sign up
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | const styles = StyleSheet.create({
50 | container: {
51 | flex: 1,
52 | justifyContent: 'space-between',
53 | },
54 | video: {
55 | width: '100%',
56 | height: '100%',
57 | position: 'absolute',
58 | },
59 | header: {
60 | fontSize: 36,
61 | fontWeight: '900',
62 | textTransform: 'uppercase',
63 | color: 'white',
64 | },
65 | buttons: {
66 | flexDirection: 'row',
67 | justifyContent: 'center',
68 | gap: 20,
69 | marginBottom: 60,
70 | paddingHorizontal: 20,
71 | },
72 | });
73 | export default Page;
74 |
--------------------------------------------------------------------------------
/app/login.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { defaultStyles } from '@/constants/Styles';
3 | import { isClerkAPIResponseError, useSignIn } from '@clerk/clerk-expo';
4 | import { Ionicons } from '@expo/vector-icons';
5 | import { Link, useRouter } from 'expo-router';
6 | import { useState } from 'react';
7 | import {
8 | View,
9 | Text,
10 | StyleSheet,
11 | TextInput,
12 | TouchableOpacity,
13 | KeyboardAvoidingView,
14 | Platform,
15 | Alert,
16 | } from 'react-native';
17 |
18 | enum SignInType {
19 | Phone,
20 | Email,
21 | Google,
22 | Apple,
23 | }
24 |
25 | const Page = () => {
26 | const [countryCode, setCountryCode] = useState('+49');
27 | const [phoneNumber, setPhoneNumber] = useState('');
28 | const keyboardVerticalOffset = Platform.OS === 'ios' ? 80 : 0;
29 | const router = useRouter();
30 | const { signIn } = useSignIn();
31 |
32 | const onSignIn = async (type: SignInType) => {
33 | if (type === SignInType.Phone) {
34 | try {
35 | const fullPhoneNumber = `${countryCode}${phoneNumber}`;
36 |
37 | const { supportedFirstFactors } = await signIn!.create({
38 | identifier: fullPhoneNumber,
39 | });
40 | const firstPhoneFactor: any = supportedFirstFactors.find((factor: any) => {
41 | return factor.strategy === 'phone_code';
42 | });
43 |
44 | const { phoneNumberId } = firstPhoneFactor;
45 |
46 | await signIn!.prepareFirstFactor({
47 | strategy: 'phone_code',
48 | phoneNumberId,
49 | });
50 |
51 | router.push({
52 | pathname: '/verify/[phone]',
53 | params: { phone: fullPhoneNumber, signin: 'true' },
54 | });
55 | } catch (err) {
56 | console.log('error', JSON.stringify(err, null, 2));
57 | if (isClerkAPIResponseError(err)) {
58 | if (err.errors[0].code === 'form_identifier_not_found') {
59 | Alert.alert('Error', err.errors[0].message);
60 | }
61 | }
62 | }
63 | }
64 | };
65 |
66 | return (
67 |
71 |
72 | Welcome back
73 |
74 | Enter the phone number associated with your account
75 |
76 |
77 |
83 |
91 |
92 |
93 | onSignIn(SignInType.Phone)}>
100 | Continue
101 |
102 |
103 |
104 |
107 | or
108 |
111 |
112 |
113 | onSignIn(SignInType.Email)}
115 | style={[
116 | defaultStyles.pillButton,
117 | {
118 | flexDirection: 'row',
119 | gap: 16,
120 | marginTop: 20,
121 | backgroundColor: '#fff',
122 | },
123 | ]}>
124 |
125 | Continue with email
126 |
127 |
128 | onSignIn(SignInType.Google)}
130 | style={[
131 | defaultStyles.pillButton,
132 | {
133 | flexDirection: 'row',
134 | gap: 16,
135 | marginTop: 20,
136 | backgroundColor: '#fff',
137 | },
138 | ]}>
139 |
140 | Continue with email
141 |
142 |
143 | onSignIn(SignInType.Apple)}
145 | style={[
146 | defaultStyles.pillButton,
147 | {
148 | flexDirection: 'row',
149 | gap: 16,
150 | marginTop: 20,
151 | backgroundColor: '#fff',
152 | },
153 | ]}>
154 |
155 | Continue with email
156 |
157 |
158 |
159 | );
160 | };
161 | const styles = StyleSheet.create({
162 | inputContainer: {
163 | marginVertical: 40,
164 | flexDirection: 'row',
165 | },
166 | input: {
167 | backgroundColor: Colors.lightGray,
168 | padding: 20,
169 | borderRadius: 16,
170 | fontSize: 20,
171 | marginRight: 10,
172 | },
173 | enabled: {
174 | backgroundColor: Colors.primary,
175 | },
176 | disabled: {
177 | backgroundColor: Colors.primaryMuted,
178 | },
179 | });
180 | export default Page;
181 |
--------------------------------------------------------------------------------
/app/signup.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { defaultStyles } from '@/constants/Styles';
3 | import { useSignUp } from '@clerk/clerk-expo';
4 | import { Link, useRouter } from 'expo-router';
5 | import { useState } from 'react';
6 | import {
7 | View,
8 | Text,
9 | StyleSheet,
10 | TextInput,
11 | TouchableOpacity,
12 | KeyboardAvoidingView,
13 | Platform,
14 | } from 'react-native';
15 | const Page = () => {
16 | const [countryCode, setCountryCode] = useState('+49');
17 | const [phoneNumber, setPhoneNumber] = useState('');
18 | const keyboardVerticalOffset = Platform.OS === 'ios' ? 80 : 0;
19 | const router = useRouter();
20 | const { signUp } = useSignUp();
21 |
22 | const onSignup = async () => {
23 | const fullPhoneNumber = `${countryCode}${phoneNumber}`;
24 |
25 | try {
26 | await signUp!.create({
27 | phoneNumber: fullPhoneNumber,
28 | });
29 | signUp!.preparePhoneNumberVerification();
30 |
31 | router.push({ pathname: '/verify/[phone]', params: { phone: fullPhoneNumber } });
32 | } catch (error) {
33 | console.error('Error signing up:', error);
34 | }
35 | };
36 |
37 | return (
38 |
42 |
43 | Let's get started!
44 |
45 | Enter your phone number. We will send you a confirmation code there
46 |
47 |
48 |
54 |
62 |
63 |
64 |
65 |
66 | Already have an account? Log in
67 |
68 |
69 |
70 |
71 |
72 |
79 | Sign up
80 |
81 |
82 |
83 | );
84 | };
85 | const styles = StyleSheet.create({
86 | inputContainer: {
87 | marginVertical: 40,
88 | flexDirection: 'row',
89 | },
90 | input: {
91 | backgroundColor: Colors.lightGray,
92 | padding: 20,
93 | borderRadius: 16,
94 | fontSize: 20,
95 | marginRight: 10,
96 | },
97 | enabled: {
98 | backgroundColor: Colors.primary,
99 | },
100 | disabled: {
101 | backgroundColor: Colors.primaryMuted,
102 | },
103 | });
104 | export default Page;
105 |
--------------------------------------------------------------------------------
/app/verify/[phone].tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { defaultStyles } from '@/constants/Styles';
3 | import { isClerkAPIResponseError, useSignIn, useSignUp } from '@clerk/clerk-expo';
4 | import { Link, useLocalSearchParams } from 'expo-router';
5 | import { Fragment, useEffect, useState } from 'react';
6 | import { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native';
7 | import {
8 | CodeField,
9 | Cursor,
10 | useBlurOnFulfill,
11 | useClearByFocusCell,
12 | } from 'react-native-confirmation-code-field';
13 | const CELL_COUNT = 6;
14 |
15 | const Page = () => {
16 | const { phone, signin } = useLocalSearchParams<{ phone: string; signin: string }>();
17 | const [code, setCode] = useState('');
18 | const { signIn } = useSignIn();
19 | const { signUp, setActive } = useSignUp();
20 |
21 | const ref = useBlurOnFulfill({ value: code, cellCount: CELL_COUNT });
22 | const [props, getCellOnLayoutHandler] = useClearByFocusCell({
23 | value: code,
24 | setValue: setCode,
25 | });
26 |
27 | useEffect(() => {
28 | if (code.length === 6) {
29 | if (signin === 'true') {
30 | verifySignIn();
31 | } else {
32 | verifyCode();
33 | }
34 | }
35 | }, [code]);
36 |
37 | const verifyCode = async () => {
38 | try {
39 | await signUp!.attemptPhoneNumberVerification({
40 | code,
41 | });
42 | await setActive!({ session: signUp!.createdSessionId });
43 | } catch (err) {
44 | console.log('error', JSON.stringify(err, null, 2));
45 | if (isClerkAPIResponseError(err)) {
46 | Alert.alert('Error', err.errors[0].message);
47 | }
48 | }
49 | };
50 |
51 | const verifySignIn = async () => {
52 | try {
53 | await signIn!.attemptFirstFactor({
54 | strategy: 'phone_code',
55 | code,
56 | });
57 | await setActive!({ session: signIn!.createdSessionId });
58 | } catch (err) {
59 | console.log('error', JSON.stringify(err, null, 2));
60 | if (isClerkAPIResponseError(err)) {
61 | Alert.alert('Error', err.errors[0].message);
62 | }
63 | }
64 | };
65 |
66 | return (
67 |
68 | 6-digit code
69 |
70 | Code sent to {phone} unless you already have an account
71 |
72 |
73 | (
83 |
84 |
89 | {symbol || (isFocused ? : null)}
90 |
91 | {index === 2 ? : null}
92 |
93 | )}
94 | />
95 |
96 |
97 |
98 | Already have an account? Log in
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | const styles = StyleSheet.create({
106 | codeFieldRoot: {
107 | marginVertical: 20,
108 | marginLeft: 'auto',
109 | marginRight: 'auto',
110 | gap: 12,
111 | },
112 | cellRoot: {
113 | width: 45,
114 | height: 60,
115 | justifyContent: 'center',
116 | alignItems: 'center',
117 | backgroundColor: Colors.lightGray,
118 | borderRadius: 8,
119 | },
120 | cellText: {
121 | color: '#000',
122 | fontSize: 36,
123 | textAlign: 'center',
124 | },
125 | focusCell: {
126 | paddingBottom: 8,
127 | },
128 | separator: {
129 | height: 2,
130 | width: 10,
131 | backgroundColor: Colors.gray,
132 | alignSelf: 'center',
133 | },
134 | });
135 | export default Page;
136 |
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/fonts.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.ttf';
2 |
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/germany.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/germany.png
--------------------------------------------------------------------------------
/assets/images/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/icon-dark.png
--------------------------------------------------------------------------------
/assets/images/icon-vivid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/icon-vivid.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/videos/intro.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/videos/intro.mp4
--------------------------------------------------------------------------------
/assets/videos/intro2.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/assets/videos/intro2.mp4
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: ['react-native-reanimated/plugin'],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/banner.png
--------------------------------------------------------------------------------
/commands.sh:
--------------------------------------------------------------------------------
1 | npx create-expo-app fintech -t tabs
2 | npx expo install expo-dev-client react-native-reanimated react-native-gesture-handler
3 | npx expo install expo-av
4 | npx expo install expo-asset
5 |
6 | npm install react-native-confirmation-code-field
7 |
8 | npm install @clerk/clerk-expo
9 | npx expo install expo-secure-store
10 |
11 | npm install zeego react-native-ios-context-menu react-native-ios-utilities
12 |
13 | npm install zustand
14 | npx expo install react-native-mmkv
15 |
16 | npx expo install expo-blur
17 |
18 | npm i @tanstack/react-query
19 |
20 | npm i @shopify/react-native-skia victory-native
21 | npm i date-fns
22 | npx expo install expo-haptics
23 |
24 | npx expo install expo-local-authentication
25 |
26 | npx expo install expo-image-picker
27 |
28 | npx expo install expo-dynamic-app-icon
--------------------------------------------------------------------------------
/components/CustomHeader.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { Ionicons } from '@expo/vector-icons';
3 | import { View, Text, StyleSheet } from 'react-native';
4 | import { TextInput, TouchableOpacity } from 'react-native-gesture-handler';
5 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
6 | import { BlurView } from 'expo-blur';
7 | import { Link } from 'expo-router';
8 |
9 | const CustomHeader = () => {
10 | const { top } = useSafeAreaInsets();
11 |
12 | return (
13 |
14 |
24 |
25 |
34 | SG
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const styles = StyleSheet.create({
53 | container: {
54 | flexDirection: 'row',
55 | justifyContent: 'center',
56 | alignItems: 'center',
57 | },
58 | btn: {
59 | padding: 10,
60 | backgroundColor: Colors.gray,
61 | },
62 | searchSection: {
63 | flex: 1,
64 | flexDirection: 'row',
65 | justifyContent: 'center',
66 | alignItems: 'center',
67 | backgroundColor: Colors.lightGray,
68 | borderRadius: 30,
69 | },
70 | searchIcon: {
71 | padding: 10,
72 | },
73 | input: {
74 | flex: 1,
75 | paddingTop: 10,
76 | paddingRight: 10,
77 | paddingBottom: 10,
78 | paddingLeft: 0,
79 | backgroundColor: Colors.lightGray,
80 | color: Colors.dark,
81 | borderRadius: 30,
82 | },
83 | circle: {
84 | width: 40,
85 | height: 40,
86 | borderRadius: 30,
87 | backgroundColor: Colors.lightGray,
88 | justifyContent: 'center',
89 | alignItems: 'center',
90 | },
91 | });
92 | export default CustomHeader;
93 |
--------------------------------------------------------------------------------
/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import RoundBtn from '@/components/RoundBtn';
2 | import * as DropdownMenu from 'zeego/dropdown-menu';
3 |
4 | const Dropdown = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Statement
14 |
19 |
20 |
21 |
22 | Converter
23 |
28 |
29 |
30 |
31 | Background
32 |
37 |
38 |
39 |
40 | Add new account
41 |
46 |
47 |
48 |
49 | );
50 | };
51 | export default Dropdown;
52 |
--------------------------------------------------------------------------------
/components/RoundBtn.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { Ionicons } from '@expo/vector-icons';
3 | import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
4 |
5 | type RoundBtnProps = {
6 | icon: typeof Ionicons.defaultProps;
7 | text: string;
8 | onPress?: () => void;
9 | };
10 |
11 | const RoundBtn = ({ icon, text, onPress }: RoundBtnProps) => {
12 | return (
13 |
14 |
15 |
16 |
17 | {text}
18 |
19 | );
20 | };
21 | const styles = StyleSheet.create({
22 | container: {
23 | alignItems: 'center',
24 | gap: 10,
25 | },
26 | circle: {
27 | width: 60,
28 | height: 60,
29 | borderRadius: 30,
30 | backgroundColor: Colors.lightGray,
31 | justifyContent: 'center',
32 | alignItems: 'center',
33 | },
34 | label: {
35 | fontSize: 16,
36 | fontWeight: '500',
37 | color: Colors.dark,
38 | },
39 | });
40 | export default RoundBtn;
41 |
--------------------------------------------------------------------------------
/components/SortableList/Config.tsx:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 | import { Easing } from 'react-native-reanimated';
3 |
4 | export interface Positions {
5 | [id: string]: number;
6 | }
7 |
8 | const { width } = Dimensions.get('window');
9 | export const MARGIN = 20;
10 | export const SIZE = width / 2 - MARGIN;
11 | export const COL = 2;
12 |
13 | export const animationConfig = {
14 | easing: Easing.inOut(Easing.ease),
15 | duration: 350,
16 | };
17 |
18 | export const getPosition = (position: number) => {
19 | 'worklet';
20 |
21 | return {
22 | x: position % COL === 0 ? 0 : SIZE * (position % COL),
23 | y: Math.floor(position / COL) * SIZE,
24 | };
25 | };
26 |
27 | export const getOrder = (tx: number, ty: number, max: number) => {
28 | 'worklet';
29 |
30 | const x = Math.round(tx / SIZE) * SIZE;
31 | const y = Math.round(ty / SIZE) * SIZE;
32 | const row = Math.max(y, 0) / SIZE;
33 | const col = Math.max(x, 0) / SIZE;
34 | return Math.min(row * COL + col, max);
35 | };
36 |
--------------------------------------------------------------------------------
/components/SortableList/Item.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, RefObject } from 'react';
2 | import { Dimensions, StyleSheet } from 'react-native';
3 | import Animated, {
4 | useAnimatedGestureHandler,
5 | useAnimatedStyle,
6 | useAnimatedReaction,
7 | withSpring,
8 | scrollTo,
9 | withTiming,
10 | useSharedValue,
11 | runOnJS,
12 | SharedValue,
13 | AnimatedRef,
14 | } from 'react-native-reanimated';
15 | import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
16 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
17 |
18 | import { animationConfig, COL, getOrder, getPosition, Positions, SIZE } from './Config';
19 |
20 | interface ItemProps {
21 | children: ReactNode;
22 | positions: SharedValue;
23 | id: string;
24 | editing: boolean;
25 | onDragEnd: (diffs: Positions) => void;
26 | scrollView: AnimatedRef;
27 | scrollY: SharedValue;
28 | }
29 |
30 | const Item = ({ children, positions, id, onDragEnd, scrollView, scrollY, editing }: ItemProps) => {
31 | const inset = useSafeAreaInsets();
32 | const containerHeight = Dimensions.get('window').height - inset.top - inset.bottom;
33 | const contentHeight = (Object.keys(positions.value).length / COL) * SIZE;
34 | const isGestureActive = useSharedValue(false);
35 |
36 | const position = getPosition(positions.value[id]!);
37 | const translateX = useSharedValue(position.x);
38 | const translateY = useSharedValue(position.y);
39 |
40 | useAnimatedReaction(
41 | () => positions.value[id]!,
42 | (newOrder) => {
43 | if (!isGestureActive.value) {
44 | const pos = getPosition(newOrder);
45 | translateX.value = withTiming(pos.x, animationConfig);
46 | translateY.value = withTiming(pos.y, animationConfig);
47 | }
48 | }
49 | );
50 |
51 | const onGestureEvent = useAnimatedGestureHandler<
52 | PanGestureHandlerGestureEvent,
53 | { x: number; y: number }
54 | >({
55 | onStart: (_, ctx) => {
56 | // dont allow drag start if we're done editing
57 | if (editing) {
58 | ctx.x = translateX.value;
59 | ctx.y = translateY.value;
60 | isGestureActive.value = true;
61 | }
62 | },
63 | onActive: ({ translationX, translationY }, ctx) => {
64 | // dont allow drag if we're done editing
65 | if (editing) {
66 | translateX.value = ctx.x + translationX;
67 | translateY.value = ctx.y + translationY;
68 | // 1. We calculate where the tile should be
69 | const newOrder = getOrder(
70 | translateX.value,
71 | translateY.value,
72 | Object.keys(positions.value).length - 1
73 | );
74 |
75 | // 2. We swap the positions
76 | const oldOlder = positions.value[id];
77 | if (newOrder !== oldOlder) {
78 | const idToSwap = Object.keys(positions.value).find(
79 | (key) => positions.value[key] === newOrder
80 | );
81 | if (idToSwap) {
82 | // Spread operator is not supported in worklets
83 | // And Object.assign doesn't seem to be working on alpha.6
84 | const newPositions = JSON.parse(JSON.stringify(positions.value));
85 | newPositions[id] = newOrder;
86 | newPositions[idToSwap] = oldOlder;
87 | positions.value = newPositions;
88 | }
89 | }
90 |
91 | // 3. Scroll up and down if necessary
92 | const lowerBound = scrollY.value;
93 | const upperBound = lowerBound + containerHeight - SIZE;
94 | const maxScroll = contentHeight - containerHeight;
95 | const leftToScrollDown = maxScroll - scrollY.value;
96 | if (translateY.value < lowerBound) {
97 | const diff = Math.min(lowerBound - translateY.value, lowerBound);
98 | scrollY.value -= diff;
99 | scrollTo(scrollView, 0, scrollY.value, false);
100 | ctx.y -= diff;
101 | translateY.value = ctx.y + translationY;
102 | }
103 | if (translateY.value > upperBound) {
104 | const diff = Math.min(translateY.value - upperBound, leftToScrollDown);
105 | scrollY.value += diff;
106 | scrollTo(scrollView, 0, scrollY.value, false);
107 | ctx.y += diff;
108 | translateY.value = ctx.y + translationY;
109 | }
110 | }
111 | },
112 | onEnd: () => {
113 | const newPosition = getPosition(positions.value[id]!);
114 | translateX.value = withTiming(newPosition.x, animationConfig, () => {
115 | isGestureActive.value = false;
116 | runOnJS(onDragEnd)(positions.value);
117 | });
118 | translateY.value = withTiming(newPosition.y, animationConfig);
119 | },
120 | });
121 | const style = useAnimatedStyle(() => {
122 | const zIndex = isGestureActive.value ? 100 : 0;
123 | const scale = withSpring(isGestureActive.value ? 1.05 : 1);
124 | return {
125 | position: 'absolute',
126 | top: 0,
127 | left: 0,
128 | width: SIZE,
129 | height: SIZE,
130 | zIndex,
131 | transform: [{ translateX: translateX.value }, { translateY: translateY.value }, { scale }],
132 | };
133 | });
134 | return (
135 |
136 |
137 | {children}
138 |
139 |
140 | );
141 | };
142 |
143 | export default Item;
144 |
--------------------------------------------------------------------------------
/components/SortableList/SortableList.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import Animated, {
3 | useAnimatedRef,
4 | useAnimatedScrollHandler,
5 | useSharedValue,
6 | } from 'react-native-reanimated';
7 |
8 | import Item from './Item';
9 | import { COL, Positions, SIZE } from './Config';
10 |
11 | interface ListProps {
12 | children: ReactElement<{ id: string }>[];
13 | editing: boolean;
14 | onDragEnd: (diff: Positions) => void;
15 | }
16 |
17 | const List = ({ children, editing, onDragEnd }: ListProps) => {
18 | const scrollY = useSharedValue(0);
19 | const scrollView = useAnimatedRef();
20 | const positions = useSharedValue(
21 | Object.assign({}, ...children.map((child, index) => ({ [child.props.id]: index })))
22 | );
23 | const onScroll = useAnimatedScrollHandler({
24 | onScroll: ({ contentOffset: { y } }) => {
25 | scrollY.value = y;
26 | },
27 | });
28 |
29 | return (
30 |
39 | {children.map((child) => {
40 | return (
41 | -
49 | {child}
50 |
51 | );
52 | })}
53 |
54 | );
55 | };
56 |
57 | export default List;
58 |
--------------------------------------------------------------------------------
/components/SortableList/Tile.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text } from 'react-native';
3 |
4 | import { SIZE } from './Config';
5 | import Colors from '@/constants/Colors';
6 | import { useBalanceStore } from '@/store/balanceStore';
7 | import { Ionicons } from '@expo/vector-icons';
8 |
9 | const styles = StyleSheet.create({
10 | container: {
11 | width: SIZE - 20,
12 | height: 150,
13 | backgroundColor: 'white',
14 | borderRadius: 20,
15 | shadowColor: '#000',
16 | shadowOffset: { width: 0, height: 1 },
17 | shadowOpacity: 0.25,
18 | shadowRadius: 2,
19 | elevation: 5,
20 | padding: 14,
21 | alignSelf: 'center',
22 | },
23 | });
24 | interface TileProps {
25 | id: string;
26 | onLongPress: () => void;
27 | }
28 |
29 | const Tile = ({ id }: TileProps) => {
30 | const { transactions } = useBalanceStore();
31 |
32 | if (id === 'spent') {
33 | return (
34 |
35 |
36 | Spent this month
37 |
38 |
39 | 1024€
40 |
41 |
42 | );
43 | }
44 |
45 | if (id === 'cashback') {
46 | return (
47 |
50 |
51 |
60 | 5%
61 |
62 | Cashback
63 |
64 |
65 | );
66 | }
67 |
68 | if (id === 'recent') {
69 | return (
70 |
71 |
72 |
73 | Recent transaction
74 |
75 |
76 | {transactions.length === 0 && (
77 |
78 | No transactions
79 |
80 | )}
81 |
82 | {transactions.length > 0 && (
83 | <>
84 |
91 | {transactions[transactions.length - 1].amount}€
92 |
93 |
94 | {transactions[transactions.length - 1].title}
95 |
96 | >
97 | )}
98 |
99 |
100 | );
101 | }
102 |
103 | if (id === 'cards') {
104 | return (
105 |
106 | Cards
107 |
113 |
114 | );
115 | }
116 | };
117 |
118 | export default Tile;
119 |
--------------------------------------------------------------------------------
/components/SortableList/WidgetList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { MARGIN } from './Config';
4 | import Tile from './Tile';
5 | import SortableList from './SortableList';
6 | import { View } from 'react-native';
7 |
8 | const tiles = [
9 | {
10 | id: 'spent',
11 | },
12 | {
13 | id: 'cashback',
14 | },
15 | {
16 | id: 'recent',
17 | },
18 | {
19 | id: 'cards',
20 | },
21 | ];
22 |
23 | const WidgetList = () => {
24 | return (
25 |
30 | console.log(JSON.stringify(positions, null, 2))}>
33 | {[...tiles].map((tile, index) => (
34 | true} key={tile.id + '-' + index} id={tile.id} />
35 | ))}
36 |
37 |
38 | );
39 | };
40 |
41 | export default WidgetList;
42 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | primary: '#3D38ED',
3 | primaryMuted: '#C9C8FA',
4 | background: '#F5F5F5',
5 | dark: '#141518',
6 | gray: '#626D77',
7 | lightGray: '#D8DCE2',
8 | };
9 |
--------------------------------------------------------------------------------
/constants/Styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import Colors from '@/constants/Colors';
3 |
4 | export const defaultStyles = StyleSheet.create({
5 | container: {
6 | flex: 1,
7 | backgroundColor: Colors.background,
8 | padding: 16,
9 | },
10 | header: {
11 | fontSize: 40,
12 | fontWeight: '700',
13 | },
14 | pillButton: {
15 | padding: 10,
16 | height: 60,
17 | borderRadius: 40,
18 | justifyContent: 'center',
19 | alignItems: 'center',
20 | },
21 | textLink: {
22 | color: Colors.primary,
23 | fontSize: 18,
24 | fontWeight: '500',
25 | },
26 | descriptionText: {
27 | fontSize: 18,
28 | marginTop: 20,
29 | color: Colors.gray,
30 | },
31 | buttonText: {
32 | color: '#fff',
33 | fontSize: 18,
34 | fontWeight: '500',
35 | },
36 | pillButtonSmall: {
37 | paddingHorizontal: 20,
38 | height: 40,
39 | borderRadius: 20,
40 | justifyContent: 'center',
41 | alignItems: 'center',
42 | },
43 | buttonTextSmall: {
44 | color: '#fff',
45 | fontSize: 16,
46 | fontWeight: '500',
47 | },
48 | sectionHeader: {
49 | fontSize: 20,
50 | fontWeight: 'bold',
51 | margin: 20,
52 | marginBottom: 10,
53 | },
54 | block: {
55 | marginHorizontal: 20,
56 | padding: 14,
57 | backgroundColor: '#fff',
58 | borderRadius: 16,
59 | gap: 20,
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/context/UserInactivity.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth } from '@clerk/clerk-expo';
2 | import { useRouter } from 'expo-router';
3 | import { useEffect, useRef } from 'react';
4 | import { AppState, AppStateStatus } from 'react-native';
5 | import { MMKV } from 'react-native-mmkv';
6 |
7 | const storage = new MMKV({
8 | id: 'inactivty-storage',
9 | });
10 |
11 | export const UserInactivityProvider = ({ children }: any) => {
12 | const appState = useRef(AppState.currentState);
13 | const router = useRouter();
14 | const { isSignedIn } = useAuth();
15 |
16 | useEffect(() => {
17 | const subscription = AppState.addEventListener('change', handleAppStateChange);
18 |
19 | return () => {
20 | subscription.remove();
21 | };
22 | }, []);
23 |
24 | const handleAppStateChange = async (nextAppState: AppStateStatus) => {
25 | console.log('🚀 ~ handleAppStateChange ~ nextAppState', nextAppState);
26 |
27 | if (nextAppState === 'background') {
28 | recordStartTime();
29 | } else if (nextAppState === 'active' && appState.current.match(/background/)) {
30 | const elapsed = Date.now() - (storage.getNumber('startTime') || 0);
31 | console.log('🚀 ~ handleAppStateChange ~ elapsed:', elapsed);
32 |
33 | if (elapsed > 3000 && isSignedIn) {
34 | router.replace('/(authenticated)/(modals)/lock');
35 | }
36 | }
37 | appState.current = nextAppState;
38 | };
39 |
40 | const recordStartTime = () => {
41 | storage.set('startTime', Date.now());
42 | };
43 |
44 | return children;
45 | };
46 |
--------------------------------------------------------------------------------
/interfaces/crypto.ts:
--------------------------------------------------------------------------------
1 | export interface Currency {
2 | id: number;
3 | name: string;
4 | symbol: string;
5 | slug: string;
6 | num_market_pairs: number;
7 | date_added: string;
8 | tags: string[];
9 | max_supply: number;
10 | circulating_supply: number;
11 | total_supply: number;
12 | infinite_supply: boolean;
13 | platform?: any;
14 | cmc_rank: number;
15 | self_reported_circulating_supply?: any;
16 | self_reported_market_cap?: any;
17 | tvl_ratio?: any;
18 | last_updated: string;
19 | quote: Quote;
20 | }
21 |
22 | interface Quote {
23 | EUR: EUR;
24 | }
25 |
26 | interface EUR {
27 | price: number;
28 | volume_24h: number;
29 | volume_change_24h: number;
30 | percent_change_1h: number;
31 | percent_change_24h: number;
32 | percent_change_7d: number;
33 | percent_change_30d: number;
34 | percent_change_60d: number;
35 | percent_change_90d: number;
36 | market_cap: number;
37 | market_cap_dominance: number;
38 | fully_diluted_market_cap: number;
39 | tvl?: any;
40 | last_updated: string;
41 | }
42 |
43 | export interface Ticker {
44 | timestamp: string;
45 | price: number;
46 | volume_24h: number;
47 | market_cap: number;
48 | }
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fintech",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "web": "expo start --web",
10 | "test": "jest --watchAll"
11 | },
12 | "jest": {
13 | "preset": "jest-expo"
14 | },
15 | "dependencies": {
16 | "@clerk/clerk-expo": "^0.20.5",
17 | "@expo/vector-icons": "^14.0.0",
18 | "@react-native-async-storage/async-storage": "1.21.0",
19 | "@react-navigation/native": "^6.0.2",
20 | "@shopify/react-native-skia": "^0.1.241",
21 | "@tanstack/react-query": "^5.24.8",
22 | "date-fns": "^3.3.1",
23 | "expo": "^51.0.38",
24 | "expo-asset": "~9.0.2",
25 | "expo-av": "~13.10.5",
26 | "expo-blur": "~12.9.2",
27 | "expo-dev-client": "~3.3.8",
28 | "expo-dynamic-app-icon": "^1.2.0",
29 | "expo-font": "~11.10.2",
30 | "expo-haptics": "~12.8.1",
31 | "expo-image-picker": "~14.7.1",
32 | "expo-linking": "~6.2.2",
33 | "expo-local-authentication": "~13.8.0",
34 | "expo-router": "~3.4.7",
35 | "expo-secure-store": "~12.8.1",
36 | "expo-splash-screen": "~0.26.4",
37 | "expo-status-bar": "~1.11.1",
38 | "expo-system-ui": "~2.9.3",
39 | "expo-web-browser": "~12.8.2",
40 | "react": "18.2.0",
41 | "react-dom": "18.2.0",
42 | "react-native": "0.73.4",
43 | "react-native-confirmation-code-field": "^7.3.2",
44 | "react-native-gesture-handler": "~2.14.0",
45 | "react-native-ios-context-menu": "^2.4.3",
46 | "react-native-ios-utilities": "^4.3.2",
47 | "react-native-mmkv": "^2.12.1",
48 | "react-native-reanimated": "^3.7.2",
49 | "react-native-safe-area-context": "4.8.2",
50 | "react-native-screens": "~3.29.0",
51 | "react-native-web": "~0.19.6",
52 | "victory-native": "^40.0.4",
53 | "zeego": "^1.9.1",
54 | "zustand": "^4.5.2"
55 | },
56 | "devDependencies": {
57 | "@babel/core": "^7.20.0",
58 | "@types/react": "~18.2.45",
59 | "jest": "^29.2.1",
60 | "jest-expo": "~50.0.2",
61 | "react-test-renderer": "18.2.0",
62 | "typescript": "^5.1.3"
63 | },
64 | "private": true
65 | }
66 |
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/10.png
--------------------------------------------------------------------------------
/screenshots/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/11.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/6.png
--------------------------------------------------------------------------------
/screenshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/7.png
--------------------------------------------------------------------------------
/screenshots/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/8.png
--------------------------------------------------------------------------------
/screenshots/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/9.png
--------------------------------------------------------------------------------
/screenshots/charts.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/charts.gif
--------------------------------------------------------------------------------
/screenshots/icon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/icon.gif
--------------------------------------------------------------------------------
/screenshots/lockscreen.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/lockscreen.gif
--------------------------------------------------------------------------------
/screenshots/login.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/login.gif
--------------------------------------------------------------------------------
/screenshots/state.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/fintech-clone-react-native/ff23a1f50bbdefcee5f85cc856a03e92b3a1d8df/screenshots/state.gif
--------------------------------------------------------------------------------
/store/balanceStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { zustandStorage } from '@/store/mmkv-storage';
3 | import { createJSONStorage, persist } from 'zustand/middleware';
4 |
5 | export interface Transaction {
6 | id: string;
7 | title: string;
8 | amount: number;
9 | date: Date;
10 | }
11 |
12 | export interface BalanceState {
13 | transactions: Array;
14 | runTransaction: (transaction: Transaction) => void;
15 | balance: () => number;
16 | clearTransactions: () => void;
17 | }
18 |
19 | export const useBalanceStore = create()(
20 | persist(
21 | (set, get) => ({
22 | transactions: [],
23 | runTransaction: (transaction: Transaction) => {
24 | set((state) => ({ transactions: [...state.transactions, transaction] }));
25 | },
26 | balance: () => get().transactions.reduce((acc, transaction) => acc + transaction.amount, 0),
27 | clearTransactions: () => {
28 | set({ transactions: [] });
29 | },
30 | }),
31 | {
32 | name: 'balance',
33 | storage: createJSONStorage(() => zustandStorage),
34 | }
35 | )
36 | );
37 |
--------------------------------------------------------------------------------
/store/mmkv-storage.ts:
--------------------------------------------------------------------------------
1 | import { StateStorage } from 'zustand/middleware';
2 | import { MMKV } from 'react-native-mmkv';
3 |
4 | const storage = new MMKV({
5 | id: 'balance-storage',
6 | });
7 |
8 | export const zustandStorage: StateStorage = {
9 | setItem: (name, value) => {
10 | return storage.set(name, value);
11 | },
12 | getItem: (name) => {
13 | const value = storage.getString(name);
14 | return value ?? null;
15 | },
16 | removeItem: (name) => {
17 | return storage.delete(name);
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": ["./*"]
7 | }
8 | },
9 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "assets/videos/*.mp4"]
10 | }
11 |
--------------------------------------------------------------------------------