├── .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 | 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 | --------------------------------------------------------------------------------