fix(shared): legacy conv-key migration query used .eq(null) instead of .is(null)
PostgREST translates .eq('col', null) to `col = NULL` which is always false
in SQL. The migration silently returned zero rows -> setupNewUserIdentity
fired but re-wrapped nothing -> users could set a PIN but every send threw
'Awaiting key'. Switching to .is('col', null) emits `col IS NULL` and the
migration finally finds its work.
Also makes the migration trigger idempotent and re-fires it on:
- every successful loadOrUnlockUserKey
- AuthContext startup when the user-key is already cached
so users stuck on 0.18.0 auto-recover the moment they install 0.18.1.
PinInput: focused + active-slot now show a brand-coloured ring, glow, and
a blinking caret so users see where the next keystroke lands.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
@@ -12,7 +12,12 @@ interface Props {
|
||||
|
||||
export function PinInput({ value, onChange, length = 6, autoFocus, disabled, ariaLabel, onSubmit }: Props) {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const [focused, setFocused] = useState(false);
|
||||
useEffect(() => { if (autoFocus) ref.current?.focus(); }, [autoFocus]);
|
||||
// Index of the next empty slot the next keystroke will fill. When the user
|
||||
// has typed all `length` digits, no slot is "active" — the form should
|
||||
// submit instead of pretending one is still focused.
|
||||
const activeIndex = value.length < length ? value.length : -1;
|
||||
return (
|
||||
<div className="relative flex justify-center" onClick={() => ref.current?.focus()}>
|
||||
<input
|
||||
@@ -26,20 +31,30 @@ export function PinInput({ value, onChange, length = 6, autoFocus, disabled, ari
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value.replace(/\D/g, '').slice(0, length))}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && value.length === length) onSubmit?.(); }}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
className="absolute h-px w-px overflow-hidden p-0 opacity-0"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length }).map((_, i) => {
|
||||
const filled = i < value.length;
|
||||
const active = focused && i === activeIndex;
|
||||
let classes =
|
||||
'flex h-12 w-10 items-center justify-center rounded-lg border text-lg font-semibold transition ';
|
||||
if (filled) {
|
||||
classes += 'border-brand-400 bg-brand-500/10 text-white';
|
||||
} else if (active) {
|
||||
// Brand-coloured ring + glow so the user immediately sees where
|
||||
// the next keystroke lands. The animated cursor inside reinforces
|
||||
// the "input is alive" feeling.
|
||||
classes += 'border-brand-400 bg-brand-500/10 text-brand-300 ring-2 ring-brand-400/40 shadow-[0_0_12px_-2px] shadow-brand-500/40';
|
||||
} else {
|
||||
classes += 'border-white/10 bg-ink-800 text-neutral-500';
|
||||
}
|
||||
return (
|
||||
<span key={i}
|
||||
className={
|
||||
'flex h-12 w-10 items-center justify-center rounded-lg border text-lg font-semibold ' +
|
||||
(filled
|
||||
? 'border-brand-400 bg-brand-500/10 text-white'
|
||||
: 'border-white/10 bg-ink-800 text-neutral-500')
|
||||
}
|
||||
>{filled ? '•' : ''}</span>
|
||||
<span key={i} className={classes}>
|
||||
{filled ? '•' : active ? <span className="animate-pulse">|</span> : ''}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user