Email confirmation, change email/password, show/hide passwords
CI / check (push) Canceled after 0s

- Sign-up emails a confirmation link (24h); unconfirmed accounts see a
  banner on the web with "send it again", and status in Settings.
- Change email (needs the password): the new address gets a confirmation
  link and only replaces the old one once clicked; the old address gets a
  heads-up with a reset-password link. Pending changes can be cancelled.
- Change password (needs the current one): signs out other sessions.
- Password fields have Show/Hide on web and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-06 13:05:26 -04:00
co-authored by Claude Opus 5.5
parent 45a7bd190a
commit 85e1643c29
25 changed files with 2077 additions and 19 deletions
+26 -9
View File
@@ -97,22 +97,37 @@ export function Button({
);
}
/** A text field. Password fields (secureTextEntry) get a Show/Hide button. */
export const Field = forwardRef<TextInput, TextInputProps & { label?: string }>(function Field(
{ label, style, onFocus, onBlur, ...props },
{ label, style, onFocus, onBlur, secureTextEntry, ...props },
ref,
) {
const [focused, setFocused] = useState(false);
const [revealed, setRevealed] = useState(false);
return (
<View style={{ gap: space.xs }}>
{label && <Text style={styles.label}>{label}</Text>}
<TextInput
ref={ref}
placeholderTextColor={colors.muted}
style={[styles.input, focused && styles.inputFocused, style]}
onFocus={(e) => (setFocused(true), onFocus?.(e))}
onBlur={(e) => (setFocused(false), onBlur?.(e))}
{...props}
/>
<View>
<TextInput
ref={ref}
placeholderTextColor={colors.muted}
style={[styles.input, focused && styles.inputFocused, secureTextEntry && { paddingRight: 64 }, style]}
onFocus={(e) => (setFocused(true), onFocus?.(e))}
onBlur={(e) => (setFocused(false), onBlur?.(e))}
secureTextEntry={secureTextEntry && !revealed}
{...props}
/>
{secureTextEntry && (
<Pressable
accessibilityRole="button"
accessibilityLabel={revealed ? 'Hide password' : 'Show password'}
onPress={() => setRevealed((v) => !v)}
hitSlop={8}
style={styles.reveal}>
<Text style={styles.revealText}>{revealed ? 'Hide' : 'Show'}</Text>
</Pressable>
)}
</View>
</View>
);
});
@@ -175,6 +190,8 @@ const styles = StyleSheet.create({
buttonText: { color: colors.text, fontWeight: '700', fontSize: 15 },
buttonTextSmall: { fontSize: 13 },
label: { color: colors.text, fontWeight: '600', fontSize: 14 },
reveal: { position: 'absolute', right: 0, top: 0, bottom: 0, justifyContent: 'center', paddingHorizontal: space.md },
revealText: { color: colors.accent2, fontWeight: '600', fontSize: 14 },
input: {
minHeight: 48,
paddingHorizontal: space.md + 2,