Skip to content

Commit 0fb05f8

Browse files
committed
refactor: replace reka-ui PinInput with custom implementation and optimize TwoFactor UI components
1 parent 62c4b52 commit 0fb05f8

File tree

3 files changed

+111
-142
lines changed

3 files changed

+111
-142
lines changed

resources/js/components/ui/pin-input/PinInput.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
<script setup lang="ts">
1+
<script setup lang="ts" generic="Type extends 'text' | 'number' = 'text'">
22
import type { PinInputRootEmits, PinInputRootProps } from "reka-ui"
33
import type { HTMLAttributes } from "vue"
44
import { reactiveOmit } from "@vueuse/core"
55
import { PinInputRoot, useForwardPropsEmits } from "reka-ui"
66
import { cn } from "@/lib/utils"
77
8-
const props = withDefaults(defineProps<PinInputRootProps & { class?: HTMLAttributes["class"] }>(), {
8+
const props = withDefaults(defineProps<PinInputRootProps<Type> & { class?: HTMLAttributes["class"] }>(), {
99
modelValue: () => [],
1010
})
11-
const emits = defineEmits<PinInputRootEmits>()
11+
const emits = defineEmits<PinInputRootEmits<Type>>()
1212
1313
const delegatedProps = reactiveOmit(props, "class")
1414

resources/js/pages/auth/TwoFactorChallenge.vue

Lines changed: 93 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -5,134 +5,115 @@ import { Button } from '@/components/ui/button';
55
import { Input } from '@/components/ui/input';
66
import InputError from '@/components/InputError.vue';
77
import AuthLayout from '@/layouts/AuthLayout.vue';
8-
import { PinInputRoot, PinInputInput } from 'reka-ui';
8+
import { PinInput, PinInputGroup, PinInputSlot } from '@/components/ui/pin-input';
99
1010
const recovery = ref(false);
11-
const pinValue = ref<string[]>([]);
11+
const pinValue = ref<number[]>([]);
1212
const pinInputContainerRef = ref<HTMLElement | null>(null);
1313
const recoveryInputRef = ref<HTMLInputElement | null>(null);
1414
1515
const codeValue = computed(() => pinValue.value.join(''));
1616
1717
watch(recovery, (value) => {
18-
if (value) {
19-
nextTick(() => {
20-
if (recoveryInputRef.value) {
21-
recoveryInputRef.value?.focus();
22-
}
23-
});
24-
} else {
25-
nextTick(() => {
26-
if (pinInputContainerRef.value) {
27-
const firstInput = pinInputContainerRef.value.querySelector('input');
28-
if (firstInput) firstInput.focus();
29-
}
30-
});
31-
}
32-
})
18+
if (value) {
19+
nextTick(() => {
20+
if (recoveryInputRef.value) {
21+
recoveryInputRef.value?.focus();
22+
}
23+
});
24+
} else {
25+
nextTick(() => {
26+
if (pinInputContainerRef.value) {
27+
const firstInput = pinInputContainerRef.value.querySelector('input');
28+
if (firstInput) firstInput.focus();
29+
}
30+
});
31+
}
32+
});
3333
3434
const toggleRecovery = (clearErrors: () => void) => {
35-
recovery.value = !recovery.value;
36-
clearErrors();
37-
pinValue.value = [];
38-
const codeInput = document.querySelector('input[name="code"]') as HTMLInputElement;
39-
const recoveryInput = document.querySelector('input[name="recovery_code"]') as HTMLInputElement;
40-
if (codeInput) codeInput.value = '';
41-
if (recoveryInput) recoveryInput.value = '';
35+
recovery.value = !recovery.value;
36+
clearErrors();
37+
pinValue.value = [];
38+
const codeInput = document.querySelector('input[name="code"]') as HTMLInputElement;
39+
const recoveryInput = document.querySelector('input[name="recovery_code"]') as HTMLInputElement;
40+
if (codeInput) codeInput.value = '';
41+
if (recoveryInput) recoveryInput.value = '';
4242
};
4343
</script>
4444

4545
<template>
46-
<AuthLayout
47-
:title="recovery ? 'Recovery Code' : 'Authentication Code'"
48-
:description="recovery
49-
? 'Please confirm access to your account by entering one of your emergency recovery codes.'
50-
: 'Enter the authentication code provided by your authenticator application.'"
51-
>
52-
<Head title="Two Factor Authentication" />
46+
<AuthLayout
47+
:title="recovery ? 'Recovery Code' : 'Authentication Code'"
48+
:description="
49+
recovery
50+
? 'Please confirm access to your account by entering one of your emergency recovery codes.'
51+
: 'Enter the authentication code provided by your authenticator application.'
52+
"
53+
>
54+
<Head title="Two Factor Authentication" />
5355

54-
<div class="relative space-y-2">
55-
<template v-if="!recovery">
56-
<Form
57-
:action="route('two-factor.login')"
58-
method="post"
59-
class="space-y-4"
60-
#default="{ errors, processing, clearErrors }"
61-
>
62-
<input type="hidden" name="code" :value="codeValue" />
63-
<div class="flex flex-col items-center justify-center space-y-3 text-center">
64-
<div ref="pinInputContainerRef" class="w-full flex items-center justify-center">
65-
<PinInputRoot
66-
id="otp"
67-
v-model="pinValue"
68-
placeholder=""
69-
class="flex gap-2 items-center mt-1"
70-
>
71-
<PinInputInput
72-
v-for="(id, index) in 6"
73-
:key="id"
74-
:index="index"
75-
:autofocus="index === 0"
76-
class="w-10 h-10 rounded-lg text-center shadow-sm border text-green10 placeholder:text-mauve8 focus:shadow-[0_0_0_2px] focus:shadow-stone-800 outline-none"
77-
/>
78-
</PinInputRoot>
79-
</div>
80-
<InputError :message="errors.code" />
81-
</div>
82-
<Button
83-
type="submit"
84-
class="w-full"
85-
:disabled="processing || pinValue.length !== 6"
86-
>
87-
Continue
88-
</Button>
56+
<div class="relative space-y-2">
57+
<template v-if="!recovery">
58+
<Form :action="route('two-factor.login')" method="post" class="space-y-4" #default="{ errors, processing, clearErrors }">
59+
<input type="hidden" name="code" :value="codeValue" />
60+
<div class="flex flex-col items-center justify-center space-y-3 text-center">
61+
<div ref="pinInputContainerRef" class="flex w-full items-center justify-center">
62+
<PinInput id="otp" v-model="pinValue" class="mt-1" type="number" otp>
63+
<PinInputGroup class="gap-2">
64+
<PinInputSlot
65+
v-for="(id, index) in 6"
66+
:key="id"
67+
:index="index"
68+
:autofocus="index === 0"
69+
class="h-10 w-10 rounded-lg"
70+
/>
71+
</PinInputGroup>
72+
</PinInput>
73+
</div>
74+
<InputError :message="errors.code" />
75+
</div>
76+
<Button type="submit" class="w-full" :disabled="processing || pinValue.length !== 6"> Continue </Button>
8977

90-
<div class="space-x-0.5 text-center text-sm leading-5 text-muted-foreground mt-4">
91-
<span class="opacity-80">or you can </span>
92-
<button
93-
type="button"
94-
class="font-medium underline opacity-80 cursor-pointer bg-transparent border-0 p-0"
95-
@click="() => toggleRecovery(clearErrors)"
96-
>
97-
login using a recovery code
98-
</button>
99-
</div>
100-
</Form>
101-
</template>
78+
<div class="mt-4 space-x-0.5 text-center text-sm leading-5 text-muted-foreground">
79+
<span class="opacity-80">or you can </span>
80+
<button
81+
type="button"
82+
class="cursor-pointer border-0 bg-transparent p-0 font-medium underline opacity-80"
83+
@click="() => toggleRecovery(clearErrors)"
84+
>
85+
login using a recovery code
86+
</button>
87+
</div>
88+
</Form>
89+
</template>
10290

103-
<template v-else>
104-
<Form
105-
:action="route('two-factor.login')"
106-
method="post"
107-
class="space-y-4"
108-
#default="{ errors, processing, clearErrors }"
109-
>
110-
<Input
111-
ref="recoveryInputRef"
112-
name="recovery_code"
113-
type="text"
114-
class="block w-full"
115-
placeholder="Enter recovery code"
116-
autofocus
117-
required
118-
/>
119-
<InputError :message="errors.recovery_code" />
120-
<Button type="submit" class="w-full" :disabled="processing">
121-
Continue
122-
</Button>
91+
<template v-else>
92+
<Form :action="route('two-factor.login')" method="post" class="space-y-4" #default="{ errors, processing, clearErrors }">
93+
<Input
94+
ref="recoveryInputRef"
95+
name="recovery_code"
96+
type="text"
97+
class="block w-full"
98+
placeholder="Enter recovery code"
99+
autofocus
100+
required
101+
/>
102+
<InputError :message="errors.recovery_code" />
103+
<Button type="submit" class="w-full" :disabled="processing"> Continue </Button>
123104

124-
<div class="space-x-0.5 text-center text-sm leading-5 text-muted-foreground mt-4">
125-
<span class="opacity-80">or you can </span>
126-
<button
127-
type="button"
128-
class="font-medium underline opacity-80 cursor-pointer bg-transparent border-0 p-0"
129-
@click="() => toggleRecovery(clearErrors)"
130-
>
131-
login using an authentication code
132-
</button>
133-
</div>
134-
</Form>
135-
</template>
136-
</div>
137-
</AuthLayout>
105+
<div class="mt-4 space-x-0.5 text-center text-sm leading-5 text-muted-foreground">
106+
<span class="opacity-80">or you can </span>
107+
<button
108+
type="button"
109+
class="cursor-pointer border-0 bg-transparent p-0 font-medium underline opacity-80"
110+
@click="() => toggleRecovery(clearErrors)"
111+
>
112+
login using an authentication code
113+
</button>
114+
</div>
115+
</Form>
116+
</template>
117+
</div>
118+
</AuthLayout>
138119
</template>

resources/js/pages/settings/TwoFactor.vue

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import HeadingSmall from '@/components/HeadingSmall.vue';
33
import { Badge } from '@/components/ui/badge';
44
import { Button } from '@/components/ui/button';
5+
import { PinInput, PinInputGroup, PinInputSlot } from '@/components/ui/pin-input';
56
import AppLayout from '@/layouts/AppLayout.vue';
67
import SettingsLayout from '@/layouts/settings/Layout.vue';
78
import { Form, Head } from '@inertiajs/vue3';
8-
import { PinInputInput, PinInputRoot } from 'reka-ui';
99
import { computed, ComputedRef, nextTick, ref } from 'vue';
1010
1111
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
@@ -136,10 +136,7 @@ const toggleRecoveryCodes = () => {
136136
<Loader2 class="size-6 animate-spin" />
137137
</div>
138138
<div v-else class="relative z-10 overflow-hidden border p-5">
139-
<div
140-
v-html="props.qrCodeSvg"
141-
class="flex aspect-square size-full items-center justify-center"
142-
></div>
139+
<div v-html="props.qrCodeSvg" class="flex aspect-square size-full items-center justify-center"></div>
143140
<div v-if="props.qrCodeSvg" class="animate-scanning-line absolute inset-0 h-full w-full">
144141
<div
145142
class="absolute inset-x-0 h-0.5 bg-blue-500 opacity-60 transition-all duration-300 ease-in-out"
@@ -209,21 +206,17 @@ const toggleRecoveryCodes = () => {
209206
<input type="hidden" name="code" :value="code" />
210207
<div ref="pinInputContainerRef" class="relative w-full space-y-3">
211208
<div class="flex w-full flex-col items-center justify-center space-y-3 py-2">
212-
<PinInputRoot
213-
id="otp"
214-
type="number"
215-
v-model="pinValue"
216-
placeholder=""
217-
class="mt-1 flex items-center gap-2"
218-
>
219-
<PinInputInput
220-
v-for="(id, index) in 6"
221-
:key="id"
222-
:index="index"
223-
:disabled="processing"
224-
class="h-10 w-10 rounded-lg border border-input bg-background text-center text-foreground shadow-sm outline-none placeholder:text-muted-foreground focus:shadow-[0_0_0_2px] focus:shadow-ring disabled:cursor-not-allowed disabled:opacity-50"
225-
/>
226-
</PinInputRoot>
209+
<PinInput id="otp" v-model="pinValue" class="mt-1" type="number" otp>
210+
<PinInputGroup class="gap-2">
211+
<PinInputSlot
212+
v-for="(id, index) in 6"
213+
:key="id"
214+
:index="index"
215+
:disabled="processing"
216+
class="h-10 w-10 rounded-lg"
217+
/>
218+
</PinInputGroup>
219+
</PinInput>
227220
<div v-if="error" class="mt-2 text-sm text-red-600">
228221
{{ error }}
229222
</div>
@@ -269,7 +262,7 @@ const toggleRecoveryCodes = () => {
269262
</div>
270263
</div>
271264

272-
<div class="rounded-b-xl border border-t-0 border-stone-200 bg-secondary dark:border-stone-700 text-sm">
265+
<div class="rounded-b-xl border border-t-0 border-stone-200 bg-secondary text-sm dark:border-stone-700">
273266
<div
274267
@click="toggleRecoveryCodes"
275268
class="group flex h-10 cursor-pointer items-center justify-between px-5 text-xs select-none"
@@ -317,12 +310,7 @@ const toggleRecoveryCodes = () => {
317310
</div>
318311

319312
<div class="relative inline">
320-
<Form
321-
:action="route('two-factor.disable')"
322-
method="delete"
323-
async="true"
324-
#default="{ processing }"
325-
>
313+
<Form :action="route('two-factor.disable')" method="delete" async="true" #default="{ processing }">
326314
<Button variant="destructive" type="submit" :disabled="processing">
327315
{{ processing ? 'Disabling...' : 'Disable 2FA' }}
328316
</Button>

0 commit comments

Comments
 (0)