ICS React Native App
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add account overview & auth

+1402 -363
-13
app/.expo/README.md
··· 1 - > Why do I have a folder named ".expo" in my project? 2 - 3 - The ".expo" folder is created when an Expo project is started using "expo start" command. 4 - 5 - > What do the files contain? 6 - 7 - - "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. 8 - - "settings.json": contains the server configuration that is used to serve the application manifest. 9 - 10 - > Should I commit the ".expo" folder? 11 - 12 - No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. 13 - Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
-3
app/.expo/devices.json
··· 1 - { 2 - "devices": [] 3 - }
-14
app/.expo/types/router.d.ts
··· 1 - /* eslint-disable */ 2 - import * as Router from 'expo-router'; 3 - 4 - export * from 'expo-router'; 5 - 6 - declare module 'expo-router' { 7 - export namespace ExpoRouter { 8 - export interface __routes<T extends string | object = string> { 9 - hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/modal`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/explore` | `/explore`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}` | `/`; params?: Router.UnknownInputParams; }; 10 - hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/modal`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(tabs)'}/explore` | `/explore`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(tabs)'}` | `/`; params?: Router.UnknownOutputParams; }; 11 - href: Router.RelativePathString | Router.ExternalPathString | `/modal${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | `${'/(tabs)'}/explore${`?${string}` | `#${string}` | ''}` | `/explore${`?${string}` | `#${string}` | ''}` | `${'/(tabs)'}${`?${string}` | `#${string}` | ''}` | `/${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/modal`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/explore` | `/explore`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}` | `/`; params?: Router.UnknownInputParams; }; 12 - } 13 - } 14 - }
+4 -1
app/.gitignore
··· 3 3 # The following patterns were generated by expo-cli 4 4 5 5 expo-env.d.ts 6 - # @end expo-cli 6 + # @end expo-cli 7 + 8 + .expo/ 9 + node_modules/
+6 -5
app/app.json
··· 1 1 { 2 2 "expo": { 3 - "name": "wonderlandzor-app", 4 - "slug": "wonderlandzor-app", 5 - "version": "1.0.0", 3 + "name": "ics-banking-demo-app", 4 + "slug": "ics-banking-demo-app", 5 + "version": "0.0.1", 6 6 "orientation": "portrait", 7 7 "icon": "./assets/images/icon.png", 8 - "scheme": "wonderlandzorapp", 8 + "scheme": "icsbankingdemoapp", 9 9 "userInterfaceStyle": "automatic", 10 10 "newArchEnabled": true, 11 11 "ios": { ··· 38 38 "backgroundColor": "#000000" 39 39 } 40 40 } 41 - ] 41 + ], 42 + "expo-secure-store" 42 43 ], 43 44 "experiments": { 44 45 "typedRoutes": true,
+13
app/app/(auth)/_layout.tsx
··· 1 + import { Stack } from "expo-router"; 2 + import React from "react"; 3 + 4 + export default function Layout() { 5 + return ( 6 + <Stack> 7 + <Stack.Screen 8 + name="index" 9 + options={{ headerShown: false, navigationBarHidden: true }} 10 + /> 11 + </Stack> 12 + ); 13 + }
+32
app/app/(auth)/index.tsx
··· 1 + import { StyleSheet } from "react-native"; 2 + 3 + import { ThemedText } from "@/components/themed-text"; 4 + import { ThemedView } from "@/components/themed-view"; 5 + import { useLogin } from "@/hooks/use-login"; 6 + import { Button } from "@react-navigation/elements"; 7 + 8 + export default function HomeScreen() { 9 + const { login } = useLogin(); 10 + 11 + return ( 12 + <ThemedView style={styles.titleContainer}> 13 + <ThemedText type="title">Welcome!</ThemedText> 14 + <Button 15 + onPressOut={() => 16 + login({ username: "test@test.test", password: "password@123" }) 17 + } 18 + > 19 + login 20 + </Button> 21 + </ThemedView> 22 + ); 23 + } 24 + 25 + const styles = StyleSheet.create({ 26 + titleContainer: { 27 + paddingTop: 60, 28 + flexDirection: "row", 29 + alignItems: "center", 30 + gap: 8, 31 + }, 32 + });
+20
app/app/(auth)/settings.tsx
··· 1 + import { StyleSheet } from "react-native"; 2 + 3 + import { ThemedText } from "@/components/themed-text"; 4 + import { ThemedView } from "@/components/themed-view"; 5 + 6 + export default function SettingsScreen() { 7 + return ( 8 + <ThemedView style={styles.titleContainer}> 9 + <ThemedText type="title">Settings</ThemedText> 10 + </ThemedView> 11 + ); 12 + } 13 + 14 + const styles = StyleSheet.create({ 15 + titleContainer: { 16 + flexDirection: "row", 17 + alignItems: "center", 18 + gap: 8, 19 + }, 20 + });
+10
app/app/(tabs)/_layout.tsx
··· 5 5 import { IconSymbol } from '@/components/ui/icon-symbol'; 6 6 import { Colors } from '@/constants/theme'; 7 7 import { useColorScheme } from '@/hooks/use-color-scheme'; 8 + import { useAuthentication } from '@/hooks/use-authentication'; 8 9 9 10 export default function TabLayout() { 10 11 const colorScheme = useColorScheme(); 12 + 13 + useAuthentication(); 11 14 12 15 return ( 13 16 <Tabs ··· 28 31 options={{ 29 32 title: 'Explore', 30 33 tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, 34 + }} 35 + /> 36 + <Tabs.Screen 37 + name="account" 38 + options={{ 39 + title: 'Account', 40 + tabBarIcon: ({ color }) => <IconSymbol size={28} name="chevron.right" color={color} />, 31 41 }} 32 42 /> 33 43 </Tabs>
+51
app/app/(tabs)/account.tsx
··· 1 + import { Image } from "expo-image"; 2 + import { StyleSheet } from "react-native"; 3 + import ParallaxScrollView from "@/components/parallax-scroll-view"; 4 + import { ThemedText } from "@/components/themed-text"; 5 + import { ThemedView } from "@/components/themed-view"; 6 + import { Button } from "@react-navigation/elements"; 7 + import { useCurrentUser } from "@/hooks/use-current-user"; 8 + import { useLogout } from "@/hooks/use-logout"; 9 + 10 + export default function ProfileScreen() { 11 + const { logout } = useLogout(); 12 + 13 + const { user } = useCurrentUser(); 14 + 15 + return ( 16 + <ParallaxScrollView 17 + headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }} 18 + headerImage={ 19 + <Image 20 + source={require("@/assets/images/partial-react-logo.png")} 21 + style={styles.reactLogo} 22 + /> 23 + } 24 + > 25 + <ThemedView style={styles.titleContainer}> 26 + <ThemedText type="title">{`Welcome ${user?.fullname}`}</ThemedText> 27 + </ThemedView> 28 + <ThemedView style={styles.titleContainer}> 29 + <ThemedText type="subtitle">{`username: ${user?.username}`}</ThemedText> 30 + </ThemedView> 31 + <ThemedView style={styles.titleContainer}> 32 + <Button onPressOut={() => logout()}>logout</Button> 33 + </ThemedView> 34 + </ParallaxScrollView> 35 + ); 36 + } 37 + 38 + const styles = StyleSheet.create({ 39 + titleContainer: { 40 + flexDirection: "row", 41 + alignItems: "center", 42 + gap: 8, 43 + }, 44 + reactLogo: { 45 + height: 178, 46 + width: 290, 47 + bottom: 0, 48 + left: 0, 49 + position: "absolute", 50 + }, 51 + });
+52 -66
app/app/(tabs)/index.tsx
··· 1 - import { Image } from 'expo-image'; 2 - import { Platform, StyleSheet } from 'react-native'; 1 + import { Image } from "expo-image"; 2 + import { StyleSheet, TouchableOpacity } from "react-native"; 3 3 4 - import { HelloWave } from '@/components/hello-wave'; 5 - import ParallaxScrollView from '@/components/parallax-scroll-view'; 6 - import { ThemedText } from '@/components/themed-text'; 7 - import { ThemedView } from '@/components/themed-view'; 8 - import { Link } from 'expo-router'; 4 + import ParallaxScrollView from "@/components/parallax-scroll-view"; 5 + import { ThemedText } from "@/components/themed-text"; 6 + import { ThemedView } from "@/components/themed-view"; 7 + import { useQuery } from "@tanstack/react-query"; 8 + import { getAccountsOptions } from "@/generated/@tanstack/react-query.gen"; 9 + import { useCurrentUser } from "@/hooks/use-current-user"; 10 + import { IconSymbol } from "@/components/ui/icon-symbol"; 11 + import { Price } from "@/components/ui/price"; 12 + import { AccountNumber } from "@/components/ui/accountnumber"; 9 13 10 14 export default function HomeScreen() { 15 + const { data: accountData } = useQuery(getAccountsOptions()); 16 + 17 + const { user } = useCurrentUser(); 18 + 11 19 return ( 12 20 <ParallaxScrollView 13 - headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} 21 + headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }} 14 22 headerImage={ 15 23 <Image 16 - source={require('@/assets/images/partial-react-logo.png')} 24 + source={require("@/assets/images/partial-react-logo.png")} 17 25 style={styles.reactLogo} 18 26 /> 19 - }> 27 + } 28 + > 20 29 <ThemedView style={styles.titleContainer}> 21 - <ThemedText type="title">Welcome!</ThemedText> 22 - <HelloWave /> 23 - </ThemedView> 24 - <ThemedView style={styles.stepContainer}> 25 - <ThemedText type="subtitle">Step 1: Try it</ThemedText> 26 - <ThemedText> 27 - Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes. 28 - Press{' '} 29 - <ThemedText type="defaultSemiBold"> 30 - {Platform.select({ 31 - ios: 'cmd + d', 32 - android: 'cmd + m', 33 - web: 'F12', 34 - })} 35 - </ThemedText>{' '} 36 - to open developer tools. 37 - </ThemedText> 38 - </ThemedView> 39 - <ThemedView style={styles.stepContainer}> 40 - <Link href="/modal"> 41 - <Link.Trigger> 42 - <ThemedText type="subtitle">Step 2: Explore</ThemedText> 43 - </Link.Trigger> 44 - <Link.Preview /> 45 - <Link.Menu> 46 - <Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} /> 47 - <Link.MenuAction 48 - title="Share" 49 - icon="square.and.arrow.up" 50 - onPress={() => alert('Share pressed')} 51 - /> 52 - <Link.Menu title="More" icon="ellipsis"> 53 - <Link.MenuAction 54 - title="Delete" 55 - icon="trash" 56 - destructive 57 - onPress={() => alert('Delete pressed')} 58 - /> 59 - </Link.Menu> 60 - </Link.Menu> 61 - </Link> 62 - 63 - <ThemedText> 64 - {`Tap the Explore tab to learn more about what's included in this starter app.`} 65 - </ThemedText> 30 + <ThemedText type="default">Welcome back, {user?.fullname}!</ThemedText> 66 31 </ThemedView> 67 32 <ThemedView style={styles.stepContainer}> 68 - <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText> 69 - <ThemedText> 70 - {`When you're ready, run `} 71 - <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '} 72 - <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '} 73 - <ThemedText type="defaultSemiBold">app</ThemedText> to{' '} 74 - <ThemedText type="defaultSemiBold">app-example</ThemedText>. 75 - </ThemedText> 33 + {accountData?.map(({ id, name, balance, iban }) => ( 34 + <TouchableOpacity key={id}> 35 + <ThemedView style={styles.accountContainer}> 36 + <ThemedView style={[styles.accountDetail, { flexGrow: 1 }]}> 37 + <ThemedText>{name}</ThemedText> 38 + <AccountNumber hidden>{iban}</AccountNumber> 39 + </ThemedView> 40 + <ThemedView style={styles.accountDetail}> 41 + <Price>{balance}</Price> 42 + <ThemedText>+20%</ThemedText> 43 + </ThemedView> 44 + <ThemedView style={styles.accountDetail}> 45 + <IconSymbol size={20} name="chevron.right" color="black" /> 46 + </ThemedView> 47 + </ThemedView> 48 + </TouchableOpacity> 49 + ))} 76 50 </ThemedView> 77 51 </ParallaxScrollView> 78 52 ); ··· 80 54 81 55 const styles = StyleSheet.create({ 82 56 titleContainer: { 83 - flexDirection: 'row', 84 - alignItems: 'center', 57 + flexDirection: "row", 58 + alignItems: "center", 85 59 gap: 8, 86 60 }, 87 61 stepContainer: { ··· 93 67 width: 290, 94 68 bottom: 0, 95 69 left: 0, 96 - position: 'absolute', 70 + position: "absolute", 71 + }, 72 + accountContainer: { 73 + display: "flex", 74 + flexDirection: "row", 75 + justifyContent: "space-between", 76 + paddingVertical: 4, 77 + gap: 4, 78 + }, 79 + accountDetail: { 80 + flexDirection: "column", 81 + justifyContent: "center", 82 + paddingVertical: 2, 97 83 }, 98 84 });
+27 -13
app/app/_layout.tsx
··· 1 - import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 2 - import { Stack } from 'expo-router'; 3 - import { StatusBar } from 'expo-status-bar'; 4 - import 'react-native-reanimated'; 1 + import { 2 + DarkTheme, 3 + DefaultTheme, 4 + ThemeProvider, 5 + } from "@react-navigation/native"; 6 + import { Stack } from "expo-router"; 7 + import { StatusBar } from "expo-status-bar"; 8 + import "react-native-reanimated"; 5 9 6 - import { useColorScheme } from '@/hooks/use-color-scheme'; 10 + import { useColorScheme } from "@/hooks/use-color-scheme"; 11 + import { TokenProvider } from "@/providers/token-provider"; 12 + import { QueryClientProvider } from "@/providers/query-client-provider"; 7 13 8 14 export const unstable_settings = { 9 - anchor: '(tabs)', 15 + anchor: "(tabs)", 10 16 }; 11 17 12 18 export default function RootLayout() { 13 19 const colorScheme = useColorScheme(); 14 20 15 21 return ( 16 - <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> 17 - <Stack> 18 - <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> 19 - <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} /> 20 - </Stack> 21 - <StatusBar style="auto" /> 22 - </ThemeProvider> 22 + <TokenProvider> 23 + <QueryClientProvider> 24 + <ThemeProvider 25 + value={colorScheme === "dark" ? DarkTheme : DefaultTheme} 26 + > 27 + <Stack screenOptions={{ headerShown: false, navigationBarHidden: true}}> 28 + <Stack.Screen 29 + name="(tabs)" 30 + options={{ headerShown: false, navigationBarHidden: true }} 31 + /> 32 + </Stack> 33 + <StatusBar style="auto" /> 34 + </ThemeProvider> 35 + </QueryClientProvider> 36 + </TokenProvider> 23 37 ); 24 38 }
-29
app/app/modal.tsx
··· 1 - import { Link } from 'expo-router'; 2 - import { StyleSheet } from 'react-native'; 3 - 4 - import { ThemedText } from '@/components/themed-text'; 5 - import { ThemedView } from '@/components/themed-view'; 6 - 7 - export default function ModalScreen() { 8 - return ( 9 - <ThemedView style={styles.container}> 10 - <ThemedText type="title">This is a modal</ThemedText> 11 - <Link href="/" dismissTo style={styles.link}> 12 - <ThemedText type="link">Go to home screen</ThemedText> 13 - </Link> 14 - </ThemedView> 15 - ); 16 - } 17 - 18 - const styles = StyleSheet.create({ 19 - container: { 20 - flex: 1, 21 - alignItems: 'center', 22 - justifyContent: 'center', 23 - padding: 20, 24 - }, 25 - link: { 26 - marginTop: 15, 27 - paddingVertical: 15, 28 - }, 29 - });
+18 -11
app/components/external-link.tsx
··· 1 - import { Href, Link } from 'expo-router'; 2 - import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser'; 3 - import { type ComponentProps } from 'react'; 1 + import { Href, Link } from "expo-router"; 2 + import { 3 + openBrowserAsync, 4 + WebBrowserPresentationStyle, 5 + } from "expo-web-browser"; 6 + import { type ComponentProps } from "react"; 4 7 5 - type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string }; 8 + type Props = Omit<ComponentProps<typeof Link>, "href"> & { 9 + href: Href & string; 10 + }; 6 11 7 12 export function ExternalLink({ href, ...rest }: Props) { 8 13 return ( ··· 11 16 {...rest} 12 17 href={href} 13 18 onPress={async (event) => { 14 - if (process.env.EXPO_OS !== 'web') { 15 - // Prevent the default behavior of linking to the default browser on native. 16 - event.preventDefault(); 17 - // Open the link in an in-app browser. 18 - await openBrowserAsync(href, { 19 - presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, 20 - }); 19 + if (process.env.EXPO_OS !== "web") { 20 + return; 21 21 } 22 + 23 + // Prevent the default behavior of linking to the default browser on native. 24 + event.preventDefault(); 25 + // Open the link in an in-app browser. 26 + await openBrowserAsync(href, { 27 + presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, 28 + }); 22 29 }} 23 30 /> 24 31 );
-19
app/components/hello-wave.tsx
··· 1 - import Animated from 'react-native-reanimated'; 2 - 3 - export function HelloWave() { 4 - return ( 5 - <Animated.Text 6 - style={{ 7 - fontSize: 28, 8 - lineHeight: 32, 9 - marginTop: -6, 10 - animationName: { 11 - '50%': { transform: [{ rotate: '25deg' }] }, 12 - }, 13 - animationIterationCount: 4, 14 - animationDuration: '300ms', 15 - }}> 16 - 👋 17 - </Animated.Text> 18 - ); 19 - }
+38
app/components/ui/accountnumber.tsx
··· 1 + import { TouchableOpacity } from "react-native"; 2 + import { ThemedText } from "../themed-text"; 3 + import { useToggle } from "@/hooks/use-toggle"; 4 + import { blurAccountNumber } from "@/services/blur-accountnumber"; 5 + import { PropsWithChildren } from "react"; 6 + import { ThemedView } from "../themed-view"; 7 + 8 + interface AccountNumberProps { 9 + children: string; 10 + hidden?: boolean; 11 + } 12 + 13 + export const AccountNumber = ({ 14 + children, 15 + hidden = false, 16 + }: AccountNumberProps) => { 17 + const { isHidden, toggleHidden } = useToggle(true); 18 + 19 + return ( 20 + <Wrapper onPress={toggleHidden} toggleable={!hidden}> 21 + <ThemedText> 22 + {isHidden ? blurAccountNumber(children.toString()) : children} 23 + </ThemedText> 24 + </Wrapper> 25 + ); 26 + }; 27 + 28 + type WrapperProps = PropsWithChildren<{ 29 + toggleable: boolean; 30 + onPress: () => void | Promise<void>; 31 + }>; 32 + 33 + const Wrapper = ({ children, toggleable, onPress }: WrapperProps) => 34 + toggleable ? ( 35 + <TouchableOpacity onPressOut={onPress}>{children}</TouchableOpacity> 36 + ) : ( 37 + <ThemedView>{children}</ThemedView> 38 + );
+13
app/components/ui/price.tsx
··· 1 + import { ThemedText } from "../themed-text"; 2 + 3 + interface PriceProps { 4 + children: number; 5 + currency?: string; 6 + } 7 + 8 + export const Price = ({ children, currency = "€" }: PriceProps) => ( 9 + <ThemedText> 10 + {currency} 11 + {children.toFixed(2)} 12 + </ThemedText> 13 + );
+21 -29
app/constants/theme.ts
··· 1 - /** 2 - * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 - * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 - */ 1 + import { Platform } from "react-native"; 5 2 6 - import { Platform } from 'react-native'; 7 - 8 - const tintColorLight = '#0a7ea4'; 9 - const tintColorDark = '#fff'; 3 + const tintColorLight = "#0a7ea4"; 4 + const tintColorDark = "#fff"; 10 5 11 6 export const Colors = { 12 7 light: { 13 - text: '#11181C', 14 - background: '#fff', 8 + text: "#11181C", 9 + background: "#fff", 15 10 tint: tintColorLight, 16 - icon: '#687076', 17 - tabIconDefault: '#687076', 11 + icon: "#687076", 12 + tabIconDefault: "#687076", 18 13 tabIconSelected: tintColorLight, 19 14 }, 20 15 dark: { 21 - text: '#ECEDEE', 22 - background: '#151718', 16 + text: "#ECEDEE", 17 + background: "#151718", 23 18 tint: tintColorDark, 24 - icon: '#9BA1A6', 25 - tabIconDefault: '#9BA1A6', 19 + icon: "#9BA1A6", 20 + tabIconDefault: "#9BA1A6", 26 21 tabIconSelected: tintColorDark, 27 22 }, 28 23 }; 29 24 30 25 export const Fonts = Platform.select({ 31 26 ios: { 32 - /** iOS `UIFontDescriptorSystemDesignDefault` */ 33 - sans: 'system-ui', 34 - /** iOS `UIFontDescriptorSystemDesignSerif` */ 35 - serif: 'ui-serif', 36 - /** iOS `UIFontDescriptorSystemDesignRounded` */ 37 - rounded: 'ui-rounded', 38 - /** iOS `UIFontDescriptorSystemDesignMonospaced` */ 39 - mono: 'ui-monospace', 27 + sans: "system-ui", 28 + serif: "ui-serif", 29 + rounded: "ui-rounded", 30 + mono: "ui-monospace", 40 31 }, 41 32 default: { 42 - sans: 'normal', 43 - serif: 'serif', 44 - rounded: 'normal', 45 - mono: 'monospace', 33 + sans: "normal", 34 + serif: "serif", 35 + rounded: "normal", 36 + mono: "monospace", 46 37 }, 47 38 web: { 48 39 sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", 49 40 serif: "Georgia, 'Times New Roman', serif", 50 - rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", 41 + rounded: 42 + "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", 51 43 mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", 52 44 }, 53 45 });
+18
app/hooks/use-authentication.ts
··· 1 + import { useCallback } from "react"; 2 + import { useFocusEffect, useRouter } from "expo-router"; 3 + import { useToken } from "@/providers/token-provider"; 4 + 5 + export const useAuthentication = () => { 6 + const { token } = useToken(); 7 + const router = useRouter(); 8 + 9 + useFocusEffect( 10 + useCallback(() => { 11 + if (token) { 12 + return; 13 + } 14 + 15 + router.replace("/(auth)"); 16 + }, [token, router]), 17 + ); 18 + };
+8
app/hooks/use-current-user.ts
··· 1 + import { getMeOptions } from "@/generated/@tanstack/react-query.gen"; 2 + import { useQuery } from "@tanstack/react-query"; 3 + 4 + export const useCurrentUser = () => { 5 + const { data } = useQuery(getMeOptions()); 6 + 7 + return { user: data }; 8 + };
+28
app/hooks/use-login.ts
··· 1 + import { useRouter } from "expo-router"; 2 + import { useMutation } from "@tanstack/react-query"; 3 + import { LoginRequest, postLogin } from "@/generated"; 4 + import { useToken } from "@/providers/token-provider"; 5 + import { useEffect } from "react"; 6 + 7 + export const useLogin = () => { 8 + const { token, setToken } = useToken(); 9 + const router = useRouter(); 10 + const { mutateAsync } = useMutation({ 11 + mutationFn: (credentials: LoginRequest) => postLogin({ body: credentials }), 12 + }); 13 + 14 + useEffect(() => { 15 + if (token) { 16 + router.replace("/(tabs)"); 17 + } 18 + }, [token, router]); 19 + 20 + return { 21 + login: (credentials: LoginRequest) => 22 + mutateAsync(credentials).then(({ data }) => { 23 + if (data) { 24 + setToken(data); 25 + } 26 + }), 27 + }; 28 + };
+13
app/hooks/use-logout.ts
··· 1 + import { useToken } from "@/providers/token-provider"; 2 + import { useRouter } from "expo-router"; 3 + 4 + export const useLogout = () => { 5 + const router = useRouter(); 6 + const { setToken } = useToken(); 7 + 8 + return { 9 + logout: () => { 10 + setToken(null).then(() => router.replace("/(auth)")); 11 + }, 12 + }; 13 + };
+11
app/hooks/use-toggle.ts
··· 1 + import { useCallback, useState } from "react"; 2 + 3 + export const useToggle = (initialValue: boolean = false) => { 4 + const [isHidden, setHidden] = useState(initialValue); 5 + 6 + const toggleHidden = useCallback(() => { 7 + setHidden((v) => !v); 8 + }, []); 9 + 10 + return { isHidden, setHidden, toggleHidden }; 11 + };
+10
app/openapi-ts.config.ts
··· 1 + import { defineConfig } from "@hey-api/openapi-ts"; 2 + 3 + export default defineConfig({ 4 + input: "http://localhost:3001/openapi.yaml", 5 + output: "generated", 6 + plugins: [ 7 + "@tanstack/react-query", 8 + { name: "@hey-api/transformers", dates: true }, 9 + ], 10 + });
+671 -5
app/package-lock.json
··· 1 1 { 2 - "name": "wonderlandzor-app", 2 + "name": "ics-banking-demo-app", 3 3 "version": "1.0.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 - "name": "wonderlandzor-app", 8 + "name": "ics-banking-demo-app", 9 9 "version": "1.0.0", 10 10 "dependencies": { 11 11 "@expo/metro-runtime": "~6.1.2", 12 12 "@expo/vector-icons": "^15.0.2", 13 + "@hey-api/openapi-ts": "^0.84.3", 13 14 "@react-navigation/bottom-tabs": "^7.4.0", 14 15 "@react-navigation/elements": "^2.6.3", 15 16 "@react-navigation/native": "^7.1.8", 17 + "@tanstack/react-query": "^5.90.2", 16 18 "expo": "~54.0.10", 17 19 "expo-constants": "~18.0.9", 18 20 "expo-font": "~14.0.8", ··· 20 22 "expo-image": "~3.0.8", 21 23 "expo-linking": "~8.0.8", 22 24 "expo-router": "~6.0.8", 25 + "expo-secure-store": "~15.0.7", 23 26 "expo-splash-screen": "~31.0.10", 24 27 "expo-status-bar": "~3.0.8", 25 28 "expo-symbols": "~1.0.7", ··· 33 36 "react-native-safe-area-context": "~5.6.0", 34 37 "react-native-screens": "~4.16.0", 35 38 "react-native-web": "~0.21.0", 36 - "react-native-worklets": "0.5.1" 39 + "react-native-worklets": "0.5.1", 40 + "zod": "^3.25.76" 37 41 }, 38 42 "devDependencies": { 39 43 "@types/react": "~19.1.0", ··· 2272 2276 "@babel/highlight": "^7.10.4" 2273 2277 } 2274 2278 }, 2279 + "node_modules/@hey-api/codegen-core": { 2280 + "version": "0.2.0", 2281 + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.2.0.tgz", 2282 + "integrity": "sha512-c7VjBy/8ed0EVLNgaeS9Xxams1Tuv/WK/b4xXH3Qr4wjzYeJUtxOcoP8YdwNLavqKP8pGiuctjX2Z1Pwc4jMgQ==", 2283 + "license": "MIT", 2284 + "engines": { 2285 + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" 2286 + }, 2287 + "funding": { 2288 + "url": "https://github.com/sponsors/hey-api" 2289 + }, 2290 + "peerDependencies": { 2291 + "typescript": ">=5.5.3" 2292 + } 2293 + }, 2294 + "node_modules/@hey-api/json-schema-ref-parser": { 2295 + "version": "1.1.0", 2296 + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.1.0.tgz", 2297 + "integrity": "sha512-+5eg9pgAAM9oSqJQuUtfTKbLz8yieFKN91myyXiLnprqFj8ROfxUKJLr9DKq/hGKyybKT1WxFSetDqCFm80pCA==", 2298 + "license": "MIT", 2299 + "dependencies": { 2300 + "@jsdevtools/ono": "^7.1.3", 2301 + "@types/json-schema": "^7.0.15", 2302 + "js-yaml": "^4.1.0", 2303 + "lodash": "^4.17.21" 2304 + }, 2305 + "engines": { 2306 + "node": ">= 16" 2307 + }, 2308 + "funding": { 2309 + "url": "https://github.com/sponsors/hey-api" 2310 + } 2311 + }, 2312 + "node_modules/@hey-api/openapi-ts": { 2313 + "version": "0.84.3", 2314 + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.84.3.tgz", 2315 + "integrity": "sha512-WFjGGBzmIzGRdHw+CC7g8TSzl4UjdWbNwiDO07a3BUEhSBZbLGidfh+E4SOGbePak6sWptVh9WqO3QrThFH25A==", 2316 + "license": "MIT", 2317 + "dependencies": { 2318 + "@hey-api/codegen-core": "^0.2.0", 2319 + "@hey-api/json-schema-ref-parser": "1.1.0", 2320 + "ansi-colors": "4.1.3", 2321 + "c12": "2.0.1", 2322 + "color-support": "1.1.3", 2323 + "commander": "13.0.0", 2324 + "handlebars": "4.7.8", 2325 + "open": "10.1.2", 2326 + "semver": "7.7.2" 2327 + }, 2328 + "bin": { 2329 + "openapi-ts": "bin/index.cjs" 2330 + }, 2331 + "engines": { 2332 + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" 2333 + }, 2334 + "funding": { 2335 + "url": "https://github.com/sponsors/hey-api" 2336 + }, 2337 + "peerDependencies": { 2338 + "typescript": ">=5.5.3" 2339 + } 2340 + }, 2341 + "node_modules/@hey-api/openapi-ts/node_modules/commander": { 2342 + "version": "13.0.0", 2343 + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", 2344 + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", 2345 + "license": "MIT", 2346 + "engines": { 2347 + "node": ">=18" 2348 + } 2349 + }, 2350 + "node_modules/@hey-api/openapi-ts/node_modules/define-lazy-prop": { 2351 + "version": "3.0.0", 2352 + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", 2353 + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", 2354 + "license": "MIT", 2355 + "engines": { 2356 + "node": ">=12" 2357 + }, 2358 + "funding": { 2359 + "url": "https://github.com/sponsors/sindresorhus" 2360 + } 2361 + }, 2362 + "node_modules/@hey-api/openapi-ts/node_modules/is-wsl": { 2363 + "version": "3.1.0", 2364 + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", 2365 + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 2366 + "license": "MIT", 2367 + "dependencies": { 2368 + "is-inside-container": "^1.0.0" 2369 + }, 2370 + "engines": { 2371 + "node": ">=16" 2372 + }, 2373 + "funding": { 2374 + "url": "https://github.com/sponsors/sindresorhus" 2375 + } 2376 + }, 2377 + "node_modules/@hey-api/openapi-ts/node_modules/open": { 2378 + "version": "10.1.2", 2379 + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", 2380 + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", 2381 + "license": "MIT", 2382 + "dependencies": { 2383 + "default-browser": "^5.2.1", 2384 + "define-lazy-prop": "^3.0.0", 2385 + "is-inside-container": "^1.0.0", 2386 + "is-wsl": "^3.1.0" 2387 + }, 2388 + "engines": { 2389 + "node": ">=18" 2390 + }, 2391 + "funding": { 2392 + "url": "https://github.com/sponsors/sindresorhus" 2393 + } 2394 + }, 2395 + "node_modules/@hey-api/openapi-ts/node_modules/semver": { 2396 + "version": "7.7.2", 2397 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 2398 + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 2399 + "license": "ISC", 2400 + "bin": { 2401 + "semver": "bin/semver.js" 2402 + }, 2403 + "engines": { 2404 + "node": ">=10" 2405 + } 2406 + }, 2275 2407 "node_modules/@humanfs/core": { 2276 2408 "version": "0.19.1", 2277 2409 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 2623 2755 "@jridgewell/resolve-uri": "^3.1.0", 2624 2756 "@jridgewell/sourcemap-codec": "^1.4.14" 2625 2757 } 2758 + }, 2759 + "node_modules/@jsdevtools/ono": { 2760 + "version": "7.1.3", 2761 + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", 2762 + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", 2763 + "license": "MIT" 2626 2764 }, 2627 2765 "node_modules/@napi-rs/wasm-runtime": { 2628 2766 "version": "0.2.12", ··· 3566 3704 "@sinonjs/commons": "^3.0.0" 3567 3705 } 3568 3706 }, 3707 + "node_modules/@tanstack/query-core": { 3708 + "version": "5.90.2", 3709 + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", 3710 + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", 3711 + "license": "MIT", 3712 + "funding": { 3713 + "type": "github", 3714 + "url": "https://github.com/sponsors/tannerlinsley" 3715 + } 3716 + }, 3717 + "node_modules/@tanstack/react-query": { 3718 + "version": "5.90.2", 3719 + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", 3720 + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", 3721 + "license": "MIT", 3722 + "dependencies": { 3723 + "@tanstack/query-core": "5.90.2" 3724 + }, 3725 + "funding": { 3726 + "type": "github", 3727 + "url": "https://github.com/sponsors/tannerlinsley" 3728 + }, 3729 + "peerDependencies": { 3730 + "react": "^18 || ^19" 3731 + } 3732 + }, 3569 3733 "node_modules/@tybys/wasm-util": { 3570 3734 "version": "0.10.1", 3571 3735 "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", ··· 3668 3832 "version": "7.0.15", 3669 3833 "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 3670 3834 "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 3671 - "dev": true, 3672 3835 "license": "MIT" 3673 3836 }, 3674 3837 "node_modules/@types/json5": { ··· 4374 4537 "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", 4375 4538 "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", 4376 4539 "license": "MIT" 4540 + }, 4541 + "node_modules/ansi-colors": { 4542 + "version": "4.1.3", 4543 + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", 4544 + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", 4545 + "license": "MIT", 4546 + "engines": { 4547 + "node": ">=6" 4548 + } 4377 4549 }, 4378 4550 "node_modules/ansi-escapes": { 4379 4551 "version": "4.3.2", ··· 5064 5236 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 5065 5237 "license": "MIT" 5066 5238 }, 5239 + "node_modules/bundle-name": { 5240 + "version": "4.1.0", 5241 + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", 5242 + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 5243 + "license": "MIT", 5244 + "dependencies": { 5245 + "run-applescript": "^7.0.0" 5246 + }, 5247 + "engines": { 5248 + "node": ">=18" 5249 + }, 5250 + "funding": { 5251 + "url": "https://github.com/sponsors/sindresorhus" 5252 + } 5253 + }, 5067 5254 "node_modules/bytes": { 5068 5255 "version": "3.1.2", 5069 5256 "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", ··· 5073 5260 "node": ">= 0.8" 5074 5261 } 5075 5262 }, 5263 + "node_modules/c12": { 5264 + "version": "2.0.1", 5265 + "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", 5266 + "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", 5267 + "license": "MIT", 5268 + "dependencies": { 5269 + "chokidar": "^4.0.1", 5270 + "confbox": "^0.1.7", 5271 + "defu": "^6.1.4", 5272 + "dotenv": "^16.4.5", 5273 + "giget": "^1.2.3", 5274 + "jiti": "^2.3.0", 5275 + "mlly": "^1.7.1", 5276 + "ohash": "^1.1.4", 5277 + "pathe": "^1.1.2", 5278 + "perfect-debounce": "^1.0.0", 5279 + "pkg-types": "^1.2.0", 5280 + "rc9": "^2.1.2" 5281 + }, 5282 + "peerDependencies": { 5283 + "magicast": "^0.3.5" 5284 + }, 5285 + "peerDependenciesMeta": { 5286 + "magicast": { 5287 + "optional": true 5288 + } 5289 + } 5290 + }, 5076 5291 "node_modules/call-bind": { 5077 5292 "version": "1.0.8", 5078 5293 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", ··· 5214 5429 "url": "https://github.com/chalk/chalk?sponsor=1" 5215 5430 } 5216 5431 }, 5432 + "node_modules/chokidar": { 5433 + "version": "4.0.3", 5434 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", 5435 + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 5436 + "license": "MIT", 5437 + "dependencies": { 5438 + "readdirp": "^4.0.1" 5439 + }, 5440 + "engines": { 5441 + "node": ">= 14.16.0" 5442 + }, 5443 + "funding": { 5444 + "url": "https://paulmillr.com/funding/" 5445 + } 5446 + }, 5217 5447 "node_modules/chownr": { 5218 5448 "version": "3.0.0", 5219 5449 "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", ··· 5260 5490 "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", 5261 5491 "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", 5262 5492 "license": "MIT" 5493 + }, 5494 + "node_modules/citty": { 5495 + "version": "0.1.6", 5496 + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", 5497 + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", 5498 + "license": "MIT", 5499 + "dependencies": { 5500 + "consola": "^3.2.3" 5501 + } 5263 5502 }, 5264 5503 "node_modules/cli-cursor": { 5265 5504 "version": "2.1.0", ··· 5404 5643 "simple-swizzle": "^0.2.2" 5405 5644 } 5406 5645 }, 5646 + "node_modules/color-support": { 5647 + "version": "1.1.3", 5648 + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", 5649 + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", 5650 + "license": "ISC", 5651 + "bin": { 5652 + "color-support": "bin.js" 5653 + } 5654 + }, 5407 5655 "node_modules/commander": { 5408 5656 "version": "7.2.0", 5409 5657 "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", ··· 5473 5721 "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 5474 5722 "license": "MIT" 5475 5723 }, 5724 + "node_modules/confbox": { 5725 + "version": "0.1.8", 5726 + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", 5727 + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", 5728 + "license": "MIT" 5729 + }, 5476 5730 "node_modules/connect": { 5477 5731 "version": "3.7.0", 5478 5732 "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", ··· 5502 5756 "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 5503 5757 "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 5504 5758 "license": "MIT" 5759 + }, 5760 + "node_modules/consola": { 5761 + "version": "3.4.2", 5762 + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", 5763 + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", 5764 + "license": "MIT", 5765 + "engines": { 5766 + "node": "^14.18.0 || >=16.10.0" 5767 + } 5505 5768 }, 5506 5769 "node_modules/convert-source-map": { 5507 5770 "version": "2.0.0", ··· 5734 5997 "node": ">=0.10.0" 5735 5998 } 5736 5999 }, 6000 + "node_modules/default-browser": { 6001 + "version": "5.2.1", 6002 + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", 6003 + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 6004 + "license": "MIT", 6005 + "dependencies": { 6006 + "bundle-name": "^4.1.0", 6007 + "default-browser-id": "^5.0.0" 6008 + }, 6009 + "engines": { 6010 + "node": ">=18" 6011 + }, 6012 + "funding": { 6013 + "url": "https://github.com/sponsors/sindresorhus" 6014 + } 6015 + }, 6016 + "node_modules/default-browser-id": { 6017 + "version": "5.0.0", 6018 + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", 6019 + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", 6020 + "license": "MIT", 6021 + "engines": { 6022 + "node": ">=18" 6023 + }, 6024 + "funding": { 6025 + "url": "https://github.com/sponsors/sindresorhus" 6026 + } 6027 + }, 5737 6028 "node_modules/defaults": { 5738 6029 "version": "1.0.4", 5739 6030 "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", ··· 5791 6082 "url": "https://github.com/sponsors/ljharb" 5792 6083 } 5793 6084 }, 6085 + "node_modules/defu": { 6086 + "version": "6.1.4", 6087 + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 6088 + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 6089 + "license": "MIT" 6090 + }, 5794 6091 "node_modules/depd": { 5795 6092 "version": "2.0.0", 5796 6093 "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", ··· 5799 6096 "engines": { 5800 6097 "node": ">= 0.8" 5801 6098 } 6099 + }, 6100 + "node_modules/destr": { 6101 + "version": "2.0.5", 6102 + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", 6103 + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", 6104 + "license": "MIT" 5802 6105 }, 5803 6106 "node_modules/destroy": { 5804 6107 "version": "1.2.0", ··· 6848 7151 "node": ">=10" 6849 7152 } 6850 7153 }, 7154 + "node_modules/expo-secure-store": { 7155 + "version": "15.0.7", 7156 + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.7.tgz", 7157 + "integrity": "sha512-9q7+G1Zxr5P6J5NRIlm86KulvmYwc6UnQlYPjQLDu1drDnerz6AT6l884dPu29HgtDTn4rR0heYeeGFhMKM7/Q==", 7158 + "license": "MIT", 7159 + "peerDependencies": { 7160 + "expo": "*" 7161 + } 7162 + }, 6851 7163 "node_modules/expo-splash-screen": { 6852 7164 "version": "31.0.10", 6853 7165 "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.10.tgz", ··· 7407 7719 "node": ">= 0.6" 7408 7720 } 7409 7721 }, 7722 + "node_modules/fs-minipass": { 7723 + "version": "2.1.0", 7724 + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", 7725 + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 7726 + "license": "ISC", 7727 + "dependencies": { 7728 + "minipass": "^3.0.0" 7729 + }, 7730 + "engines": { 7731 + "node": ">= 8" 7732 + } 7733 + }, 7734 + "node_modules/fs-minipass/node_modules/minipass": { 7735 + "version": "3.3.6", 7736 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", 7737 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", 7738 + "license": "ISC", 7739 + "dependencies": { 7740 + "yallist": "^4.0.0" 7741 + }, 7742 + "engines": { 7743 + "node": ">=8" 7744 + } 7745 + }, 7746 + "node_modules/fs-minipass/node_modules/yallist": { 7747 + "version": "4.0.0", 7748 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 7749 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 7750 + "license": "ISC" 7751 + }, 7410 7752 "node_modules/fs.realpath": { 7411 7753 "version": "1.0.0", 7412 7754 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", ··· 7582 7924 "node": ">=6" 7583 7925 } 7584 7926 }, 7927 + "node_modules/giget": { 7928 + "version": "1.2.5", 7929 + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz", 7930 + "integrity": "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==", 7931 + "license": "MIT", 7932 + "dependencies": { 7933 + "citty": "^0.1.6", 7934 + "consola": "^3.4.0", 7935 + "defu": "^6.1.4", 7936 + "node-fetch-native": "^1.6.6", 7937 + "nypm": "^0.5.4", 7938 + "pathe": "^2.0.3", 7939 + "tar": "^6.2.1" 7940 + }, 7941 + "bin": { 7942 + "giget": "dist/cli.mjs" 7943 + } 7944 + }, 7945 + "node_modules/giget/node_modules/chownr": { 7946 + "version": "2.0.0", 7947 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", 7948 + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", 7949 + "license": "ISC", 7950 + "engines": { 7951 + "node": ">=10" 7952 + } 7953 + }, 7954 + "node_modules/giget/node_modules/minipass": { 7955 + "version": "5.0.0", 7956 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", 7957 + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", 7958 + "license": "ISC", 7959 + "engines": { 7960 + "node": ">=8" 7961 + } 7962 + }, 7963 + "node_modules/giget/node_modules/minizlib": { 7964 + "version": "2.1.2", 7965 + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", 7966 + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", 7967 + "license": "MIT", 7968 + "dependencies": { 7969 + "minipass": "^3.0.0", 7970 + "yallist": "^4.0.0" 7971 + }, 7972 + "engines": { 7973 + "node": ">= 8" 7974 + } 7975 + }, 7976 + "node_modules/giget/node_modules/minizlib/node_modules/minipass": { 7977 + "version": "3.3.6", 7978 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", 7979 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", 7980 + "license": "ISC", 7981 + "dependencies": { 7982 + "yallist": "^4.0.0" 7983 + }, 7984 + "engines": { 7985 + "node": ">=8" 7986 + } 7987 + }, 7988 + "node_modules/giget/node_modules/pathe": { 7989 + "version": "2.0.3", 7990 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 7991 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 7992 + "license": "MIT" 7993 + }, 7994 + "node_modules/giget/node_modules/tar": { 7995 + "version": "6.2.1", 7996 + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", 7997 + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", 7998 + "license": "ISC", 7999 + "dependencies": { 8000 + "chownr": "^2.0.0", 8001 + "fs-minipass": "^2.0.0", 8002 + "minipass": "^5.0.0", 8003 + "minizlib": "^2.1.1", 8004 + "mkdirp": "^1.0.3", 8005 + "yallist": "^4.0.0" 8006 + }, 8007 + "engines": { 8008 + "node": ">=10" 8009 + } 8010 + }, 8011 + "node_modules/giget/node_modules/yallist": { 8012 + "version": "4.0.0", 8013 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 8014 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 8015 + "license": "ISC" 8016 + }, 7585 8017 "node_modules/glob": { 7586 8018 "version": "10.4.5", 7587 8019 "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", ··· 7707 8139 "dev": true, 7708 8140 "license": "MIT" 7709 8141 }, 8142 + "node_modules/handlebars": { 8143 + "version": "4.7.8", 8144 + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", 8145 + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", 8146 + "license": "MIT", 8147 + "dependencies": { 8148 + "minimist": "^1.2.5", 8149 + "neo-async": "^2.6.2", 8150 + "source-map": "^0.6.1", 8151 + "wordwrap": "^1.0.0" 8152 + }, 8153 + "bin": { 8154 + "handlebars": "bin/handlebars" 8155 + }, 8156 + "engines": { 8157 + "node": ">=0.4.7" 8158 + }, 8159 + "optionalDependencies": { 8160 + "uglify-js": "^3.1.4" 8161 + } 8162 + }, 8163 + "node_modules/handlebars/node_modules/source-map": { 8164 + "version": "0.6.1", 8165 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 8166 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 8167 + "license": "BSD-3-Clause", 8168 + "engines": { 8169 + "node": ">=0.10.0" 8170 + } 8171 + }, 7710 8172 "node_modules/has-bigints": { 7711 8173 "version": "1.1.0", 7712 8174 "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", ··· 8281 8743 "node": ">=0.10.0" 8282 8744 } 8283 8745 }, 8746 + "node_modules/is-inside-container": { 8747 + "version": "1.0.0", 8748 + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", 8749 + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 8750 + "license": "MIT", 8751 + "dependencies": { 8752 + "is-docker": "^3.0.0" 8753 + }, 8754 + "bin": { 8755 + "is-inside-container": "cli.js" 8756 + }, 8757 + "engines": { 8758 + "node": ">=14.16" 8759 + }, 8760 + "funding": { 8761 + "url": "https://github.com/sponsors/sindresorhus" 8762 + } 8763 + }, 8764 + "node_modules/is-inside-container/node_modules/is-docker": { 8765 + "version": "3.0.0", 8766 + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", 8767 + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", 8768 + "license": "MIT", 8769 + "bin": { 8770 + "is-docker": "cli.js" 8771 + }, 8772 + "engines": { 8773 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 8774 + }, 8775 + "funding": { 8776 + "url": "https://github.com/sponsors/sindresorhus" 8777 + } 8778 + }, 8284 8779 "node_modules/is-map": { 8285 8780 "version": "2.0.3", 8286 8781 "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", ··· 8740 9235 "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", 8741 9236 "license": "MIT" 8742 9237 }, 9238 + "node_modules/jiti": { 9239 + "version": "2.6.0", 9240 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", 9241 + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", 9242 + "license": "MIT", 9243 + "bin": { 9244 + "jiti": "lib/jiti-cli.mjs" 9245 + } 9246 + }, 8743 9247 "node_modules/js-tokens": { 8744 9248 "version": "4.0.0", 8745 9249 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", ··· 9155 9659 "funding": { 9156 9660 "url": "https://github.com/sponsors/sindresorhus" 9157 9661 } 9662 + }, 9663 + "node_modules/lodash": { 9664 + "version": "4.17.21", 9665 + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 9666 + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 9667 + "license": "MIT" 9158 9668 }, 9159 9669 "node_modules/lodash.debounce": { 9160 9670 "version": "4.0.8", ··· 9704 10214 "node": ">=10" 9705 10215 } 9706 10216 }, 10217 + "node_modules/mlly": { 10218 + "version": "1.8.0", 10219 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", 10220 + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", 10221 + "license": "MIT", 10222 + "dependencies": { 10223 + "acorn": "^8.15.0", 10224 + "pathe": "^2.0.3", 10225 + "pkg-types": "^1.3.1", 10226 + "ufo": "^1.6.1" 10227 + } 10228 + }, 10229 + "node_modules/mlly/node_modules/pathe": { 10230 + "version": "2.0.3", 10231 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 10232 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 10233 + "license": "MIT" 10234 + }, 9707 10235 "node_modules/ms": { 9708 10236 "version": "2.1.3", 9709 10237 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 9771 10299 "node": ">= 0.6" 9772 10300 } 9773 10301 }, 10302 + "node_modules/neo-async": { 10303 + "version": "2.6.2", 10304 + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", 10305 + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", 10306 + "license": "MIT" 10307 + }, 9774 10308 "node_modules/nested-error-stacks": { 9775 10309 "version": "2.0.1", 9776 10310 "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", ··· 9796 10330 "optional": true 9797 10331 } 9798 10332 } 10333 + }, 10334 + "node_modules/node-fetch-native": { 10335 + "version": "1.6.7", 10336 + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", 10337 + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", 10338 + "license": "MIT" 9799 10339 }, 9800 10340 "node_modules/node-forge": { 9801 10341 "version": "1.3.1", ··· 9860 10400 "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", 9861 10401 "license": "MIT" 9862 10402 }, 10403 + "node_modules/nypm": { 10404 + "version": "0.5.4", 10405 + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz", 10406 + "integrity": "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==", 10407 + "license": "MIT", 10408 + "dependencies": { 10409 + "citty": "^0.1.6", 10410 + "consola": "^3.4.0", 10411 + "pathe": "^2.0.3", 10412 + "pkg-types": "^1.3.1", 10413 + "tinyexec": "^0.3.2", 10414 + "ufo": "^1.5.4" 10415 + }, 10416 + "bin": { 10417 + "nypm": "dist/cli.mjs" 10418 + }, 10419 + "engines": { 10420 + "node": "^14.16.0 || >=16.10.0" 10421 + } 10422 + }, 10423 + "node_modules/nypm/node_modules/pathe": { 10424 + "version": "2.0.3", 10425 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 10426 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 10427 + "license": "MIT" 10428 + }, 9863 10429 "node_modules/ob1": { 9864 10430 "version": "0.83.1", 9865 10431 "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.1.tgz", ··· 9993 10559 "funding": { 9994 10560 "url": "https://github.com/sponsors/ljharb" 9995 10561 } 10562 + }, 10563 + "node_modules/ohash": { 10564 + "version": "1.1.6", 10565 + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.6.tgz", 10566 + "integrity": "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==", 10567 + "license": "MIT" 9996 10568 }, 9997 10569 "node_modules/on-finished": { 9998 10570 "version": "2.3.0", ··· 10343 10915 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 10344 10916 "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 10345 10917 "license": "ISC" 10918 + }, 10919 + "node_modules/pathe": { 10920 + "version": "1.1.2", 10921 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", 10922 + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", 10923 + "license": "MIT" 10924 + }, 10925 + "node_modules/perfect-debounce": { 10926 + "version": "1.0.0", 10927 + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", 10928 + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", 10929 + "license": "MIT" 10346 10930 }, 10347 10931 "node_modules/picocolors": { 10348 10932 "version": "1.1.1", ··· 10371 10955 "node": ">= 6" 10372 10956 } 10373 10957 }, 10958 + "node_modules/pkg-types": { 10959 + "version": "1.3.1", 10960 + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", 10961 + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", 10962 + "license": "MIT", 10963 + "dependencies": { 10964 + "confbox": "^0.1.8", 10965 + "mlly": "^1.7.4", 10966 + "pathe": "^2.0.1" 10967 + } 10968 + }, 10969 + "node_modules/pkg-types/node_modules/pathe": { 10970 + "version": "2.0.3", 10971 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 10972 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 10973 + "license": "MIT" 10974 + }, 10374 10975 "node_modules/plist": { 10375 10976 "version": "3.1.0", 10376 10977 "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", ··· 10649 11250 "node": ">=0.10.0" 10650 11251 } 10651 11252 }, 11253 + "node_modules/rc9": { 11254 + "version": "2.1.2", 11255 + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", 11256 + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", 11257 + "license": "MIT", 11258 + "dependencies": { 11259 + "defu": "^6.1.4", 11260 + "destr": "^2.0.3" 11261 + } 11262 + }, 10652 11263 "node_modules/react": { 10653 11264 "version": "19.1.0", 10654 11265 "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", ··· 11036 11647 } 11037 11648 } 11038 11649 }, 11650 + "node_modules/readdirp": { 11651 + "version": "4.1.2", 11652 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", 11653 + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", 11654 + "license": "MIT", 11655 + "engines": { 11656 + "node": ">= 14.18.0" 11657 + }, 11658 + "funding": { 11659 + "type": "individual", 11660 + "url": "https://paulmillr.com/funding/" 11661 + } 11662 + }, 11039 11663 "node_modules/reflect.getprototypeof": { 11040 11664 "version": "1.0.10", 11041 11665 "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", ··· 11312 11936 "url": "https://github.com/sponsors/isaacs" 11313 11937 } 11314 11938 }, 11939 + "node_modules/run-applescript": { 11940 + "version": "7.1.0", 11941 + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", 11942 + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", 11943 + "license": "MIT", 11944 + "engines": { 11945 + "node": ">=18" 11946 + }, 11947 + "funding": { 11948 + "url": "https://github.com/sponsors/sindresorhus" 11949 + } 11950 + }, 11315 11951 "node_modules/run-parallel": { 11316 11952 "version": "1.2.0", 11317 11953 "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", ··· 12344 12980 "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", 12345 12981 "license": "MIT" 12346 12982 }, 12983 + "node_modules/tinyexec": { 12984 + "version": "0.3.2", 12985 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 12986 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 12987 + "license": "MIT" 12988 + }, 12347 12989 "node_modules/tinyglobby": { 12348 12990 "version": "0.2.15", 12349 12991 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", ··· 12589 13231 "version": "5.9.2", 12590 13232 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", 12591 13233 "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", 12592 - "dev": true, 12593 13234 "license": "Apache-2.0", 12594 13235 "bin": { 12595 13236 "tsc": "bin/tsc", ··· 12623 13264 }, 12624 13265 "engines": { 12625 13266 "node": "*" 13267 + } 13268 + }, 13269 + "node_modules/ufo": { 13270 + "version": "1.6.1", 13271 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", 13272 + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", 13273 + "license": "MIT" 13274 + }, 13275 + "node_modules/uglify-js": { 13276 + "version": "3.19.3", 13277 + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", 13278 + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", 13279 + "license": "BSD-2-Clause", 13280 + "optional": true, 13281 + "bin": { 13282 + "uglifyjs": "bin/uglifyjs" 13283 + }, 13284 + "engines": { 13285 + "node": ">=0.8.0" 12626 13286 } 12627 13287 }, 12628 13288 "node_modules/unbox-primitive": { ··· 13099 13759 "engines": { 13100 13760 "node": ">=0.10.0" 13101 13761 } 13762 + }, 13763 + "node_modules/wordwrap": { 13764 + "version": "1.0.0", 13765 + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 13766 + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", 13767 + "license": "MIT" 13102 13768 }, 13103 13769 "node_modules/wrap-ansi": { 13104 13770 "version": "8.1.0",
+12 -8
app/package.json
··· 1 1 { 2 - "name": "wonderlandzor-app", 2 + "name": "ics-banking-demo-app", 3 3 "main": "expo-router/entry", 4 4 "version": "1.0.0", 5 5 "scripts": { 6 6 "start": "expo start", 7 - "reset-project": "node ./scripts/reset-project.js", 8 7 "android": "expo start --android", 9 8 "ios": "expo start --ios", 10 9 "web": "expo start --web", 11 - "lint": "expo lint" 10 + "lint": "expo lint", 11 + "generate": "npx openapi-ts" 12 12 }, 13 13 "dependencies": { 14 + "@expo/metro-runtime": "~6.1.2", 14 15 "@expo/vector-icons": "^15.0.2", 15 - "@expo/metro-runtime": "~6.1.2", 16 + "@hey-api/openapi-ts": "^0.84.3", 16 17 "@react-navigation/bottom-tabs": "^7.4.0", 17 18 "@react-navigation/elements": "^2.6.3", 18 19 "@react-navigation/native": "^7.1.8", 20 + "@tanstack/react-query": "^5.90.2", 19 21 "expo": "~54.0.10", 20 22 "expo-constants": "~18.0.9", 21 23 "expo-font": "~14.0.8", ··· 23 25 "expo-image": "~3.0.8", 24 26 "expo-linking": "~8.0.8", 25 27 "expo-router": "~6.0.8", 28 + "expo-secure-store": "~15.0.7", 26 29 "expo-splash-screen": "~31.0.10", 27 30 "expo-status-bar": "~3.0.8", 28 31 "expo-symbols": "~1.0.7", ··· 32 35 "react-dom": "19.1.0", 33 36 "react-native": "0.81.4", 34 37 "react-native-gesture-handler": "~2.28.0", 35 - "react-native-worklets": "0.5.1", 36 38 "react-native-reanimated": "~4.1.1", 37 39 "react-native-safe-area-context": "~5.6.0", 38 40 "react-native-screens": "~4.16.0", 39 - "react-native-web": "~0.21.0" 41 + "react-native-web": "~0.21.0", 42 + "react-native-worklets": "0.5.1", 43 + "zod": "^3.25.76" 40 44 }, 41 45 "devDependencies": { 42 46 "@types/react": "~19.1.0", 43 - "typescript": "~5.9.2", 44 47 "eslint": "^9.25.0", 45 - "eslint-config-expo": "~10.0.0" 48 + "eslint-config-expo": "~10.0.0", 49 + "typescript": "~5.9.2" 46 50 }, 47 51 "private": true 48 52 }
+20
app/providers/auth-provider.tsx
··· 1 + import { postLogin } from "@/generated"; 2 + import { useQuery, } from "@tanstack/react-query"; 3 + import { createContext, PropsWithChildren } from "react" 4 + 5 + const AuthContext = createContext(null); 6 + 7 + type AuthProviderProps = PropsWithChildren 8 + 9 + export const AuthProvider = ({ children }: AuthProviderProps) => { 10 + 11 + const { data } = useQuery({ 12 + queryKey: ['auth'], 13 + queryFn: () => postLogin({ body: { username: 'test@test.test', password: 'test@123' } }), 14 + staleTime: 1000 * 60 * 5, // 5 minutes 15 + }) 16 + 17 + console.log(data) 18 + 19 + return <AuthContext.Provider value={null}>{children}</AuthContext.Provider> 20 + }
+50
app/providers/query-client-provider.tsx
··· 1 + import { PropsWithChildren, useEffect } from "react"; 2 + import { useToken } from "./token-provider"; 3 + import { 4 + QueryClient, 5 + QueryClientProvider as RQProvider, 6 + } from "@tanstack/react-query"; 7 + import { client } from "@/generated/client.gen"; 8 + import { TokenPair } from "@/generated"; 9 + import { refreshToken } from "@/services/token-service"; 10 + 11 + type Interceptor = Parameters<typeof client.interceptors.request.use>[0]; 12 + type Request = Parameters<Interceptor>[0]; 13 + 14 + const queryClient = new QueryClient(); 15 + 16 + type MiddlewareParams = { 17 + token: TokenPair; 18 + onRefresh: (token: TokenPair | null) => void; 19 + }; 20 + 21 + const createMiddleware = 22 + ({ token, onRefresh }: MiddlewareParams) => 23 + async (request: Request) => { 24 + const freshToken = await refreshToken({ token, onRefresh }); 25 + 26 + if (freshToken) { 27 + // TODO: it would be nice to have an AbortController here 28 + request.headers.set("Authorization", `Bearer ${freshToken.accessToken}`); 29 + } 30 + 31 + return request; 32 + }; 33 + 34 + export const QueryClientProvider = ({ children }: PropsWithChildren) => { 35 + const { token, setToken } = useToken(); 36 + 37 + useEffect(() => { 38 + if (!token) { 39 + return; 40 + } 41 + 42 + const id = client.interceptors.request.use( 43 + createMiddleware({ token, onRefresh: setToken }), 44 + ); 45 + 46 + return () => client.interceptors.request.eject(id); 47 + }); 48 + 49 + return <RQProvider client={queryClient}>{children}</RQProvider>; 50 + };
+67
app/providers/token-provider.tsx
··· 1 + import { 2 + createContext, 3 + PropsWithChildren, 4 + useCallback, 5 + useContext, 6 + useEffect, 7 + useState, 8 + } from "react"; 9 + import * as SecureStore from "expo-secure-store"; 10 + import { z } from "zod"; 11 + import { refreshToken } from "@/services/token-service"; 12 + 13 + const TokenContext = createContext<{ 14 + token: TokenPair | null; 15 + setToken: (token: TokenPair | null) => Promise<void>; 16 + }>({ 17 + token: null, 18 + setToken: () => Promise.reject("TokenProvider not initialized yet"), 19 + }); 20 + 21 + type TokenProviderProps = PropsWithChildren; 22 + 23 + const tokenSymbol = "ics-app-server-token"; 24 + 25 + const tokenSchema = z.object({ 26 + accessToken: z.string(), 27 + refreshToken: z.string(), 28 + expires: z.coerce.date(), 29 + }); 30 + 31 + type TokenPair = z.infer<typeof tokenSchema>; 32 + 33 + export const useToken = () => useContext(TokenContext); 34 + 35 + export const TokenProvider = ({ children }: TokenProviderProps) => { 36 + const [ready, setReady] = useState(false); 37 + const [token, setTokenState] = useState<TokenPair | null>(null); 38 + 39 + const setToken = useCallback( 40 + async (t: TokenPair | null) => { 41 + await (t 42 + ? SecureStore.setItemAsync(tokenSymbol, JSON.stringify(t)) 43 + : SecureStore.deleteItemAsync(tokenSymbol)); 44 + setTokenState(t); 45 + }, 46 + [setTokenState], 47 + ); 48 + 49 + useEffect(() => { 50 + (async () => { 51 + const t = await SecureStore.getItemAsync(tokenSymbol); 52 + const state = t ? tokenSchema.parse(JSON.parse(t)) : null; 53 + const fresh = state 54 + ? await refreshToken({ token: state, onRefresh: setToken }) 55 + : null; 56 + 57 + setTokenState(fresh); 58 + setReady(true); 59 + })(); 60 + }, [setToken]); 61 + 62 + return ready ? ( 63 + <TokenContext.Provider value={{ token, setToken }}> 64 + {children} 65 + </TokenContext.Provider> 66 + ) : null; 67 + };
-112
app/scripts/reset-project.js
··· 1 - #!/usr/bin/env node 2 - 3 - /** 4 - * This script is used to reset the project to a blank state. 5 - * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. 6 - * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 - */ 8 - 9 - const fs = require("fs"); 10 - const path = require("path"); 11 - const readline = require("readline"); 12 - 13 - const root = process.cwd(); 14 - const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; 15 - const exampleDir = "app-example"; 16 - const newAppDir = "app"; 17 - const exampleDirPath = path.join(root, exampleDir); 18 - 19 - const indexContent = `import { Text, View } from "react-native"; 20 - 21 - export default function Index() { 22 - return ( 23 - <View 24 - style={{ 25 - flex: 1, 26 - justifyContent: "center", 27 - alignItems: "center", 28 - }} 29 - > 30 - <Text>Edit app/index.tsx to edit this screen.</Text> 31 - </View> 32 - ); 33 - } 34 - `; 35 - 36 - const layoutContent = `import { Stack } from "expo-router"; 37 - 38 - export default function RootLayout() { 39 - return <Stack />; 40 - } 41 - `; 42 - 43 - const rl = readline.createInterface({ 44 - input: process.stdin, 45 - output: process.stdout, 46 - }); 47 - 48 - const moveDirectories = async (userInput) => { 49 - try { 50 - if (userInput === "y") { 51 - // Create the app-example directory 52 - await fs.promises.mkdir(exampleDirPath, { recursive: true }); 53 - console.log(`📁 /${exampleDir} directory created.`); 54 - } 55 - 56 - // Move old directories to new app-example directory or delete them 57 - for (const dir of oldDirs) { 58 - const oldDirPath = path.join(root, dir); 59 - if (fs.existsSync(oldDirPath)) { 60 - if (userInput === "y") { 61 - const newDirPath = path.join(root, exampleDir, dir); 62 - await fs.promises.rename(oldDirPath, newDirPath); 63 - console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); 64 - } else { 65 - await fs.promises.rm(oldDirPath, { recursive: true, force: true }); 66 - console.log(`❌ /${dir} deleted.`); 67 - } 68 - } else { 69 - console.log(`➡️ /${dir} does not exist, skipping.`); 70 - } 71 - } 72 - 73 - // Create new /app directory 74 - const newAppDirPath = path.join(root, newAppDir); 75 - await fs.promises.mkdir(newAppDirPath, { recursive: true }); 76 - console.log("\n📁 New /app directory created."); 77 - 78 - // Create index.tsx 79 - const indexPath = path.join(newAppDirPath, "index.tsx"); 80 - await fs.promises.writeFile(indexPath, indexContent); 81 - console.log("📄 app/index.tsx created."); 82 - 83 - // Create _layout.tsx 84 - const layoutPath = path.join(newAppDirPath, "_layout.tsx"); 85 - await fs.promises.writeFile(layoutPath, layoutContent); 86 - console.log("📄 app/_layout.tsx created."); 87 - 88 - console.log("\n✅ Project reset complete. Next steps:"); 89 - console.log( 90 - `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ 91 - userInput === "y" 92 - ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` 93 - : "" 94 - }` 95 - ); 96 - } catch (error) { 97 - console.error(`❌ Error during script execution: ${error.message}`); 98 - } 99 - }; 100 - 101 - rl.question( 102 - "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", 103 - (answer) => { 104 - const userInput = answer.trim().toLowerCase() || "y"; 105 - if (userInput === "y" || userInput === "n") { 106 - moveDirectories(userInput).finally(() => rl.close()); 107 - } else { 108 - console.log("❌ Invalid input. Please enter 'Y' or 'N'."); 109 - rl.close(); 110 - } 111 - } 112 - );
+17
app/services/blur-accountnumber.ts
··· 1 + interface BlurAccountNumberOptions { 2 + replacement?: string; 3 + digits?: number; 4 + } 5 + 6 + export const blurAccountNumber = ( 7 + value: string, 8 + { replacement = "*", digits = 4 }: BlurAccountNumberOptions = {}, 9 + ) => { 10 + if (value.length <= digits) { 11 + return value.replaceAll(/./g, replacement); 12 + } 13 + 14 + return ( 15 + value.slice(0, -digits).replaceAll(/./g, replacement) + value.slice(-digits) 16 + ); 17 + };
+25
app/services/token-service.ts
··· 1 + import { postRefreshToken, TokenPair } from "@/generated"; 2 + 3 + type MaybeToken = TokenPair | null; 4 + 5 + interface TokenServiceParams { 6 + token: TokenPair; 7 + onRefresh: (token: MaybeToken) => void | Promise<void>; 8 + } 9 + 10 + export const refreshToken = async ({ 11 + token, 12 + onRefresh, 13 + }: TokenServiceParams): Promise<MaybeToken> => { 14 + const { expires, refreshToken } = token; 15 + 16 + if (expires >= new Date()) { 17 + return token; 18 + } 19 + 20 + const refreshed = await postRefreshToken({ body: { refreshToken } }); 21 + const newToken = refreshed?.data ?? null; 22 + onRefresh(newToken); 23 + 24 + return newToken; 25 + };
+45 -4
server/src/openapi.yaml
··· 56 56 57 57 TokenPair: 58 58 type: object 59 - required: [token, refreshToken] 59 + required: [accessToken, refreshToken, expires] 60 60 properties: 61 - token: 61 + accessToken: 62 62 type: string 63 63 description: Access JWT (short-lived). 64 64 refreshToken: ··· 79 79 80 80 Account: 81 81 type: object 82 - required: [id, user_id, name, balance] 82 + required: [id, user_id, name, iban, balance] 83 83 properties: 84 84 id: { type: integer } 85 85 user_id: { type: integer } 86 + iban: { type: string } 86 87 name: { type: string } 87 88 balance: 88 89 type: number ··· 135 136 description: ISO 8601 timestamp. 136 137 example: "2024-06-01T10:00:00Z" 137 138 139 + User: 140 + type: object 141 + required: [id, username, fullname, created] 142 + properties: 143 + id: 144 + type: integer 145 + description: Unique user ID 146 + example: 2 147 + username: 148 + type: string 149 + description: login/username of the user 150 + example: nmokkenstorm 151 + fullname: 152 + type: string 153 + description: government name of the user 154 + example: Niels Mokkenstorm 155 + created: 156 + type: string 157 + format: date-time 158 + description: ISO 8601 timestamp. 159 + example: "2024-06-01T10:00:00Z" 160 + 138 161 Error: 139 162 type: object 140 163 properties: ··· 174 197 application/yaml: 175 198 schema: 176 199 type: string 177 - 200 + /me: 201 + get: 202 + summary: Retrieves information about the authenticated user 203 + tags: [Auth] 204 + security: 205 + - BearerAuth: [] 206 + responses: 207 + "200": 208 + description: User object 209 + content: 210 + application/json: 211 + schema: 212 + $ref: "#/components/schemas/User" 213 + "401": 214 + $ref: "#/components/responses/Error401" 215 + "403": 216 + $ref: "#/components/responses/Error403" 217 + "500": 218 + $ref: "#/components/responses/Error500" 178 219 /login: 179 220 post: 180 221 summary: Login with username and password
+2
server/src/schema.ts
··· 9 9 export type User = { 10 10 id: number; 11 11 username: string; 12 + fullname: string; 12 13 password: string; 14 + created: Date; 13 15 }; 14 16 15 17 export const LoginSchema = z.object({
+14 -13
server/src/seeder.ts
··· 15 15 export const testUser = { 16 16 username: "test@test.test", 17 17 password: "password@123", 18 + fullname: "Test User", 18 19 }; 19 20 20 21 export const seed = async (db: Database): Promise<void> => { ··· 24 25 ` 25 26 id INTEGER PRIMARY KEY AUTOINCREMENT, 26 27 username TEXT UNIQUE, 27 - password TEXT 28 + password TEXT, 29 + fullname TEXT, 30 + created DATETIME DEFAULT CURRENT_TIMESTAMP 28 31 `, 29 32 ); 30 33 ··· 34 37 ` 35 38 id INTEGER PRIMARY KEY AUTOINCREMENT, 36 39 user_id INTEGER, 37 - name TEXT 40 + name TEXT, 41 + iban TEXT UNIQUE 38 42 `, 39 43 ); 40 44 ··· 67 71 const hash = hashSync(testUser.password, 10); 68 72 69 73 const { lastID: userId } = await db.run( 70 - "INSERT INTO users (username, password) VALUES (?, ?);", 71 - [testUser.username, hash], 74 + "INSERT INTO users (username, password, fullname) VALUES (?, ?, ?);", 75 + [testUser.username, hash, testUser.fullname], 72 76 ); 73 77 74 78 // Seed accounts 75 79 const accounts = range(5) 76 - .map(() => `(${userId}, '${faker.finance.accountName()}')`) 80 + .map( 81 + () => 82 + `(${userId}, '${faker.finance.accountName()} ', '${faker.finance.iban()}')`, 83 + ) 77 84 .join(", "); 78 85 79 - const result = await db.run( 80 - `INSERT INTO accounts (user_id, name) VALUES ${accounts}`, 81 - ); 82 - 83 - console.log( 84 - `Seeded user with id ${userId}, ${result.changes} accounts created.`, 85 - ); 86 + await db.run(`INSERT INTO accounts (user_id, name, iban) VALUES ${accounts}`); 86 87 87 88 // Seed cards 88 89 const cards = range(5).map(() => [ ··· 108 109 const transactions = range(100).map(() => [ 109 110 userId, 110 111 accountIds[Math.floor(Math.random() * accountIds.length)], 111 - faker.finance.amount({ min: -25, max: 100, dec: 2 }), 112 + faker.finance.amount({ min: -1500, max: 2500, dec: 2 }), 112 113 faker.finance.transactionType(), 113 114 faker.commerce.productName(), 114 115 faker.date.recent({ days: 30 }).toISOString(),
+42 -9
server/src/server.ts
··· 11 11 JWT_SECRET, 12 12 TOKEN_EXPIRY_MINUTES, 13 13 } from "./config"; 14 - import { LoginRequest } from "../generated"; 14 + import { LoginRequest, TokenPair, User as UserResponse } from "../generated"; 15 15 import { readFileSync } from "fs"; 16 16 import { generateRefreshToken, generateToken, verifyToken } from "./auth"; 17 17 import { User, Request, TransactionsQuerySchema, LoginSchema } from "./schema"; ··· 80 80 return; 81 81 } 82 82 83 - res.status(500).json({ error: "Internal server error" }).send(); 83 + res 84 + .status(500) 85 + .json({ error: `Internal server error: ${err.message}` }) 86 + .send(); 84 87 }); 85 88 86 89 app.post("/login", async ({ body }: Request<LoginRequest>, res: Response) => { ··· 95 98 } 96 99 97 100 const now = new Date(); 98 - const token = generateToken(user); 99 - const refreshToken = generateRefreshToken(user); 100 101 101 102 const expires = new Date(now.getTime() + TOKEN_EXPIRY_MINUTES * 60 * 1000); 102 103 103 - res.json({ token, refreshToken, expires }); 104 + const response: TokenPair = { 105 + expires: expires.toISOString(), 106 + accessToken: generateToken(user), 107 + refreshToken: generateRefreshToken(user), 108 + }; 109 + 110 + res.json(response); 104 111 }); 105 112 106 113 app.post("/refresh-token", async ({ body }: Request, res: Response) => { ··· 113 120 try { 114 121 const user = await verifyToken(refreshToken, JWT_REFRESH_SECRET); 115 122 116 - const token = generateToken(user); 117 - const newRefreshRoken = generateRefreshToken(user); 123 + const now = new Date(); 124 + const expires = new Date( 125 + now.getTime() + TOKEN_EXPIRY_MINUTES * 60 * 1000, 126 + ); 127 + 128 + const response: TokenPair = { 129 + expires: expires.toISOString(), 130 + accessToken: generateToken(user), 131 + refreshToken: generateRefreshToken(user), 132 + }; 118 133 119 - res.json({ token, refreshToken: newRefreshRoken }); 134 + res.json(response); 120 135 } catch (err) { 121 - return res.status(401).json({ message: "Invalid refresh token" }).send(); 136 + return res 137 + .status(401) 138 + .json({ message: `Invalid refresh token: ${err.message}` }) 139 + .send(); 122 140 } 123 141 }); 124 142 125 143 // Protected endpoints 144 + app.get("/me", authenticateToken, async (req: Request, res: Response) => { 145 + const user = await db.get<User>("SELECT * FROM users WHERE id = ?", [ 146 + req.user.id, 147 + ]); 148 + 149 + const response: UserResponse = { 150 + id: user.id, 151 + username: user.username, 152 + fullname: user.fullname, 153 + created: new Date(user.created).toISOString(), 154 + }; 155 + 156 + res.json(response); 157 + }); 158 + 126 159 app.get( 127 160 "/accounts", 128 161 authenticateToken,
+9 -9
server/tests/api.spec.ts
··· 21 21 22 22 await request(app) 23 23 .get("/accounts") 24 - .set("Authorization", `Bearer ${auth.body.token}`) 24 + .set("Authorization", `Bearer ${auth.body.accessToken}`) 25 25 .expect(200) 26 26 .expect(({ body }) => expect(body).not.toHaveLength(0)); 27 27 }); ··· 37 37 38 38 await request(app) 39 39 .get("/transactions") 40 - .set("Authorization", `Bearer ${auth.body.token}`) 40 + .set("Authorization", `Bearer ${auth.body.accessToken}`) 41 41 .expect(200); 42 42 }); 43 43 ··· 57 57 58 58 const account = await request(app) 59 59 .get("/accounts") 60 - .set("Authorization", `Bearer ${auth.body.token}`) 60 + .set("Authorization", `Bearer ${auth.body.accessToken}`) 61 61 .expect(200) 62 62 .expect(({ body }) => expect(body).not.toHaveLength(0)); 63 63 64 64 await request(app) 65 65 .get(`/transactions?accountId=${account.body[0].id}`) 66 - .set("Authorization", `Bearer ${auth.body.token}`) 66 + .set("Authorization", `Bearer ${auth.body.accessToken}`) 67 67 .expect(200) 68 68 .expect(({ body }) => expect(body).not.toHaveLength(0)) 69 69 .expect(({ body }: { body: Transaction[] }) => { ··· 78 78 79 79 const transactionType = await request(app) 80 80 .get("/transaction-types") 81 - .set("Authorization", `Bearer ${auth.body.token}`) 81 + .set("Authorization", `Bearer ${auth.body.accessToken}`) 82 82 .expect(200) 83 83 .expect(({ body }) => expect(body).not.toHaveLength(0)); 84 84 85 85 await request(app) 86 86 .get(`/transactions?type=${transactionType.body[0].name}`) 87 - .set("Authorization", `Bearer ${auth.body.token}`) 87 + .set("Authorization", `Bearer ${auth.body.accessToken}`) 88 88 .expect(200) 89 89 .expect(({ body }) => expect(body).not.toHaveLength(0)) 90 - .expect(({ body }: { body: Transaction[] }) => { 91 - body.forEach((transaction) => { 92 - expect(transaction.type).toBe(transactionType.body[0].name); 90 + .expect(({ body }) => { 91 + body.forEach(({ type }: Transaction) => { 92 + expect(type).toBe(transactionType.body[0].name); 93 93 }); 94 94 }); 95 95 });
+35
server/tests/auth.spec.ts
··· 68 68 .expect(401) 69 69 .expect("Content-Type", /json/); 70 70 }); 71 + 72 + it("rejects invalid login", async () => { 73 + await request(app) 74 + .post("/login") 75 + .send({ username: "test@test.be", password: "wrong-password" }) 76 + .set("Accept", "application/json") 77 + .expect(401) 78 + .expect("Content-Type", /json/); 79 + }); 80 + }); 81 + 82 + describe("GET /me", () => { 83 + it("can grab the current user when logged in", async () => { 84 + const authResponse = await request(app) 85 + .post("/login") 86 + .send(testUser) 87 + .set("Accept", "application/json") 88 + .expect(200) 89 + .expect("Content-Type", /json/); 90 + 91 + await request(app) 92 + .get("/me") 93 + .set("Authorization", `Bearer ${authResponse.body.accessToken}`) 94 + .set("Accept", "application/json") 95 + .expect(200) 96 + .expect("Content-Type", /json/); 97 + }); 98 + 99 + it("fails to fetch the current user when not logged in", async () => { 100 + await request(app) 101 + .get("/me") 102 + .set("Accept", "application/json") 103 + .expect(401) 104 + .expect("Content-Type", /json/); 105 + }); 71 106 });