Skip to content

Commit 8e00180

Browse files
committed
fix: fix focus out for the OTP inputs
1 parent 651a787 commit 8e00180

File tree

2 files changed

+69
-41
lines changed

2 files changed

+69
-41
lines changed

custom/TwoFAModal.vue

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
</template>
6969

7070
<script setup lang="ts">
71+
7172
import VOtpInput from 'vue3-otp-input';
7273
import { ref, nextTick, watch, onMounted } from 'vue';
7374
import { useUserStore } from '@/stores/user';
@@ -98,27 +99,26 @@
9899
(e: 'closed'): void
99100
}>();
100101
101-
function removeListeners() {
102+
async function addEventListenerForOTPInput(){
103+
document.addEventListener('focusin', handleGlobalFocusIn, true);
104+
focusFirstAvailableOtpInput();
105+
isLoading.value = false;
106+
await nextTick();
107+
const rootEl = otpRoot.value;
108+
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
109+
}
110+
111+
function removeEventListenerForOTPInput() {
102112
window.removeEventListener('paste', handlePaste);
103113
document.removeEventListener('focusin', handleGlobalFocusIn, true);
104114
const rootEl = otpRoot.value;
105115
rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
116+
// Abort any in-flight WebAuthn request when leaving the component
106117
}
107118
108-
async function addListeners() {
109-
document.addEventListener('focusin', handleGlobalFocusIn, true);
110-
111-
// Wait for DOM to be ready and OTP inputs to be rendered
112-
await nextTick();
113-
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for v-otp-input to render
114-
115-
focusFirstAvailableOtpInput();
116-
const rootEl = otpRoot.value;
117-
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
118-
}
119119
120120
const modelShow = ref(false);
121-
let resolveFn: ((confirmationResult: string) => void) | null = null;
121+
let resolveFn: ((confirmationResult: any) => void) | null = null;
122122
let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
123123
let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
124124
let rejectFn: ((err?: any) => void) | null = null;
@@ -133,7 +133,9 @@
133133
customDialogTitle.value = title;
134134
}
135135
modelShow.value = true;
136-
await addListeners();
136+
if (modalMode.value === 'totp') {
137+
await addEventListenerForOTPInput();
138+
}
137139
resolveFn = resolve;
138140
rejectFn = reject;
139141
verifyFn = verifyingCallback ?? null;
@@ -163,23 +165,25 @@
163165
return null;
164166
} else if (name === 'InvalidStateError' || name === 'OperationError' || /pending/i.test(message)) {
165167
adminforth.alert({ message: t('Another security prompt is already open. Please try again.'), variant: 'warning' });
168+
onCancel();
166169
return null;
167170
} else if (name === 'NotAllowedError') {
168171
adminforth.alert({ message: `The operation either timed out or was not allowed`, variant: 'warning' });
172+
onCancel();
169173
return null;
170174
} else {
171175
adminforth.alert({message: `Error during authentication: ${error}`, variant: 'warning'});
176+
onCancel();
172177
return null;
173178
}
174-
onCancel();
175179
}
176180
modelShow.value = false;
177181
const dataToReturn = {
178182
mode: "passkey",
179183
result: passkeyData
180184
}
181185
customDialogTitle.value = "";
182-
removeListeners();
186+
removeEventListenerForOTPInput();
183187
resolveFn(dataToReturn);
184188
}
185189
@@ -231,7 +235,7 @@
231235
result: value
232236
}
233237
customDialogTitle.value = "";
234-
removeListeners();
238+
removeEventListenerForOTPInput();
235239
resolveFn(dataToReturn);
236240
}
237241
@@ -240,19 +244,17 @@
240244
modelShow.value = false;
241245
bindValue.value = '';
242246
confirmationResult.value?.clearInput();
243-
removeListeners();
247+
removeEventListenerForOTPInput();
244248
rejectFn("Cancel");
245249
emit('rejected', new Error('cancelled'));
246250
emit('closed');
247251
}
248252
249253
watch(modalMode, async (newMode) => {
250-
if (newMode === 'totp' && modelShow.value && !isLoading.value) {
251-
await nextTick();
252-
setTimeout(() => {
253-
tagOtpInputs();
254-
focusFirstAvailableOtpInput();
255-
}, 100);
254+
if (newMode === 'totp') {
255+
await addEventListenerForOTPInput();
256+
} else {
257+
removeEventListenerForOTPInput();
256258
}
257259
});
258260
@@ -287,22 +289,29 @@
287289
288290
async function checkIfUserHasPasskeys() {
289291
isLoading.value = true;
290-
callAdminForthApi({
291-
method: 'GET',
292-
path: '/plugin/passkeys/getPasskeys',
293-
}).then((response) => {
292+
try {
293+
const response = await callAdminForthApi({
294+
method: 'GET',
295+
path: '/plugin/passkeys/getPasskeys',
296+
});
297+
294298
if (response.ok) {
295299
if (response.data.length >= 1) {
296300
doesUserHavePasskeys.value = true;
297301
modalMode.value = "passkey";
298-
isLoading.value = false;
299302
} else {
300303
doesUserHavePasskeys.value = false;
301304
modalMode.value = "totp";
302-
isLoading.value = false;
303305
}
304306
}
305-
});
307+
} catch (error) {
308+
console.error('Error checking passkeys:', error);
309+
// Fallback to TOTP if there's an error
310+
doesUserHavePasskeys.value = false;
311+
modalMode.value = "totp";
312+
} finally {
313+
isLoading.value = false;
314+
}
306315
}
307316
308317
function getOtpInputs() {

custom/TwoFactorsConfirmation.vue

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,20 @@
181181
if (isPasskeysSupported.value === true) {
182182
await checkIfUserHasPasskeys();
183183
}
184-
document.addEventListener('focusin', handleGlobalFocusIn, true);
185-
focusFirstAvailableOtpInput();
186-
const rootEl = otpRoot.value;
187-
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
184+
if (confirmationMode.value === 'code') {
185+
await addEventListenerForOTPInput();
186+
}
188187
}
189188
isLoading.value = false;
190189
});
191190
191+
watch(confirmationMode, async (newMode) => {
192+
if (newMode === 'code') {
193+
await addEventListenerForOTPInput();
194+
} else {
195+
removeEventListenerForOTPInput();
196+
}
197+
});
192198
watch(route, (newRoute) => {
193199
codeError.value = null;
194200
if ( newRoute.hash === '#passkey' ) {
@@ -200,6 +206,24 @@
200206
}
201207
});
202208
209+
async function addEventListenerForOTPInput(){
210+
document.addEventListener('focusin', handleGlobalFocusIn, true);
211+
focusFirstAvailableOtpInput();
212+
isLoading.value = false;
213+
await nextTick();
214+
const rootEl = otpRoot.value;
215+
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
216+
}
217+
218+
function removeEventListenerForOTPInput() {
219+
window.removeEventListener('paste', handlePaste);
220+
document.removeEventListener('focusin', handleGlobalFocusIn, true);
221+
const rootEl = otpRoot.value;
222+
rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
223+
// Abort any in-flight WebAuthn request when leaving the component
224+
cancelPendingWebAuthn('component-unmount');
225+
}
226+
203227
async function isCMAAvailable() {
204228
if (window.PublicKeyCredential &&
205229
PublicKeyCredential.isConditionalMediationAvailable) {
@@ -211,12 +235,7 @@
211235
}
212236
213237
onBeforeUnmount(() => {
214-
window.removeEventListener('paste', handlePaste);
215-
document.removeEventListener('focusin', handleGlobalFocusIn, true);
216-
const rootEl = otpRoot.value;
217-
rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
218-
// Abort any in-flight WebAuthn request when leaving the component
219-
cancelPendingWebAuthn('component-unmount');
238+
removeEventListenerForOTPInput();
220239
});
221240
222241
async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {

0 commit comments

Comments
 (0)