fix(native/mail): duplicate add-button in empty state + intro hints in ConnectMailSheet

- mail.tsx: hide section-header "+" button when accounts.length === 0 — MailEmptyState's CTA is the sole add trigger; also replaces Pressable with TouchableOpacity
- MailEmptyState: Pressable → TouchableOpacity (no-Pressable rule)
- SheetFieldStack: add optional `intro?: ReactNode` prop rendered in a flexShrink:1 ScrollView above chips/active-input so it compresses gracefully when the keyboard is up
- ConnectMailSheet: move app-password guide + green AES block into `intro` prop so they're visible from the start, before the user types anything

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-12 23:39:22 +02:00
parent 3eaf3f098a
commit a3f892ddac
4 changed files with 280 additions and 261 deletions

View File

@ -5,6 +5,7 @@ import {
Pressable,
ScrollView,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useBottomTabBarHeight } from 'react-native-bottom-tabs';
@ -173,7 +174,8 @@ export default function MailScreen() {
</View>
)}
{/* Section header with prominent + button */}
{/* Section header with prominent + button — hidden in empty state (CTA lives there) */}
{accounts.length > 0 && (
<View
style={{
flexDirection: 'row',
@ -211,10 +213,11 @@ export default function MailScreen() {
</Text>
</View>
<Pressable
<TouchableOpacity
onPress={handleAddPress}
disabled={limitReached}
android_ripple={{ color: '#0066cc' }}
activeOpacity={limitReached ? 1 : 0.8}
accessibilityLabel={t('mail.add_account_a11y')}
style={{
backgroundColor: limitReached ? colors.surfaceElevated : '#007AFF',
borderRadius: 12,
@ -224,11 +227,6 @@ export default function MailScreen() {
shadowOpacity: limitReached ? 0 : 0.25,
shadowRadius: 8,
elevation: limitReached ? 0 : 4,
}}
accessibilityLabel={t('mail.add_account_a11y')}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
@ -250,9 +248,9 @@ export default function MailScreen() {
>
{t('mail.add_account')}
</Text>
</TouchableOpacity>
</View>
</Pressable>
</View>
)}
{/* Account cards or empty */}
{accounts.length === 0 ? (

View File

@ -27,6 +27,13 @@ export type SheetField = {
type Props = {
fields: SheetField[];
/**
* Immer sichtbarer Bereich über Chips + aktivem Input für Hinweise, die der User
* sehen soll BEVOR er tippt. Wird in einer eigenen ScrollView mit `flexShrink:1`
* gerendert, sodass er bei kleinem verfügbaren Platz (Tastatur offen) schrumpft,
* der Eingabebereich aber nie weggedrückt wird.
*/
intro?: ReactNode;
/** Rendert sich nach dem letzten Feld — sichtbar sobald alle Felder ausgefüllt sind. */
children?: ReactNode;
onComplete?: () => void;
@ -41,7 +48,7 @@ type Props = {
*
* Wird als `children` von `<FormSheet>` benutzt.
*/
export function SheetFieldStack({ fields, children, onComplete }: Props) {
export function SheetFieldStack({ fields, intro, children, onComplete }: Props) {
const colors = useColors();
const [activeIndex, setActiveIndex] = useState(0);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
@ -84,9 +91,23 @@ export function SheetFieldStack({ fields, children, onComplete }: Props) {
const isLast = activeIndex === fields.length - 1;
return (
<View style={{ flex: 1 }}>
{/* Intro: immer sichtbar, schrumpft bei wenig Platz (Tastatur offen) */}
{intro != null && (
<ScrollView
style={{ flexShrink: 1 }}
contentContainerStyle={{ padding: 16, paddingBottom: 0 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{intro}
</ScrollView>
)}
{/* Chips + aktives Feld + Post-Completion-Inhalt */}
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 16, gap: 10 }}
contentContainerStyle={{ padding: 16, paddingTop: intro != null ? 10 : 16, gap: 10 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
@ -221,5 +242,6 @@ export function SheetFieldStack({ fields, children, onComplete }: Props) {
{/* Rest des Formulars — sichtbar wenn alle Felder durch */}
{allDone && children}
</ScrollView>
</View>
);
}

View File

@ -189,9 +189,9 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
),
},
]}
onComplete={() => setFieldsComplete(true)}
>
{/* App-Password-Guide — über den Datenschutz-Hinweis */}
intro={
<View style={{ gap: 10 }}>
{/* App-Password-Guide — provider-spezifisch, nicht für 'other' */}
{selectedProvider && selectedProvider.id !== 'other' && (
<View
style={{
@ -202,7 +202,6 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
borderRadius: 12,
borderWidth: 1,
borderColor: '#bfdbfe',
marginBottom: 10,
}}
>
<Ionicons
@ -248,7 +247,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
</View>
)}
{/* Datenschutz-Hinweis */}
{/* Datenschutz-Zusicherung — immer sichtbar */}
<View
style={{
flexDirection: 'row',
@ -258,7 +257,6 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
borderRadius: 12,
borderWidth: 1,
borderColor: '#bbf7d0',
marginBottom: 10,
}}
>
<Ionicons name="shield-checkmark" size={16} color="#16a34a" style={{ marginTop: 1 }} />
@ -274,7 +272,10 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
{t('mail.form_privacy_note')}
</Text>
</View>
</View>
}
onComplete={() => setFieldsComplete(true)}
>
{/* Fehler */}
{(formError ?? (connectError ? t(humanizeMailError(connectError)) : null)) && (
<Text

View File

@ -1,4 +1,4 @@
import { Pressable, Text, View } from 'react-native';
import { Text, TouchableOpacity, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
@ -81,12 +81,10 @@ export function MailEmptyState({ onConnectPress }: Props) {
</View>
{/* CTA */}
<Pressable
<TouchableOpacity
onPress={onConnectPress}
style={({ pressed }) => ({
opacity: pressed ? 0.85 : 1,
alignSelf: 'stretch',
})}
activeOpacity={0.85}
style={{ alignSelf: 'stretch' }}
>
<View style={{
backgroundColor: '#007AFF',
@ -99,7 +97,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
{t('mail.empty_state_cta')}
</Text>
</View>
</Pressable>
</TouchableOpacity>
</View>
);
}