diff --git a/package.json b/package.json index fbedb7fe2..1f7aee7d3 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/react-dom": "^18.0.0", "@uiw/codemirror-extensions-langs": "^4.21.9", "@uiw/react-codemirror": "^4.15.1", + "@xstate/react": "^3.2.2", "crypto-js": "^4.1.1", "formik": "^2.4.3", "http-server": "^14.1.1", @@ -42,6 +43,7 @@ "wasm-pack": "^0.12.1", "web-vitals": "^2.1.0", "ws": "^8.13.0", + "xstate": "^4.38.2", "zustand": "^4.1.4" }, "scripts": { diff --git a/src/App.test.tsx b/src/App.test.tsx index 9548e99b7..a943f1423 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react' import { App } from './App' import { describe, test, vi } from 'vitest' import { BrowserRouter } from 'react-router-dom' +import { GlobalStateProvider } from './hooks/useAuthMachine' let listener: ((rect: any) => void) | undefined = undefined ;(global as any).ResizeObserver = class ResizeObserver { @@ -27,9 +28,9 @@ describe('App tests', () => { } }) render( - + - + ) const linkElement = screen.getByText(/Variables/i) expect(linkElement).toBeInTheDocument() @@ -37,3 +38,12 @@ describe('App tests', () => { vi.restoreAllMocks() }) }) + +function TestWrap({ children }: { children: React.ReactNode }) { + // wrap in router and xState context + return ( + + {children} + + ) +} diff --git a/src/App.tsx b/src/App.tsx index 7ad832516..cd3326d4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ import { writeTextFile } from '@tauri-apps/api/fs' import { PROJECT_ENTRYPOINT } from './lib/tauriFS' import { IndexLoaderData } from './Router' import { toast } from 'react-hot-toast' +import { useAuthMachine } from './hooks/useAuthMachine' export function App() { const { code: loadedCode, project } = useLoaderData() as IndexLoaderData @@ -81,7 +82,6 @@ export function App() { isMouseDownInStream, cmdId, setCmdId, - token, formatCode, debugPanel, theme, @@ -121,7 +121,6 @@ export function App() { isMouseDownInStream: s.isMouseDownInStream, cmdId: s.cmdId, setCmdId: s.setCmdId, - token: s.token, formatCode: s.formatCode, debugPanel: s.debugPanel, addKCLError: s.addKCLError, @@ -134,6 +133,7 @@ export function App() { setStreamDimensions: s.setStreamDimensions, streamDimensions: s.streamDimensions, })) + const [token] = useAuthMachine((s) => s?.context?.token) const editorTheme = theme === Themes.System ? getSystemTheme() : theme diff --git a/src/Auth.tsx b/src/Auth.tsx index 8e672c1f9..b179ce4db 100644 --- a/src/Auth.tsx +++ b/src/Auth.tsx @@ -1,38 +1,11 @@ -import useSWR from 'swr' -import fetcher from './lib/fetcher' -import withBaseUrl from './lib/withBaseURL' -import { User, useStore } from './useStore' -import { useNavigate } from 'react-router-dom' -import { useEffect } from 'react' -import { isTauri } from './lib/isTauri' import Loading from './components/Loading' -import { paths } from './Router' +import { useAuthMachine } from './hooks/useAuthMachine' // Wrapper around protected routes, used in src/Router.tsx export const Auth = ({ children }: React.PropsWithChildren) => { - const { data: user, isLoading } = useSWR< - User | Partial<{ error_code: string }> - >(withBaseUrl('/user'), fetcher) - const { token, setUser } = useStore((s) => ({ - token: s.token, - setUser: s.setUser, - })) - const navigate = useNavigate() + const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn')) - useEffect(() => { - if (user && 'id' in user) setUser(user) - }, [user, setUser]) - - useEffect(() => { - if ( - (isTauri() && !token) || - (!isTauri() && !isLoading && !(user && 'id' in user)) - ) { - navigate(paths.SIGN_IN) - } - }, [user, token, navigate, isLoading]) - - return isLoading ? ( + return isLoggedIn ? ( Loading KittyCAD Modeling App... ) : ( <>{children} diff --git a/src/Router.tsx b/src/Router.tsx index efdd43135..7cfd8062f 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -24,6 +24,7 @@ import { } from './lib/tauriFS' import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api' import DownloadAppBanner from './components/DownloadAppBanner' +import { GlobalStateProvider } from './hooks/useAuthMachine' const prependRoutes = (routesObject: Record) => (prepend: string) => { @@ -58,124 +59,142 @@ export type HomeLoaderData = { projects: ProjectWithEntryPointMetadata[] } -const router = createBrowserRouter([ - { - path: paths.INDEX, - loader: () => - isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'), - }, - { - path: paths.FILE + '/:id', - element: ( - - - - {!isTauri() && import.meta.env.PROD && } - - ), - errorElement: , - id: paths.FILE, - loader: async ({ - request, - params, - }): Promise => { - const store = localStorage.getItem('store') - if (store === null) { - return redirect(paths.ONBOARDING.INDEX) - } else { - const status = JSON.parse(store).state.onboardingStatus || '' - const notEnRouteToOnboarding = - !request.url.includes(paths.ONBOARDING.INDEX) && - request.method === 'GET' - // '' is the initial state, 'done' and 'dismissed' are the final states - const hasValidOnboardingStatus = - (status !== undefined && status.length === 0) || - !(status === 'done' || status === 'dismissed') - const shouldRedirectToOnboarding = - notEnRouteToOnboarding && hasValidOnboardingStatus +type CreateBrowserRouterArg = Parameters[0] - if (shouldRedirectToOnboarding) { - return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status) +const addGlobalContextToElements = ( + routes: CreateBrowserRouterArg +): CreateBrowserRouterArg => + routes.map((route) => + 'element' in route + ? { + ...route, + element: {route.element}, } - } + : route + ) - if (params.id && params.id !== 'new') { - // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files - const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT) - const entrypoint_metadata = await metadata( - params.id + '/' + PROJECT_ENTRYPOINT - ) - const children = await readDir(params.id) +const router = createBrowserRouter( + addGlobalContextToElements([ + { + path: paths.INDEX, + loader: () => + isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'), + }, + { + path: paths.FILE + '/:id', + element: ( + + + + {!isTauri() && import.meta.env.PROD && } + + ), + errorElement: , + id: paths.FILE, + loader: async ({ + request, + params, + }): Promise => { + const store = localStorage.getItem('store') + if (store === null) { + return redirect(paths.ONBOARDING.INDEX) + } else { + const status = JSON.parse(store).state.onboardingStatus || '' + const notEnRouteToOnboarding = + !request.url.includes(paths.ONBOARDING.INDEX) && + request.method === 'GET' + // '' is the initial state, 'done' and 'dismissed' are the final states + const hasValidOnboardingStatus = + (status !== undefined && status.length === 0) || + !(status === 'done' || status === 'dismissed') + const shouldRedirectToOnboarding = + notEnRouteToOnboarding && hasValidOnboardingStatus + + if (shouldRedirectToOnboarding) { + return redirect( + makeUrlPathRelative(paths.ONBOARDING.INDEX) + status + ) + } + } + + if (params.id && params.id !== 'new') { + // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files + const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT) + const entrypoint_metadata = await metadata( + params.id + '/' + PROJECT_ENTRYPOINT + ) + const children = await readDir(params.id) + + return { + code, + project: { + name: params.id.slice(params.id.lastIndexOf('/') + 1), + path: params.id, + children, + entrypoint_metadata, + }, + } + } return { - code, - project: { - name: params.id.slice(params.id.lastIndexOf('/') + 1), - path: params.id, - children, - entrypoint_metadata, - }, + code: '', } - } - - return { - code: '', - } + }, + children: [ + { + path: makeUrlPathRelative(paths.SETTINGS), + element: , + }, + { + path: makeUrlPathRelative(paths.ONBOARDING.INDEX), + element: , + children: onboardingRoutes, + }, + ], }, - children: [ - { - path: makeUrlPathRelative(paths.SETTINGS), - element: , - }, - { - path: makeUrlPathRelative(paths.ONBOARDING.INDEX), - element: , - children: onboardingRoutes, - }, - ], - }, - { - path: paths.HOME, - element: ( - - - - - ), - loader: async () => { - if (!isTauri()) { - return redirect(paths.FILE + '/new') - } + { + path: paths.HOME, + element: ( + + + + + ), + loader: async () => { + if (!isTauri()) { + return redirect(paths.FILE + '/new') + } - const projectDir = await initializeProjectDirectory() - const projectsNoMeta = (await readDir(projectDir.dir)).filter( - isProjectDirectory - ) - const projects = await Promise.all( - projectsNoMeta.map(async (p) => ({ - entrypoint_metadata: await metadata( - p.path + '/' + PROJECT_ENTRYPOINT - ), - ...p, - })) - ) + const projectDir = await initializeProjectDirectory() + const projectsNoMeta = (await readDir(projectDir.dir)).filter( + isProjectDirectory + ) + const projects = await Promise.all( + projectsNoMeta.map(async (p) => ({ + entrypoint_metadata: await metadata( + p.path + '/' + PROJECT_ENTRYPOINT + ), + ...p, + })) + ) - return { - projects, - } + return { + projects, + } + }, + children: [ + { + path: makeUrlPathRelative(paths.SETTINGS), + element: , + }, + ], }, - children: [ - { - path: makeUrlPathRelative(paths.SETTINGS), - element: , - }, - ], - }, - { - path: paths.SIGN_IN, - element: , - }, -]) + { + path: paths.SIGN_IN, + element: , + }, + ]) +) /** * All routes in the app, used in src/index.tsx diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 056911ea6..d80a92b2a 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -1,8 +1,8 @@ import { Toolbar } from '../Toolbar' -import { useStore } from '../useStore' import UserSidebarMenu from './UserSidebarMenu' import { ProjectWithEntryPointMetadata } from '../Router' import ProjectSidebarMenu from './ProjectSidebarMenu' +import { useAuthMachine } from '../hooks/useAuthMachine' interface AppHeaderProps extends React.PropsWithChildren { showToolbar?: boolean @@ -18,9 +18,7 @@ export const AppHeader = ({ className = '', enableMenu = false, }: AppHeaderProps) => { - const { user } = useStore((s) => ({ - user: s.user, - })) + const [user] = useAuthMachine((s) => s?.context?.user) return (
{ test("Renders user's name and email if available", () => { @@ -12,12 +15,18 @@ describe('UserSidebarMenu tests', () => { image: 'https://placekitten.com/200/200', created_at: 'yesteryear', updated_at: 'today', + company: 'Test Company', + discord: 'Test User#1234', + github: 'testuser', + phone: '555-555-5555', + first_name: 'Test', + last_name: 'User', } render( - + - + ) fireEvent.click(screen.getByTestId('user-sidebar-toggle')) @@ -35,12 +44,19 @@ describe('UserSidebarMenu tests', () => { image: 'https://placekitten.com/200/200', created_at: 'yesteryear', updated_at: 'today', + company: 'Test Company', + discord: 'Test User#1234', + github: 'testuser', + phone: '555-555-5555', + first_name: '', + last_name: '', + name: '', } render( - + - + ) fireEvent.click(screen.getByTestId('user-sidebar-toggle')) @@ -55,14 +71,30 @@ describe('UserSidebarMenu tests', () => { email: 'kittycad.sidebar.test@example.com', created_at: 'yesteryear', updated_at: 'today', + company: 'Test Company', + discord: 'Test User#1234', + github: 'testuser', + phone: '555-555-5555', + first_name: 'Test', + last_name: 'User', + image: '', } render( - + - + ) expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu') }) }) + +function TestWrap({ children }: { children: React.ReactNode }) { + // wrap in router and xState context + return ( + + {children} + + ) +} diff --git a/src/components/UserSidebarMenu.tsx b/src/components/UserSidebarMenu.tsx index 75d46fff7..9134c949c 100644 --- a/src/components/UserSidebarMenu.tsx +++ b/src/components/UserSidebarMenu.tsx @@ -1,5 +1,4 @@ import { Popover } from '@headlessui/react' -import { User, useStore } from '../useStore' import { ActionButton } from './ActionButton' import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons' @@ -7,14 +6,16 @@ import { useNavigate } from 'react-router-dom' import { useState } from 'react' import { paths } from '../Router' import makeUrlPathRelative from '../lib/makeUrlPathRelative' +import { useAuthMachine } from '../hooks/useAuthMachine' +import { Models } from '@kittycad/lib' + +type User = Models['User_type'] const UserSidebarMenu = ({ user }: { user?: User }) => { const displayedName = getDisplayName(user) const [imageLoadFailed, setImageLoadFailed] = useState(false) const navigate = useNavigate() - const { setToken } = useStore((s) => ({ - setToken: s.setToken, - })) + const [_, send] = useAuthMachine() // Fallback logic for displaying user's "name": // 1. user.name @@ -119,10 +120,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { { - setToken('') - navigate(paths.SIGN_IN) - }} + onClick={() => send('logout')} icon={{ icon: faSignOutAlt, bgClassName: 'bg-destroy-80', diff --git a/src/hooks/useAuthMachine.tsx b/src/hooks/useAuthMachine.tsx new file mode 100644 index 000000000..11c07c458 --- /dev/null +++ b/src/hooks/useAuthMachine.tsx @@ -0,0 +1,54 @@ +import { createActorContext } from '@xstate/react' +import { useNavigate } from 'react-router-dom' +import { paths } from '../Router' +import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine' +import withBaseUrl from '../lib/withBaseURL' + +export const AuthMachineContext = createActorContext(authMachine) + +export const GlobalStateProvider = ({ + children, +}: { + children: React.ReactNode +}) => { + const navigate = useNavigate() + return ( + + authMachine.withConfig({ + actions: { + goToSignInPage: () => { + navigate(paths.SIGN_IN) + logout() + }, + goToIndexPage: () => navigate(paths.INDEX), + }, + }) + } + > + {children} + + ) +} + +export function useAuthMachine( + selector: ( + state: Parameters[0]>[0] + ) => T = () => null as T +): [T, ReturnType[1]] { + // useActor api normally `[state, send] = useActor` + // we're only interested in send because of the selector + const send = AuthMachineContext.useActor()[1] + + const selection = AuthMachineContext.useSelector(selector) + return [selection, send] +} + +export function logout() { + const url = withBaseUrl('/logout') + localStorage.removeItem(TOKEN_PERSIST_KEY) + return fetch(url, { + method: 'POST', + credentials: 'include', + }) +} diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index c7795ba3d..bf98fafcf 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -363,7 +363,6 @@ export class EngineCommandManager { this.onHoverCallback = callback } onClick(callback: (selection?: SelectionsArgs) => void) { - // TODO talk to the gang about this // It's when the user clicks on a part in the 3d scene, and so the engine should tell the // frontend about that (with it's id) so that the FE can put the user's cursor on the right // line of code diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index 55a987cff..61de8820b 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -136,11 +136,6 @@ export const lineTo: SketchLineHelper = { sourceRange, data, }) - // engineCommandManager.sendModellingCommand({ - // id, - // params: [lineData, previousSketch], - // range: sourceRange, - // }) const currentPath: Path = { type: 'toPoint', to, @@ -673,11 +668,6 @@ export const angledLine: SketchLineHelper = { sourceRange, data, }) - // engineCommandManager.sendModellingCommand({ - // id, - // params: [lineData, previousSketch], - // range: sourceRange, - // }) const currentPath: Path = { type: 'toPoint', to, diff --git a/src/lib/authMachine.ts b/src/lib/authMachine.ts new file mode 100644 index 000000000..fc37fa22b --- /dev/null +++ b/src/lib/authMachine.ts @@ -0,0 +1,102 @@ +import { createMachine, assign } from 'xstate' +import { Models } from '@kittycad/lib' +import withBaseURL from '../lib/withBaseURL' + +export interface UserContext { + user?: Models['User_type'] + token?: string +} + +export type Events = + | { + type: 'logout' + } + | { + type: 'tryLogin' + token?: string + } + +export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' +const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' + +export const authMachine = createMachine( + { + id: 'Auth', + initial: 'checkIfLoggedIn', + states: { + checkIfLoggedIn: { + id: 'check-if-logged-in', + invoke: { + src: 'getUser', + id: 'check-logged-in', + onDone: [ + { + target: 'loggedIn', + actions: assign({ + user: (context, event) => event.data, + }), + }, + ], + onError: [ + { + target: 'loggedOut', + actions: assign({ + user: () => undefined, + }), + }, + ], + }, + }, + loggedIn: { + entry: ['goToIndexPage'], + on: { + logout: { + target: 'loggedOut', + }, + }, + }, + loggedOut: { + entry: ['goToSignInPage'], + on: { + tryLogin: { + target: 'checkIfLoggedIn', + actions: assign({ + token: (context, event) => { + const token = event.token || '' + localStorage.setItem(TOKEN_PERSIST_KEY, token) + return token + }, + }), + }, + }, + }, + }, + schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } }, + predictableActionArguments: true, + preserveActionOrder: true, + context: { token: persistedToken }, + }, + { + actions: {}, + services: { getUser }, + guards: {}, + delays: {}, + } +) + +async function getUser(context: UserContext) { + const url = withBaseURL('/user') + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + } + if (!context.token && '__TAURI__' in window) throw 'not log in' + if (context.token) headers['Authorization'] = `Bearer ${context.token}` + const response = await fetch(url, { + method: 'GET', + credentials: 'include', + headers, + }) + const user = await response.json() + if ('error_code' in user) throw new Error(user.message) + return user +} diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index dd9c6b34c..4bedb6928 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -1,10 +1,10 @@ -import { useStore } from '../useStore' +import { useAuthMachine } from '../hooks/useAuthMachine' export default async function fetcher( input: RequestInfo, init: RequestInit = {} ): Promise { - const { token } = useStore.getState() + const [token] = useAuthMachine((s) => s?.context?.token) const headers = { ...init.headers } as Record if (token) { headers.Authorization = `Bearer ${token}` diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index de78b346f..843f247a5 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -7,13 +7,14 @@ import { useNavigate } from 'react-router-dom' import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env' import { getSystemTheme } from '../lib/getSystemTheme' import { paths } from '../Router' +import { useAuthMachine } from '../hooks/useAuthMachine' const SignIn = () => { const navigate = useNavigate() - const { setToken, theme } = useStore((s) => ({ - setToken: s.setToken, + const { theme } = useStore((s) => ({ theme: s.theme, })) + const [_, send] = useAuthMachine() const appliedTheme = theme === Themes.System ? getSystemTheme() : theme const signInTauri = async () => { // We want to invoke our command to login via device auth. @@ -21,8 +22,7 @@ const SignIn = () => { const token: string = await invoke('login', { host: VITE_KC_API_BASE_URL, }) - setToken(token) - navigate(paths.INDEX) + send({ type: 'tryLogin', token }) } catch (error) { console.error('login button', error) } diff --git a/src/useStore.ts b/src/useStore.ts index abf49fe6b..a8ecb1d52 100644 --- a/src/useStore.ts +++ b/src/useStore.ts @@ -114,20 +114,6 @@ interface DefaultDir { export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs' -// TODO: import real OpenAPI User type from schema -export interface User { - company?: string - created_at: string - email: string - first_name?: string - id: string - image?: string - last_name?: string - name?: string - phone?: string - updated_at: string -} - export interface StoreState { editorView: EditorView | null setEditorView: (editorView: EditorView) => void @@ -219,10 +205,6 @@ export interface StoreState { path: string }[] setHomeMenuItems: (items: { name: string; path: string }[]) => void - token: string - setToken: (token: string) => void - user?: User - setUser: (user: User | undefined) => void debugPanel: boolean setDebugPanel: (debugPanel: boolean) => void } @@ -423,10 +405,6 @@ export const useStore = create()( setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }), homeMenuItems: [], setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }), - token: '', - setToken: (token) => set({ token }), - user: undefined, - setUser: (user) => set({ user }), debugPanel: false, setDebugPanel: (debugPanel) => set({ debugPanel }), }), @@ -441,7 +419,6 @@ export const useStore = create()( 'defaultProjectName', 'defaultUnitSystem', 'defaultBaseUnit', - 'token', 'debugPanel', 'onboardingStatus', 'theme', diff --git a/yarn.lock b/yarn.lock index c5ebd3533..ff074d0bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2506,6 +2506,14 @@ loupe "^2.3.6" pretty-format "^29.5.0" +"@xstate/react@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8" + integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ== + dependencies: + use-isomorphic-layout-effect "^1.1.2" + use-sync-external-store "^1.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -5893,7 +5901,7 @@ use-composed-ref@^1.3.0: resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== -use-isomorphic-layout-effect@^1.1.1: +use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== @@ -5905,7 +5913,7 @@ use-latest@^1.2.1: dependencies: use-isomorphic-layout-effect "^1.1.1" -use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -6112,6 +6120,11 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +xstate@^4.38.2: + version "4.38.2" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804" + integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"