Add cross-platform confirm dialog wrapper for daemon restart

This commit is contained in:
Mohamed Boudra
2026-02-17 10:04:58 +07:00
parent 12ba65d6e7
commit 459af22670
7 changed files with 345 additions and 22 deletions
@@ -0,0 +1,116 @@
import { afterEach, describe, expect, it, vi } from "vitest";
type MockPlatform = "web" | "ios" | "android";
type AlertButton = {
onPress?: () => void;
};
async function loadModuleForPlatform(platform: MockPlatform): Promise<{
confirmDialog: typeof import("./confirm-dialog").confirmDialog;
alertMock: ReturnType<typeof vi.fn>;
}> {
vi.resetModules();
const alertMock = vi.fn();
vi.doMock("react-native", () => ({
Alert: {
alert: alertMock,
},
Platform: { OS: platform },
}));
const module = await import("./confirm-dialog");
return { confirmDialog: module.confirmDialog, alertMock };
}
function clearDialogGlobals(): void {
delete (globalThis as { __TAURI__?: unknown }).__TAURI__;
delete (globalThis as { confirm?: unknown }).confirm;
}
describe("confirmDialog", () => {
afterEach(() => {
vi.doUnmock("react-native");
vi.restoreAllMocks();
vi.resetModules();
clearDialogGlobals();
});
it("uses Tauri dialog.ask on web when available", async () => {
const askMock = vi.fn(async () => true);
(globalThis as { __TAURI__?: unknown }).__TAURI__ = {
dialog: { ask: askMock },
};
const { confirmDialog, alertMock } = await loadModuleForPlatform("web");
const confirmed = await confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
confirmLabel: "Restart",
cancelLabel: "Cancel",
destructive: true,
});
expect(confirmed).toBe(true);
expect(alertMock).not.toHaveBeenCalled();
expect(askMock).toHaveBeenCalledWith("This will restart the daemon.", {
title: "Restart host",
okLabel: "Restart",
cancelLabel: "Cancel",
kind: "warning",
});
});
it("falls back to browser confirm on web when Tauri APIs are unavailable", async () => {
const browserConfirm = vi.fn(() => true);
(globalThis as { confirm?: unknown }).confirm = browserConfirm;
const { confirmDialog } = await loadModuleForPlatform("web");
const confirmed = await confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
});
expect(confirmed).toBe(true);
expect(browserConfirm).toHaveBeenCalledWith(
"Restart host\n\nThis will restart the daemon."
);
});
it("throws on web when no confirm backend exists", async () => {
const { confirmDialog } = await loadModuleForPlatform("web");
await expect(
confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
})
).rejects.toThrow("[ConfirmDialog] No web confirmation backend is available.");
});
it("uses native Alert on iOS/Android", async () => {
const { confirmDialog, alertMock } = await loadModuleForPlatform("ios");
alertMock.mockImplementation(
(
_title: string,
_message: string,
buttons?: AlertButton[]
) => {
const confirmButton = buttons?.[1];
confirmButton?.onPress?.();
}
);
const confirmed = await confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
confirmLabel: "Restart",
cancelLabel: "Cancel",
destructive: true,
});
expect(confirmed).toBe(true);
expect(alertMock).toHaveBeenCalled();
});
});
+144
View File
@@ -0,0 +1,144 @@
import { Alert, Platform } from "react-native";
export interface ConfirmDialogInput {
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
destructive?: boolean;
}
interface TauriDialogAskOptions {
title?: string;
okLabel?: string;
cancelLabel?: string;
kind?: "info" | "warning" | "error";
}
interface TauriDialogApi {
ask?: (message: string, options?: TauriDialogAskOptions) => Promise<boolean>;
}
interface TauriCoreApi {
invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
}
interface TauriGlobalApi {
dialog?: TauriDialogApi;
core?: TauriCoreApi;
}
interface ConfirmButtonConfig {
confirmLabel: string;
cancelLabel: string;
}
function resolveButtonLabels(input: ConfirmDialogInput): ConfirmButtonConfig {
return {
confirmLabel: input.confirmLabel ?? "Confirm",
cancelLabel: input.cancelLabel ?? "Cancel",
};
}
async function showNativeConfirmDialog(input: ConfirmDialogInput): Promise<boolean> {
const labels = resolveButtonLabels(input);
return new Promise<boolean>((resolve) => {
Alert.alert(
input.title,
input.message,
[
{
text: labels.cancelLabel,
style: "cancel",
onPress: () => resolve(false),
},
{
text: labels.confirmLabel,
style: input.destructive ? "destructive" : "default",
onPress: () => resolve(true),
},
],
{
cancelable: true,
onDismiss: () => resolve(false),
}
);
});
}
function getTauriApi(): TauriGlobalApi | null {
if (Platform.OS !== "web") {
return null;
}
const tauriApi = (globalThis as { __TAURI__?: TauriGlobalApi }).__TAURI__;
return tauriApi ?? null;
}
function buildTauriAskOptions(input: ConfirmDialogInput): TauriDialogAskOptions {
const labels = resolveButtonLabels(input);
return {
title: input.title,
okLabel: labels.confirmLabel,
cancelLabel: labels.cancelLabel,
kind: input.destructive ? "warning" : "info",
};
}
async function showTauriConfirmDialog(input: ConfirmDialogInput): Promise<boolean | null> {
const tauriApi = getTauriApi();
if (!tauriApi) {
return null;
}
const options = buildTauriAskOptions(input);
const tauriAsk = tauriApi.dialog?.ask;
if (typeof tauriAsk === "function") {
try {
return Boolean(await tauriAsk(input.message, options));
} catch (error) {
console.warn("[ConfirmDialog] Tauri dialog.ask failed", error);
}
}
const tauriInvoke = tauriApi.core?.invoke;
if (typeof tauriInvoke === "function") {
try {
const result = await tauriInvoke("plugin:dialog|ask", {
message: input.message,
...options,
});
return result === true;
} catch (error) {
console.warn("[ConfirmDialog] Tauri plugin:dialog|ask failed", error);
}
}
return null;
}
function showWebConfirmDialog(input: ConfirmDialogInput): boolean {
const browserConfirm = (globalThis as { confirm?: (message?: string) => boolean }).confirm;
if (typeof browserConfirm !== "function") {
throw new Error("[ConfirmDialog] No web confirmation backend is available.");
}
const promptMessage = `${input.title}\n\n${input.message}`;
return browserConfirm(promptMessage);
}
export async function confirmDialog(input: ConfirmDialogInput): Promise<boolean> {
if (Platform.OS !== "web") {
return showNativeConfirmDialog(input);
}
const tauriResult = await showTauriConfirmDialog(input);
if (tauriResult !== null) {
return tauriResult;
}
return showWebConfirmDialog(input);
}