From cf7a84d7ce3b2537694a61735fcb75fbad9ae5b2 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 16 Oct 2021 02:41:17 +0100 Subject: [PATCH] proper ts types --- chartered-frontend/.parcelrc | 3 +++ chartered-frontend/package.json | 2 ++ chartered-frontend/tsconfig.json | 7 ++++--- chartered-frontend/yarn.lock | 40 ++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/index.tsx | 8 ++++---- chartered-frontend/src/overscrollColourFixer.ts | 2 +- chartered-frontend/src/useAuth.tsx | 14 +++++++------- chartered-frontend/src/util.tsx | 11 +++++++---- chartered-frontend/src/pages/Dashboard.tsx | 15 ++++++++++++++- chartered-frontend/src/pages/Login.tsx | 23 ++++++++++++----------- chartered-frontend/src/pages/Search.tsx | 23 +++++++++++++++++++---- chartered-frontend/src/pages/User.tsx | 13 ++++++++++--- chartered-frontend/src/sections/Nav.tsx | 16 ++++++++-------- chartered-frontend/src/pages/crate/CrateView.tsx | 42 +++++++++++++++++++++++++++++++----------- chartered-frontend/src/pages/crate/Members.tsx | 56 ++++++++++++++++++++++++++++++++++++-------------------- chartered-frontend/src/pages/crate/OrganisationView.tsx | 49 +++++++++++++++++++++++++++++-------------------- chartered-frontend/src/pages/organisations/CreateOrganisation.tsx | 14 +++++++------- chartered-frontend/src/pages/organisations/ListOrganisations.tsx | 6 +++++- chartered-frontend/src/pages/ssh-keys/AddSshKeys.tsx | 16 +++++++++------- chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 22 +++++++++++++++------- 20 files changed, 252 insertions(+), 130 deletions(-) diff --git a/chartered-frontend/.parcelrc b/chartered-frontend/.parcelrc new file mode 100644 index 0000000..47d1b5e 100644 --- /dev/null +++ a/chartered-frontend/.parcelrc @@ -1,0 +1,3 @@ +{ + "extends": "@parcel/config-default" +} diff --git a/chartered-frontend/package.json b/chartered-frontend/package.json index ec4db40..d4f80b1 100644 --- a/chartered-frontend/package.json +++ a/chartered-frontend/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@parcel/reporter-bundle-analyzer": "^2.0.0", "@parcel/transformer-sass": "^2.0.0", + "@parcel/validator-typescript": "^2.0.0", "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "@types/react-syntax-highlighter": "^13.5.2", @@ -29,6 +30,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.15", "@types/node": "^16.10.1", + "@types/react-router-dom": "^5.3.1", "bootstrap": "^5.1.1", "react": "^17.0.2", "react-bootstrap": "^2.0.0-beta.6", diff --git a/chartered-frontend/tsconfig.json b/chartered-frontend/tsconfig.json index 7a170d5..1d6875e 100644 --- a/chartered-frontend/tsconfig.json +++ a/chartered-frontend/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "jsx": "react", - "allowSyntheticDefaultImports": true + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "strict": true }, - "lib": ["ES2015"] + "lib": ["ES2021"] } diff --git a/chartered-frontend/yarn.lock b/chartered-frontend/yarn.lock index c54fccd..3a30f26 100644 --- a/chartered-frontend/yarn.lock +++ a/chartered-frontend/yarn.lock @@ -909,6 +909,13 @@ posthtml-render "^3.0.0" semver "^5.4.1" +"@parcel/ts-utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@parcel/ts-utils/-/ts-utils-2.0.0.tgz#89954d341a8942cb6ddcda217f5f7b38914bef12" + integrity sha512-+jMwsBu5+gyp8iw+h6CN1MR8hYDALdO1ccphHPD2WMzQHpvvp+hhZ/gUfN70ioPz9DOG/d6BMlfm/HVaZLePsA== + dependencies: + nullthrows "^1.1.1" + "@parcel/types@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.0.0.tgz#31f6eb7c24b2d9ae752ce49adaf01469a9e9067a" @@ -948,6 +955,17 @@ nullthrows "^1.1.1" open "^7.0.3" terminal-link "^2.1.1" + +"@parcel/validator-typescript@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@parcel/validator-typescript/-/validator-typescript-2.0.0.tgz#2894cdd06db9e156a3b6e67d4f8e73a94c221a5f" + integrity sha512-qv3SuwuKwtLU/EC9NzqYoJAkCicQp/t4vdM1dms19Cre1od8ZhSHDjEi+oDvUnwPZ1yZrLLBY/zWxQh/yUsP+A== + dependencies: + "@parcel/diagnostic" "^2.0.0" + "@parcel/plugin" "^2.0.0" + "@parcel/ts-utils" "^2.0.0" + "@parcel/types" "^2.0.0" + "@parcel/utils" "^2.0.0" "@parcel/watcher@^2.0.0": version "2.0.0" @@ -1039,6 +1057,11 @@ integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== dependencies: "@types/unist" "*" + +"@types/history@*": + version "4.7.9" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724" + integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ== "@types/http-proxy@^1.17.5": version "1.17.7" @@ -1088,7 +1111,24 @@ version "17.0.9" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add" integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg== + dependencies: + "@types/react" "*" + +"@types/react-router-dom@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.1.tgz#76700ccce6529413ec723024b71f01fc77a4a980" + integrity sha512-UvyRy73318QI83haXlaMwmklHHzV9hjl3u71MmM6wYNu0hOVk9NLTa0vGukf8zXUqnwz4O06ig876YSPpeK28A== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.17" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.17.tgz#087091006213b11042f39570e5cd414863693968" + integrity sha512-RNSXOyb3VyRs/EOGmjBhhGKTbnN6fHWvy5FNLzWfOWOGjgVUKqJZXfpKzLmgoU8h6Hj8mpALj/mbXQASOb92wQ== dependencies: + "@types/history" "*" "@types/react" "*" "@types/react-syntax-highlighter@^13.5.2": diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index bdbd8eb..e9fc46e 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -122,7 +122,7 @@ return ( { + render={(props: any) => { // TODO: check if valid key if (!unauthedOnly || !auth || !auth?.getAuthKey()) { return ; @@ -137,7 +137,7 @@ ); } }} - > + /> ); } @@ -158,7 +158,7 @@ const isAuthenticated = auth?.getAuthKey(); useEffect(() => { if (!isAuthenticated) { - auth.logout(); + auth?.logout(); } }, [isAuthenticated]); @@ -177,6 +177,6 @@ ); } }} - > + /> ); } diff --git a/chartered-frontend/src/overscrollColourFixer.ts b/chartered-frontend/src/overscrollColourFixer.ts index 1701d96..62c4a72 100644 --- a/chartered-frontend/src/overscrollColourFixer.ts +++ a/chartered-frontend/src/overscrollColourFixer.ts @@ -1,10 +1,10 @@ // A quick little utility to fix the overscroll colour at the bottom // of the page vs the top of the page. We don't have a footer so we // just want to carry on the body background, whereas the header is // white so we want to use that at the top of the page. window.addEventListener("load", () => { - let ticking; + let ticking = false; const backgroundFix = () => { if (!ticking) { diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx index 73fc141..29fd9df 100644 --- a/chartered-frontend/src/useAuth.tsx +++ a/chartered-frontend/src/useAuth.tsx @@ -44,8 +44,8 @@ ); let json = await result.json(); - auth.handleLoginResponse(json); - } catch (err) { + auth?.handleLoginResponse(json); + } catch (err: any) { setResult( { + const [auth, setAuth] = useState(() => { let authStorage = getAuthStorage(); return [ authStorage.userUuid, @@ -141,7 +141,7 @@ const getAuthKey = () => { if (auth?.[2] > new Date()) { - return auth[1]; + return auth?.[1]; } else if (auth) { return null; } @@ -149,7 +149,7 @@ const getUserUuid = () => { if (auth?.[2] > new Date()) { - return auth[0]; + return auth?.[0]; } else if (auth) { return null; } @@ -157,7 +157,7 @@ const getPictureUrl = () => { if (auth?.[2] > new Date()) { - return auth[3]; + return auth?.[3]; } else if (auth) { return null; } @@ -176,7 +176,7 @@ function getAuthStorage() { const saved = localStorage.getItem("charteredAuthentication"); - const initial = JSON.parse(saved); + const initial = saved ? JSON.parse(saved) : {}; return { userUuid: initial?.userUuid || null, authKey: initial?.authKey || null, diff --git a/chartered-frontend/src/util.tsx b/chartered-frontend/src/util.tsx index cd10aaf..7f217d0 100644 --- a/chartered-frontend/src/util.tsx +++ a/chartered-frontend/src/util.tsx @@ -18,7 +18,7 @@ export function useAuthenticatedRequest( { auth, endpoint }: { auth: AuthContext; endpoint: string }, - reloadOn = [] + reloadOn: any[] = [] ): { response: S | null; error: string | null } { const [error, setError] = useState(null); const [response, setResponse] = useState(null); @@ -40,7 +40,7 @@ } else { setResponse(jsonRes); } - } catch (e) { + } catch (e: any) { setError(e.message); } }, reloadOn); @@ -65,7 +65,7 @@ } else { setResponse(jsonRes); } - } catch (e) { + } catch (e: any) { setError(e.message); } }, reloadOn); @@ -79,7 +79,7 @@ width, className, }: { - src: string; + src?: string | null; height: string; width: string; className?: string; @@ -113,11 +113,13 @@ export function RoundedPicture({ src, + alt, height, width, className, }: { src: string; + alt?: string; height: string; width: string; className?: string; @@ -143,6 +145,7 @@ height, width, }} + alt={alt} src={src} onLoad={() => setImageLoaded(true)} className="rounded-circle" diff --git a/chartered-frontend/src/pages/Dashboard.tsx b/chartered-frontend/src/pages/Dashboard.tsx index 74d2851..24ad18a 100644 --- a/chartered-frontend/src/pages/Dashboard.tsx +++ a/chartered-frontend/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from "react"; -import { Link } from "react-router-dom"; +import {Link, Redirect} from "react-router-dom"; import { useAuth } from "../useAuth"; import Nav from "../sections/Nav"; import { Calendar3, ChevronRight, Download } from "react-bootstrap-icons"; @@ -41,6 +41,10 @@ export default function Dashboard() { const auth = useAuth(); + if (!auth) { + return ; + } + const { response: recentlyCreated, error: recentlyCreatedError } = useAuthenticatedRequest({ auth, @@ -83,6 +87,9 @@

Newly Created

+ {recentlyCreatedError ?
+ {recentlyCreatedError} +
: <>} {(recentlyCreated?.crates || []).map((v) => (

Recently Updated

+ {recentlyUpdatedError ?
+ {recentlyUpdatedError} +
: <>} {(recentlyUpdated?.versions || []).map((v) => (

Most Downloaded

+ {mostDownloadedError ?
+ {mostDownloadedError} +
: <>} {(mostDownloaded?.crates || []).map((v) => ( (isMountedRef.current = false); + return () => { isMountedRef.current = false }; }); - const handleSubmit = async (evt) => { + const handleSubmit = async (evt: SyntheticEvent) => { evt.preventDefault(); setError(""); setLoading("password"); try { - await auth.login(username, password); - } catch (e) { + await auth?.login(username, password); + } catch (e: any) { setError(e.message); } finally { if (isMountedRef.current) { @@ -58,13 +57,13 @@ } }; - const handleOAuthLogin = async (provider) => { + const handleOAuthLogin = async (provider: string) => { setError(""); setLoading(provider); try { - await auth.oauthLogin(provider); - } catch (e) { + await auth?.oauthLogin(provider); + } catch (e: any) { setError(e.message); } }; @@ -180,7 +179,7 @@ }; function getIconForProvider(provider: string): [IconDefinition, string] { - return BRANDS[provider] || BRANDS["default"]; + return BRANDS[provider] || BRANDS.default; } function ButtonOrSpinner({ @@ -200,7 +199,7 @@ text: string; icon?: IconDefinition; background?: string; - onClick: (evt) => any; + onClick: MouseEventHandler; }) { if (showSpinner) { return ( @@ -227,4 +226,6 @@ ); } + + return <>; } diff --git a/chartered-frontend/src/pages/Search.tsx b/chartered-frontend/src/pages/Search.tsx index 6c6e3e8..7787aa5 100644 --- a/chartered-frontend/src/pages/Search.tsx +++ a/chartered-frontend/src/pages/Search.tsx @@ -1,19 +1,16 @@ -import { useState, useEffect } from "react"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import Nav from "../sections/Nav"; import { useAuth } from "../useAuth"; import { - authenticatedEndpoint, ProfilePicture, useAuthenticatedRequest, } from "../util"; -import { BoxSeam, Plus } from "react-bootstrap-icons"; +import { BoxSeam } from "react-bootstrap-icons"; import { LoadingSpinner } from "./Loading"; export default function Search() { - const auth = useAuth(); const location = useLocation(); const query = @@ -47,6 +44,10 @@ function UsersResults({ query }: { query: string }) { const auth = useAuth(); + + if (!auth) { + return <>; + } const { response: results, error } = useAuthenticatedRequest( @@ -56,6 +57,10 @@ }, [query] ); + + if (error) { + return
{error}
; + } if (!results) { return ( @@ -118,6 +123,10 @@ className?: string; }) { const auth = useAuth(); + + if (!auth) { + return <>; + } const { response: results, error } = useAuthenticatedRequest( @@ -127,6 +136,10 @@ }, [query] ); + + if (error) { + return
{error}
+ } if (!results) { return ( diff --git a/chartered-frontend/src/pages/User.tsx b/chartered-frontend/src/pages/User.tsx index a776ac8..af0d27f 100644 --- a/chartered-frontend/src/pages/User.tsx +++ a/chartered-frontend/src/pages/User.tsx @@ -1,8 +1,7 @@ -import { useParams } from "react-router-dom"; +import {Redirect, useParams} from "react-router-dom"; import { useAuth } from "../useAuth"; import { ProfilePicture, - RoundedPicture, useAuthenticatedRequest, } from "../util"; import Nav from "../sections/Nav"; @@ -20,9 +19,17 @@ picture_url?: string; } +interface UrlParams { + uuid: string; +} + export default function User() { const auth = useAuth(); - const { uuid } = useParams(); + const { uuid } = useParams(); + + if (!auth) { + return ; + } const { response: user, error } = useAuthenticatedRequest({ auth, diff --git a/chartered-frontend/src/sections/Nav.tsx b/chartered-frontend/src/sections/Nav.tsx index aaeb23f..ed6a58f 100644 --- a/chartered-frontend/src/sections/Nav.tsx +++ a/chartered-frontend/src/sections/Nav.tsx @@ -1,10 +1,10 @@ -import { useState } from "react"; +import {SyntheticEvent, useState} from "react"; import { useHistory, useLocation } from "react-router-dom"; import { NavLink, Link } from "react-router-dom"; -import { BoxArrowRight, CaretDownFill, Search } from "react-bootstrap-icons"; +import { BoxArrowRight, Search } from "react-bootstrap-icons"; import { useAuth } from "../useAuth"; -import { Dropdown, Navbar, NavDropdown, NavItem } from "react-bootstrap"; +import { Dropdown, Navbar } from "react-bootstrap"; import { ProfilePicture } from "../util"; export default function Nav() { @@ -12,9 +12,9 @@ const history = useHistory(); const location = useLocation(); - const logout = async (e) => { + const logout = async (e: SyntheticEvent) => { e.preventDefault(); - await auth.logout(); + await auth?.logout(); }; const [search, setSearch] = useState( @@ -22,7 +22,7 @@ ? new URLSearchParams(location.search).get("q") || "" : "" ); - const submitSearchForm = (e) => { + const submitSearchForm = (e: SyntheticEvent) => { e.preventDefault(); if (search != "") { @@ -85,7 +85,7 @@ className="d-inline-flex align-items-center" > @@ -95,7 +95,7 @@ align={{ md: "end" }} style={{ marginTop: "5px" }} > - + Your profile diff --git a/chartered-frontend/src/pages/crate/CrateView.tsx b/chartered-frontend/src/pages/crate/CrateView.tsx index 493b024..7201119 100644 --- a/chartered-frontend/src/pages/crate/CrateView.tsx +++ a/chartered-frontend/src/pages/crate/CrateView.tsx @@ -53,7 +53,7 @@ export interface CrateInfoVersion { vers: string; deps: CrateInfoVersionDependency[]; - features: any[]; + features: { [key: string]: any }; size: number; uploader: CrateInfoVersionUploader; created_at: string; @@ -65,11 +65,20 @@ registry?: string; } +interface UrlParameters { + organisation: string; + crate: string; + subview: Tab | undefined; +} + export default function SingleCrate() { const auth = useAuth(); - const { organisation, crate, subview } = useParams(); - const currentTab: Tab | undefined = subview; + const { organisation, crate, subview: currentTab } = useParams(); + if (!auth) { + return ; + } + if (!currentTab) { return ; } @@ -274,7 +283,7 @@ }) { let link = <>{dep.name}; - if (dep.registry === null) { + if (dep.registry === null || dep.registry === undefined) { link = ( {link} @@ -299,16 +308,21 @@ {link} = "{dep.req}" ); +} + +interface MembersProps { + organisation: string; + crate: string; } function Members({ organisation, crate, -}: { - organisation: string; - crate: string; -}) { +}: MembersProps) { const auth = useAuth(); + + if (!auth) { return <>; } + const [reload, setReload] = useState(0); const { response, error } = useAuthenticatedRequest( { @@ -331,9 +345,9 @@ } const saveMemberPermissions = async ( - prospectiveMember, - uuid, - selectedPermissions + prospectiveMember: boolean, + uuid: string, + selectedPermissions: string[], ) => { let res = await fetch( authenticatedEndpoint(auth, `crates/${organisation}/${crate}/members`), @@ -358,7 +372,7 @@ setReload(reload + 1); }; - const deleteMember = async (uuid) => { + const deleteMember = async (uuid: string) => { let res = await fetch( authenticatedEndpoint(auth, `crates/${organisation}/${crate}/members`), { @@ -392,8 +406,8 @@ } function Versions(props: { crate: CrateInfo }) { - const humanFileSize = (size) => { - var i = Math.floor(Math.log(size) / Math.log(1024)); + const humanFileSize = (size: number) => { + const i = Math.floor(Math.log(size) / Math.log(1024)); return ( Number((size / Math.pow(1024, i)).toFixed(2)) + " " + diff --git a/chartered-frontend/src/pages/crate/Members.tsx b/chartered-frontend/src/pages/crate/Members.tsx index 243ee52..eca38c9 100644 --- a/chartered-frontend/src/pages/crate/Members.tsx +++ a/chartered-frontend/src/pages/crate/Members.tsx @@ -1,26 +1,22 @@ import { useState, useEffect, useRef } from "react"; import { Link } from "react-router-dom"; import { PersonPlus, Trash, CheckLg, - Save, PlusLg, } from "react-bootstrap-icons"; import { authenticatedEndpoint, ProfilePicture, - RoundedPicture, - useAuthenticatedRequest, } from "../../util"; import { useAuth } from "../../useAuth"; import { Button, Modal } from "react-bootstrap"; import { AsyncTypeahead } from "react-bootstrap-typeahead"; -import ReactPlaceholder from "react-placeholder"; interface Member { uuid: string; - permissions?: string[]; + permissions: string[]; display_name: string; picture_url?: string; } @@ -40,7 +36,7 @@ ) => Promise; deleteMember: (uuid: string) => Promise; }) { - const [prospectiveMembers, setProspectiveMembers] = useState([]); + const [prospectiveMembers, setProspectiveMembers] = useState([]); useEffect(() => { setProspectiveMembers( @@ -122,12 +118,12 @@ deleteMember: (uuid: string) => Promise; }) { const [selectedPermissions, setSelectedPermissions] = useState( - member.permissions + member.permissions || [] ); const auth = useAuth(); const [deleting, setDeleting] = useState(false); const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(undefined); let itemAction = <>; @@ -151,7 +147,7 @@ setSaving(true); try { - deleteMember(member.uuid); + await deleteMember(member.uuid); } catch (e) { setError(error); } finally { @@ -203,13 +199,13 @@ return ( <> setDeleting(false)} onConfirm={() => doDelete()} username={member.display_name} /> - setError(null)} /> + setError(undefined)} /> @@ -222,7 +218,7 @@ {member.display_name} - {auth.getUserUuid() === member.uuid ? ( + {auth?.getUserUuid() === member.uuid ? ( <>
(that's you!) @@ -253,20 +249,32 @@ ); } +interface MemberListInserterProps { + existingMembers: Member[]; + onInsert: (username: string, user_uuid: string, picture_url: string | null) => any; +} + +interface SearchOption { + user_uuid: string; + display_name: string; + picture_url: string | null; +} + function MemberListInserter({ onInsert, existingMembers, -}: { - existingMembers: Member[]; - onInsert: (username, picture_url, user_uuid) => any; -}) { +}: MemberListInserterProps) { const auth = useAuth(); const searchRef = useRef(null); const [loading, setLoading] = useState(false); const [options, setOptions] = useState([]); const [error, setError] = useState(""); - const handleSearch = async (query) => { + if (!auth) { + return <>; + } + + const handleSearch = async (query: string) => { setLoading(true); setError(""); @@ -284,20 +292,20 @@ } setOptions(json.users || []); - } catch (e) { + } catch (e: any) { setError(e.message); } finally { setLoading(false); } }; - const handleChange = (selected) => { + const handleChange = (selected: SearchOption[]) => { onInsert( selected[0].display_name, selected[0].picture_url, selected[0].user_uuid ); - searchRef.current.clear(); + searchRef.current?.clear(); }; return ( @@ -320,7 +328,7 @@ { + filterBy={(option: SearchOption) => { for (const existing of existingMembers) { if (option.user_uuid === existing.uuid) { return false; @@ -335,7 +343,7 @@ placeholder="Search for User" onChange={handleChange} ref={searchRef} - renderMenuItemChildren={(option, props) => ( + renderMenuItemChildren={(option: SearchOption) => ( <> {error}
- + + />
@@ -77,7 +79,7 @@ value={sshKey} /> -
+
+ />
@@ -211,7 +219,7 @@ show: boolean; onCancel: () => void; onConfirm: () => void; - fingerprint: string; + fingerprint?: string; }) { return (