chahinebrini 5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow
mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail.
GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift.

- KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding
- react-native-keyboard-controller installiert + KeyboardProvider in Root
- Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO)
- Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn
- DPad-Buttons 60→48, more bg, no scale
- useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates
- dm.tsx: resolveAvatar wrap (iron.png-Warning)
- Mail-error-humanizer + locales

Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:59:25 +02:00

80 lines
2.0 KiB
TypeScript

/**
* Übersetzt rohe Backend/IMAP-Fehlermeldungen in benutzerfreundliche Sätze.
*
* Backend liefert oft IMAP-Server-Antworten 1:1 durch (z.B.
* `[AUTHENTICATIONFAILED] Invalid credentials (Failure)`). Die zeigen wir
* dem User NICHT — stattdessen humane Übersetzung mit Hinweis was zu tun ist.
*/
export type MailErrorReason =
| 'auth_failed'
| 'app_password_required'
| 'connection_failed'
| 'host_unreachable'
| 'tls_error'
| 'rate_limited'
| 'unknown';
export function classifyMailError(raw: string | null | undefined): MailErrorReason {
if (!raw) return 'unknown';
const s = raw.toLowerCase();
if (
s.includes('authenticationfailed') ||
s.includes('invalid credentials') ||
s.includes('authentication failed') ||
s.includes('login failed') ||
s.includes('auth failed') ||
s.includes('bad password') ||
s.includes('wrong password')
) {
return 'auth_failed';
}
if (
s.includes('application-specific password') ||
s.includes('app password required') ||
s.includes('weblogin_required') ||
s.includes('two-factor')
) {
return 'app_password_required';
}
if (
s.includes('etimedout') ||
s.includes('econnrefused') ||
s.includes('connection timeout') ||
s.includes('socket timeout') ||
s.includes('connection reset')
) {
return 'connection_failed';
}
if (
s.includes('enotfound') ||
s.includes('host not found') ||
s.includes('getaddrinfo') ||
s.includes('dns')
) {
return 'host_unreachable';
}
if (s.includes('tls') || s.includes('ssl') || s.includes('certificate')) {
return 'tls_error';
}
if (s.includes('rate limit') || s.includes('too many') || s.includes('throttl')) {
return 'rate_limited';
}
return 'unknown';
}
/**
* Liefert den i18n-Schlüssel für die humane Variante eines Mail-Errors.
* Caller ruft `t(humanizeMailError(rawError))` für den finalen Text.
*/
export function humanizeMailError(raw: string | null | undefined): string {
const reason = classifyMailError(raw);
return `mail.errors.${reason}`;
}