Added deeplinks, added qr code login, added settings, other stuff
This commit is contained in:
parent
462fc19591
commit
8ff8bd9528
32 changed files with 917 additions and 78 deletions
|
|
@ -17,21 +17,22 @@
|
|||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tanstack/react-virtual": "^3.0.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tensamin/call": "workspace:*",
|
||||
"@tensamin/chat": "workspace:*",
|
||||
"@tensamin/crypto": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/storage": "workspace:*",
|
||||
"@tensamin/tauri": "workspace:*",
|
||||
"@tensamin/ttp": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"@tensamin/user": "workspace:*",
|
||||
"@tensamin/tauri": "workspace:*",
|
||||
"@tauri-apps/api": "^2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"comlink": "^4.4.2",
|
||||
"framer-motion": "^12.38.0",
|
||||
"lucide-react": "^0.564.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"shadcn": "^4.0.7",
|
||||
|
|
@ -44,10 +45,11 @@
|
|||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.0.3",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "@tensamin/ui/cmp/button";
|
||||
import { ArrowLeft, House, Phone, User } from "lucide-react";
|
||||
import { ArrowLeft, House, Phone, Settings, User } from "lucide-react";
|
||||
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useCall } from "@tensamin/call/context";
|
||||
import Wrapper from "@tensamin/user/wrapper";
|
||||
|
|
@ -22,6 +22,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
|||
const { contacts } = useTTP();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { id } = useSearch({ strict: false });
|
||||
|
||||
const currentCalls = id ? contacts?.[id]?.calls || [] : [];
|
||||
|
|
@ -43,13 +44,23 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
|||
<ArrowLeft className="size-4.5" />
|
||||
</SidebarTrigger>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => navigate({ to: "/" })}
|
||||
className="w-9 h-9 aspect-square rounded-lg"
|
||||
variant="outline"
|
||||
>
|
||||
<House className="size-4.5" />
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
onClick={() => navigate({ to: "/" })}
|
||||
className="w-9 h-9 aspect-square rounded-lg"
|
||||
variant="outline"
|
||||
>
|
||||
<House className="size-4.5" />
|
||||
</Button>
|
||||
<Button
|
||||
// @ts-expect-error TanStack router doesn't properly detect the settings route
|
||||
onClick={() => navigate({ to: "/settings" })}
|
||||
className="w-9 h-9 aspect-square rounded-lg"
|
||||
variant="outline"
|
||||
>
|
||||
<Settings className="size-4.5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{pathname === "/chat" && (
|
||||
<Wrapper
|
||||
|
|
@ -126,22 +137,36 @@ export function MobileNavbar() {
|
|||
|
||||
return (
|
||||
<div className="z-51 w-full pb-[env(safe-area-inset-bottom)] bg-card border-t">
|
||||
<div className="z-51 w-full h-14 flex justify-center items-center gap-8">
|
||||
<div className="z-51 w-full h-15 grid grid-cols-3 items-center place-items-center px-5">
|
||||
<SidebarTrigger
|
||||
className="text-foreground w-12! h-12! aspect-square rounded-xl"
|
||||
className="text-foreground w-12! h-12! aspect-square rounded-xl flex flex-col gap-1"
|
||||
variant="link"
|
||||
>
|
||||
<User className="size-5.5" />
|
||||
<User className="size-4.5" />
|
||||
<p className="text-xs">Chats</p>
|
||||
</SidebarTrigger>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate({ to: "/" });
|
||||
setOpenMobile(false);
|
||||
}}
|
||||
className="text-foreground w-12 h-12 aspect-square rounded-xl"
|
||||
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1"
|
||||
variant="link"
|
||||
>
|
||||
<House className="size-5.5" />
|
||||
<House className="size-4.5" />
|
||||
<p className="text-xs">Home</p>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// @ts-expect-error TanStack router doesn't properly detect the settings route
|
||||
navigate({ to: "/settings" });
|
||||
setOpenMobile(false);
|
||||
}}
|
||||
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1"
|
||||
variant="link"
|
||||
>
|
||||
<Settings className="size-4.5" />
|
||||
<p className="text-xs">Settings</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { log, toast } from "@tensamin/shared/log";
|
|||
import { File } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { z } from "zod";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import QrCodeScanner from "@tensamin/tauri/qrCodeScanner";
|
||||
|
||||
const fetchedUser = z.object({
|
||||
id: z.uuidv4(),
|
||||
|
|
@ -145,15 +147,46 @@ export default function Form() {
|
|||
[save],
|
||||
);
|
||||
|
||||
const isTauriEnv = isTauri();
|
||||
|
||||
return (
|
||||
<div className="flex md:flex-row flex-col gap-15">
|
||||
<div
|
||||
onClick={() => uploadRef.current?.click()}
|
||||
className="flex flex-col gap-3 cursor-pointer w-55 aspect-square bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-3 items-center justify-center rounded-lg"
|
||||
>
|
||||
<File className="text-foreground" size={27} />
|
||||
<p className="text-md">Select .tu file</p>
|
||||
</div>
|
||||
{isTauriEnv ? (
|
||||
<>
|
||||
<QrCodeScanner
|
||||
onData={(data) => {
|
||||
if (!data.startsWith("tensamin://tu::")) {
|
||||
toast("error", "Invalid QR code");
|
||||
return;
|
||||
} else {
|
||||
const decoded = data.replace("tensamin://tu::", "");
|
||||
|
||||
try {
|
||||
const { userId, privateKey } = parseTuFileContent(decoded);
|
||||
save("session_id", Date.now());
|
||||
save("user_id", userId);
|
||||
save("private_key", privateKey);
|
||||
location.href = "/";
|
||||
} catch (error) {
|
||||
log(0, "login", "red", error);
|
||||
toast("error", "Failed to parse QR code data");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => uploadRef.current?.click()}>
|
||||
Select .tu file
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => uploadRef.current?.click()}
|
||||
className="flex flex-col gap-3 cursor-pointer w-55 aspect-square bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-3 items-center justify-center rounded-lg"
|
||||
>
|
||||
<File className="text-foreground" size={27} />
|
||||
<p className="text-md">Select .tu file</p>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
accept=".tu"
|
||||
onChange={handleFileInputChange}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import {
|
|||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { useIsMobile } from "@tensamin/ui/utils";
|
||||
import { MobileNavbar } from "./navbar";
|
||||
import { useLocation } from "@tanstack/react-router";
|
||||
|
||||
import SidebarBox from "@tensamin/call/sidebarBox";
|
||||
import { useShowMobileNavbar } from "@/routes/app/layout";
|
||||
|
||||
/**
|
||||
* Renders the conversation sidebar with account summary and conversation list.
|
||||
|
|
@ -20,7 +20,7 @@ import SidebarBox from "@tensamin/call/sidebarBox";
|
|||
*/
|
||||
export default function Sidebar() {
|
||||
const isMobile = useIsMobile();
|
||||
const location = useLocation();
|
||||
const showMobileNavbar = useShowMobileNavbar();
|
||||
|
||||
return (
|
||||
<SidebarRoot>
|
||||
|
|
@ -42,7 +42,7 @@ export default function Sidebar() {
|
|||
</div>
|
||||
</div>
|
||||
</SidebarContent>
|
||||
{isMobile && location.pathname === "/chat" && (
|
||||
{isMobile && !showMobileNavbar && (
|
||||
<SidebarFooter className="p-0!">
|
||||
<MobileNavbar />
|
||||
</SidebarFooter>
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export default function Screen(props: { children: React.ReactNode }) {
|
|||
|
||||
return (
|
||||
<CreateScreen>
|
||||
<div className="h-full flex flex-col gap-15 p-10 md:p-40 w-full lg:w-2/3">
|
||||
<div className="h-full flex flex-col gap-15 p-10 py-20 md:p-40 w-full lg:w-2/3">
|
||||
<h1 className="text-3xl md:text-4xl font-bold">
|
||||
Privacy Policy & ToS
|
||||
<p className="text-muted-foreground text-[20px] font-normal pt-3">
|
||||
|
|
|
|||
35
apps/web/src/features/settings/components.tsx
Normal file
35
apps/web/src/features/settings/components.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Label } from "@tensamin/ui/cmp/label";
|
||||
import { Switch as UISwitch } from "@tensamin/ui/cmp/switch";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { settingsStorageDefaults } from "@tensamin/shared/settings";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
|
||||
export function Switch({
|
||||
label,
|
||||
id,
|
||||
}: {
|
||||
label: string;
|
||||
id: keyof typeof settingsStorageDefaults;
|
||||
}) {
|
||||
const { save, load } = useStorage();
|
||||
const [value, setValue] = useState<boolean>(settingsStorageDefaults[id]);
|
||||
|
||||
useEffect(() => {
|
||||
load(id).then((value) => setValue(value));
|
||||
}, [id, load]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<UISwitch
|
||||
id={id}
|
||||
checked={value}
|
||||
onCheckedChange={(value) => {
|
||||
setValue(value);
|
||||
save(id, value);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
apps/web/src/features/settings/layout.tsx
Normal file
64
apps/web/src/features/settings/layout.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Button } from "@tensamin/ui/cmp/button";
|
||||
|
||||
import options from "@tensamin/shared/settings";
|
||||
import { cn, useIsMobile } from "@tensamin/ui/utils";
|
||||
import { Outlet, useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export default function Screen() {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
{!isMobile && <SettingsSidebar />}
|
||||
|
||||
{/* Page */}
|
||||
<div className="bg-background w-full h-full p-3">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsSidebar() {
|
||||
const settingsOptions = options as Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
>;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isMobile ? "w-full" : "rounded-tl-2xl border-r bg-input/15 w-50",
|
||||
"flex flex-col gap-6 p-3",
|
||||
)}
|
||||
>
|
||||
{/* Settings */}
|
||||
{Object.keys(settingsOptions).map((category) => (
|
||||
// Category
|
||||
<div key={category} className="flex flex-col gap-1">
|
||||
<h2 className="font-bold text-xs uppercase">{category}</h2>
|
||||
{Object.keys(settingsOptions[category]).map((page) => (
|
||||
// Page
|
||||
<div key={page}>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/settings/" + page.toLowerCase(),
|
||||
})
|
||||
}
|
||||
>
|
||||
{(page as string).charAt(0).toUpperCase() +
|
||||
(page as string).slice(1)}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@ import "./index.css";
|
|||
|
||||
import NotFound from "@/routes/404";
|
||||
|
||||
import RootLayout from "./routes/layout";
|
||||
import AppLayout from "./routes/app/layout";
|
||||
import RootLayout from "@/routes/layout";
|
||||
import AppLayout from "@/routes/app/layout";
|
||||
import SettingsLayout from "@/features/settings/layout";
|
||||
|
||||
import Home from "@/routes/app/home";
|
||||
import ChatScreen from "@tensamin/chat/screen";
|
||||
|
|
@ -22,6 +23,7 @@ import ChatContext from "@tensamin/chat/context";
|
|||
import CallContext from "@tensamin/call/context";
|
||||
import SocketContext from "@tensamin/ttp/context";
|
||||
import UserContext from "@tensamin/user/context";
|
||||
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
|
||||
|
||||
import { ThemeProvider } from "@tensamin/ui/theme";
|
||||
import z from "zod";
|
||||
|
|
@ -47,13 +49,15 @@ window.setLogLevelToMax = () => {
|
|||
|
||||
function RootShell() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="w-screen h-screen overflow-hidden">
|
||||
<RootLayout>
|
||||
<Outlet />
|
||||
</RootLayout>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<DeeplinkContext>
|
||||
<ThemeProvider>
|
||||
<div className="w-screen h-screen overflow-hidden">
|
||||
<RootLayout>
|
||||
<Outlet />
|
||||
</RootLayout>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</DeeplinkContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -89,10 +93,55 @@ const appRoute = createRoute({
|
|||
component: AppShell,
|
||||
});
|
||||
|
||||
const settingsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: "settings",
|
||||
component: SettingsLayout,
|
||||
staticData: {
|
||||
showMobileNavbar: true,
|
||||
},
|
||||
});
|
||||
|
||||
type SettingsRouteModule = {
|
||||
default?: () => React.JSX.Element;
|
||||
component?: () => React.JSX.Element;
|
||||
};
|
||||
|
||||
const settingsRouteModules = import.meta.glob<SettingsRouteModule>(
|
||||
"./routes/settings/*.tsx",
|
||||
{ eager: true },
|
||||
);
|
||||
|
||||
const settingsChildren = Object.entries(settingsRouteModules).map(
|
||||
([filePath, module]) => {
|
||||
const fileName = filePath.split("/").pop()?.replace(".tsx", "") ?? "";
|
||||
const path = fileName === "index" ? "/" : fileName;
|
||||
const component = module.component ?? module.default;
|
||||
|
||||
if (!component) {
|
||||
throw new Error(
|
||||
`Settings route module "${filePath}" must export a default component`,
|
||||
);
|
||||
}
|
||||
|
||||
return createRoute({
|
||||
getParentRoute: () => settingsRoute,
|
||||
path,
|
||||
component,
|
||||
staticData: {
|
||||
showMobileNavbar: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const homeRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: "/",
|
||||
component: Home,
|
||||
staticData: {
|
||||
showMobileNavbar: true,
|
||||
},
|
||||
});
|
||||
|
||||
const chatRoute = createRoute({
|
||||
|
|
@ -102,22 +151,36 @@ const chatRoute = createRoute({
|
|||
validateSearch: z.object({
|
||||
id: z.number().optional(),
|
||||
}),
|
||||
staticData: {
|
||||
showMobileNavbar: false,
|
||||
},
|
||||
});
|
||||
|
||||
const callRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: "call",
|
||||
component: CallScreen,
|
||||
staticData: {
|
||||
showMobileNavbar: false,
|
||||
},
|
||||
});
|
||||
|
||||
const loginRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "login",
|
||||
component: Login,
|
||||
staticData: {
|
||||
showMobileNavbar: false,
|
||||
},
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
appRoute.addChildren([homeRoute, chatRoute, callRoute]),
|
||||
appRoute.addChildren([
|
||||
homeRoute,
|
||||
chatRoute,
|
||||
callRoute,
|
||||
settingsRoute.addChildren(settingsChildren),
|
||||
]),
|
||||
loginRoute,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,31 +7,31 @@ import { SidebarProvider } from "@tensamin/ui/cmp/sidebar";
|
|||
import { useIsMobile, cn } from "@tensamin/ui/utils";
|
||||
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { useLocation } from "@tanstack/react-router";
|
||||
import { useLocation, useMatches } from "@tanstack/react-router";
|
||||
|
||||
/**
|
||||
* Executes Layout.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Layout(props: { children: ReactNode }) {
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const isMobile = useIsMobile();
|
||||
const location = useLocation();
|
||||
const showMobileNavbar = useShowMobileNavbar();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex bg-sidebar">
|
||||
<SidebarProvider>
|
||||
<Sidebar />
|
||||
<div
|
||||
// Background of ui that is overlapping with the system ui
|
||||
className={cn(
|
||||
"w-full h-full flex flex-col",
|
||||
isTauri() && isMobile && "pt-[env(safe-area-inset-top)]",
|
||||
isMobile && showMobileNavbar && "bg-background",
|
||||
)}
|
||||
>
|
||||
{!isMobile && <Navbar forMobile={false} />}
|
||||
{isMobile && location.pathname === "/chat" && (
|
||||
<Navbar forMobile={true} />
|
||||
)}
|
||||
{isMobile && !showMobileNavbar && <Navbar forMobile={true} />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -39,11 +39,26 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
!isMobile && "rounded-tl-3xl border-t border-l",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
{isMobile && location.pathname !== "/chat" && <MobileNavbar />}
|
||||
|
||||
{isMobile && showMobileNavbar && <MobileNavbar />}
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useShowMobileNavbar(): boolean {
|
||||
const matches = useMatches();
|
||||
const location = useLocation();
|
||||
|
||||
const value = matches.some(
|
||||
(match) =>
|
||||
match.pathname === location.pathname &&
|
||||
// @ts-expect-error Stuff
|
||||
match.staticData?.showMobileNavbar === true,
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
12
apps/web/src/routes/settings/chat.tsx
Normal file
12
apps/web/src/routes/settings/chat.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Switch } from "@/features/settings/components";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<Switch
|
||||
label="Reverse Enter Key Behavior"
|
||||
id="settings.reverse_enter_behavior"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
apps/web/src/routes/settings/index.tsx
Normal file
7
apps/web/src/routes/settings/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { SettingsSidebar } from "@/features/settings/layout";
|
||||
import { useIsMobile } from "@tensamin/ui/utils";
|
||||
|
||||
export default function Page() {
|
||||
const isMobile = useIsMobile();
|
||||
return isMobile && <SettingsSidebar />;
|
||||
}
|
||||
111
apps/web/src/routes/settings/security.tsx
Normal file
111
apps/web/src/routes/settings/security.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { useEffect, useState } from "react";
|
||||
import QRCode from "qrcode";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@tensamin/ui/cmp/alert-dialog";
|
||||
import { Button } from "@tensamin/ui/cmp/button";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<QrCodeLogin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// QR Code Login
|
||||
const generateQR = async (text: string): Promise<string> => {
|
||||
try {
|
||||
const url = await QRCode.toDataURL(text, {
|
||||
errorCorrectionLevel: "H",
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: "#000000FF",
|
||||
light: "#FFFFFFFF",
|
||||
},
|
||||
});
|
||||
return url;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
function QrCodeLogin() {
|
||||
const { load } = useStorage();
|
||||
const [userId, setUserId] = useState<number>(0);
|
||||
const [privateKey, setPrivateKey] = useState<string>("");
|
||||
const [qrCodeBase64, setQrCodeBase64] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
load("private_key").then((value) => {
|
||||
if (value) {
|
||||
setPrivateKey(value);
|
||||
}
|
||||
});
|
||||
load("user_id").then((value) => {
|
||||
if (value) {
|
||||
setUserId(value);
|
||||
}
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId && privateKey) {
|
||||
generateQR(`tensamin://tu::${userId}::${privateKey}`).then(
|
||||
setQrCodeBase64,
|
||||
);
|
||||
}
|
||||
}, [userId, privateKey]);
|
||||
|
||||
const [qrCodeVisible, setQrCodeVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative rounded-lg w-50 border-3 aspect-square overflow-hidden">
|
||||
{qrCodeBase64 && (
|
||||
<img className="w-full h-full" src={qrCodeBase64} alt="QR Code" />
|
||||
)}
|
||||
{!qrCodeVisible && (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 w-full h-full backdrop-blur-sm bg-background/50" />
|
||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button>Show QR Code</Button>} />
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will expose your private key! This is the same as
|
||||
sharing the .tu file! It's very tedious to change the
|
||||
private key, so only do this if you are sure no one can
|
||||
steal it!
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
render={
|
||||
<Button onClick={() => setQrCodeVisible(true)}>
|
||||
Show QR Code
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue