mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-19 15:24:25 +01:00
add emergency kit and new error states
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { ConfirmDialog } from "~/components/Dialog";
|
import { ConfirmDialog } from "~/components/Dialog";
|
||||||
import { Button } from "~/components/layout";
|
import { Button } from "~/components/layout";
|
||||||
@@ -5,8 +6,8 @@ import { showToast } from "~/components/Toaster";
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
|
|
||||||
export function DeleteEverything() {
|
export function DeleteEverything(props: { emergency?: boolean }) {
|
||||||
const [_state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
async function confirmReset() {
|
async function confirmReset() {
|
||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
@@ -18,7 +19,21 @@ export function DeleteEverything() {
|
|||||||
async function resetNode() {
|
async function resetNode() {
|
||||||
try {
|
try {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
await actions.deleteMutinyWallet();
|
// If we're in a context where the wallet is loaded we want to use the regular action to delete it
|
||||||
|
// Otherwise we just call the import_json method directly
|
||||||
|
if (state.mutiny_wallet && !props.emergency) {
|
||||||
|
try {
|
||||||
|
await actions.deleteMutinyWallet();
|
||||||
|
} catch (e) {
|
||||||
|
// If we can't stop we want to keep going
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If there's no mutiny_wallet loaded we might need to initialize WASM
|
||||||
|
await initMutinyWallet();
|
||||||
|
await MutinyWallet.import_json("{}");
|
||||||
|
}
|
||||||
|
|
||||||
showToast({ title: "Deleted", description: `Deleted all data` });
|
showToast({ title: "Deleted", description: `Deleted all data` });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Title } from "solid-start";
|
import { A, Title } from "solid-start";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
LargeHeader,
|
LargeHeader,
|
||||||
|
NiceP,
|
||||||
SafeArea,
|
SafeArea,
|
||||||
SmallHeader
|
SmallHeader
|
||||||
} from "~/components/layout";
|
} from "~/components/layout";
|
||||||
|
import { ExternalLink } from "./layout/ExternalLink";
|
||||||
|
|
||||||
export default function ErrorDisplay(props: { error: Error }) {
|
export default function ErrorDisplay(props: { error: Error }) {
|
||||||
return (
|
return (
|
||||||
@@ -18,6 +20,17 @@ export default function ErrorDisplay(props: { error: Error }) {
|
|||||||
<span class="font-bold">{props.error.name}</span>:{" "}
|
<span class="font-bold">{props.error.name}</span>:{" "}
|
||||||
{props.error.message}
|
{props.error.message}
|
||||||
</p>
|
</p>
|
||||||
|
<NiceP>
|
||||||
|
Try reloading this page or clicking the "Dangit" button. If
|
||||||
|
you keep having problems,{" "}
|
||||||
|
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||||
|
reach out to us for support.
|
||||||
|
</ExternalLink>
|
||||||
|
</NiceP>
|
||||||
|
<NiceP>
|
||||||
|
Getting desperate? Try the{" "}
|
||||||
|
<A href="/emergencykit">emergency kit.</A>
|
||||||
|
</NiceP>
|
||||||
<div class="h-full" />
|
<div class="h-full" />
|
||||||
<Button
|
<Button
|
||||||
onClick={() => (window.location.href = "/")}
|
onClick={() => (window.location.href = "/")}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { showToast } from "./Toaster";
|
|||||||
import { downloadTextFile } from "~/utils/download";
|
import { downloadTextFile } from "~/utils/download";
|
||||||
import { createFileUploader } from "@solid-primitives/upload";
|
import { createFileUploader } from "@solid-primitives/upload";
|
||||||
import { ConfirmDialog } from "./Dialog";
|
import { ConfirmDialog } from "./Dialog";
|
||||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
|
|
||||||
export function ImportExport() {
|
export function ImportExport(props: { emergency?: boolean }) {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const json = await state.mutiny_wallet?.export_json();
|
const json = await MutinyWallet.export_json();
|
||||||
downloadTextFile(json || "", "mutiny-state.json");
|
downloadTextFile(json || "", "mutiny-state.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,17 +39,28 @@ export function ImportExport() {
|
|||||||
fileReader.readAsText(file, "UTF-8");
|
fileReader.readAsText(file, "UTF-8");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (state.mutiny_wallet && !props.emergency) {
|
||||||
|
console.log("Mutiny wallet loaded, stopping");
|
||||||
|
try {
|
||||||
|
await state.mutiny_wallet.stop();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If there's no mutiny wallet loaded we need to initialize WASM
|
||||||
|
console.log("Initializing WASM");
|
||||||
|
await initMutinyWallet();
|
||||||
|
}
|
||||||
|
|
||||||
// This should throw if there's a parse error, so we won't end up clearing
|
// This should throw if there's a parse error, so we won't end up clearing
|
||||||
if (text) {
|
if (text) {
|
||||||
JSON.parse(text);
|
JSON.parse(text);
|
||||||
MutinyWallet.import_json(text);
|
await MutinyWallet.import_json(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.mutiny_wallet) {
|
setTimeout(() => {
|
||||||
await state.mutiny_wallet.stop();
|
window.location.href = "/";
|
||||||
}
|
}, 1000);
|
||||||
|
|
||||||
window.location.href = "/";
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(eify(e));
|
showToast(eify(e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
|
||||||
import { downloadTextFile } from "~/utils/download";
|
import { downloadTextFile } from "~/utils/download";
|
||||||
|
|
||||||
export function Logs() {
|
export function Logs() {
|
||||||
const [state, _] = useMegaStore();
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
try {
|
try {
|
||||||
const logs = await state.mutiny_wallet?.get_logs();
|
const logs = await MutinyWallet.get_logs();
|
||||||
|
|
||||||
downloadTextFile(
|
downloadTextFile(
|
||||||
logs.join("") || "",
|
logs.join("") || "",
|
||||||
"mutiny-logs.txt",
|
"mutiny-logs.txt",
|
||||||
|
|||||||
@@ -1,7 +1,32 @@
|
|||||||
import { Title } from "solid-start";
|
import { Title } from "solid-start";
|
||||||
import { DefaultMain, LargeHeader, NiceP, SafeArea } from "~/components/layout";
|
import {
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
NiceP,
|
||||||
|
SafeArea,
|
||||||
|
SmallHeader
|
||||||
|
} from "~/components/layout";
|
||||||
import { ExternalLink } from "./layout/ExternalLink";
|
import { ExternalLink } from "./layout/ExternalLink";
|
||||||
import { Match, Switch } from "solid-js";
|
import { Match, Switch } from "solid-js";
|
||||||
|
import { ImportExport } from "./ImportExport";
|
||||||
|
import { Logs } from "./Logs";
|
||||||
|
import { DeleteEverything } from "./DeleteEverything";
|
||||||
|
|
||||||
|
function ErrorFooter() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="h-full" />
|
||||||
|
<p class="self-center text-neutral-500 mt-4">
|
||||||
|
Bugs? Feedback?{" "}
|
||||||
|
<span class="text-neutral-400">
|
||||||
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
||||||
|
Create an issue
|
||||||
|
</ExternalLink>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SetupErrorDisplay(props: { error: Error }) {
|
export default function SetupErrorDisplay(props: { error: Error }) {
|
||||||
return (
|
return (
|
||||||
@@ -22,18 +47,10 @@ export default function SetupErrorDisplay(props: { error: Error }) {
|
|||||||
this page, or close this tab and refresh the other
|
this page, or close this tab and refresh the other
|
||||||
one.
|
one.
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<div class="h-full" />
|
<ErrorFooter />
|
||||||
<p class="self-center text-neutral-500 mt-4">
|
|
||||||
Bugs? Feedback?{" "}
|
|
||||||
<span class="text-neutral-400">
|
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
|
||||||
Create an issue
|
|
||||||
</ExternalLink>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={props.error.message.startsWith("Browser error")}>
|
||||||
<Title>Incompatible browser</Title>
|
<Title>Incompatible browser</Title>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Incompatible browser detected</LargeHeader>
|
<LargeHeader>Incompatible browser detected</LargeHeader>
|
||||||
@@ -61,15 +78,39 @@ export default function SetupErrorDisplay(props: { error: Error }) {
|
|||||||
Supported Browsers
|
Supported Browsers
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
|
||||||
<div class="h-full" />
|
<ErrorFooter />
|
||||||
<p class="self-center text-neutral-500 mt-4">
|
</DefaultMain>
|
||||||
Bugs? Feedback?{" "}
|
</Match>
|
||||||
<span class="text-neutral-400">
|
<Match when={true}>
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
<Title>Failed to load</Title>
|
||||||
Create an issue
|
<DefaultMain>
|
||||||
</ExternalLink>
|
<LargeHeader>Failed to load Mutiny</LargeHeader>
|
||||||
</span>
|
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||||
|
<span class="font-bold">{props.error.name}</span>:{" "}
|
||||||
|
{props.error.message}
|
||||||
</p>
|
</p>
|
||||||
|
<NiceP>
|
||||||
|
Something went wrong while booting up Mutiny Wallet.
|
||||||
|
</NiceP>
|
||||||
|
<NiceP>
|
||||||
|
If your wallet seems broken, here are some tools to
|
||||||
|
try to debug and repair it.
|
||||||
|
</NiceP>
|
||||||
|
<NiceP>
|
||||||
|
If you have any questions on what these buttons do,
|
||||||
|
please{" "}
|
||||||
|
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||||
|
reach out to us for support.
|
||||||
|
</ExternalLink>
|
||||||
|
</NiceP>
|
||||||
|
<ImportExport emergency />
|
||||||
|
<Logs />
|
||||||
|
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red">
|
||||||
|
<SmallHeader>Danger zone</SmallHeader>
|
||||||
|
<DeleteEverything emergency />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ErrorFooter />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { JSX, ParentComponent, Show, Suspense, createResource } from "solid-js";
|
import {
|
||||||
|
JSX,
|
||||||
|
ParentComponent,
|
||||||
|
Show,
|
||||||
|
Suspense,
|
||||||
|
createResource,
|
||||||
|
createSignal
|
||||||
|
} from "solid-js";
|
||||||
import Linkify from "./Linkify";
|
import Linkify from "./Linkify";
|
||||||
import { Button, ButtonLink } from "./Button";
|
import { Button, ButtonLink } from "./Button";
|
||||||
import { Checkbox as KCheckbox, Separator } from "@kobalte/core";
|
import { Checkbox as KCheckbox, Separator } from "@kobalte/core";
|
||||||
@@ -7,6 +14,7 @@ import check from "~/assets/icons/check.svg";
|
|||||||
import { MutinyTagItem } from "~/utils/tags";
|
import { MutinyTagItem } from "~/utils/tags";
|
||||||
import { generateGradient } from "~/utils/gradientHash";
|
import { generateGradient } from "~/utils/gradientHash";
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
|
import { A } from "solid-start";
|
||||||
|
|
||||||
export { Button, ButtonLink, Linkify };
|
export { Button, ButtonLink, Linkify };
|
||||||
|
|
||||||
@@ -84,9 +92,24 @@ export const DefaultMain: ParentComponent = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FullscreenLoader = () => {
|
export const FullscreenLoader = () => {
|
||||||
|
const [waitedTooLong, setWaitedTooLong] = createSignal(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setWaitedTooLong(true);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-full h-[100dvh] flex justify-center items-center">
|
<div class="w-full h-[100dvh] flex flex-col gap-4 justify-center items-center">
|
||||||
<LoadingSpinner wide />
|
<LoadingSpinner wide />
|
||||||
|
<Show when={waitedTooLong()}>
|
||||||
|
<p class="max-w-[20rem] text-neutral-400">
|
||||||
|
Stuck on this screen? Try reloading. If that doesn't work,
|
||||||
|
check out the{" "}
|
||||||
|
<A class="text-white" href="/emergencykit">
|
||||||
|
emergency kit.
|
||||||
|
</A>
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function checkBrowserCompatibility(): Promise<boolean> {
|
|||||||
localStorage.removeItem("test");
|
localStorage.removeItem("test");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error("LocalStorage is not supported.");
|
throw new Error("Browser error: LocalStorage is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the browser supports WebAssembly
|
// Check if the browser supports WebAssembly
|
||||||
@@ -17,7 +17,7 @@ export async function checkBrowserCompatibility(): Promise<boolean> {
|
|||||||
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
|
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error("WebAssembly is not supported.");
|
throw new Error("Browser error: WebAssembly is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("Checking indexedDB");
|
console.debug("Checking indexedDB");
|
||||||
@@ -26,7 +26,7 @@ export async function checkBrowserCompatibility(): Promise<boolean> {
|
|||||||
await openDatabase();
|
await openDatabase();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error("IndexedDB is not supported.");
|
throw new Error("Browser error: IndexedDB is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -117,15 +117,18 @@ export async function setupMutinyWallet(
|
|||||||
console.log("Using esplora address", esplora);
|
console.log("Using esplora address", esplora);
|
||||||
console.log("Using rgs address", rgs);
|
console.log("Using rgs address", rgs);
|
||||||
console.log("Using lsp address", lsp);
|
console.log("Using lsp address", lsp);
|
||||||
|
|
||||||
const mutinyWallet = await new MutinyWallet(
|
const mutinyWallet = await new MutinyWallet(
|
||||||
|
// Password
|
||||||
"",
|
"",
|
||||||
|
// Mnemonic
|
||||||
undefined,
|
undefined,
|
||||||
proxy,
|
proxy,
|
||||||
network,
|
network,
|
||||||
esplora,
|
esplora,
|
||||||
rgs,
|
rgs,
|
||||||
lsp
|
lsp,
|
||||||
|
// Do not connect peers
|
||||||
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodes = await mutinyWallet.list_nodes();
|
const nodes = await mutinyWallet.list_nodes();
|
||||||
|
|||||||
45
src/routes/EmergencyKit.tsx
Normal file
45
src/routes/EmergencyKit.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { DeleteEverything } from "~/components/DeleteEverything";
|
||||||
|
import { ImportExport } from "~/components/ImportExport";
|
||||||
|
import { Logs } from "~/components/Logs";
|
||||||
|
import NavBar from "~/components/NavBar";
|
||||||
|
import {
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
NiceP,
|
||||||
|
SafeArea,
|
||||||
|
SmallHeader,
|
||||||
|
VStack
|
||||||
|
} from "~/components/layout";
|
||||||
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
|
import { ExternalLink } from "~/components/layout/ExternalLink";
|
||||||
|
|
||||||
|
export default function EmergencyKit() {
|
||||||
|
return (
|
||||||
|
<SafeArea>
|
||||||
|
<DefaultMain>
|
||||||
|
<BackLink />
|
||||||
|
<LargeHeader>Emergency Kit</LargeHeader>
|
||||||
|
<VStack>
|
||||||
|
<NiceP>
|
||||||
|
If your wallet seems broken, here are some tools to try
|
||||||
|
to debug and repair it.
|
||||||
|
</NiceP>
|
||||||
|
<NiceP>
|
||||||
|
If you have any questions on what these buttons do,
|
||||||
|
please{" "}
|
||||||
|
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||||
|
reach out to us for support.
|
||||||
|
</ExternalLink>
|
||||||
|
</NiceP>
|
||||||
|
<ImportExport emergency />
|
||||||
|
<Logs />
|
||||||
|
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden">
|
||||||
|
<SmallHeader>Danger zone</SmallHeader>
|
||||||
|
<DeleteEverything emergency />
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="none" />
|
||||||
|
</SafeArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ import { SeedWords } from "~/components/SeedWords";
|
|||||||
import { SettingsStringsEditor } from "~/components/SettingsStringsEditor";
|
import { SettingsStringsEditor } from "~/components/SettingsStringsEditor";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { LiquidityMonitor } from "~/components/LiquidityMonitor";
|
import { LiquidityMonitor } from "~/components/LiquidityMonitor";
|
||||||
|
import { A } from "solid-start";
|
||||||
|
import { Suspense } from "solid-js";
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const [store, _actions] = useMegaStore();
|
const [store, _actions] = useMegaStore();
|
||||||
@@ -41,6 +43,13 @@ export default function Settings() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
<SettingsStringsEditor />
|
<SettingsStringsEditor />
|
||||||
|
<Card title="Emergency Kit">
|
||||||
|
<NiceP>
|
||||||
|
Having some serious problems with your wallet?
|
||||||
|
Check out the{" "}
|
||||||
|
<A href="/emergencykit">emergency kit.</A>
|
||||||
|
</NiceP>
|
||||||
|
</Card>
|
||||||
<Card title="If you know what you're doing">
|
<Card title="If you know what you're doing">
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
|
|||||||
@@ -151,17 +151,23 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
setState({ setup_error: eify(e) });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteMutinyWallet(): Promise<void> {
|
async deleteMutinyWallet(): Promise<void> {
|
||||||
await state.mutiny_wallet?.stop();
|
try {
|
||||||
setState((prevState) => ({
|
if (state.mutiny_wallet) {
|
||||||
...prevState,
|
await state.mutiny_wallet?.stop();
|
||||||
mutiny_wallet: undefined,
|
}
|
||||||
deleting: true
|
setState((prevState) => ({
|
||||||
}));
|
...prevState,
|
||||||
MutinyWallet.import_json("{}");
|
mutiny_wallet: undefined,
|
||||||
localStorage.clear();
|
deleting: true
|
||||||
|
}));
|
||||||
|
MutinyWallet.import_json("{}");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setWaitlistId(waitlist_id: string) {
|
setWaitlistId(waitlist_id: string) {
|
||||||
setState({ waitlist_id });
|
setState({ waitlist_id });
|
||||||
|
|||||||
Reference in New Issue
Block a user