mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 14:46:04 +02:00
add OtpDialogModal
This commit is contained in:
@@ -319,5 +319,156 @@ describe('useModalStore', () => {
|
||||
store.handlePromptOk('test');
|
||||
expect(store.promptOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('handleOtpOk does nothing without pending otp', () => {
|
||||
store.handleOtpOk('123456');
|
||||
expect(store.otpOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('otpPrompt', () => {
|
||||
test('opens otp dialog with correct mode and text', () => {
|
||||
store.otpPrompt({
|
||||
title: 'TOTP Verification',
|
||||
description: 'Enter your 6-digit code',
|
||||
mode: 'totp',
|
||||
confirmText: 'Verify',
|
||||
cancelText: 'Use recovery code'
|
||||
});
|
||||
|
||||
expect(store.otpOpen).toBe(true);
|
||||
expect(store.otpTitle).toBe('TOTP Verification');
|
||||
expect(store.otpDescription).toBe('Enter your 6-digit code');
|
||||
expect(store.otpMode).toBe('totp');
|
||||
expect(store.otpOkText).toBe('Verify');
|
||||
expect(store.otpCancelText).toBe('Use recovery code');
|
||||
|
||||
store.handleOtpCancel('');
|
||||
});
|
||||
|
||||
test('resolves with value on handleOtpOk', async () => {
|
||||
const promise = store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
mode: 'totp'
|
||||
});
|
||||
|
||||
store.handleOtpOk('123456');
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.reason).toBe('ok');
|
||||
expect(result.value).toBe('123456');
|
||||
expect(store.otpOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('resolves ok:false on handleOtpCancel', async () => {
|
||||
const promise = store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
mode: 'emailOtp'
|
||||
});
|
||||
|
||||
store.handleOtpCancel('123');
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('cancel');
|
||||
expect(result.value).toBe('123');
|
||||
expect(store.otpOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('resolves ok:false on handleOtpDismiss when dismissible', async () => {
|
||||
const promise = store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
mode: 'totp'
|
||||
});
|
||||
|
||||
store.handleOtpDismiss('');
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('dismiss');
|
||||
});
|
||||
|
||||
test('does not dismiss when dismissible is false', async () => {
|
||||
const promise = store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
mode: 'totp',
|
||||
dismissible: false
|
||||
});
|
||||
|
||||
expect(store.otpDismissible).toBe(false);
|
||||
store.handleOtpDismiss('');
|
||||
expect(store.otpOpen).toBe(true);
|
||||
|
||||
store.handleOtpCancel('');
|
||||
await promise;
|
||||
});
|
||||
|
||||
test('sets otp mode correctly', () => {
|
||||
store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
mode: 'otp'
|
||||
});
|
||||
|
||||
expect(store.otpMode).toBe('otp');
|
||||
|
||||
store.handleOtpCancel('');
|
||||
});
|
||||
|
||||
test('defaults mode to totp when not specified', () => {
|
||||
store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D'
|
||||
});
|
||||
|
||||
expect(store.otpMode).toBe('totp');
|
||||
|
||||
store.handleOtpCancel('');
|
||||
});
|
||||
|
||||
test('replaces previous otp dialog with reason replaced', async () => {
|
||||
const first = store.otpPrompt({
|
||||
title: 'First',
|
||||
description: 'D',
|
||||
mode: 'totp'
|
||||
});
|
||||
const second = store.otpPrompt({
|
||||
title: 'Second',
|
||||
description: 'D',
|
||||
mode: 'emailOtp'
|
||||
});
|
||||
|
||||
const firstResult = await first;
|
||||
expect(firstResult.ok).toBe(false);
|
||||
expect(firstResult.reason).toBe('replaced');
|
||||
expect(firstResult.value).toBe('');
|
||||
|
||||
expect(store.otpTitle).toBe('Second');
|
||||
expect(store.otpMode).toBe('emailOtp');
|
||||
expect(store.otpOpen).toBe(true);
|
||||
|
||||
store.handleOtpOk('654321');
|
||||
const secondResult = await second;
|
||||
expect(secondResult.ok).toBe(true);
|
||||
expect(secondResult.value).toBe('654321');
|
||||
});
|
||||
|
||||
test('uses default button text from i18n', () => {
|
||||
store.otpPrompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
mode: 'totp'
|
||||
});
|
||||
|
||||
expect(store.otpOkText).toBe(en.dialog.alertdialog.confirm);
|
||||
expect(store.otpCancelText).toBe(en.dialog.alertdialog.cancel);
|
||||
|
||||
store.handleOtpCancel('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-13
@@ -613,13 +613,12 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
AppApi.FlashWindow();
|
||||
twoFactorAuthDialogVisible.value = true;
|
||||
modalStore
|
||||
.prompt({
|
||||
.otpPrompt({
|
||||
title: t('prompt.totp.header'),
|
||||
description: t('prompt.totp.description'),
|
||||
mode: 'totp',
|
||||
cancelText: t('prompt.totp.use_otp'),
|
||||
confirmText: t('prompt.totp.verify'),
|
||||
pattern: /^[0-9]{6}$/,
|
||||
errorMessage: t('prompt.totp.input_error')
|
||||
confirmText: t('prompt.totp.verify')
|
||||
})
|
||||
.then(({ ok, reason, value }) => {
|
||||
twoFactorAuthDialogVisible.value = false;
|
||||
@@ -653,13 +652,12 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
}
|
||||
twoFactorAuthDialogVisible.value = true;
|
||||
modalStore
|
||||
.prompt({
|
||||
.otpPrompt({
|
||||
title: t('prompt.otp.header'),
|
||||
description: t('prompt.otp.description'),
|
||||
mode: 'otp',
|
||||
cancelText: t('prompt.otp.use_totp'),
|
||||
confirmText: t('prompt.otp.verify'),
|
||||
pattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/,
|
||||
errorMessage: t('prompt.otp.input_error')
|
||||
confirmText: t('prompt.otp.verify')
|
||||
})
|
||||
.then(({ ok, reason, value }) => {
|
||||
twoFactorAuthDialogVisible.value = false;
|
||||
@@ -672,7 +670,7 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
|
||||
authRequest
|
||||
.verifyOTP({
|
||||
code: value.trim()
|
||||
code: `${value.slice(0, 4)}-${value.slice(4)}`
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
@@ -694,13 +692,12 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
AppApi.FlashWindow();
|
||||
twoFactorAuthDialogVisible.value = true;
|
||||
modalStore
|
||||
.prompt({
|
||||
.otpPrompt({
|
||||
title: t('prompt.email_otp.header'),
|
||||
description: t('prompt.email_otp.description'),
|
||||
mode: 'emailOtp',
|
||||
cancelText: t('prompt.email_otp.resend'),
|
||||
confirmText: t('prompt.email_otp.verify'),
|
||||
pattern: /^[0-9]{6}$/,
|
||||
errorMessage: t('prompt.email_otp.input_error')
|
||||
confirmText: t('prompt.email_otp.verify')
|
||||
})
|
||||
.then(({ ok, reason, value }) => {
|
||||
twoFactorAuthDialogVisible.value = false;
|
||||
|
||||
+115
-1
@@ -45,6 +45,16 @@ import { useI18n } from 'vue-i18n';
|
||||
* @property {boolean=} dismissible
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} OtpPromptOptions
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {'totp' | 'emailOtp' | 'otp'} mode
|
||||
* @property {string=} confirmText
|
||||
* @property {string=} cancelText
|
||||
* @property {boolean=} dismissible
|
||||
*/
|
||||
|
||||
export const useModalStore = defineStore('Modal', () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -67,10 +77,20 @@ export const useModalStore = defineStore('Modal', () => {
|
||||
const promptPattern = ref(null);
|
||||
const promptErrorMessage = ref('');
|
||||
|
||||
const otpOpen = ref(false);
|
||||
const otpTitle = ref('');
|
||||
const otpDescription = ref('');
|
||||
const otpOkText = ref('');
|
||||
const otpCancelText = ref('');
|
||||
const otpDismissible = ref(true);
|
||||
const otpMode = ref('totp'); // 'totp' | 'emailOtp' | 'otp'
|
||||
|
||||
/** @type {{ resolve: ((result: ConfirmResult) => void) | null } | null} */
|
||||
let pending = null;
|
||||
/** @type {{ resolve: ((result: PromptResult) => void) | null } | null} */
|
||||
let pendingPrompt = null;
|
||||
/** @type {{ resolve: ((result: PromptResult) => void) | null } | null} */
|
||||
let pendingOtp = null;
|
||||
|
||||
function closeDialog() {
|
||||
alertOpen.value = false;
|
||||
@@ -271,6 +291,88 @@ export const useModalStore = defineStore('Modal', () => {
|
||||
promptOpen.value = !!open;
|
||||
}
|
||||
|
||||
// --- OTP dialog ---
|
||||
|
||||
function closeOtpDialog() {
|
||||
otpOpen.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'ok' | 'cancel' | 'dismiss' | 'replaced'} reason
|
||||
* @param {string} value
|
||||
*/
|
||||
function finishOtp(reason, value) {
|
||||
const resolve = pendingOtp?.resolve;
|
||||
pendingOtp = null;
|
||||
closeOtpDialog();
|
||||
if (resolve) resolve({ ok: reason === 'ok', reason, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'ok' | 'cancel' | 'dismiss' | 'replaced'} reason
|
||||
* @param {string} value
|
||||
*/
|
||||
function finishOtpWithoutClosing(reason, value) {
|
||||
const resolve = pendingOtp?.resolve;
|
||||
pendingOtp = null;
|
||||
if (resolve) resolve({ ok: reason === 'ok', reason, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OtpPromptOptions} options
|
||||
* @returns {Promise<PromptResult>}
|
||||
*/
|
||||
function openOtp(options) {
|
||||
if (pendingOtp) {
|
||||
finishOtpWithoutClosing('replaced', '');
|
||||
}
|
||||
|
||||
otpTitle.value = options.title;
|
||||
otpDescription.value = options.description;
|
||||
otpDismissible.value = options.dismissible !== false;
|
||||
otpMode.value = options.mode || 'totp';
|
||||
|
||||
otpOkText.value =
|
||||
options.confirmText || t('dialog.alertdialog.confirm');
|
||||
otpCancelText.value =
|
||||
options.cancelText || t('dialog.alertdialog.cancel');
|
||||
|
||||
otpOpen.value = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
pendingOtp = { resolve };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* otpPrompt: always resolve({ok, reason, value})
|
||||
* @param {OtpPromptOptions} options
|
||||
* @returns {Promise<PromptResult>}
|
||||
*/
|
||||
function otpPrompt(options) {
|
||||
return openOtp(options);
|
||||
}
|
||||
|
||||
function handleOtpOk(value) {
|
||||
if (!pendingOtp) return;
|
||||
finishOtp('ok', value ?? '');
|
||||
}
|
||||
|
||||
function handleOtpCancel(value) {
|
||||
if (!pendingOtp) return;
|
||||
finishOtp('cancel', value ?? '');
|
||||
}
|
||||
|
||||
function handleOtpDismiss(value) {
|
||||
if (!pendingOtp) return;
|
||||
if (!otpDismissible.value) return;
|
||||
finishOtp('dismiss', value ?? '');
|
||||
}
|
||||
|
||||
function setOtpOpen(open) {
|
||||
otpOpen.value = !!open;
|
||||
}
|
||||
|
||||
return {
|
||||
alertOpen,
|
||||
alertMode,
|
||||
@@ -289,10 +391,18 @@ export const useModalStore = defineStore('Modal', () => {
|
||||
promptInputType,
|
||||
promptPattern,
|
||||
promptErrorMessage,
|
||||
otpOpen,
|
||||
otpTitle,
|
||||
otpDescription,
|
||||
otpOkText,
|
||||
otpCancelText,
|
||||
otpDismissible,
|
||||
otpMode,
|
||||
|
||||
confirm,
|
||||
alert,
|
||||
prompt,
|
||||
otpPrompt,
|
||||
|
||||
handleOk,
|
||||
handleCancel,
|
||||
@@ -301,6 +411,10 @@ export const useModalStore = defineStore('Modal', () => {
|
||||
handlePromptCancel,
|
||||
handlePromptDismiss,
|
||||
setAlertOpen,
|
||||
setPromptOpen
|
||||
setPromptOpen,
|
||||
handleOtpOk,
|
||||
handleOtpCancel,
|
||||
handleOtpDismiss,
|
||||
setOtpOpen
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user