diff --git a/src/apps/components/firstrun/FirstRun/Page/TOTPFailed.svelte b/src/apps/components/firstrun/FirstRun/Page/TOTPFailed.svelte
new file mode 100755
index 000000000..8d2c1ff0a
--- /dev/null
+++ b/src/apps/components/firstrun/FirstRun/Page/TOTPFailed.svelte
@@ -0,0 +1,9 @@
+
+
+
+
2FA Setup Failed
+Something went wrong when trying to spawn the 2FA setup overlay.
+
+You can always try again later in the settings app under 'Your ArcOS Identity'.
diff --git a/src/apps/components/firstrun/FirstRun/Page/TOTPSetup.svelte b/src/apps/components/firstrun/FirstRun/Page/TOTPSetup.svelte
new file mode 100755
index 000000000..1590f2f70
--- /dev/null
+++ b/src/apps/components/firstrun/FirstRun/Page/TOTPSetup.svelte
@@ -0,0 +1,7 @@
+
+
+
+Set-up 2FA?
+Do you want to secure your ArcOS account by setting up 2FA using your authenticator app?
diff --git a/src/apps/components/firstrun/store.ts b/src/apps/components/firstrun/store.ts
index 675ee6de6..56c2bcba6 100755
--- a/src/apps/components/firstrun/store.ts
+++ b/src/apps/components/firstrun/store.ts
@@ -1,12 +1,18 @@
-import { Daemon } from "$ts/env";
+import type { ITotpConnector } from "$interfaces/modules/server/ITotpConnector";
+import { Daemon, SoundBus } from "$ts/env";
import { UserPaths } from "$ts/user/store";
+import { MessageBox } from "$ts/util/dialog";
import { join } from "$ts/util/fs";
+import { ElevationLevel } from "$types/system/elevation";
import type { ArcShortcut } from "$types/system/shortcut";
+import type { TotpSetupGuiRuntime } from "../totpsetupgui/runtime";
import DisplayName from "./FirstRun/Page/DisplayName.svelte";
import Finish from "./FirstRun/Page/Finish.svelte";
import ProfilePicture from "./FirstRun/Page/ProfilePicture.svelte";
import Style from "./FirstRun/Page/Style.svelte";
import ThirdParty from "./FirstRun/Page/ThirdParty.svelte";
+import TOTPFailed from "./FirstRun/Page/TOTPFailed.svelte";
+import TOTPSetup from "./FirstRun/Page/TOTPSetup.svelte";
import Welcome from "./FirstRun/Page/Welcome.svelte";
import { DarkModeGraphic, LightModeGraphic } from "./images";
import type { FirstRunPage, FirstRunTheme } from "./types";
@@ -127,7 +133,7 @@ export const FirstRunPages = new Map([
right: [
{
caption: "Not now",
- action: (process) => process.switchPage("finish"),
+ action: (process) => process.switchPage("totpSetup"),
},
{
caption: "Enable",
@@ -136,7 +142,46 @@ export const FirstRunPages = new Map([
v.security.enableThirdParty = true;
return v;
});
- process.switchPage("finish");
+ process.switchPage("totpSetup");
+ },
+ suggested: true,
+ },
+ ],
+ },
+ },
+ ],
+ [
+ "totpSetup",
+ {
+ name: "Set-up 2FA?",
+ hero: true,
+ component: TOTPSetup,
+ actions: {
+ left: [
+ {
+ caption: "Go back",
+ action: (process) => process.switchPage("thirdParty"),
+ },
+ ],
+ right: [
+ {
+ caption: "Skip",
+ action: (process) => process.switchPage("finish"),
+ },
+ {
+ caption: "Set-up 2FA",
+ action: async (process) => {
+ const totpSetupProc = await process.spawnOverlayApp("TotpSetupGui", process.pid, true);
+ if (!totpSetupProc) {
+ process.switchPage("totpFailed");
+ SoundBus.playSound("arcos.dialog.error");
+ }
+
+ totpSetupProc?.setupState.subscribe((v) => {
+ if (v === "successful") {
+ process.switchPage("finish");
+ }
+ });
},
suggested: true,
},
@@ -144,6 +189,30 @@ export const FirstRunPages = new Map([
},
},
],
+ [
+ "totpFailed",
+ {
+ name: "2FA Setup Failed",
+ hero: true,
+ component: TOTPFailed,
+ actions: {
+ left: [
+ {
+ caption: "Go back",
+ action: (process) => process.switchPage("thirdParty"),
+ },
+ ],
+ right: [
+ {
+ caption: "Try again",
+ action: (process) => process.switchPage("totpSetup"),
+ },
+
+ { caption: "Finish later", action: (process) => process.switchPage("finish"), suggested: true },
+ ],
+ },
+ },
+ ],
[
"finish",
{
diff --git a/src/apps/components/totpsetupgui/TotpSetupGui.svelte b/src/apps/components/totpsetupgui/TotpSetupGui.svelte
index a3b2b9992..4a428ac55 100755
--- a/src/apps/components/totpsetupgui/TotpSetupGui.svelte
+++ b/src/apps/components/totpsetupgui/TotpSetupGui.svelte
@@ -7,7 +7,7 @@
import Input from "./TotpSetupGui/Input.svelte";
const { process }: { process: ITotpSetupGuiRuntime } = $props();
- const { code, url } = process;
+ const { code, url, setupState, firstTimeSetup } = process;
let errored = $state(false);
let locked = $state(false);
@@ -51,6 +51,28 @@
);
$code = "";
} else {
+ if (firstTimeSetup) {
+ await MessageBox(
+ {
+ title: "ArcOS Security",
+ message: "Two-factor authentication has now been enabled on your account.",
+ buttons: [
+ {
+ caption: "Okay",
+ action: () => {
+ setupState.set("successful");
+ },
+ },
+ ],
+ sound: "arcos.dialog.info",
+ image: "GoodStatusIcon",
+ },
+ process.parentPid,
+ true
+ );
+ return;
+ }
+
MessageBox(
{
title: "ArcOS Security",
diff --git a/src/apps/components/totpsetupgui/runtime.ts b/src/apps/components/totpsetupgui/runtime.ts
index 9a10dcb72..2fec658de 100755
--- a/src/apps/components/totpsetupgui/runtime.ts
+++ b/src/apps/components/totpsetupgui/runtime.ts
@@ -4,16 +4,21 @@ import { AppProcess } from "$ts/apps/process";
import { Daemon } from "$ts/env";
import { Store } from "$ts/writable";
import type { AppProcessData } from "$types/apps/app";
+import type { SetupState } from "./types";
export class TotpSetupGuiRuntime extends AppProcess implements ITotpSetupGuiRuntime {
public code = Store("");
public url = Store("");
+ public setupState = Store("unfinished");
+ public firstTimeSetup: boolean;
//#region LIFECYCLE
- constructor(pid: number, parentPid: number, app: AppProcessData) {
+ constructor(pid: number, parentPid: number, app: AppProcessData, firstTimeSetup: boolean = false) {
super(pid, parentPid, app);
+ this.firstTimeSetup = firstTimeSetup;
+
this.setSource(__SOURCE__);
}
@@ -27,6 +32,12 @@ export class TotpSetupGuiRuntime extends AppProcess implements ITotpSetupGuiRunt
this.url.set(result.result!.url);
}
+ async onClose(): Promise {
+ if (this.setupState() === "unfinished") this.setupState.set("canceled");
+
+ return true;
+ }
+
//#endregion
//#region ACCESS
diff --git a/src/apps/components/totpsetupgui/types.ts b/src/apps/components/totpsetupgui/types.ts
new file mode 100644
index 000000000..a81af96ad
--- /dev/null
+++ b/src/apps/components/totpsetupgui/types.ts
@@ -0,0 +1 @@
+export type SetupState = "unfinished" | "canceled" | "successful";
diff --git a/src/apps/core/newloginapp/runtime.ts b/src/apps/core/newloginapp/runtime.ts
index 1ba59437c..25df0ec0e 100644
--- a/src/apps/core/newloginapp/runtime.ts
+++ b/src/apps/core/newloginapp/runtime.ts
@@ -132,7 +132,7 @@ export class NewLoginAppRuntime extends AppProcess implements INewLoginAppRuntim
this.Persistence.update((v) => {
const userConnector = Server.GetConn("UserConnector", "");
const persistence: LoginPersistenceUser = {
- displayName: info.preferences.account.displayName ?? info.username,
+ displayName: info.preferences?.account?.displayName ?? info.username,
username: info.username,
userId: info._id,
profilePictureUrl: userConnector.PictureUrl(info._id),
diff --git a/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts b/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts
index 0efafa313..61405eee0 100644
--- a/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts
+++ b/src/interfaces/runtimes/ITotpSetupGuiRuntime.ts
@@ -1,3 +1,4 @@
+import type { SetupState } from "$apps/components/totpsetupgui/types";
import type { IAppProcess } from "$interfaces/IAppProcess";
import type { ReadableStore } from "$types/shared/writable";
@@ -5,6 +6,8 @@ import type { ReadableStore } from "$types/shared/writable";
export interface ITotpSetupGuiRuntime extends IAppProcess {
code: ReadableStore;
url: ReadableStore;
+ setupState: ReadableStore;
+ firstTimeSetup: boolean;
validate(): boolean;
activateTotp(): Promise;