diff --git a/app/.expo/README.md b/app/.expo/README.md deleted file mode 100644 index ce8c4b6..0000000 --- a/app/.expo/README.md +++ /dev/null @@ -1,13 +0,0 @@ -> Why do I have a folder named ".expo" in my project? - -The ".expo" folder is created when an Expo project is started using "expo start" command. - -> What do the files contain? - -- "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. -- "settings.json": contains the server configuration that is used to serve the application manifest. - -> Should I commit the ".expo" folder? - -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. -Upon project creation, the ".expo" folder is already added to your ".gitignore" file. diff --git a/app/.expo/devices.json b/app/.expo/devices.json deleted file mode 100644 index 5efff6c..0000000 --- a/app/.expo/devices.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "devices": [] -} diff --git a/app/.expo/types/router.d.ts b/app/.expo/types/router.d.ts deleted file mode 100644 index 5979891..0000000 --- a/app/.expo/types/router.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable */ -import * as Router from 'expo-router'; - -export * from 'expo-router'; - -declare module 'expo-router' { - export namespace ExpoRouter { - export interface __routes { - 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; }; - 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; }; - 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; }; - } - } -} diff --git a/app/.gitignore b/app/.gitignore index 5873d9a..a7ef828 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -3,4 +3,7 @@ # The following patterns were generated by expo-cli expo-env.d.ts -# @end expo-cli \ No newline at end of file +# @end expo-cli + +.expo/ +node_modules/ diff --git a/app/app.json b/app/app.json index 8f6f3f3..2522ad7 100644 --- a/app/app.json +++ b/app/app.json @@ -1,11 +1,11 @@ { "expo": { - "name": "wonderlandzor-app", - "slug": "wonderlandzor-app", - "version": "1.0.0", + "name": "ics-banking-demo-app", + "slug": "ics-banking-demo-app", + "version": "0.0.1", "orientation": "portrait", "icon": "./assets/images/icon.png", - "scheme": "wonderlandzorapp", + "scheme": "icsbankingdemoapp", "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { @@ -38,7 +38,8 @@ "backgroundColor": "#000000" } } - ] + ], + "expo-secure-store" ], "experiments": { "typedRoutes": true, diff --git a/app/app/(auth)/_layout.tsx b/app/app/(auth)/_layout.tsx new file mode 100644 index 0000000..92c12d3 --- /dev/null +++ b/app/app/(auth)/_layout.tsx @@ -0,0 +1,13 @@ +import { Stack } from "expo-router"; +import React from "react"; + +export default function Layout() { + return ( + + + + ); +} diff --git a/app/app/(auth)/index.tsx b/app/app/(auth)/index.tsx new file mode 100644 index 0000000..3fef06e --- /dev/null +++ b/app/app/(auth)/index.tsx @@ -0,0 +1,32 @@ +import { StyleSheet } from "react-native"; + +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { useLogin } from "@/hooks/use-login"; +import { Button } from "@react-navigation/elements"; + +export default function HomeScreen() { + const { login } = useLogin(); + + return ( + + Welcome! + + + ); +} + +const styles = StyleSheet.create({ + titleContainer: { + paddingTop: 60, + flexDirection: "row", + alignItems: "center", + gap: 8, + }, +}); diff --git a/app/app/(auth)/settings.tsx b/app/app/(auth)/settings.tsx new file mode 100644 index 0000000..ed15708 --- /dev/null +++ b/app/app/(auth)/settings.tsx @@ -0,0 +1,20 @@ +import { StyleSheet } from "react-native"; + +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; + +export default function SettingsScreen() { + return ( + + Settings + + ); +} + +const styles = StyleSheet.create({ + titleContainer: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, +}); diff --git a/app/app/(tabs)/_layout.tsx b/app/app/(tabs)/_layout.tsx index 54e11d0..a04b196 100644 --- a/app/app/(tabs)/_layout.tsx +++ b/app/app/(tabs)/_layout.tsx @@ -5,10 +5,13 @@ import { HapticTab } from '@/components/haptic-tab'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors } from '@/constants/theme'; import { useColorScheme } from '@/hooks/use-color-scheme'; +import { useAuthentication } from '@/hooks/use-authentication'; export default function TabLayout() { const colorScheme = useColorScheme(); + useAuthentication(); + return ( , }} /> + , + }} + /> ); } diff --git a/app/app/(tabs)/account.tsx b/app/app/(tabs)/account.tsx new file mode 100644 index 0000000..c09a4a4 --- /dev/null +++ b/app/app/(tabs)/account.tsx @@ -0,0 +1,51 @@ +import { Image } from "expo-image"; +import { StyleSheet } from "react-native"; +import ParallaxScrollView from "@/components/parallax-scroll-view"; +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { Button } from "@react-navigation/elements"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { useLogout } from "@/hooks/use-logout"; + +export default function ProfileScreen() { + const { logout } = useLogout(); + + const { user } = useCurrentUser(); + + return ( + + } + > + + {`Welcome ${user?.fullname}`} + + + {`username: ${user?.username}`} + + + + + + ); +} + +const styles = StyleSheet.create({ + titleContainer: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + reactLogo: { + height: 178, + width: 290, + bottom: 0, + left: 0, + position: "absolute", + }, +}); diff --git a/app/app/(tabs)/index.tsx b/app/app/(tabs)/index.tsx index 786b736..d374ae1 100644 --- a/app/app/(tabs)/index.tsx +++ b/app/app/(tabs)/index.tsx @@ -1,78 +1,52 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; +import { Image } from "expo-image"; +import { StyleSheet, TouchableOpacity } from "react-native"; -import { HelloWave } from '@/components/hello-wave'; -import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { Link } from 'expo-router'; +import ParallaxScrollView from "@/components/parallax-scroll-view"; +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { useQuery } from "@tanstack/react-query"; +import { getAccountsOptions } from "@/generated/@tanstack/react-query.gen"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { Price } from "@/components/ui/price"; +import { AccountNumber } from "@/components/ui/accountnumber"; export default function HomeScreen() { + const { data: accountData } = useQuery(getAccountsOptions()); + + const { user } = useCurrentUser(); + return ( - }> + } + > - Welcome! - + Welcome back, {user?.fullname}! - Step 1: Try it - - Edit app/(tabs)/index.tsx to see changes. - Press{' '} - - {Platform.select({ - ios: 'cmd + d', - android: 'cmd + m', - web: 'F12', - })} - {' '} - to open developer tools. - - - - - - Step 2: Explore - - - - alert('Action pressed')} /> - alert('Share pressed')} - /> - - alert('Delete pressed')} - /> - - - - - - {`Tap the Explore tab to learn more about what's included in this starter app.`} - - - - Step 3: Get a fresh start - - {`When you're ready, run `} - npm run reset-project to get a fresh{' '} - app directory. This will move the current{' '} - app to{' '} - app-example. - + {accountData?.map(({ id, name, balance, iban }) => ( + + + + {name} + + + + {balance} + +20% + + + + + + + ))} ); @@ -80,8 +54,8 @@ export default function HomeScreen() { const styles = StyleSheet.create({ titleContainer: { - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", gap: 8, }, stepContainer: { @@ -93,6 +67,18 @@ const styles = StyleSheet.create({ width: 290, bottom: 0, left: 0, - position: 'absolute', + position: "absolute", + }, + accountContainer: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + paddingVertical: 4, + gap: 4, + }, + accountDetail: { + flexDirection: "column", + justifyContent: "center", + paddingVertical: 2, }, }); diff --git a/app/app/_layout.tsx b/app/app/_layout.tsx index f518c9b..a168088 100644 --- a/app/app/_layout.tsx +++ b/app/app/_layout.tsx @@ -1,24 +1,38 @@ -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import 'react-native-reanimated'; +import { + DarkTheme, + DefaultTheme, + ThemeProvider, +} from "@react-navigation/native"; +import { Stack } from "expo-router"; +import { StatusBar } from "expo-status-bar"; +import "react-native-reanimated"; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { useColorScheme } from "@/hooks/use-color-scheme"; +import { TokenProvider } from "@/providers/token-provider"; +import { QueryClientProvider } from "@/providers/query-client-provider"; export const unstable_settings = { - anchor: '(tabs)', + anchor: "(tabs)", }; export default function RootLayout() { const colorScheme = useColorScheme(); return ( - - - - - - - + + + + + + + + + + ); } diff --git a/app/app/modal.tsx b/app/app/modal.tsx deleted file mode 100644 index 6dfbc1a..0000000 --- a/app/app/modal.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Link } from 'expo-router'; -import { StyleSheet } from 'react-native'; - -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; - -export default function ModalScreen() { - return ( - - This is a modal - - Go to home screen - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: 20, - }, - link: { - marginTop: 15, - paddingVertical: 15, - }, -}); diff --git a/app/components/external-link.tsx b/app/components/external-link.tsx index 883e515..2913a8c 100644 --- a/app/components/external-link.tsx +++ b/app/components/external-link.tsx @@ -1,8 +1,13 @@ -import { Href, Link } from 'expo-router'; -import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser'; -import { type ComponentProps } from 'react'; +import { Href, Link } from "expo-router"; +import { + openBrowserAsync, + WebBrowserPresentationStyle, +} from "expo-web-browser"; +import { type ComponentProps } from "react"; -type Props = Omit, 'href'> & { href: Href & string }; +type Props = Omit, "href"> & { + href: Href & string; +}; export function ExternalLink({ href, ...rest }: Props) { return ( @@ -11,14 +16,16 @@ export function ExternalLink({ href, ...rest }: Props) { {...rest} href={href} onPress={async (event) => { - if (process.env.EXPO_OS !== 'web') { - // Prevent the default behavior of linking to the default browser on native. - event.preventDefault(); - // Open the link in an in-app browser. - await openBrowserAsync(href, { - presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, - }); + if (process.env.EXPO_OS !== "web") { + return; } + + // Prevent the default behavior of linking to the default browser on native. + event.preventDefault(); + // Open the link in an in-app browser. + await openBrowserAsync(href, { + presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, + }); }} /> ); diff --git a/app/components/hello-wave.tsx b/app/components/hello-wave.tsx deleted file mode 100644 index 5def547..0000000 --- a/app/components/hello-wave.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Animated from 'react-native-reanimated'; - -export function HelloWave() { - return ( - - šŸ‘‹ - - ); -} diff --git a/app/components/ui/accountnumber.tsx b/app/components/ui/accountnumber.tsx new file mode 100644 index 0000000..24b2872 --- /dev/null +++ b/app/components/ui/accountnumber.tsx @@ -0,0 +1,38 @@ +import { TouchableOpacity } from "react-native"; +import { ThemedText } from "../themed-text"; +import { useToggle } from "@/hooks/use-toggle"; +import { blurAccountNumber } from "@/services/blur-accountnumber"; +import { PropsWithChildren } from "react"; +import { ThemedView } from "../themed-view"; + +interface AccountNumberProps { + children: string; + hidden?: boolean; +} + +export const AccountNumber = ({ + children, + hidden = false, +}: AccountNumberProps) => { + const { isHidden, toggleHidden } = useToggle(true); + + return ( + + + {isHidden ? blurAccountNumber(children.toString()) : children} + + + ); +}; + +type WrapperProps = PropsWithChildren<{ + toggleable: boolean; + onPress: () => void | Promise; +}>; + +const Wrapper = ({ children, toggleable, onPress }: WrapperProps) => + toggleable ? ( + {children} + ) : ( + {children} + ); diff --git a/app/components/ui/price.tsx b/app/components/ui/price.tsx new file mode 100644 index 0000000..acf15cc --- /dev/null +++ b/app/components/ui/price.tsx @@ -0,0 +1,13 @@ +import { ThemedText } from "../themed-text"; + +interface PriceProps { + children: number; + currency?: string; +} + +export const Price = ({ children, currency = "€" }: PriceProps) => ( + + {currency} + {children.toFixed(2)} + +); diff --git a/app/constants/theme.ts b/app/constants/theme.ts index f06facd..4574f32 100644 --- a/app/constants/theme.ts +++ b/app/constants/theme.ts @@ -1,53 +1,45 @@ -/** - * Below are the colors that are used in the app. The colors are defined in the light and dark mode. - * 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. - */ +import { Platform } from "react-native"; -import { Platform } from 'react-native'; - -const tintColorLight = '#0a7ea4'; -const tintColorDark = '#fff'; +const tintColorLight = "#0a7ea4"; +const tintColorDark = "#fff"; export const Colors = { light: { - text: '#11181C', - background: '#fff', + text: "#11181C", + background: "#fff", tint: tintColorLight, - icon: '#687076', - tabIconDefault: '#687076', + icon: "#687076", + tabIconDefault: "#687076", tabIconSelected: tintColorLight, }, dark: { - text: '#ECEDEE', - background: '#151718', + text: "#ECEDEE", + background: "#151718", tint: tintColorDark, - icon: '#9BA1A6', - tabIconDefault: '#9BA1A6', + icon: "#9BA1A6", + tabIconDefault: "#9BA1A6", tabIconSelected: tintColorDark, }, }; export const Fonts = Platform.select({ ios: { - /** iOS `UIFontDescriptorSystemDesignDefault` */ - sans: 'system-ui', - /** iOS `UIFontDescriptorSystemDesignSerif` */ - serif: 'ui-serif', - /** iOS `UIFontDescriptorSystemDesignRounded` */ - rounded: 'ui-rounded', - /** iOS `UIFontDescriptorSystemDesignMonospaced` */ - mono: 'ui-monospace', + sans: "system-ui", + serif: "ui-serif", + rounded: "ui-rounded", + mono: "ui-monospace", }, default: { - sans: 'normal', - serif: 'serif', - rounded: 'normal', - mono: 'monospace', + sans: "normal", + serif: "serif", + rounded: "normal", + mono: "monospace", }, web: { sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", serif: "Georgia, 'Times New Roman', serif", - rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", + rounded: + "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", }, }); diff --git a/app/hooks/use-authentication.ts b/app/hooks/use-authentication.ts new file mode 100644 index 0000000..0a20e5a --- /dev/null +++ b/app/hooks/use-authentication.ts @@ -0,0 +1,18 @@ +import { useCallback } from "react"; +import { useFocusEffect, useRouter } from "expo-router"; +import { useToken } from "@/providers/token-provider"; + +export const useAuthentication = () => { + const { token } = useToken(); + const router = useRouter(); + + useFocusEffect( + useCallback(() => { + if (token) { + return; + } + + router.replace("/(auth)"); + }, [token, router]), + ); +}; diff --git a/app/hooks/use-current-user.ts b/app/hooks/use-current-user.ts new file mode 100644 index 0000000..dc09c17 --- /dev/null +++ b/app/hooks/use-current-user.ts @@ -0,0 +1,8 @@ +import { getMeOptions } from "@/generated/@tanstack/react-query.gen"; +import { useQuery } from "@tanstack/react-query"; + +export const useCurrentUser = () => { + const { data } = useQuery(getMeOptions()); + + return { user: data }; +}; diff --git a/app/hooks/use-login.ts b/app/hooks/use-login.ts new file mode 100644 index 0000000..b008fc0 --- /dev/null +++ b/app/hooks/use-login.ts @@ -0,0 +1,28 @@ +import { useRouter } from "expo-router"; +import { useMutation } from "@tanstack/react-query"; +import { LoginRequest, postLogin } from "@/generated"; +import { useToken } from "@/providers/token-provider"; +import { useEffect } from "react"; + +export const useLogin = () => { + const { token, setToken } = useToken(); + const router = useRouter(); + const { mutateAsync } = useMutation({ + mutationFn: (credentials: LoginRequest) => postLogin({ body: credentials }), + }); + + useEffect(() => { + if (token) { + router.replace("/(tabs)"); + } + }, [token, router]); + + return { + login: (credentials: LoginRequest) => + mutateAsync(credentials).then(({ data }) => { + if (data) { + setToken(data); + } + }), + }; +}; diff --git a/app/hooks/use-logout.ts b/app/hooks/use-logout.ts new file mode 100644 index 0000000..a9b820c --- /dev/null +++ b/app/hooks/use-logout.ts @@ -0,0 +1,13 @@ +import { useToken } from "@/providers/token-provider"; +import { useRouter } from "expo-router"; + +export const useLogout = () => { + const router = useRouter(); + const { setToken } = useToken(); + + return { + logout: () => { + setToken(null).then(() => router.replace("/(auth)")); + }, + }; +}; diff --git a/app/hooks/use-toggle.ts b/app/hooks/use-toggle.ts new file mode 100644 index 0000000..a3d5298 --- /dev/null +++ b/app/hooks/use-toggle.ts @@ -0,0 +1,11 @@ +import { useCallback, useState } from "react"; + +export const useToggle = (initialValue: boolean = false) => { + const [isHidden, setHidden] = useState(initialValue); + + const toggleHidden = useCallback(() => { + setHidden((v) => !v); + }, []); + + return { isHidden, setHidden, toggleHidden }; +}; diff --git a/app/openapi-ts.config.ts b/app/openapi-ts.config.ts new file mode 100644 index 0000000..bff2f7b --- /dev/null +++ b/app/openapi-ts.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "@hey-api/openapi-ts"; + +export default defineConfig({ + input: "http://localhost:3001/openapi.yaml", + output: "generated", + plugins: [ + "@tanstack/react-query", + { name: "@hey-api/transformers", dates: true }, + ], +}); diff --git a/app/package-lock.json b/app/package-lock.json index 9000f10..2a9d351 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,18 +1,20 @@ { - "name": "wonderlandzor-app", + "name": "ics-banking-demo-app", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "wonderlandzor-app", + "name": "ics-banking-demo-app", "version": "1.0.0", "dependencies": { "@expo/metro-runtime": "~6.1.2", "@expo/vector-icons": "^15.0.2", + "@hey-api/openapi-ts": "^0.84.3", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "@tanstack/react-query": "^5.90.2", "expo": "~54.0.10", "expo-constants": "~18.0.9", "expo-font": "~14.0.8", @@ -20,6 +22,7 @@ "expo-image": "~3.0.8", "expo-linking": "~8.0.8", "expo-router": "~6.0.8", + "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", @@ -33,7 +36,8 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", - "react-native-worklets": "0.5.1" + "react-native-worklets": "0.5.1", + "zod": "^3.25.76" }, "devDependencies": { "@types/react": "~19.1.0", @@ -2272,6 +2276,134 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@hey-api/codegen-core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.2.0.tgz", + "integrity": "sha512-c7VjBy/8ed0EVLNgaeS9Xxams1Tuv/WK/b4xXH3Qr4wjzYeJUtxOcoP8YdwNLavqKP8pGiuctjX2Z1Pwc4jMgQ==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.1.0.tgz", + "integrity": "sha512-+5eg9pgAAM9oSqJQuUtfTKbLz8yieFKN91myyXiLnprqFj8ROfxUKJLr9DKq/hGKyybKT1WxFSetDqCFm80pCA==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.84.3", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.84.3.tgz", + "integrity": "sha512-WFjGGBzmIzGRdHw+CC7g8TSzl4UjdWbNwiDO07a3BUEhSBZbLGidfh+E4SOGbePak6sWptVh9WqO3QrThFH25A==", + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "^0.2.0", + "@hey-api/json-schema-ref-parser": "1.1.0", + "ansi-colors": "4.1.3", + "c12": "2.0.1", + "color-support": "1.1.3", + "commander": "13.0.0", + "handlebars": "4.7.8", + "open": "10.1.2", + "semver": "7.7.2" + }, + "bin": { + "openapi-ts": "bin/index.cjs" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2624,6 +2756,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3566,6 +3704,32 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3668,7 +3832,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -4375,6 +4538,15 @@ "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "license": "MIT" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5064,6 +5236,21 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5073,6 +5260,34 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", + "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.1", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^2.3.0", + "mlly": "^1.7.1", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5214,6 +5429,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -5261,6 +5491,15 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT" }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -5404,6 +5643,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -5473,6 +5721,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -5503,6 +5757,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5734,6 +5997,34 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -5791,6 +6082,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5800,6 +6097,12 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -6848,6 +7151,15 @@ "node": ">=10" } }, + "node_modules/expo-secure-store": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.7.tgz", + "integrity": "sha512-9q7+G1Zxr5P6J5NRIlm86KulvmYwc6UnQlYPjQLDu1drDnerz6AT6l884dPu29HgtDTn4rR0heYeeGFhMKM7/Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-splash-screen": { "version": "31.0.10", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.10.tgz", @@ -7407,6 +7719,36 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7582,6 +7924,96 @@ "node": ">=6" } }, + "node_modules/giget": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz", + "integrity": "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.5.4", + "pathe": "^2.0.3", + "tar": "^6.2.1" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/giget/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/giget/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/giget/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/giget/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/giget/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/giget/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/giget/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -7707,6 +8139,36 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8281,6 +8743,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -8740,6 +9235,15 @@ "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", "license": "MIT" }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9156,6 +9660,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9704,6 +10214,24 @@ "node": ">=10" } }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9771,6 +10299,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/nested-error-stacks": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", @@ -9797,6 +10331,12 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -9860,6 +10400,32 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "license": "MIT" }, + "node_modules/nypm": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz", + "integrity": "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "tinyexec": "^0.3.2", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/ob1": { "version": "0.83.1", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.1.tgz", @@ -9994,6 +10560,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.6.tgz", + "integrity": "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -10344,6 +10916,18 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10371,6 +10955,23 @@ "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -10649,6 +11250,16 @@ "node": ">=0.10.0" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -11036,6 +11647,19 @@ } } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -11312,6 +11936,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12344,6 +12980,12 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12589,7 +13231,6 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12625,6 +13266,25 @@ "node": "*" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -13100,6 +13760,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/app/package.json b/app/package.json index 003d996..a6030c5 100644 --- a/app/package.json +++ b/app/package.json @@ -1,21 +1,23 @@ { - "name": "wonderlandzor-app", + "name": "ics-banking-demo-app", "main": "expo-router/entry", "version": "1.0.0", "scripts": { "start": "expo start", - "reset-project": "node ./scripts/reset-project.js", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", - "lint": "expo lint" + "lint": "expo lint", + "generate": "npx openapi-ts" }, "dependencies": { - "@expo/vector-icons": "^15.0.2", "@expo/metro-runtime": "~6.1.2", + "@expo/vector-icons": "^15.0.2", + "@hey-api/openapi-ts": "^0.84.3", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "@tanstack/react-query": "^5.90.2", "expo": "~54.0.10", "expo-constants": "~18.0.9", "expo-font": "~14.0.8", @@ -23,6 +25,7 @@ "expo-image": "~3.0.8", "expo-linking": "~8.0.8", "expo-router": "~6.0.8", + "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", @@ -32,17 +35,18 @@ "react-dom": "19.1.0", "react-native": "0.81.4", "react-native-gesture-handler": "~2.28.0", - "react-native-worklets": "0.5.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0" + "react-native-web": "~0.21.0", + "react-native-worklets": "0.5.1", + "zod": "^3.25.76" }, "devDependencies": { "@types/react": "~19.1.0", - "typescript": "~5.9.2", "eslint": "^9.25.0", - "eslint-config-expo": "~10.0.0" + "eslint-config-expo": "~10.0.0", + "typescript": "~5.9.2" }, "private": true } diff --git a/app/providers/auth-provider.tsx b/app/providers/auth-provider.tsx new file mode 100644 index 0000000..7262293 --- /dev/null +++ b/app/providers/auth-provider.tsx @@ -0,0 +1,20 @@ +import { postLogin } from "@/generated"; +import { useQuery, } from "@tanstack/react-query"; +import { createContext, PropsWithChildren } from "react" + +const AuthContext = createContext(null); + +type AuthProviderProps = PropsWithChildren + +export const AuthProvider = ({ children }: AuthProviderProps) => { + + const { data } = useQuery({ + queryKey: ['auth'], + queryFn: () => postLogin({ body: { username: 'test@test.test', password: 'test@123' } }), + staleTime: 1000 * 60 * 5, // 5 minutes + }) + + console.log(data) + + return {children} +} diff --git a/app/providers/query-client-provider.tsx b/app/providers/query-client-provider.tsx new file mode 100644 index 0000000..2af00de --- /dev/null +++ b/app/providers/query-client-provider.tsx @@ -0,0 +1,50 @@ +import { PropsWithChildren, useEffect } from "react"; +import { useToken } from "./token-provider"; +import { + QueryClient, + QueryClientProvider as RQProvider, +} from "@tanstack/react-query"; +import { client } from "@/generated/client.gen"; +import { TokenPair } from "@/generated"; +import { refreshToken } from "@/services/token-service"; + +type Interceptor = Parameters[0]; +type Request = Parameters[0]; + +const queryClient = new QueryClient(); + +type MiddlewareParams = { + token: TokenPair; + onRefresh: (token: TokenPair | null) => void; +}; + +const createMiddleware = + ({ token, onRefresh }: MiddlewareParams) => + async (request: Request) => { + const freshToken = await refreshToken({ token, onRefresh }); + + if (freshToken) { + // TODO: it would be nice to have an AbortController here + request.headers.set("Authorization", `Bearer ${freshToken.accessToken}`); + } + + return request; + }; + +export const QueryClientProvider = ({ children }: PropsWithChildren) => { + const { token, setToken } = useToken(); + + useEffect(() => { + if (!token) { + return; + } + + const id = client.interceptors.request.use( + createMiddleware({ token, onRefresh: setToken }), + ); + + return () => client.interceptors.request.eject(id); + }); + + return {children}; +}; diff --git a/app/providers/token-provider.tsx b/app/providers/token-provider.tsx new file mode 100644 index 0000000..41e3177 --- /dev/null +++ b/app/providers/token-provider.tsx @@ -0,0 +1,67 @@ +import { + createContext, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import * as SecureStore from "expo-secure-store"; +import { z } from "zod"; +import { refreshToken } from "@/services/token-service"; + +const TokenContext = createContext<{ + token: TokenPair | null; + setToken: (token: TokenPair | null) => Promise; +}>({ + token: null, + setToken: () => Promise.reject("TokenProvider not initialized yet"), +}); + +type TokenProviderProps = PropsWithChildren; + +const tokenSymbol = "ics-app-server-token"; + +const tokenSchema = z.object({ + accessToken: z.string(), + refreshToken: z.string(), + expires: z.coerce.date(), +}); + +type TokenPair = z.infer; + +export const useToken = () => useContext(TokenContext); + +export const TokenProvider = ({ children }: TokenProviderProps) => { + const [ready, setReady] = useState(false); + const [token, setTokenState] = useState(null); + + const setToken = useCallback( + async (t: TokenPair | null) => { + await (t + ? SecureStore.setItemAsync(tokenSymbol, JSON.stringify(t)) + : SecureStore.deleteItemAsync(tokenSymbol)); + setTokenState(t); + }, + [setTokenState], + ); + + useEffect(() => { + (async () => { + const t = await SecureStore.getItemAsync(tokenSymbol); + const state = t ? tokenSchema.parse(JSON.parse(t)) : null; + const fresh = state + ? await refreshToken({ token: state, onRefresh: setToken }) + : null; + + setTokenState(fresh); + setReady(true); + })(); + }, [setToken]); + + return ready ? ( + + {children} + + ) : null; +}; diff --git a/app/scripts/reset-project.js b/app/scripts/reset-project.js deleted file mode 100755 index 51dff15..0000000 --- a/app/scripts/reset-project.js +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env node - -/** - * This script is used to reset the project to a blank state. - * 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. - * You can remove the `reset-project` script from package.json and safely delete this file after running it. - */ - -const fs = require("fs"); -const path = require("path"); -const readline = require("readline"); - -const root = process.cwd(); -const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; -const exampleDir = "app-example"; -const newAppDir = "app"; -const exampleDirPath = path.join(root, exampleDir); - -const indexContent = `import { Text, View } from "react-native"; - -export default function Index() { - return ( - - Edit app/index.tsx to edit this screen. - - ); -} -`; - -const layoutContent = `import { Stack } from "expo-router"; - -export default function RootLayout() { - return ; -} -`; - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}); - -const moveDirectories = async (userInput) => { - try { - if (userInput === "y") { - // Create the app-example directory - await fs.promises.mkdir(exampleDirPath, { recursive: true }); - console.log(`šŸ“ /${exampleDir} directory created.`); - } - - // Move old directories to new app-example directory or delete them - for (const dir of oldDirs) { - const oldDirPath = path.join(root, dir); - if (fs.existsSync(oldDirPath)) { - if (userInput === "y") { - const newDirPath = path.join(root, exampleDir, dir); - await fs.promises.rename(oldDirPath, newDirPath); - console.log(`āž”ļø /${dir} moved to /${exampleDir}/${dir}.`); - } else { - await fs.promises.rm(oldDirPath, { recursive: true, force: true }); - console.log(`āŒ /${dir} deleted.`); - } - } else { - console.log(`āž”ļø /${dir} does not exist, skipping.`); - } - } - - // Create new /app directory - const newAppDirPath = path.join(root, newAppDir); - await fs.promises.mkdir(newAppDirPath, { recursive: true }); - console.log("\nšŸ“ New /app directory created."); - - // Create index.tsx - const indexPath = path.join(newAppDirPath, "index.tsx"); - await fs.promises.writeFile(indexPath, indexContent); - console.log("šŸ“„ app/index.tsx created."); - - // Create _layout.tsx - const layoutPath = path.join(newAppDirPath, "_layout.tsx"); - await fs.promises.writeFile(layoutPath, layoutContent); - console.log("šŸ“„ app/_layout.tsx created."); - - console.log("\nāœ… Project reset complete. Next steps:"); - console.log( - `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ - userInput === "y" - ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` - : "" - }` - ); - } catch (error) { - console.error(`āŒ Error during script execution: ${error.message}`); - } -}; - -rl.question( - "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", - (answer) => { - const userInput = answer.trim().toLowerCase() || "y"; - if (userInput === "y" || userInput === "n") { - moveDirectories(userInput).finally(() => rl.close()); - } else { - console.log("āŒ Invalid input. Please enter 'Y' or 'N'."); - rl.close(); - } - } -); diff --git a/app/services/blur-accountnumber.ts b/app/services/blur-accountnumber.ts new file mode 100644 index 0000000..a6ed1d9 --- /dev/null +++ b/app/services/blur-accountnumber.ts @@ -0,0 +1,17 @@ +interface BlurAccountNumberOptions { + replacement?: string; + digits?: number; +} + +export const blurAccountNumber = ( + value: string, + { replacement = "*", digits = 4 }: BlurAccountNumberOptions = {}, +) => { + if (value.length <= digits) { + return value.replaceAll(/./g, replacement); + } + + return ( + value.slice(0, -digits).replaceAll(/./g, replacement) + value.slice(-digits) + ); +}; diff --git a/app/services/token-service.ts b/app/services/token-service.ts new file mode 100644 index 0000000..36e9896 --- /dev/null +++ b/app/services/token-service.ts @@ -0,0 +1,25 @@ +import { postRefreshToken, TokenPair } from "@/generated"; + +type MaybeToken = TokenPair | null; + +interface TokenServiceParams { + token: TokenPair; + onRefresh: (token: MaybeToken) => void | Promise; +} + +export const refreshToken = async ({ + token, + onRefresh, +}: TokenServiceParams): Promise => { + const { expires, refreshToken } = token; + + if (expires >= new Date()) { + return token; + } + + const refreshed = await postRefreshToken({ body: { refreshToken } }); + const newToken = refreshed?.data ?? null; + onRefresh(newToken); + + return newToken; +}; diff --git a/server/src/openapi.yaml b/server/src/openapi.yaml index a88145e..acec276 100644 --- a/server/src/openapi.yaml +++ b/server/src/openapi.yaml @@ -56,9 +56,9 @@ components: TokenPair: type: object - required: [token, refreshToken] + required: [accessToken, refreshToken, expires] properties: - token: + accessToken: type: string description: Access JWT (short-lived). refreshToken: @@ -79,10 +79,11 @@ components: Account: type: object - required: [id, user_id, name, balance] + required: [id, user_id, name, iban, balance] properties: id: { type: integer } user_id: { type: integer } + iban: { type: string } name: { type: string } balance: type: number @@ -135,6 +136,28 @@ components: description: ISO 8601 timestamp. example: "2024-06-01T10:00:00Z" + User: + type: object + required: [id, username, fullname, created] + properties: + id: + type: integer + description: Unique user ID + example: 2 + username: + type: string + description: login/username of the user + example: nmokkenstorm + fullname: + type: string + description: government name of the user + example: Niels Mokkenstorm + created: + type: string + format: date-time + description: ISO 8601 timestamp. + example: "2024-06-01T10:00:00Z" + Error: type: object properties: @@ -174,7 +197,25 @@ paths: application/yaml: schema: type: string - + /me: + get: + summary: Retrieves information about the authenticated user + tags: [Auth] + security: + - BearerAuth: [] + responses: + "200": + description: User object + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "401": + $ref: "#/components/responses/Error401" + "403": + $ref: "#/components/responses/Error403" + "500": + $ref: "#/components/responses/Error500" /login: post: summary: Login with username and password diff --git a/server/src/schema.ts b/server/src/schema.ts index 79c7c2e..99d96d5 100644 --- a/server/src/schema.ts +++ b/server/src/schema.ts @@ -9,7 +9,9 @@ export type Request = {}> = export type User = { id: number; username: string; + fullname: string; password: string; + created: Date; }; export const LoginSchema = z.object({ diff --git a/server/src/seeder.ts b/server/src/seeder.ts index a83d8cc..8068cde 100644 --- a/server/src/seeder.ts +++ b/server/src/seeder.ts @@ -15,6 +15,7 @@ const createTable = async ( export const testUser = { username: "test@test.test", password: "password@123", + fullname: "Test User", }; export const seed = async (db: Database): Promise => { @@ -24,7 +25,9 @@ export const seed = async (db: Database): Promise => { ` id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, - password TEXT + password TEXT, + fullname TEXT, + created DATETIME DEFAULT CURRENT_TIMESTAMP `, ); @@ -34,7 +37,8 @@ export const seed = async (db: Database): Promise => { ` id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, - name TEXT + name TEXT, + iban TEXT UNIQUE `, ); @@ -67,22 +71,19 @@ export const seed = async (db: Database): Promise => { const hash = hashSync(testUser.password, 10); const { lastID: userId } = await db.run( - "INSERT INTO users (username, password) VALUES (?, ?);", - [testUser.username, hash], + "INSERT INTO users (username, password, fullname) VALUES (?, ?, ?);", + [testUser.username, hash, testUser.fullname], ); // Seed accounts const accounts = range(5) - .map(() => `(${userId}, '${faker.finance.accountName()}')`) + .map( + () => + `(${userId}, '${faker.finance.accountName()} ', '${faker.finance.iban()}')`, + ) .join(", "); - const result = await db.run( - `INSERT INTO accounts (user_id, name) VALUES ${accounts}`, - ); - - console.log( - `Seeded user with id ${userId}, ${result.changes} accounts created.`, - ); + await db.run(`INSERT INTO accounts (user_id, name, iban) VALUES ${accounts}`); // Seed cards const cards = range(5).map(() => [ @@ -108,7 +109,7 @@ export const seed = async (db: Database): Promise => { const transactions = range(100).map(() => [ userId, accountIds[Math.floor(Math.random() * accountIds.length)], - faker.finance.amount({ min: -25, max: 100, dec: 2 }), + faker.finance.amount({ min: -1500, max: 2500, dec: 2 }), faker.finance.transactionType(), faker.commerce.productName(), faker.date.recent({ days: 30 }).toISOString(), diff --git a/server/src/server.ts b/server/src/server.ts index 19ff70f..155f73b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -11,7 +11,7 @@ import { JWT_SECRET, TOKEN_EXPIRY_MINUTES, } from "./config"; -import { LoginRequest } from "../generated"; +import { LoginRequest, TokenPair, User as UserResponse } from "../generated"; import { readFileSync } from "fs"; import { generateRefreshToken, generateToken, verifyToken } from "./auth"; import { User, Request, TransactionsQuerySchema, LoginSchema } from "./schema"; @@ -80,7 +80,10 @@ export const build = ({ db, specPath = "./src/openapi.yaml" }: AppConfig) => { return; } - res.status(500).json({ error: "Internal server error" }).send(); + res + .status(500) + .json({ error: `Internal server error: ${err.message}` }) + .send(); }); app.post("/login", async ({ body }: Request, res: Response) => { @@ -95,12 +98,16 @@ export const build = ({ db, specPath = "./src/openapi.yaml" }: AppConfig) => { } const now = new Date(); - const token = generateToken(user); - const refreshToken = generateRefreshToken(user); const expires = new Date(now.getTime() + TOKEN_EXPIRY_MINUTES * 60 * 1000); - res.json({ token, refreshToken, expires }); + const response: TokenPair = { + expires: expires.toISOString(), + accessToken: generateToken(user), + refreshToken: generateRefreshToken(user), + }; + + res.json(response); }); app.post("/refresh-token", async ({ body }: Request, res: Response) => { @@ -113,16 +120,42 @@ export const build = ({ db, specPath = "./src/openapi.yaml" }: AppConfig) => { try { const user = await verifyToken(refreshToken, JWT_REFRESH_SECRET); - const token = generateToken(user); - const newRefreshRoken = generateRefreshToken(user); + const now = new Date(); + const expires = new Date( + now.getTime() + TOKEN_EXPIRY_MINUTES * 60 * 1000, + ); - res.json({ token, refreshToken: newRefreshRoken }); + const response: TokenPair = { + expires: expires.toISOString(), + accessToken: generateToken(user), + refreshToken: generateRefreshToken(user), + }; + + res.json(response); } catch (err) { - return res.status(401).json({ message: "Invalid refresh token" }).send(); + return res + .status(401) + .json({ message: `Invalid refresh token: ${err.message}` }) + .send(); } }); // Protected endpoints + app.get("/me", authenticateToken, async (req: Request, res: Response) => { + const user = await db.get("SELECT * FROM users WHERE id = ?", [ + req.user.id, + ]); + + const response: UserResponse = { + id: user.id, + username: user.username, + fullname: user.fullname, + created: new Date(user.created).toISOString(), + }; + + res.json(response); + }); + app.get( "/accounts", authenticateToken, diff --git a/server/tests/api.spec.ts b/server/tests/api.spec.ts index 93ce1fe..b31489e 100644 --- a/server/tests/api.spec.ts +++ b/server/tests/api.spec.ts @@ -21,7 +21,7 @@ describe("GET /accounts", () => { await request(app) .get("/accounts") - .set("Authorization", `Bearer ${auth.body.token}`) + .set("Authorization", `Bearer ${auth.body.accessToken}`) .expect(200) .expect(({ body }) => expect(body).not.toHaveLength(0)); }); @@ -37,7 +37,7 @@ describe("GET /transactions", () => { await request(app) .get("/transactions") - .set("Authorization", `Bearer ${auth.body.token}`) + .set("Authorization", `Bearer ${auth.body.accessToken}`) .expect(200); }); @@ -57,13 +57,13 @@ describe("GET /transactions", () => { const account = await request(app) .get("/accounts") - .set("Authorization", `Bearer ${auth.body.token}`) + .set("Authorization", `Bearer ${auth.body.accessToken}`) .expect(200) .expect(({ body }) => expect(body).not.toHaveLength(0)); await request(app) .get(`/transactions?accountId=${account.body[0].id}`) - .set("Authorization", `Bearer ${auth.body.token}`) + .set("Authorization", `Bearer ${auth.body.accessToken}`) .expect(200) .expect(({ body }) => expect(body).not.toHaveLength(0)) .expect(({ body }: { body: Transaction[] }) => { @@ -78,18 +78,18 @@ describe("GET /transactions", () => { const transactionType = await request(app) .get("/transaction-types") - .set("Authorization", `Bearer ${auth.body.token}`) + .set("Authorization", `Bearer ${auth.body.accessToken}`) .expect(200) .expect(({ body }) => expect(body).not.toHaveLength(0)); await request(app) .get(`/transactions?type=${transactionType.body[0].name}`) - .set("Authorization", `Bearer ${auth.body.token}`) + .set("Authorization", `Bearer ${auth.body.accessToken}`) .expect(200) .expect(({ body }) => expect(body).not.toHaveLength(0)) - .expect(({ body }: { body: Transaction[] }) => { - body.forEach((transaction) => { - expect(transaction.type).toBe(transactionType.body[0].name); + .expect(({ body }) => { + body.forEach(({ type }: Transaction) => { + expect(type).toBe(transactionType.body[0].name); }); }); }); diff --git a/server/tests/auth.spec.ts b/server/tests/auth.spec.ts index 33a5977..a1a356c 100644 --- a/server/tests/auth.spec.ts +++ b/server/tests/auth.spec.ts @@ -68,4 +68,39 @@ describe("POST /refresh-token", () => { .expect(401) .expect("Content-Type", /json/); }); + + it("rejects invalid login", async () => { + await request(app) + .post("/login") + .send({ username: "test@test.be", password: "wrong-password" }) + .set("Accept", "application/json") + .expect(401) + .expect("Content-Type", /json/); + }); +}); + +describe("GET /me", () => { + it("can grab the current user when logged in", async () => { + const authResponse = await request(app) + .post("/login") + .send(testUser) + .set("Accept", "application/json") + .expect(200) + .expect("Content-Type", /json/); + + await request(app) + .get("/me") + .set("Authorization", `Bearer ${authResponse.body.accessToken}`) + .set("Accept", "application/json") + .expect(200) + .expect("Content-Type", /json/); + }); + + it("fails to fetch the current user when not logged in", async () => { + await request(app) + .get("/me") + .set("Accept", "application/json") + .expect(401) + .expect("Content-Type", /json/); + }); });