From 129dfa66cafb9eae28f31e7bcae3ee65fbed6db9 Mon Sep 17 00:00:00 2001 From: tdgao Date: Mon, 15 Jun 2026 22:27:40 -0700 Subject: [PATCH 1/3] feat: implement 2fa code input component --- .../base/TwoFactorAuthCodeInput.vue | 186 ++++++++++++++++++ packages/ui/src/components/base/index.ts | 1 + .../base/TwoFactorAuthCodeInput.stories.ts | 51 +++++ 3 files changed, 238 insertions(+) create mode 100644 packages/ui/src/components/base/TwoFactorAuthCodeInput.vue create mode 100644 packages/ui/src/stories/base/TwoFactorAuthCodeInput.stories.ts diff --git a/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue b/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue new file mode 100644 index 0000000000..838ffcbb78 --- /dev/null +++ b/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue @@ -0,0 +1,186 @@ + + + diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index 5afcec8f57..8357bf5aa0 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -97,4 +97,5 @@ export type { export { default as TimeFramePicker } from './TimeFramePicker.vue' export { default as Timeline } from './Timeline.vue' export { default as Toggle } from './Toggle.vue' +export { default as TwoFactorAuthCodeInput } from './TwoFactorAuthCodeInput.vue' export { default as UnsavedChangesPopup } from './UnsavedChangesPopup.vue' diff --git a/packages/ui/src/stories/base/TwoFactorAuthCodeInput.stories.ts b/packages/ui/src/stories/base/TwoFactorAuthCodeInput.stories.ts new file mode 100644 index 0000000000..14a4ac8bea --- /dev/null +++ b/packages/ui/src/stories/base/TwoFactorAuthCodeInput.stories.ts @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import TwoFactorAuthCodeInput from '../../components/base/TwoFactorAuthCodeInput.vue' + +const meta = { + title: 'Base/TwoFactorAuthCodeInput', + component: TwoFactorAuthCodeInput, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + components: { TwoFactorAuthCodeInput }, + setup() { + const code = ref('') + return { code } + }, + template: ` + + `, + }), +} + +export const Filled: Story = { + render: () => ({ + components: { TwoFactorAuthCodeInput }, + setup() { + const code = ref('123456') + return { code } + }, + template: ` + + `, + }), +} + +export const Disabled: Story = { + render: () => ({ + components: { TwoFactorAuthCodeInput }, + setup() { + const code = ref('123456') + return { code } + }, + template: ` + + `, + }), +} From dc569ecd8921f6b110a641ed9d7577bbbdcf3ce9 Mon Sep 17 00:00:00 2001 From: tdgao Date: Tue, 30 Jun 2026 10:58:29 -0600 Subject: [PATCH 2/3] feat: improve 2fa UX --- .../base/TwoFactorAuthCodeInput.vue | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue b/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue index 838ffcbb78..9c9e4dad49 100644 --- a/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue +++ b/packages/ui/src/components/base/TwoFactorAuthCodeInput.vue @@ -21,6 +21,7 @@ :aria-label="`Code digit ${index}`" class="h-12 w-11 appearance-none rounded-xl border-none bg-surface-4 p-1 text-center text-base font-medium text-primary focus:text-primary focus:ring-4 focus:ring-brand-shadow disabled:cursor-not-allowed" :class="[inputClass, 'outline-none']" + @focus="handleFocus($event)" @input="handleInput($event, index - 1)" @keydown="handleKeydown($event, index - 1)" @paste.prevent="handlePaste" @@ -84,7 +85,7 @@ function setCodeInput(element: Element | ComponentPublicInstance | null, index: function focusInput(index: number) { const input = codeInputs.value[index] input?.focus() - input?.setSelectionRange(input.value.length, input.value.length) + selectInputValue(input) } function focusFirstUnfilledCodeInput() { @@ -94,7 +95,14 @@ function focusFirstUnfilledCodeInput() { } function handlePointerDown(event: PointerEvent) { - if (disabledOrReadonly()) { + if (props.disabled) { + return + } + + if (event.target instanceof HTMLInputElement && event.target.value) { + event.preventDefault() + event.target.focus() + selectInputValue(event.target) return } @@ -102,6 +110,14 @@ function handlePointerDown(event: PointerEvent) { focusFirstUnfilledCodeInput() } +function handleFocus(event: FocusEvent) { + if (props.disabled) { + return + } + + selectInputValue(event.target as HTMLInputElement) +} + function handleInput(event: Event, index: number) { if (disabledOrReadonly()) { return @@ -174,6 +190,18 @@ function disabledOrReadonly() { return props.disabled || props.readonly } +function selectInputValue(input?: HTMLInputElement) { + if (!input) { + return + } + + if (input.value) { + input.select() + } else { + input.setSelectionRange(input.value.length, input.value.length) + } +} + function clear() { digits.value = Array.from({ length: codeLength }, () => '') updateModel() From c6c671c66840ef239d76a8dce1f579c5ce413c8a Mon Sep 17 00:00:00 2001 From: tdgao Date: Tue, 30 Jun 2026 11:13:55 -0600 Subject: [PATCH 3/3] feat: update account settings 2fa set up to use 2fa code component --- apps/frontend/src/pages/settings/account.vue | 69 ++++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/apps/frontend/src/pages/settings/account.vue b/apps/frontend/src/pages/settings/account.vue index ce57a33618..690c61cf9b 100644 --- a/apps/frontend/src/pages/settings/account.vue +++ b/apps/frontend/src/pages/settings/account.vue @@ -183,24 +183,24 @@ >