Add StringMultiInput for OTPs

This commit is contained in:
Jon Staab
2026-03-06 14:58:25 -08:00
parent c56d6f4c75
commit 5f7474140f
4 changed files with 89 additions and 58 deletions
+4 -19
View File
@@ -15,10 +15,10 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import KeyDownload from "@app/components/KeyDownload.svelte" import KeyDownload from "@app/components/KeyDownload.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {POMADE_SIGNERS} from "@app/core/state"
type Props = { type Props = {
peersByPrefix: Map<string, string> peersByPrefix: Map<string, string>
@@ -32,18 +32,6 @@
} = $session as SessionPomade } = $session as SessionPomade
const confirmRecovery = async () => { const confirmRecovery = async () => {
const otps = input
.split(/\n/)
.map(x => x.trim())
.filter(x => x.match(/^[0-9]{8}$/))
if (otps.length < 2) {
return pushToast({
theme: "error",
message: "Failed to recover, not enough valid recovery codes were provided.",
})
}
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps) const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
if (!request.ok) { if (!request.ok) {
@@ -82,7 +70,7 @@
const back = () => history.back() const back = () => history.back()
let loading = $state(false) let loading = $state(false)
let input = $state("") let otps = $state<string[]>([])
</script> </script>
<Modal tag="form" onsubmit={preventDefault(submit)}> <Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -96,17 +84,14 @@
For security reasons, you may receive three or more emails with recovery codes in them. Please For security reasons, you may receive three or more emails with recovery codes in them. Please
paste <strong>all</strong> recovery codes into the text box below, on separate lines. paste <strong>all</strong> recovery codes into the text box below, on separate lines.
</p> </p>
<textarea <StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4"
bind:value={input}></textarea>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 2}>
<Spinner {loading}>Confirm recovery</Spinner> <Spinner {loading}>Confirm recovery</Spinner>
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
+4 -19
View File
@@ -13,8 +13,8 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte" import LogInSelect from "@app/components/LogInSelect.svelte"
import {POMADE_SIGNERS} from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
@@ -30,18 +30,6 @@
const back = () => history.back() const back = () => history.back()
const onSubmit = async () => { const onSubmit = async () => {
const otps = input
.split(/\n/)
.map(x => x.trim())
.filter(x => x.match(/^[0-9]{8}$/))
if (otps.length < 3) {
return pushToast({
theme: "error",
message: "Not enough valid recovery codes were provided.",
})
}
loading = true loading = true
try { try {
@@ -85,7 +73,7 @@
} }
} }
let input = $state("") let otps = $state<string[]>([])
let loading = $state(false) let loading = $state(false)
</script> </script>
@@ -100,17 +88,14 @@
For security reasons, you may receive three or more emails with login codes in them. Please For security reasons, you may receive three or more emails with login codes in them. Please
paste <strong>all</strong> login codes into the text box below, on separate lines. paste <strong>all</strong> login codes into the text box below, on separate lines.
</p> </p>
<textarea <StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4"
bind:value={input}></textarea>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 3}>
<Spinner {loading}>Log In</Spinner> <Spinner {loading}>Log In</Spinner>
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
+4 -20
View File
@@ -14,8 +14,8 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import PasswordResetConfirm from "@app/components/PasswordResetConfirm.svelte" import PasswordResetConfirm from "@app/components/PasswordResetConfirm.svelte"
import {POMADE_SIGNERS} from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {getPomadeClient} from "@app/util/pomade" import {getPomadeClient} from "@app/util/pomade"
@@ -29,19 +29,6 @@
const {email} = $session as SessionPomade const {email} = $session as SessionPomade
const confirmRecovery = async () => { const confirmRecovery = async () => {
const otps = input
.split(/\n/)
.map(x => x.trim())
.filter(x => x.match(/^[0-9]{8}$/))
if (otps.length < 2) {
return pushToast({
theme: "error",
message:
"Failed to validate email ownership, not enough valid recovery codes were provided.",
})
}
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps) const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
if (!request.ok) { if (!request.ok) {
@@ -90,7 +77,7 @@
const back = () => history.back() const back = () => history.back()
let loading = $state(false) let loading = $state(false)
let input = $state("") let otps = $state<string[]>([])
</script> </script>
<Modal tag="form" onsubmit={preventDefault(submit)}> <Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -104,17 +91,14 @@
For security reasons, you may receive three or more emails with confirmation codes in them. For security reasons, you may receive three or more emails with confirmation codes in them.
Please paste <strong>all</strong> confirmation codes into the text box below, on separate lines. Please paste <strong>all</strong> confirmation codes into the text box below, on separate lines.
</p> </p>
<textarea <StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4"
bind:value={input}></textarea>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 2}>
<Spinner {loading}>Continue</Spinner> <Spinner {loading}>Continue</Spinner>
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
@@ -0,0 +1,77 @@
<script lang="ts">
import {writable} from "svelte/store"
import type {Writable} from "svelte/store"
import {remove, uniq} from "@welshman/lib"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
interface Props {
value: string[]
term?: Writable<string>
placeholder?: string
}
let {value = $bindable(), term = writable(""), placeholder = ""}: Props = $props()
const normalizeItem = (text: string) => text.trim()
const addItems = (text: string) => {
const items = text.split(/[\n,]/).map(normalizeItem).filter(Boolean)
if (items.length > 0) {
value = uniq([...value, ...items])
}
term.set("")
}
const removeItem = (item: string) => {
value = remove(item, value)
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && $term) {
e.preventDefault()
addItems($term)
}
}
const onPaste = (e: ClipboardEvent) => {
const text = e.clipboardData?.getData("text")
if (text) {
e.preventDefault()
addItems(text)
}
}
const onBlur = () => {
if ($term.trim()) {
addItems($term)
}
}
</script>
<div class="flex flex-col gap-2">
<div>
{#each value as item (item)}
<div class="flex-inline badge badge-neutral mr-1 gap-1">
<Button class="flex items-center" onclick={() => removeItem(item)}>
<Icon icon={CloseCircle} size={4} class="-ml-1 mt-px" />
</Button>
<span>{item}</span>
</div>
{/each}
</div>
<label class="input input-bordered flex w-full items-center gap-2">
<input
bind:value={$term}
class="grow"
type="text"
{placeholder}
onkeydown={onKeyDown}
onpaste={onPaste}
onblur={onBlur} />
</label>
</div>