From bf8f449f0347384b13f5c444c917f760ad4459d0 Mon Sep 17 00:00:00 2001 From: Alois Date: Fri, 24 Jul 2026 12:18:05 +0200 Subject: [PATCH] (wip): add onboarding (fix): login page reloading --- apps/web/package.json | 1 + .../web/src/components/screens/login/form.tsx | 11 +- apps/web/src/features/legal/screen.tsx | 271 ------------------ apps/web/src/index.tsx | 52 ++-- apps/web/vite.config.ts | 1 + packages/call/src/components/buttons/deaf.tsx | 4 +- .../call/src/components/buttons/leave.tsx | 4 +- packages/call/src/components/buttons/mute.tsx | 4 +- .../src/components/buttons/tooltipButton.tsx | 22 -- packages/onboarding/package.json | 23 ++ packages/onboarding/src/index.tsx | 178 ++++++++++++ packages/onboarding/src/pages/legal.tsx | 98 +++++++ packages/onboarding/src/pages/onboarding.tsx | 9 + packages/onboarding/src/steps.tsx | 10 + packages/onboarding/tsconfig.json | 12 + packages/settings/src/components.tsx | 111 +++---- packages/shared/src/data.ts | 2 + pnpm-lock.yaml | 85 ++++-- pnpm-workspace.yaml | 2 +- 19 files changed, 461 insertions(+), 439 deletions(-) delete mode 100644 apps/web/src/features/legal/screen.tsx create mode 100644 packages/onboarding/package.json create mode 100644 packages/onboarding/src/index.tsx create mode 100644 packages/onboarding/src/pages/legal.tsx create mode 100644 packages/onboarding/src/pages/onboarding.tsx create mode 100644 packages/onboarding/src/steps.tsx create mode 100644 packages/onboarding/tsconfig.json diff --git a/apps/web/package.json b/apps/web/package.json index dfa243a..8fc3851 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -63,6 +63,7 @@ "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "@tensamin/notifications": "workspace:*", + "@tensamin/onboarding": "workspace:*", "aria-hidden": "^1.2.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index 89a0d74..d97933c 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -8,6 +8,7 @@ import * as React from "react"; import { z } from "zod"; import { isTauri } from "@tauri-apps/api/core"; import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; +import { useNavigate } from "@tanstack/react-router"; const fetchedUser = z.object({ id: z.uuidv4(), @@ -80,6 +81,7 @@ export default function Form() { const uploadRef = React.useRef(null); const [isDragging, setIsDragging] = React.useState(false); const { save } = useStorage(); + const navigate = useNavigate(); const loginPendingRef = React.useRef(false); const persistLogin = React.useCallback( @@ -91,18 +93,13 @@ export default function Form() { await save("mtp_keyring", privateKey, { secure: true }); await save("session_id", Date.now()); await save("user_id", userId); - window.history.replaceState( - null, - "", - window.location.protocol === "file:" ? "#/" : "/", - ); - window.location.reload(); + await navigate({ to: "/", replace: true }); return true; } finally { loginPendingRef.current = false; } }, - [save], + [navigate, save], ); // Process dropped files diff --git a/apps/web/src/features/legal/screen.tsx b/apps/web/src/features/legal/screen.tsx deleted file mode 100644 index 48bbb48..0000000 --- a/apps/web/src/features/legal/screen.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useStorage } from "@tensamin/storage/context"; -import { - Button, - Checkbox, - CreateScreen, - ErrorScreen, - Label, - Link, - Spinner, -} from "@tensamin/ui"; -import { z } from "zod"; - -import { legalDocsSchema } from "@tensamin/shared/features/legal/schema"; -import { log } from "@tensamin/shared/log"; - -// Prevents the user from using Tensamin without accepting the privacy policy and terms of service. -export default function Screen(props: { children: React.ReactNode }) { - const { load, save } = useStorage(); - - const [error, setError] = useState(""); - const [errorDescription, setErrorDescription] = useState(""); - - const [remoteDocs, setRemoteDocs] = useState< - z.infer | undefined - >(undefined); - const [localDocs, setLocalDocs] = useState< - z.infer | undefined - >(undefined); - - const [loading, setLoading] = useState(true); - const [showLoading, setShowLoading] = useState(false); - - const [acceptedPP, acceptPP] = useState(false); - const [acceptedTOS, acceptTOS] = useState(false); - const [hasContinued, setHasContinued] = useState(false); - - const [userId, setUserId] = useState(undefined); - - // Saves - const handleContinueLegal = useCallback((): void => { - const currentDocs = remoteDocs; - if (!currentDocs) { - return; - } - - save("accepted_privacy_policy", true); - save("accepted_terms_of_service", true); - save("legal_docs", currentDocs); - setHasContinued(true); - }, [remoteDocs, save]); - - useEffect(() => { - let active = true; - let loadingTimer: ReturnType | undefined; - - void (async () => { - try { - const id = await load("user_id"); - if (!active) { - return; - } - - setUserId(id); - - if (id === 0) { - return; - } - - loadingTimer = setTimeout(() => { - if (active) setShowLoading(true); - }, 200); - - const response = await fetch("https://legal.tensamin.net/api/current"); - if (!response.ok) { - throw new Error(`Legal documents request failed: ${response.status}`); - } - const current: unknown = await response.json(); - - if (!active) { - return; - } - - const safeCurrent = legalDocsSchema.safeParse(current); - if (!safeCurrent.success) { - setError("Failed to load legal documents"); - setErrorDescription( - "The legal documents data received from the server is invalid. Please try again later.", - ); - log( - 0, - "Legal", - "red", - "Invalid legal documents data", - safeCurrent.error, - ); - return; - } - - setRemoteDocs(safeCurrent.data); - - const currentLocalDocs = await load("legal_docs"); - setLocalDocs(currentLocalDocs); - - const [loadedAcceptedPP, loadedAcceptedTOS] = await Promise.all([ - load("accepted_privacy_policy"), - load("accepted_terms_of_service"), - ]); - - if (!active) { - return; - } - - acceptPP( - loadedAcceptedPP && - !!currentLocalDocs && - currentLocalDocs.pp.hash === safeCurrent.data.pp.hash, - ); - acceptTOS( - loadedAcceptedTOS && - !!currentLocalDocs && - currentLocalDocs.tos.hash === safeCurrent.data.tos.hash, - ); - } catch (err) { - if (!active) return; - - setError("Failed to load legal documents"); - setErrorDescription( - "An error occurred while fetching the legal documents from the server. Please try again later.", - ); - log(0, "Legal", "red", "Failed to fetch legal documents", err); - } finally { - clearTimeout(loadingTimer); - if (active) { - setLoading(false); - } - } - })(); - - return () => { - active = false; - clearTimeout(loadingTimer); - }; - }, [load]); - - if (error !== "" && errorDescription !== "") { - return ; - } - - if (loading || userId === undefined) { - if (!showLoading) return null; - - return ( - -
- -

- Fetching legal documents... -

- -
-
- ); - } - - const docsMatch = - localDocs !== undefined && - remoteDocs !== undefined && - localDocs.pp.hash === remoteDocs.pp.hash && - localDocs.tos.hash === remoteDocs.tos.hash; - - if ( - (acceptedPP && acceptedTOS && (docsMatch || hasContinued)) || - userId === 0 - ) { - return <>{props.children}; - } - - return ( - -
-

- Privacy Policy & ToS -

- {remoteDocs?.pp.version} / {remoteDocs?.tos.version} -

-

-
-
- - -
- - -
-
- -
- - ); -} - -// Components -function ContinueButton({ - onClick, - disabled, -}: { - onClick: () => void; - disabled?: boolean; -}) { - return ( -
- -
- ); -} - -function BigCheckbox({ - id, - label, - checked, - onChange, -}: { - id: string; - label: string; - checked: boolean; - onChange: (checked: boolean) => void; -}) { - return ( -
- - -
- ); -} diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index f59bfc5..f48f7f9 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -15,6 +15,7 @@ import NotFound from "@/routes/404"; import AppLayout from "@/routes/app/layout"; import { createSettingsRoute } from "@tensamin/settings"; +import OnboardingGate from "@tensamin/onboarding"; import Home from "@/routes/app/home"; import ChatScreen from "@tensamin/chat/screen"; @@ -43,7 +44,6 @@ import Crypto from "@tensamin/crypto/context"; import DesktopMediaProvider from "@tensamin/shared/desktopMedia"; import { log } from "@tensamin/shared/log"; -import LegalWrapper from "@/features/legal/screen"; import CacheSync from "@tensamin/cache/sync"; import { useStorage } from "@tensamin/storage/context"; @@ -244,13 +244,7 @@ function RootShell() { - - - - - - - + @@ -262,24 +256,30 @@ function RootShell() { function AppShell() { return ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 3e05688..b8fc6da 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -136,6 +136,7 @@ export default defineConfig({ "@tensamin/markdown", "@tensamin/mtp", "@tensamin/notifications", + "@tensamin/onboarding", "@tensamin/shared", "@tensamin/shared/data", "@tensamin/shared/log", diff --git a/packages/call/src/components/buttons/deaf.tsx b/packages/call/src/components/buttons/deaf.tsx index 6d0ede0..615deab 100644 --- a/packages/call/src/components/buttons/deaf.tsx +++ b/packages/call/src/components/buttons/deaf.tsx @@ -1,7 +1,7 @@ -import { Button } from "@tensamin/ui"; +import { Button, withTooltip } from "@tensamin/ui"; import { toggleDeaf, useCall } from "../../store"; import { HeadphoneOff, Headphones } from "lucide-react"; -import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; +import { type CallButtonProps, iconScale } from "./tooltipButton"; export default function DeafButton({ className, diff --git a/packages/call/src/components/buttons/leave.tsx b/packages/call/src/components/buttons/leave.tsx index d4f0371..57c953b 100644 --- a/packages/call/src/components/buttons/leave.tsx +++ b/packages/call/src/components/buttons/leave.tsx @@ -1,7 +1,7 @@ import { LeaveIcon } from "@livekit/components-react"; -import { Button } from "@tensamin/ui"; +import { Button, withTooltip } from "@tensamin/ui"; import { disconnect, useCall } from "../../store"; -import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; +import { type CallButtonProps, iconScale } from "./tooltipButton"; export default function LeaveButton({ className, diff --git a/packages/call/src/components/buttons/mute.tsx b/packages/call/src/components/buttons/mute.tsx index 3067a2b..8ae3f1b 100644 --- a/packages/call/src/components/buttons/mute.tsx +++ b/packages/call/src/components/buttons/mute.tsx @@ -1,7 +1,7 @@ -import { Button } from "@tensamin/ui"; +import { Button, withTooltip } from "@tensamin/ui"; import { toggleMute, useCall } from "../../store"; import { Mic, MicOff } from "lucide-react"; -import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; +import { type CallButtonProps, iconScale } from "./tooltipButton"; export default function MuteButton({ className, diff --git a/packages/call/src/components/buttons/tooltipButton.tsx b/packages/call/src/components/buttons/tooltipButton.tsx index 72110f8..f33264a 100644 --- a/packages/call/src/components/buttons/tooltipButton.tsx +++ b/packages/call/src/components/buttons/tooltipButton.tsx @@ -1,6 +1,3 @@ -import { type ReactElement } from "react"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; - export type CallButtonProps = { className?: string; iconSize?: number; @@ -11,22 +8,3 @@ export type CallButtonProps = { export function iconScale(iconSize?: number) { return { scale: (iconSize ? iconSize + 100 : 100) + "%" }; } - -export function withTooltip( - button: ReactElement, - tooltip?: string, - portalContainer?: HTMLElement, -) { - if (!tooltip) { - return button; - } - - return ( - - - - {tooltip} - - - ); -} diff --git a/packages/onboarding/package.json b/packages/onboarding/package.json new file mode 100644 index 0000000..251f850 --- /dev/null +++ b/packages/onboarding/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tensamin/onboarding", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./src/index.tsx" + }, + "scripts": { + "format": "pnpm exec prettier --write .", + "lint": "eslint src", + "build": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@tensamin/shared": "workspace:*", + "@tensamin/storage": "workspace:*", + "@tensamin/ui": "*", + "lucide-react": "^1.14.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "zod": "^4.3.6" + } +} diff --git a/packages/onboarding/src/index.tsx b/packages/onboarding/src/index.tsx new file mode 100644 index 0000000..a37ec9c --- /dev/null +++ b/packages/onboarding/src/index.tsx @@ -0,0 +1,178 @@ +import { useCallback, useEffect, useState, type ReactNode } from "react"; +import { + CreateScreen, + ErrorScreen, + Link, + OnboardingFlow, + Spinner, + type OnboardingStep, +} from "@tensamin/ui"; +import { useStorage } from "@tensamin/storage/context"; +import { legalDocsSchema } from "@tensamin/shared/features/legal/schema"; +import { log } from "@tensamin/shared/log"; +import type { z } from "zod"; + +import LegalPage from "./pages/legal"; +import { onboardingSteps } from "./steps"; + +export { + useOnboardingStep, + type OnboardingStep, + type OnboardingStepControls, +} from "@tensamin/ui"; + +type LegalDocs = z.infer; + +interface GateState { + docs: LegalDocs; + acceptedPP: boolean; + acceptedTOS: boolean; + includeLegal: boolean; + includeOnboarding: boolean; +} + +export default function OnboardingGate({ children }: { children: ReactNode }) { + const { load, save } = useStorage(); + const [state, setState] = useState(); + const [complete, setComplete] = useState(false); + const [showLoading, setShowLoading] = useState(false); + const [error, setError] = useState(""); + const [errorDescription, setErrorDescription] = useState(""); + + useEffect(() => { + let active = true; + const loadingTimer = setTimeout(() => { + if (active) setShowLoading(true); + }, 200); + + void (async () => { + try { + const response = await fetch("https://legal.tensamin.net/api/current"); + if (!response.ok) { + throw new Error(`Legal documents request failed: ${response.status}`); + } + + const current: unknown = await response.json(); + if (!active) return; + + const parsed = legalDocsSchema.safeParse(current); + if (!parsed.success) { + setError("Failed to load legal documents"); + setErrorDescription( + "The legal documents data received from the server is invalid. Please try again later.", + ); + log(0, "Legal", "red", "Invalid legal documents data", parsed.error); + return; + } + + const [localDocs, acceptedPP, acceptedTOS, onboardingDone] = + await Promise.all([ + load("legal_docs"), + load("accepted_privacy_policy"), + load("accepted_terms_of_service"), + load("onboarding_done"), + ]); + + if (!active) return; + + const currentAcceptedPP = + acceptedPP && localDocs.pp.hash === parsed.data.pp.hash; + const currentAcceptedTOS = + acceptedTOS && localDocs.tos.hash === parsed.data.tos.hash; + const existingUser = acceptedPP && acceptedTOS; + const includeOnboarding = !onboardingDone && !existingUser; + + if (!onboardingDone && existingUser) { + await save("onboarding_done", true); + if (!active) return; + } + + setState({ + docs: parsed.data, + acceptedPP: currentAcceptedPP, + acceptedTOS: currentAcceptedTOS, + includeLegal: !currentAcceptedPP || !currentAcceptedTOS, + includeOnboarding, + }); + } catch (caught) { + if (!active) return; + setError("Failed to load legal documents"); + setErrorDescription( + "An error occurred while fetching the legal documents from the server. Please try again later.", + ); + log(0, "Legal", "red", "Failed to fetch legal documents", caught); + } finally { + clearTimeout(loadingTimer); + } + })(); + + return () => { + active = false; + clearTimeout(loadingTimer); + }; + }, [load, save]); + + const acceptLegal = useCallback(async () => { + if (!state) return; + await Promise.all([ + save("accepted_privacy_policy", true), + save("accepted_terms_of_service", true), + save("legal_docs", state.docs), + ]); + setState((current) => + current ? { ...current, acceptedPP: true, acceptedTOS: true } : current, + ); + }, [save, state]); + + const finish = useCallback(async () => { + if (state?.includeOnboarding) { + await save("onboarding_done", true); + } + setComplete(true); + }, [save, state?.includeOnboarding]); + + if (error && errorDescription) { + return ; + } + + if (!state) { + if (!showLoading) return null; + return ( + +
+ +

+ Fetching legal documents... +

+ +
+
+ ); + } + + const steps: OnboardingStep[] = []; + if (state.includeLegal) { + steps.push({ + id: "legal", + defaultCanContinue: false, + content: ( + + ), + }); + } + if (state.includeOnboarding) { + steps.push(...onboardingSteps); + } + + if (complete || steps.length === 0) return <>{children}; + + return ; +} diff --git a/packages/onboarding/src/pages/legal.tsx b/packages/onboarding/src/pages/legal.tsx new file mode 100644 index 0000000..3dec522 --- /dev/null +++ b/packages/onboarding/src/pages/legal.tsx @@ -0,0 +1,98 @@ +import { useCallback, useState } from "react"; +import { Checkbox, Label, Link } from "@tensamin/ui"; +import { legalDocsSchema } from "@tensamin/shared/features/legal/schema"; +import type { z } from "zod"; + +import { useOnboardingStep } from "@tensamin/ui"; + +type LegalDocs = z.infer; + +export default function LegalPage({ + docs, + initiallyAcceptedPP, + initiallyAcceptedTOS, + onAccept, +}: { + docs: LegalDocs; + initiallyAcceptedPP: boolean; + initiallyAcceptedTOS: boolean; + onAccept: () => Promise; +}) { + const [acceptedPP, setAcceptedPP] = useState(initiallyAcceptedPP); + const [acceptedTOS, setAcceptedTOS] = useState(initiallyAcceptedTOS); + + const handleContinue = useCallback(async () => { + if (!acceptedPP || !acceptedTOS) return false; + await onAccept(); + }, [acceptedPP, acceptedTOS, onAccept]); + + useOnboardingStep({ + canContinue: acceptedPP && acceptedTOS, + onContinue: handleContinue, + }); + + return ( +
+
+

+ Privacy Policy & ToS +

+

+ {docs.pp.version} / {docs.tos.version} +

+
+ +
+
+ + +
+ + +
+
+
+ ); +} + +function BigCheckbox({ + id, + label, + checked, + onChange, +}: { + id: string; + label: string; + checked: boolean; + onChange: (checked: boolean) => void; +}) { + return ( +
+ + +
+ ); +} diff --git a/packages/onboarding/src/pages/onboarding.tsx b/packages/onboarding/src/pages/onboarding.tsx new file mode 100644 index 0000000..5a7a96b --- /dev/null +++ b/packages/onboarding/src/pages/onboarding.tsx @@ -0,0 +1,9 @@ +export default function OnboardingPage() { + return ( +
+

+ Onboarding +

+
+ ); +} diff --git a/packages/onboarding/src/steps.tsx b/packages/onboarding/src/steps.tsx new file mode 100644 index 0000000..a134a77 --- /dev/null +++ b/packages/onboarding/src/steps.tsx @@ -0,0 +1,10 @@ +import type { OnboardingStep } from "@tensamin/ui"; +import OnboardingPage from "./pages/onboarding"; + +// Add future onboarding pages here. Completed users skip this entire manifest. +export const onboardingSteps: OnboardingStep[] = [ + { + id: "onboarding", + content: , + }, +]; diff --git a/packages/onboarding/tsconfig.json b/packages/onboarding/tsconfig.json new file mode 100644 index 0000000..9714625 --- /dev/null +++ b/packages/onboarding/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/packages/settings/src/components.tsx b/packages/settings/src/components.tsx index 201d4dd..0af15fe 100644 --- a/packages/settings/src/components.tsx +++ b/packages/settings/src/components.tsx @@ -1,14 +1,4 @@ -import { - Button, - Checkbox, - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, - Input, - Label, - Switch as UISwitch, -} from "@tensamin/ui"; +import { EditableList, LabeledSwitch } from "@tensamin/ui"; import { useEffect, useState } from "react"; import { storageDefaults, type Storage } from "@tensamin/shared/data"; @@ -24,9 +14,12 @@ type ListStorageKey = { }[keyof Storage]; type ListStorageItem = - Storage[K] extends Array ? Item : never; + Storage[K] extends Array ? Item & (string | number) : never; -export function Switch({ label, id }: { +export function Switch({ + label, + id, +}: { label: React.ReactNode; id: keyof typeof settingsStorageDefaults & BooleanStorageKey; }) { @@ -38,91 +31,55 @@ export function Switch({ label, id }: { }, [id, load]); return ( -
- { + { setValue(nextValue); save(id, nextValue); - }} /> - -
+ }} + /> ); } -export function List({ label, id }: { +export function List({ + label, + id, +}: { label: React.ReactNode; id: K; }) { const { save, load } = useStorage(); const [items, setItems] = useState(storageDefaults[id]); - const [inputValue, setInputValue] = useState(""); - const [selectedItems, setSelectedItems] = useState>(new Set()); useEffect(() => { load(id).then((value) => setItems(value)); }, [id, load]); - const persistItems = (nextItems: Storage[K]) => { - setItems(nextItems); - save(id, nextItems); + const persistItems = (nextItems: ListStorageItem[]) => { + setItems(nextItems as Storage[K]); + save(id, nextItems as Storage[K]); }; const toStorageItem = (value: string): ListStorageItem => { const referenceItem = items[0] ?? storageDefaults[id][0]; - return (typeof referenceItem === "number" ? Number(value) : value) as ListStorageItem; - }; - - const addItem = () => { - const trimmedValue = inputValue.trim(); - if (!trimmedValue) return; - const nextItem = toStorageItem(trimmedValue); - if (typeof nextItem === "number" && Number.isNaN(nextItem)) return; - persistItems([...items, nextItem] as Storage[K]); - setInputValue(""); - }; - - const deleteItems = (indexes: Set) => { - const nextItems = items.filter((_, index) => !indexes.has(index)) as Storage[K]; - setItems(nextItems); - setSelectedItems(new Set()); - save(id, nextItems); + return ( + typeof referenceItem === "number" ? Number(value) : value + ) as ListStorageItem; }; return ( -
- -
-
- setInputValue(event.target.value)} onKeyDown={(event) => { - if (event.key === "Enter") addItem(); - }} /> - -
-
- {items.map((item, index) => { - const labelId = `${String(id)}-${index}`; - const selected = selectedItems.has(index); - const deletingSelectedItems = selectedItems.size > 1; - return ( - - - setSelectedItems((previous) => { - const nextSelected = new Set(previous); - if (checked) nextSelected.add(index); - else nextSelected.delete(index); - return nextSelected; - })} /> -
-
} /> - - deleteItems(deletingSelectedItems ? selectedItems : new Set([index]))}> - {deletingSelectedItems ? "Delete Selected" : "Delete"} - - -
- ); - })} -
-
-
+ > + label={label} + items={items as ListStorageItem[]} + parseItem={(value) => { + const item = toStorageItem(value); + return typeof item === "number" && Number.isNaN(item) + ? undefined + : item; + }} + onItemsChange={persistItems} + /> ); } diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index b810ad6..6ba7141 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -421,6 +421,7 @@ export interface Storage extends SettingsStorageDefaults { session_id: number; user_id: number; mtp_keyring: string; + onboarding_done: boolean; ppandtos_done: boolean; accepted_terms_of_service: boolean; accepted_privacy_policy: boolean; @@ -458,6 +459,7 @@ export const storageDefaults: Storage = { session_id: 0, user_id: 0, mtp_keyring: "", + onboarding_done: false, ppandtos_done: false, accepted_terms_of_service: false, accepted_privacy_policy: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f8c78f..44c6698 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - '@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz + '@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz importers: @@ -13,8 +13,8 @@ importers: .: dependencies: '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) mtp: specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz @@ -107,8 +107,8 @@ importers: specifier: workspace:* version: link:../../packages/shared '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) react: specifier: ^19.2.0 version: 19.2.7 @@ -257,6 +257,9 @@ importers: '@tensamin/notifications': specifier: workspace:* version: link:../../packages/notifications + '@tensamin/onboarding': + specifier: workspace:* + version: link:../../packages/onboarding '@tensamin/settings': specifier: workspace:* version: link:../../packages/settings @@ -273,8 +276,8 @@ importers: specifier: workspace:* version: link:../../packages/tauth '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@tensamin/user': specifier: workspace:* version: link:../../packages/user @@ -547,8 +550,8 @@ importers: specifier: workspace:* version: link:../storage '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@tensamin/user': specifier: workspace:* version: link:../user @@ -610,8 +613,8 @@ importers: specifier: workspace:* version: link:../storage '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@tensamin/user': specifier: workspace:* version: link:../user @@ -661,8 +664,8 @@ importers: specifier: ^6.41.1 version: 6.43.4 '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@twemoji/api': specifier: ^17.0.3 version: 17.0.3 @@ -691,8 +694,8 @@ importers: specifier: workspace:* version: link:../storage '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) lucide-react: specifier: ^1.14.0 version: 1.23.0(react@19.2.7) @@ -740,8 +743,8 @@ importers: specifier: workspace:* version: link:../storage '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@tensamin/user': specifier: workspace:* version: link:../user @@ -758,6 +761,30 @@ importers: specifier: ^4.3.6 version: 4.4.3 + packages/onboarding: + dependencies: + '@tensamin/shared': + specifier: workspace:* + version: link:../shared + '@tensamin/storage': + specifier: workspace:* + version: link:../storage + '@tensamin/ui': + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + lucide-react: + specifier: ^1.14.0 + version: 1.23.0(react@19.2.7) + react: + specifier: ^19.2.0 + version: 19.2.7 + react-dom: + specifier: ^19.2.0 + version: 19.2.7(react@19.2.7) + zod: + specifier: ^4.3.6 + version: 4.4.3 + packages/settings: dependencies: '@tanstack/react-router': @@ -779,8 +806,8 @@ importers: specifier: workspace:* version: link:../storage '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@tensamin/user': specifier: workspace:* version: link:../user @@ -801,8 +828,8 @@ importers: packages/shared: dependencies: '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) lucide-react: specifier: ^1.14.0 version: 1.23.0(react@19.2.7) @@ -831,8 +858,8 @@ importers: specifier: workspace:* version: link:../shared '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) react: specifier: ^19.2.0 version: 19.2.7 @@ -864,8 +891,8 @@ importers: specifier: workspace:* version: link:../../apps/tauri '@tensamin/ui': - specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) + specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz + version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) '@tensamin/user': specifier: workspace:* version: link:../user @@ -2125,9 +2152,9 @@ packages: '@tauri-apps/plugin-notification@2.3.3': resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} - '@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz': - resolution: {integrity: sha512-xyzw3/XAYKczV1cYQ2QuzhFzFYbU9U2U/gi/TMU/U7MC0vp1xHCVssTcB1kot8yk9YEIEcDBK2X49Ftzj6bJeQ==, tarball: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz} - version: 0.0.40 + '@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz': + resolution: {integrity: sha512-H68VGR6Dy/3cO+VhhUdUdMkPWnUqP0lDmu3v3NPdfuB++SUmSm/zVVXB0I5Y5bnNXTD3FOVLIdP+AplaXuEGDg==, tarball: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz} + version: 0.0.41 peerDependencies: react: ^19.2.7 react-dom: ^19.2.7 @@ -6206,7 +6233,7 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 - '@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)': + '@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)': dependencies: '@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fontsource-variable/inter': 5.2.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7a78a02..b3d8de6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,5 +6,5 @@ allowBuilds: electron-winstaller: true esbuild: true overrides: - "@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz" + "@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz" mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz"