From 65b0dd99eba5885f2062c347af3e167999bbe8ee Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 1 Jun 2024 16:36:08 +0200 Subject: [PATCH] auth --- apps/app/package.json | 20 +- apps/app/src/app/_layout.tsx | 102 ++++++-- apps/app/src/app/index.tsx | 14 +- apps/app/src/app/login.tsx | 48 ++++ apps/app/src/app/register.tsx | 47 ++++ apps/app/src/components/authForm.tsx | 57 +++++ apps/app/src/hooks/useInterval.ts | 24 ++ apps/app/src/hooks/useLogin.ts | 58 +++++ apps/app/src/hooks/useRegisterAndLogin.ts | 54 +++++ .../rnr/components/primitives/hooks/index.ts | 3 + .../primitives/hooks/useAugmentedRef.tsx | 29 +++ .../primitives/hooks/useControllableState.tsx | 75 ++++++ .../primitives/hooks/useRelativePosition.tsx | 227 ++++++++++++++++++ .../src/rnr/components/primitives/portal.tsx | 82 +++++++ .../src/rnr/components/ui/alert-dialog.tsx | 167 +++++++++++++ apps/app/src/rnr/lib/icons/AlertCircle.ts | 4 + apps/app/src/utils/trpc.ts | 4 + apps/server/package.json | 5 +- yarn.lock | 151 ++++++------ 19 files changed, 1063 insertions(+), 108 deletions(-) create mode 100644 apps/app/src/app/login.tsx create mode 100644 apps/app/src/app/register.tsx create mode 100644 apps/app/src/components/authForm.tsx create mode 100644 apps/app/src/hooks/useInterval.ts create mode 100644 apps/app/src/hooks/useLogin.ts create mode 100644 apps/app/src/hooks/useRegisterAndLogin.ts create mode 100644 apps/app/src/rnr/components/primitives/hooks/index.ts create mode 100644 apps/app/src/rnr/components/primitives/hooks/useAugmentedRef.tsx create mode 100644 apps/app/src/rnr/components/primitives/hooks/useControllableState.tsx create mode 100644 apps/app/src/rnr/components/primitives/hooks/useRelativePosition.tsx create mode 100644 apps/app/src/rnr/components/primitives/portal.tsx create mode 100644 apps/app/src/rnr/components/ui/alert-dialog.tsx create mode 100644 apps/app/src/rnr/lib/icons/AlertCircle.ts create mode 100644 apps/app/src/utils/trpc.ts diff --git a/apps/app/package.json b/apps/app/package.json index cb31748..fa99549 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -11,6 +11,8 @@ "@expo/metro-runtime": "~3.2.1", "@expo/vector-icons": "^14.0.0", "@react-native/assets-registry": "0.75.0-main", + "@tanstack/react-query": "^5.40.0", + "@trpc/client": "^11.0.0-rc.382", "babel-preset-expo": "~11.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -29,17 +31,18 @@ "lucide-react-native": "^0.381.0", "nativewind": "^4.0.1", "position-strings": "^2.0.1", - "react": "18.3.1", - "react-dom": "^18.2.0", + "react": "18.2.0", + "react-dom": "18.2.0", "react-native": "0.74.1", "react-native-libsodium": "^1.3.1", - "react-native-reanimated": "~3.11.0", - "react-native-safe-area-context": "4.10.3", + "react-native-opaque": "^0.3.1", + "react-native-reanimated": "~3.10.1", + "react-native-safe-area-context": "4.10.1", "react-native-screens": "3.31.1", - "react-native-svg": "15.3.0", + "react-native-svg": "15.2.0", "react-native-web": "~0.19.6", - "secsync-react-yjs": "^0.4.0", "secsync": "^0.4.0", + "secsync-react-yjs": "^0.4.0", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.0", "tailwindcss-animate": "^1.0.7", @@ -49,14 +52,13 @@ "devDependencies": { "@babel/core": "^7.24.3", "@babel/runtime": "^7.24.1", - "@types/react": "~18.3.3", - "babel-plugin-transform-vite-meta-env": "^1.0.3", + "@types/react": "~18.2.79", "eslint": "^9.3.0", "husky": "^9.0.11", "lint-staged": "^15.2.5", "prettier": "3.2.5", "prettier-plugin-tailwindcss": "^0.6.0", - "typescript": "~5.4.5" + "typescript": "~5.3.3" }, "resolutions": { "@effect/schema": "=0.64.16" diff --git a/apps/app/src/app/_layout.tsx b/apps/app/src/app/_layout.tsx index 6b7e0e3..c85a7ee 100644 --- a/apps/app/src/app/_layout.tsx +++ b/apps/app/src/app/_layout.tsx @@ -1,12 +1,21 @@ import { Theme, ThemeProvider } from "@react-navigation/native"; +import { + MutationCache, + QueryCache, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { httpBatchLink } from "@trpc/client"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import * as React from "react"; +import { useState } from "react"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { NAV_THEME } from "~/lib/constants"; import { useColorScheme } from "~/lib/useColorScheme"; import "../global.css"; import useLoadingLibsodium from "../hooks/useLoadingLibsodium"; +import { trpc } from "../utils/trpc"; const LIGHT_THEME: Theme = { dark: false, @@ -17,29 +26,92 @@ const DARK_THEME: Theme = { colors: NAV_THEME.dark, }; +// TODO PROD API URL +const apiUrl = "http://localhost:3030/api"; + export default function Layout() { const { isDarkColorScheme } = useColorScheme(); const isLoadingComplete = useLoadingLibsodium(); + const [queryClient] = useState( + () => + new QueryClient({ + queryCache: new QueryCache({ + // TODO + // onError: (error) => { + // if ( + // error instanceof TRPCClientError && + // error.data?.code === "UNAUTHORIZED" && + // window.location.pathname !== "/login" + // ) { + // removeLocalDb(); + // queryClient.clear(); + // router.navigate({ + // to: "/login", + // search: { redirect: window.location.pathname }, + // }); + // } + // }, + }), + mutationCache: new MutationCache({ + // TODO + // onError: (error) => { + // if ( + // error instanceof TRPCClientError && + // error.data?.code === "UNAUTHORIZED" && + // window.location.pathname !== "/login" + // ) { + // removeLocalDb(); + // queryClient.clear(); + // router.navigate({ + // to: "/login", + // search: { redirect: window.location.pathname }, + // }); + // } + // }, + }), + }) + ); + const [trpcClient] = useState(() => + trpc.createClient({ + links: [ + httpBatchLink({ + url: apiUrl, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], + }) + ); + if (!isLoadingComplete) { return null; } return ( - - - - - - - {/* Default Portal Host (one per app) */} - - + + + + + + + + + {/* Default Portal Host (one per app) */} + {/* */} + + + + ); } diff --git a/apps/app/src/app/index.tsx b/apps/app/src/app/index.tsx index d80cde9..ce0bec6 100644 --- a/apps/app/src/app/index.tsx +++ b/apps/app/src/app/index.tsx @@ -3,11 +3,19 @@ import * as React from "react"; import { Text } from "react-native"; const Lists: React.FC = () => { - const workouts: { id: string; startedAt: string }[] = []; - return ( - Hello WOrld LIST A + Hello WOrld{" "} + + LIST A + + Login + Register ); }; diff --git a/apps/app/src/app/login.tsx b/apps/app/src/app/login.tsx new file mode 100644 index 0000000..b5913f4 --- /dev/null +++ b/apps/app/src/app/login.tsx @@ -0,0 +1,48 @@ +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { View } from "react-native"; +import { AuthForm } from "src/components/authForm"; +import { Text } from "~/components/ui/text"; +import { AlertCircle } from "~/lib/icons/AlertCircle"; +import { useLogin } from "../hooks/useLogin"; + +const Login = () => { + const { login, isPending } = useLogin(); + const [error, setError] = useState(null); + const { redirect } = useLocalSearchParams<{ redirect?: string }>(); + + return ( + + { + const sessionKey = await login({ + userIdentifier: username, + password, + }); + if (!sessionKey) { + setError("Failed to login"); + return; + } + if (redirect) { + router.navigate(redirect); + return; + } + router.navigate("/"); + }} + children={Login} + isPending={isPending} + /> + + {error && ( + + + {/* TODO proper styling */} + Error + Failed to log in + + )} + + ); +}; + +export default Login; diff --git a/apps/app/src/app/register.tsx b/apps/app/src/app/register.tsx new file mode 100644 index 0000000..eb46c39 --- /dev/null +++ b/apps/app/src/app/register.tsx @@ -0,0 +1,47 @@ +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { View } from "react-native"; +import { Text } from "~/components/ui/text"; +import { AlertCircle } from "~/lib/icons/AlertCircle"; +import { AuthForm } from "../components/authForm"; +import { useRegisterAndLogin } from "../hooks/useRegisterAndLogin"; + +const Register = () => { + const { registerAndLogin, isPending } = useRegisterAndLogin(); + const { redirect } = useLocalSearchParams<{ redirect?: string }>(); + const [error, setError] = useState(null); + + return ( + + { + const sessionKey = await registerAndLogin({ + userIdentifier: username, + password, + }); + if (!sessionKey) { + setError("Failed to register"); + return; + } + if (redirect) { + router.navigate(redirect); + return; + } + router.navigate("/"); + }} + children={Register} + isPending={isPending} + /> + {error && ( + + + {/* TODO proper styling */} + Error + Failed to register + + )} + + ); +}; + +export default Register; diff --git a/apps/app/src/components/authForm.tsx b/apps/app/src/components/authForm.tsx new file mode 100644 index 0000000..b515af3 --- /dev/null +++ b/apps/app/src/components/authForm.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { View } from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Text } from "~/components/ui/text"; + +type Props = { + onSubmit: (params: { username: string; password: string }) => void; + isPending: boolean; + children: React.ReactNode; +}; + +export const AuthForm = ({ onSubmit, isPending, children }: Props) => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + return ( + + + {children} + + + + { + setUsername(value); + }} + /> + + { + setPassword(value); + }} + /> + + + + + ); +}; diff --git a/apps/app/src/hooks/useInterval.ts b/apps/app/src/hooks/useInterval.ts new file mode 100644 index 0000000..754b8b5 --- /dev/null +++ b/apps/app/src/hooks/useInterval.ts @@ -0,0 +1,24 @@ +import { useEffect, useRef } from "react"; + +type IntervalFunc = () => unknown | void; + +// Inspired by https://overreacted.io/making-setinterval-declarative-with-react-hooks/ +export const useInterval = (callback: IntervalFunc, delay: number | null) => { + const savedCallback = useRef(null); + + useEffect(() => { + if (delay === null) return; + savedCallback.current = callback; + }); + + useEffect(() => { + if (delay === null) return; + function tick() { + if (savedCallback.current !== null) { + savedCallback.current(); + } + } + const id = setInterval(tick, delay); + return () => clearInterval(id); + }, [delay]); +}; diff --git a/apps/app/src/hooks/useLogin.ts b/apps/app/src/hooks/useLogin.ts new file mode 100644 index 0000000..18113e5 --- /dev/null +++ b/apps/app/src/hooks/useLogin.ts @@ -0,0 +1,58 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import * as opaque from "react-native-opaque"; +import { trpc } from "../utils/trpc"; + +type LoginParams = { + userIdentifier: string; + password: string; +}; + +export const useLogin = () => { + // TODO + return { isPending: true, login: () => {} }; + + const loginStartMutation = trpc.loginStart.useMutation(); + const loginFinishMutation = trpc.loginFinish.useMutation(); + + const queryClient = useQueryClient(); + const [isPending, setIsPending] = useState(false); + + const login = async ({ userIdentifier, password }: LoginParams) => { + setIsPending(true); + try { + const { clientLoginState, startLoginRequest } = opaque.client.startLogin({ + password, + }); + + const { loginResponse } = await loginStartMutation.mutateAsync({ + userIdentifier, + startLoginRequest, + }); + + const loginResult = opaque.client.finishLogin({ + clientLoginState, + loginResponse, + password, + }); + if (!loginResult) { + return null; + } + const { sessionKey, finishLoginRequest } = loginResult; + + const { success } = await loginFinishMutation.mutateAsync({ + finishLoginRequest, + userIdentifier, + }); + + queryClient.invalidateQueries(); + + return success ? sessionKey : null; + } catch (error) { + return null; + } finally { + setIsPending(false); + } + }; + return { isPending, login }; +}; diff --git a/apps/app/src/hooks/useRegisterAndLogin.ts b/apps/app/src/hooks/useRegisterAndLogin.ts new file mode 100644 index 0000000..ef31f9b --- /dev/null +++ b/apps/app/src/hooks/useRegisterAndLogin.ts @@ -0,0 +1,54 @@ +import { useState } from "react"; +import * as opaque from "react-native-opaque"; +import { trpc } from "../utils/trpc"; +import { useLogin } from "./useLogin"; + +type RegisterParams = { + userIdentifier: string; + password: string; +}; + +export const useRegisterAndLogin = () => { + // TODO + return { isPending: true, registerAndLogin: () => {} }; + + const [isPending, setIsPending] = useState(false); + const registerStartMutation = trpc.registerStart.useMutation(); + const registerFinishMutation = trpc.registerFinish.useMutation(); + const { login } = useLogin(); + + const registerAndLogin = async ({ + userIdentifier, + password, + }: RegisterParams) => { + setIsPending(true); + try { + const { clientRegistrationState, registrationRequest } = + opaque.client.startRegistration({ password }); + const { registrationResponse } = await registerStartMutation.mutateAsync({ + userIdentifier, + registrationRequest, + }); + + const { registrationRecord } = opaque.client.finishRegistration({ + clientRegistrationState, + registrationResponse, + password, + }); + + await registerFinishMutation.mutateAsync({ + userIdentifier, + registrationRecord, + }); + + const result = await login({ userIdentifier, password }); + return result; + } catch (error) { + return null; + } finally { + setIsPending(false); + } + }; + + return { isPending, registerAndLogin }; +}; diff --git a/apps/app/src/rnr/components/primitives/hooks/index.ts b/apps/app/src/rnr/components/primitives/hooks/index.ts new file mode 100644 index 0000000..58c0d1b --- /dev/null +++ b/apps/app/src/rnr/components/primitives/hooks/index.ts @@ -0,0 +1,3 @@ +export { useAugmentedRef } from './useAugmentedRef'; +export { useRelativePosition, type LayoutPosition } from './useRelativePosition'; +export { useControllableState } from './useControllableState'; diff --git a/apps/app/src/rnr/components/primitives/hooks/useAugmentedRef.tsx b/apps/app/src/rnr/components/primitives/hooks/useAugmentedRef.tsx new file mode 100644 index 0000000..13a5669 --- /dev/null +++ b/apps/app/src/rnr/components/primitives/hooks/useAugmentedRef.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; + +interface AugmentRefProps { + ref: React.Ref; + methods?: Record any>; + deps?: any[]; +} + +export function useAugmentedRef({ + ref, + methods, + deps = [], +}: AugmentRefProps) { + const augmentedRef = React.useRef(null); + React.useImperativeHandle( + ref, + () => { + if (typeof augmentedRef === 'function' || !augmentedRef?.current) { + return {} as T; + } + return { + ...augmentedRef.current, + ...methods, + }; + }, + deps + ); + return augmentedRef; +} diff --git a/apps/app/src/rnr/components/primitives/hooks/useControllableState.tsx b/apps/app/src/rnr/components/primitives/hooks/useControllableState.tsx new file mode 100644 index 0000000..3b42b53 --- /dev/null +++ b/apps/app/src/rnr/components/primitives/hooks/useControllableState.tsx @@ -0,0 +1,75 @@ +// This project uses code from WorkOS/Radix Primitives. +// The code is licensed under the MIT License. +// https://github.com/radix-ui/primitives/tree/main + +import * as React from 'react'; + +type UseControllableStateParams = { + prop?: T | undefined; + defaultProp?: T | undefined; + onChange?: (state: T) => void; +}; + +type SetStateFn = (prevState?: T) => T; + +function useControllableState({ + prop, + defaultProp, + onChange = () => {}, +}: UseControllableStateParams) { + const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({ defaultProp, onChange }); + const isControlled = prop !== undefined; + const value = isControlled ? prop : uncontrolledProp; + const handleChange = useCallbackRef(onChange); + + const setValue: React.Dispatch> = React.useCallback( + (nextValue) => { + if (isControlled) { + const setter = nextValue as SetStateFn; + const value = typeof nextValue === 'function' ? setter(prop) : nextValue; + if (value !== prop) handleChange(value as T); + } else { + setUncontrolledProp(nextValue); + } + }, + [isControlled, prop, setUncontrolledProp, handleChange] + ); + + return [value, setValue] as const; +} + +function useUncontrolledState({ + defaultProp, + onChange, +}: Omit, 'prop'>) { + const uncontrolledState = React.useState(defaultProp); + const [value] = uncontrolledState; + const prevValueRef = React.useRef(value); + const handleChange = useCallbackRef(onChange); + + React.useEffect(() => { + if (prevValueRef.current !== value) { + handleChange(value as T); + prevValueRef.current = value; + } + }, [value, prevValueRef, handleChange]); + + return uncontrolledState; +} + +/** + * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a + * prop or avoid re-executing effects when passed as a dependency + */ +function useCallbackRef any>(callback: T | undefined): T { + const callbackRef = React.useRef(callback); + + React.useEffect(() => { + callbackRef.current = callback; + }); + + // https://github.com/facebook/react/issues/19240 + return React.useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []); +} + +export { useControllableState }; diff --git a/apps/app/src/rnr/components/primitives/hooks/useRelativePosition.tsx b/apps/app/src/rnr/components/primitives/hooks/useRelativePosition.tsx new file mode 100644 index 0000000..f1544be --- /dev/null +++ b/apps/app/src/rnr/components/primitives/hooks/useRelativePosition.tsx @@ -0,0 +1,227 @@ +import * as React from 'react'; +import { + useWindowDimensions, + type LayoutRectangle, + type ScaledSize, + type ViewStyle, +} from 'react-native'; +import type { Insets } from '~/components/primitives/types'; + +const POSITION_ABSOLUTE: ViewStyle = { + position: 'absolute', +}; + +const HIDDEN_CONTENT: ViewStyle = { + position: 'absolute', + opacity: 0, + zIndex: -9999999, +}; + +type UseRelativePositionArgs = Omit< + GetContentStyleArgs, + 'triggerPosition' | 'contentLayout' | 'dimensions' +> & { + triggerPosition: LayoutPosition | null; + contentLayout: LayoutRectangle | null; + disablePositioningStyle?: boolean; +}; + +export function useRelativePosition({ + align, + avoidCollisions, + triggerPosition, + contentLayout, + alignOffset, + insets, + sideOffset, + side, + disablePositioningStyle, +}: UseRelativePositionArgs) { + const dimensions = useWindowDimensions(); + return React.useMemo(() => { + if (disablePositioningStyle) { + return {}; + } + if (!triggerPosition || !contentLayout) { + return HIDDEN_CONTENT; + } + return getContentStyle({ + align, + avoidCollisions, + contentLayout, + side, + triggerPosition, + alignOffset, + insets, + sideOffset, + dimensions, + }); + }, [triggerPosition, contentLayout, dimensions.width, dimensions.height]); +} + +export interface LayoutPosition { + pageY: number; + pageX: number; + width: number; + height: number; +} + +interface GetPositionArgs { + dimensions: ScaledSize; + avoidCollisions: boolean; + triggerPosition: LayoutPosition; + contentLayout: LayoutRectangle; + insets?: Insets; +} + +interface GetSidePositionArgs extends GetPositionArgs { + side: 'top' | 'bottom'; + sideOffset: number; +} + +function getSidePosition({ + side, + triggerPosition, + contentLayout, + sideOffset, + insets, + avoidCollisions, + dimensions, +}: GetSidePositionArgs) { + const insetTop = insets?.top ?? 0; + const insetBottom = insets?.bottom ?? 0; + const positionTop = triggerPosition?.pageY - sideOffset - contentLayout.height; + const positionBottom = triggerPosition.pageY + triggerPosition.height + sideOffset; + + if (!avoidCollisions) { + return { + top: side === 'top' ? positionTop : positionBottom, + }; + } + + if (side === 'top') { + return { + top: Math.max(insetTop, positionTop), + }; + } + + return { + top: Math.min(dimensions.height - insetBottom - contentLayout.height, positionBottom), + }; +} + +interface GetAlignPositionArgs extends GetPositionArgs { + align: 'start' | 'center' | 'end'; + alignOffset: number; +} + +function getAlignPosition({ + align, + avoidCollisions, + contentLayout, + triggerPosition, + alignOffset, + insets, + dimensions, +}: GetAlignPositionArgs) { + const insetLeft = insets?.left ?? 0; + const insetRight = insets?.right ?? 0; + const maxContentWidth = dimensions.width - insetLeft - insetRight; + + const contentWidth = Math.min(contentLayout.width, maxContentWidth); + + let left = getLeftPosition( + align, + triggerPosition.pageX, + triggerPosition.width, + contentWidth, + alignOffset, + insetLeft, + insetRight, + dimensions + ); + + if (avoidCollisions) { + const doesCollide = left < insetLeft || left + contentWidth > dimensions.width - insetRight; + if (doesCollide) { + const spaceLeft = left - insetLeft; + const spaceRight = dimensions.width - insetRight - (left + contentWidth); + + if (spaceLeft > spaceRight && spaceLeft >= contentWidth) { + left = insetLeft; + } else if (spaceRight >= contentWidth) { + left = dimensions.width - insetRight - contentWidth; + } else { + const centeredPosition = Math.max( + insetLeft, + (dimensions.width - contentWidth - insetRight) / 2 + ); + left = centeredPosition; + } + } + } + + return { left, maxWidth: maxContentWidth }; +} + +function getLeftPosition( + align: 'start' | 'center' | 'end', + triggerPageX: number, + triggerWidth: number, + contentWidth: number, + alignOffset: number, + insetLeft: number, + insetRight: number, + dimensions: ScaledSize +) { + let left = 0; + if (align === 'start') { + left = triggerPageX; + } + if (align === 'center') { + left = triggerPageX + triggerWidth / 2 - contentWidth / 2; + } + if (align === 'end') { + left = triggerPageX + triggerWidth - contentWidth; + } + return Math.max( + insetLeft, + Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight) + ); +} + +type GetContentStyleArgs = GetPositionArgs & GetSidePositionArgs & GetAlignPositionArgs; + +function getContentStyle({ + align, + avoidCollisions, + contentLayout, + side, + triggerPosition, + alignOffset, + insets, + sideOffset, + dimensions, +}: GetContentStyleArgs) { + return Object.assign( + POSITION_ABSOLUTE, + getSidePosition({ + side, + triggerPosition, + contentLayout, + sideOffset, + insets, + avoidCollisions, + dimensions, + }), + getAlignPosition({ + align, + avoidCollisions, + triggerPosition, + contentLayout, + alignOffset, + insets, + dimensions, + }) + ); +} diff --git a/apps/app/src/rnr/components/primitives/portal.tsx b/apps/app/src/rnr/components/primitives/portal.tsx new file mode 100644 index 0000000..bd3ede8 --- /dev/null +++ b/apps/app/src/rnr/components/primitives/portal.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { Platform, type View, type ViewStyle } from 'react-native'; +import { create } from 'zustand'; + +const DEFAULT_PORTAL_HOST = 'INTERNAL_PRIMITIVE_DEFAULT_HOST_NAME'; + +type PortalMap = Map; +type PortalHostMap = Map; + +const usePortal = create<{ map: PortalHostMap }>(() => ({ + map: new Map().set(DEFAULT_PORTAL_HOST, new Map()), +})); + +const updatePortal = (hostName: string, name: string, children: React.ReactNode) => { + usePortal.setState((prev) => { + const next = new Map(prev.map); + const portal = next.get(hostName) ?? new Map(); + portal.set(name, children); + next.set(hostName, portal); + return { map: next }; + }); +}; +const removePortal = (hostName: string, name: string) => { + usePortal.setState((prev) => { + const next = new Map(prev.map); + const portal = next.get(hostName) ?? new Map(); + portal.delete(name); + next.set(hostName, portal); + return { map: next }; + }); +}; + +export function PortalHost({ name = DEFAULT_PORTAL_HOST }: { name?: string }) { + const portalMap = usePortal((state) => state.map).get(name) ?? new Map(); + if (portalMap.size === 0) return null; + return <>{Array.from(portalMap.values())}; +} + +export function Portal({ + name, + hostName = DEFAULT_PORTAL_HOST, + children, +}: { + name: string; + hostName?: string; + children: React.ReactNode; +}) { + React.useEffect(() => { + updatePortal(hostName, name, children); + }, [hostName, name, children]); + + React.useEffect(() => { + return () => { + removePortal(hostName, name); + }; + }, [hostName, name]); + + return null; +} + +const ROOT: ViewStyle = { + flex: 1, +}; + +export function useModalPortalRoot() { + const ref = React.useRef(null); + const [sideOffset, setSideOffSet] = React.useState(0); + + const onLayout = React.useCallback(() => { + if (Platform.OS === 'web') return; + ref.current?.measure((_x, _y, _width, _height, _pageX, pageY) => { + setSideOffSet(-pageY); + }); + }, []); + + return { + ref, + sideOffset, + onLayout, + style: ROOT, + }; +} diff --git a/apps/app/src/rnr/components/ui/alert-dialog.tsx b/apps/app/src/rnr/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..40ebaec --- /dev/null +++ b/apps/app/src/rnr/components/ui/alert-dialog.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; +import { Platform, StyleSheet, View } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { buttonTextVariants, buttonVariants } from '~/components/ui/button'; +import * as AlertDialogPrimitive from '~/components/primitives/alert-dialog'; +import { cn } from '~/lib/utils'; +import { TextClassContext } from '~/components/ui/text'; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlayWeb = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { open } = AlertDialogPrimitive.useRootContext(); + return ( + + ); +}); + +AlertDialogOverlayWeb.displayName = 'AlertDialogOverlayWeb'; + +const AlertDialogOverlayNative = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => { + return ( + + + {children} + + + ); +}); + +AlertDialogOverlayNative.displayName = 'AlertDialogOverlayNative'; + +const AlertDialogOverlay = Platform.select({ + web: AlertDialogOverlayWeb, + default: AlertDialogOverlayNative, +}); + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { portalHost?: string } +>(({ className, portalHost, ...props }, ref) => { + const { open } = AlertDialogPrimitive.useRootContext(); + + return ( + + + + + + ); +}); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.ComponentPropsWithoutRef) => ( + +); +AlertDialogHeader.displayName = 'AlertDialogHeader'; + +const AlertDialogFooter = ({ + className, + ...props +}: React.ComponentPropsWithoutRef) => ( + +); +AlertDialogFooter.displayName = 'AlertDialogFooter'; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +}; diff --git a/apps/app/src/rnr/lib/icons/AlertCircle.ts b/apps/app/src/rnr/lib/icons/AlertCircle.ts new file mode 100644 index 0000000..adaaeeb --- /dev/null +++ b/apps/app/src/rnr/lib/icons/AlertCircle.ts @@ -0,0 +1,4 @@ +import { AlertCircle } from "lucide-react-native"; +import { iconWithClassName } from "./iconWithClassName"; +iconWithClassName(AlertCircle); +export { AlertCircle }; diff --git a/apps/app/src/utils/trpc.ts b/apps/app/src/utils/trpc.ts new file mode 100644 index 0000000..82e3bd1 --- /dev/null +++ b/apps/app/src/utils/trpc.ts @@ -0,0 +1,4 @@ +import { createTRPCReact } from "@trpc/react-query"; +import type { AppRouter } from "../../../server/src/index"; + +export const trpc = createTRPCReact(); diff --git a/apps/server/package.json b/apps/server/package.json index 5211f62..81314e6 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -13,15 +13,16 @@ "dependencies": { "@prisma/client": "5.14.0", "@serenity-kit/opaque": "^0.8.4", - "@trpc/server": "11.0.0-rc.366", + "@trpc/react-query": "^10.45.2", + "@trpc/server": "^11.0.0-rc.382", "cookie": "^0.6.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "isomorphic-ws": "^5.0.0", - "secsync-server": "^0.4.0", "secsync": "^0.4.0", + "secsync-server": "^0.4.0", "ws": "^8.17.0", "zod": "^3.23.8" }, diff --git a/yarn.lock b/yarn.lock index 5ffebd3..3d8017a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -262,7 +262,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.6": +"@babel/parser@^7.13.16", "@babel/parser@^7.20.0", "@babel/parser@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328" integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q== @@ -766,7 +766,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.9", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.24.1": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.24.1": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e" integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw== @@ -798,7 +798,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.23.0", "@babel/types@^7.24.6": +"@babel/types@^7.20.0", "@babel/types@^7.23.0", "@babel/types@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912" integrity sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ== @@ -815,9 +815,9 @@ "@jridgewell/trace-mapping" "0.3.9" "@effect/schema@^0.67.15": - version "0.67.15" - resolved "https://registry.yarnpkg.com/@effect/schema/-/schema-0.67.15.tgz#b57ce1617a1ed6ec689a486f1ba392a1c521b9a4" - integrity sha512-+AO29qX0GIDARbQE1TcnWz3cUBCOm3x8KP6SXpWUot1cfG4ccoWrrfzVBaaCl1FDT5egvAtwfQ26GPVMJEobEg== + version "0.67.16" + resolved "https://registry.yarnpkg.com/@effect/schema/-/schema-0.67.16.tgz#b0d4cee4629d1cde467ee0b04590fa60da31b9cd" + integrity sha512-fUHo1t6k2w9UcJF/JlppRVD+KXXKtAN8fZRB48NMocLJfQnJC117c9Zio0SQWN+xP9byLl4Y/MbtFeuxcublxg== dependencies: fast-check "^3.17.2" @@ -1949,7 +1949,7 @@ component-type "^1.2.1" join-component "^1.1.0" -"@serenity-kit/opaque@^0.8.4": +"@serenity-kit/opaque@^0.8.0", "@serenity-kit/opaque@^0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@serenity-kit/opaque/-/opaque-0.8.4.tgz#c765905b338abcd8eaee8d44f6b9c40850925b40" integrity sha512-sFxGxZWdBciJ1Bp2F5Pey5q3kW+bN/RYpsZueLogb/KAWF50XuYk5bSG0Y8gH784jV+2XI/G8ptX+kWA0w6N3Q== @@ -1990,10 +1990,32 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@trpc/server@11.0.0-rc.366": - version "11.0.0-rc.366" - resolved "https://registry.yarnpkg.com/@trpc/server/-/server-11.0.0-rc.366.tgz#ffea6f1616bad4e9d77160c7aa4fe7a381317117" - integrity sha512-Pr7SdpIVrOGtIGt9vs7i3v0hqzlpXbi8/8RV7XfrXVJ5hlzKYOz5VE0AjckhHXuhrUlpTGTDm0/2nz0ywLcQ2g== +"@tanstack/query-core@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.40.0.tgz#c74ae8303752ed4b5a0ab848ec71a0e6e8179f83" + integrity sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA== + +"@tanstack/react-query@^5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.40.0.tgz#654afa2d9ab328c22be7e1f025ec9b6267c6baa9" + integrity sha512-iv/W0Axc4aXhFzkrByToE1JQqayxTPNotCoSCnarR/A1vDIHaoKpg7FTIfP3Ev2mbKn1yrxq0ZKYUdLEJxs6Tg== + dependencies: + "@tanstack/query-core" "5.40.0" + +"@trpc/client@^11.0.0-rc.382": + version "11.0.0-rc.382" + resolved "https://registry.yarnpkg.com/@trpc/client/-/client-11.0.0-rc.382.tgz#0bb2222aaa5cbbb7c8d5c9cdde21572bd148a20e" + integrity sha512-O+MSRed5r8AJJ+j3peZkd/b7WINEkhKaFilRuPH8VQsrlMZToxUzQl9aShPIgnOhhQGxvWqdh2lXwUvrFCea/A== + +"@trpc/react-query@^10.45.2": + version "10.45.2" + resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-10.45.2.tgz#22564b370a1fb8920ba3a1554f4149a151039198" + integrity sha512-BAqb9bGZIscroradlNx+Cc9522R+idY3BOSf5z0jHUtkxdMbjeGKxSSMxxu7JzoLqSIEC+LVzL3VvF8sdDWaZQ== + +"@trpc/server@^11.0.0-rc.382": + version "11.0.0-rc.382" + resolved "https://registry.yarnpkg.com/@trpc/server/-/server-11.0.0-rc.382.tgz#ef2a561cbbf97d14eb283a979be849be0cf08c78" + integrity sha512-a3A0osTgFhbClRjE+fZ3nA3TEjupnWM23L4hidjecg3xN+QghwYTjwXJcYXPsdKyT6PhFtzWBKJxShmsZRElZw== "@tsconfig/node10@^1.0.7": version "1.0.11" @@ -2015,39 +2037,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@types/babel__core@^7.1.12": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.8" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" - integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" - integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== - dependencies: - "@babel/types" "^7.20.7" - "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -2193,10 +2182,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react@~18.3.3": - version "18.3.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" - integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== +"@types/react@~18.2.79": + version "18.2.79" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" + integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -2633,14 +2622,6 @@ babel-plugin-transform-flow-enums@^0.0.2: dependencies: "@babel/plugin-syntax-flow" "^7.12.1" -babel-plugin-transform-vite-meta-env@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-vite-meta-env/-/babel-plugin-transform-vite-meta-env-1.0.3.tgz#cbf81becc95b71dcc170ee4863cb7f6919ed99bb" - integrity sha512-eyfuDEXrMu667TQpmctHeTlJrZA6jXYHyEJFjcM0yEa60LS/LXlOg2PBbMb8DVS+V9CnTj/j9itdlDVMcY2zEg== - dependencies: - "@babel/runtime" "^7.13.9" - "@types/babel__core" "^7.1.12" - babel-preset-expo@~11.0.0, babel-preset-expo@~11.0.6: version "11.0.6" resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-11.0.6.tgz#b1ea2bd9f13338a9f7ca8d7089b5d6d6c7c03f79" @@ -7085,13 +7066,13 @@ react-devtools-core@^5.0.0: shell-quote "^1.6.1" ws "^7" -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== +react-dom@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - scheduler "^0.23.2" + scheduler "^0.23.0" react-fast-compare@^3.2.2: version "3.2.2" @@ -7149,10 +7130,17 @@ react-native-libsodium@^1.3.1: libsodium-wrappers "^0.7.13" libsodium-wrappers-sumo "^0.7.13" -react-native-reanimated@~3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.11.0.tgz#d4265d4e0232623f5958ed60e1686ca884fc3452" - integrity sha512-BNw/XDgUfs8UhfY1X6IniU8kWpnotWGyt8qmQviaHisTi5lvwnaOdXQKfN1KGONx6ekdFRHRP5EFwLi0UajwKA== +react-native-opaque@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/react-native-opaque/-/react-native-opaque-0.3.1.tgz#a01672064ad0d9b12b0c07d18555ed872f39956c" + integrity sha512-DShK07S9uUquI0L9ErHvhudQOzoOrCQSdyieHrXCL2RMb2IXPhS3Qmb+av47vlHbYQQvhXRwd4fUAKGsa+vN1A== + dependencies: + "@serenity-kit/opaque" "^0.8.0" + +react-native-reanimated@~3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.10.1.tgz#3c37d1100bbba0065df39c96aab0c1ff1b50c0fa" + integrity sha512-sfxg6vYphrDc/g4jf/7iJ7NRi+26z2+BszPmvmk0Vnrz6FL7HYljJqTf531F1x6tFmsf+FEAmuCtTUIXFLVo9w== dependencies: "@babel/plugin-transform-arrow-functions" "^7.0.0-0" "@babel/plugin-transform-nullish-coalescing-operator" "^7.0.0-0" @@ -7163,10 +7151,10 @@ react-native-reanimated@~3.11.0: convert-source-map "^2.0.0" invariant "^2.2.4" -react-native-safe-area-context@4.10.3: - version "4.10.3" - resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.10.3.tgz#ad132b64e1e7cdd043b4e82a7a2449d99f4f7630" - integrity sha512-nW9B0fydpJSN798awtdslamYzRqDM/FIEh80ZPDEXVYpqYNsDLpz52pMtuKyhF5aOgJlfiroQrgdOxQNFtSM8A== +react-native-safe-area-context@4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.10.1.tgz#29fb27395ff7dfa2fa38788a27226330d73a81cc" + integrity sha512-w8tCuowDorUkPoWPXmhqosovBr33YsukkwYCDERZFHAxIkx6qBadYxfeoaJ91nCQKjkNzGrK5qhoNOeSIcYSpA== react-native-screens@3.31.1: version "3.31.1" @@ -7176,10 +7164,10 @@ react-native-screens@3.31.1: react-freeze "^1.0.0" warn-once "^0.1.0" -react-native-svg@15.3.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.3.0.tgz#e24b833fe330714c99f1dd894bb0da52ad859a4c" - integrity sha512-mBHu/fdlzUbpGX8SZFxgbKvK/sgqLfDLP8uh8G7Us+zJgdjO8OSEeqHQs+kPRdQmdLJQiqPJX2WXgCl7ToTWqw== +react-native-svg@15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.2.0.tgz#9561a6b3bd6b44689f437ba13182afee33bd5557" + integrity sha512-R0E6IhcJfVLsL0lRmnUSm72QO+mTqcAOM5Jb8FVGxJqX3NfJMlMP0YyvcajZiaRR8CqQUpEoqrY25eyZb006kw== dependencies: css-select "^5.1.0" css-tree "^1.1.3" @@ -7254,10 +7242,10 @@ react-shallow-renderer@^16.15.0: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" -react@18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== +react@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" @@ -7548,7 +7536,7 @@ scheduler@0.24.0-canary-efb381bbf-20230505: dependencies: loose-envify "^1.1.0" -scheduler@^0.23.2: +scheduler@^0.23.0: version "0.23.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== @@ -8471,11 +8459,16 @@ typedarray.prototype.slice@^1.0.3: typed-array-buffer "^1.0.2" typed-array-byte-offset "^1.0.2" -typescript@^5.4.5, typescript@~5.4.5: +typescript@^5.4.5: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@~5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + ua-parser-js@^1.0.35: version "1.0.38" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2"