diff --git a/README.md b/README.md
index b4f645ef..60a1cdf4 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,6 @@ The following code is pending a partial or full rewrite:
- `src/context/intermediate`: modal system is being rewritten from scratch
- `src/context/revoltjs`: client state management needs to be rewritten and include support for concurrent clients
- `src/lib`: this needs to be organised
-- `src/*.ts(x)`: half of these files should be moved
-- `src/*.d.ts`: should be in dedicated types folder
## Stack
diff --git a/package.json b/package.json
index 28279792..733f29e9 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0",
- "@revoltchat/ui": "1.0.36",
+ "@revoltchat/ui": "1.0.39",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.38.0",
"@styled-icons/boxicons-regular": "^10.38.0",
diff --git a/src/context/index.tsx b/src/context/index.tsx
index 8d4595cd..89026b31 100644
--- a/src/context/index.tsx
+++ b/src/context/index.tsx
@@ -16,6 +16,7 @@ import { hydrateState } from "../mobx/State";
import Locale from "./Locale";
import Theme from "./Theme";
import Intermediate from "./intermediate/Intermediate";
+import ModalRenderer from "./modals/ModalRenderer";
import Client from "./revoltjs/RevoltClient";
import SyncManager from "./revoltjs/SyncManager";
@@ -44,6 +45,7 @@ export default function Context({ children }: { children: Children }) {
+
diff --git a/src/context/modals/ModalRenderer.tsx b/src/context/modals/ModalRenderer.tsx
new file mode 100644
index 00000000..2ed3d350
--- /dev/null
+++ b/src/context/modals/ModalRenderer.tsx
@@ -0,0 +1,7 @@
+import { observer } from "mobx-react-lite";
+
+import { modalController } from ".";
+
+export default observer(() => {
+ return modalController.render();
+});
diff --git a/src/context/modals/components/MFAFlow.tsx b/src/context/modals/components/MFAFlow.tsx
new file mode 100644
index 00000000..565c4be6
--- /dev/null
+++ b/src/context/modals/components/MFAFlow.tsx
@@ -0,0 +1,164 @@
+import { Archive } from "@styled-icons/boxicons-regular";
+import { Key, Keyboard } from "@styled-icons/boxicons-solid";
+import { API } from "revolt.js";
+
+import { Text } from "preact-i18n";
+import { useCallback, useEffect, useState } from "preact/hooks";
+
+import {
+ Category,
+ CategoryButton,
+ InputBox,
+ Modal,
+ Preloader,
+} from "@revoltchat/ui";
+
+import { noopTrue } from "../../../lib/js";
+
+import { useApplicationState } from "../../../mobx/State";
+
+import { ModalProps } from "../types";
+
+const ICONS: Record> = {
+ Password: Keyboard,
+ Totp: Key,
+ Recovery: Archive,
+};
+
+function ResponseEntry({
+ type,
+ value,
+ onChange,
+}: {
+ type: API.MFAMethod;
+ value?: API.MFAResponse;
+ onChange: (v: API.MFAResponse) => void;
+}) {
+ if (type === "Password") {
+ return (
+ <>
+
+
+
+
+ onChange({ password: e.currentTarget.value })
+ }
+ />
+ >
+ );
+ } else {
+ return null;
+ }
+}
+
+/**
+ * MFA ticket creation flow
+ */
+export default function MFAFlow({
+ callback,
+ onClose,
+ ...props
+}: ModalProps<"mfa_flow">) {
+ const state = useApplicationState();
+
+ const [methods, setMethods] = useState(
+ props.state === "unknown" ? props.available_methods : undefined,
+ );
+
+ const [selectedMethod, setSelected] = useState();
+ const [response, setResponse] = useState();
+
+ useEffect(() => {
+ if (!methods && props.state === "known") {
+ props.client.api.get("/auth/mfa/methods").then(setMethods);
+ }
+ }, []);
+
+ const generateTicket = useCallback(async () => {
+ if (response) {
+ let ticket;
+
+ if (props.state === "known") {
+ ticket = await props.client.api.put(
+ "/auth/mfa/ticket",
+ response,
+ );
+ } else {
+ ticket = await state.config
+ .createClient()
+ .api.put("/auth/mfa/ticket", response, {
+ headers: {
+ "X-MFA-Ticket": props.ticket.token,
+ },
+ });
+ }
+
+ callback(ticket);
+ return true;
+ }
+
+ return false;
+ }, [response]);
+
+ return (
+ setSelected(undefined),
+ },
+ ]
+ : [
+ {
+ palette: "plain",
+ children: "Cancel",
+ onClick: noopTrue,
+ },
+ ]
+ }
+ onClose={onClose}>
+ {methods ? (
+ selectedMethod ? (
+
+ ) : (
+ methods.map((method) => {
+ const Icon = ICONS[method];
+ return (
+ }
+ onClick={() => setSelected(method)}>
+ {method}
+
+ );
+ })
+ )
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/context/modals/components/Test.tsx b/src/context/modals/components/Test.tsx
new file mode 100644
index 00000000..ed59a414
--- /dev/null
+++ b/src/context/modals/components/Test.tsx
@@ -0,0 +1,7 @@
+import { Modal } from "@revoltchat/ui";
+
+import { ModalProps } from "../types";
+
+export default function Test({ onClose }: ModalProps<"test">) {
+ return ;
+}
diff --git a/src/context/modals/index.tsx b/src/context/modals/index.tsx
new file mode 100644
index 00000000..a5843d30
--- /dev/null
+++ b/src/context/modals/index.tsx
@@ -0,0 +1,63 @@
+import { action, computed, makeAutoObservable } from "mobx";
+import { ulid } from "ulid";
+
+import MFAFlow from "./components/MFAFlow";
+import Test from "./components/Test";
+import { Modal } from "./types";
+
+type Components = Record>;
+
+/**
+ * Handles layering and displaying modals to the user.
+ */
+class ModalController {
+ stack: T[] = [];
+ components: Components;
+
+ constructor(components: Components) {
+ this.components = components;
+
+ makeAutoObservable(this);
+ this.pop = this.pop.bind(this);
+ }
+
+ /**
+ * Display a new modal on the stack
+ * @param modal Modal data
+ */
+ @action push(modal: T) {
+ this.stack = [
+ ...this.stack,
+ {
+ ...modal,
+ key: ulid(),
+ },
+ ];
+ }
+
+ /**
+ * Remove the top modal from the stack
+ */
+ @action pop() {
+ this.stack = this.stack.slice(0, this.stack.length - 1);
+ }
+
+ /**
+ * Render modals
+ */
+ @computed render() {
+ return (
+ <>
+ {this.stack.map((modal) => {
+ const Component = this.components[modal.type];
+ return ;
+ })}
+ >
+ );
+ }
+}
+
+export const modalController = new ModalController({
+ mfa_flow: MFAFlow,
+ test: Test,
+});
diff --git a/src/context/modals/types.ts b/src/context/modals/types.ts
new file mode 100644
index 00000000..e15a552c
--- /dev/null
+++ b/src/context/modals/types.ts
@@ -0,0 +1,27 @@
+import { API, Client } from "revolt.js";
+
+export type Modal = {
+ key?: string;
+} & (
+ | ({
+ type: "mfa_flow";
+ callback: (ticket: API.MFATicket) => void;
+ } & (
+ | {
+ state: "known";
+ client: Client;
+ }
+ | {
+ state: "unknown";
+ available_methods: API.MFAMethod[];
+ ticket: API.MFATicket & { validated: false };
+ }
+ ))
+ | {
+ type: "test";
+ }
+);
+
+export type ModalProps = Modal & { type: T } & {
+ onClose: () => void;
+};
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index e166c309..cfb5d4fe 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -42,10 +42,13 @@ export default observer(({ children }: Props) => {
const [status, setStatus] = useState(ClientStatus.LOADING);
const [loaded, setLoaded] = useState(false);
- const logout = useCallback((avoidReq?: boolean) => {
- setLoaded(false);
- client.logout(avoidReq);
- }, []);
+ const logout = useCallback(
+ (avoidReq?: boolean) => {
+ setLoaded(false);
+ client.logout(avoidReq);
+ },
+ [client],
+ );
useEffect(() => {
if (navigator.onLine) {
diff --git a/src/lib/js.ts b/src/lib/js.ts
index 80158291..5a35a5a2 100644
--- a/src/lib/js.ts
+++ b/src/lib/js.ts
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function */
export const noop = () => {};
export const noopAsync = async () => {};
+export const noopTrue = () => true;
/* eslint-enable @typescript-eslint/no-empty-function */
diff --git a/src/pages/settings/panes/Account.tsx b/src/pages/settings/panes/Account.tsx
index eb67ea01..bc3849a8 100644
--- a/src/pages/settings/panes/Account.tsx
+++ b/src/pages/settings/panes/Account.tsx
@@ -19,6 +19,7 @@ import { Button, CategoryButton, LineDivider, Tip } from "@revoltchat/ui";
import { stopPropagation } from "../../../lib/stopPropagation";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
+import { modalController } from "../../../context/modals";
import {
ClientStatus,
LogOutContext,
@@ -210,13 +211,19 @@ export const Account = observer(() => {
}
action="chevron"
onClick={() =>
- client.api
- .post("/auth/account/disable", undefined, {
- headers: {
- "X-MFA-Ticket": "TICKET",
- },
- })
- .then(() => logOut(true))
+ modalController.push({
+ type: "mfa_flow",
+ state: "known",
+ client,
+ callback: ({ token }) =>
+ client.api
+ .post("/auth/account/disable", undefined, {
+ headers: {
+ "X-MFA-Ticket": token,
+ },
+ })
+ .then(() => logOut(true)),
+ })
}>
@@ -227,13 +234,19 @@ export const Account = observer(() => {
}
action="chevron"
onClick={() =>
- client.api
- .post("/auth/account/delete", undefined, {
- headers: {
- "X-MFA-Ticket": "TICKET",
- },
- })
- .then(() => logOut(true))
+ modalController.push({
+ type: "mfa_flow",
+ state: "known",
+ client,
+ callback: ({ token }) =>
+ client.api
+ .post("/auth/account/delete", undefined, {
+ headers: {
+ "X-MFA-Ticket": token,
+ },
+ })
+ .then(() => logOut(true)),
+ })
}>
diff --git a/yarn.lock b/yarn.lock
index 0c7de6ce..5e07617b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2220,9 +2220,9 @@ __metadata:
languageName: node
linkType: hard
-"@revoltchat/ui@npm:1.0.36":
- version: 1.0.36
- resolution: "@revoltchat/ui@npm:1.0.36"
+"@revoltchat/ui@npm:1.0.39":
+ version: 1.0.39
+ resolution: "@revoltchat/ui@npm:1.0.39"
dependencies:
"@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0
@@ -2235,7 +2235,7 @@ __metadata:
react-device-detect: "*"
react-virtuoso: "*"
revolt.js: "*"
- checksum: 97eee93df28f2ca826c7cb1493e3c0efe0ab83d3ef8ea3d3ec013ff3b527f2692193ef50c8e44d144f96d49457c4d290a4dc708a38ab527f3a4290e0d05b41b5
+ checksum: 0376ef1e6c90a139da613a0b76d498327c7bad63941d02eb27b9d5b8208f09c01fb45330fc4e0643554a298beee416814dd41fd9992750378491450c6f773ee0
languageName: node
linkType: hard
@@ -3521,7 +3521,7 @@ __metadata:
"@hcaptcha/react-hcaptcha": ^0.3.6
"@insertish/vite-plugin-babel-macros": ^1.0.5
"@preact/preset-vite": ^2.0.0
- "@revoltchat/ui": 1.0.36
+ "@revoltchat/ui": 1.0.39
"@rollup/plugin-replace": ^2.4.2
"@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0