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:
parent
3eaf3f098a
commit
a3f892ddac
@ -5,6 +5,7 @@ import {
|
|||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Text,
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useBottomTabBarHeight } from 'react-native-bottom-tabs';
|
import { useBottomTabBarHeight } from 'react-native-bottom-tabs';
|
||||||
@ -173,7 +174,8 @@ export default function MailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Section header with prominent + button */}
|
{/* Section header with prominent + button — hidden in empty state (CTA lives there) */}
|
||||||
|
{accounts.length > 0 && (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -211,10 +213,11 @@ export default function MailScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={handleAddPress}
|
onPress={handleAddPress}
|
||||||
disabled={limitReached}
|
disabled={limitReached}
|
||||||
android_ripple={{ color: '#0066cc' }}
|
activeOpacity={limitReached ? 1 : 0.8}
|
||||||
|
accessibilityLabel={t('mail.add_account_a11y')}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: limitReached ? colors.surfaceElevated : '#007AFF',
|
backgroundColor: limitReached ? colors.surfaceElevated : '#007AFF',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
@ -224,11 +227,6 @@ export default function MailScreen() {
|
|||||||
shadowOpacity: limitReached ? 0 : 0.25,
|
shadowOpacity: limitReached ? 0 : 0.25,
|
||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
elevation: limitReached ? 0 : 4,
|
elevation: limitReached ? 0 : 4,
|
||||||
}}
|
|
||||||
accessibilityLabel={t('mail.add_account_a11y')}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
@ -250,9 +248,9 @@ export default function MailScreen() {
|
|||||||
>
|
>
|
||||||
{t('mail.add_account')}
|
{t('mail.add_account')}
|
||||||
</Text>
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
)}
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Account cards or empty */}
|
{/* Account cards or empty */}
|
||||||
{accounts.length === 0 ? (
|
{accounts.length === 0 ? (
|
||||||
|
|||||||
@ -27,6 +27,13 @@ export type SheetField = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fields: SheetField[];
|
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. */
|
/** Rendert sich nach dem letzten Feld — sichtbar sobald alle Felder ausgefüllt sind. */
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
@ -41,7 +48,7 @@ type Props = {
|
|||||||
*
|
*
|
||||||
* Wird als `children` von `<FormSheet>` benutzt.
|
* 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 colors = useColors();
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||||
@ -84,9 +91,23 @@ export function SheetFieldStack({ fields, children, onComplete }: Props) {
|
|||||||
const isLast = activeIndex === fields.length - 1;
|
const isLast = activeIndex === fields.length - 1;
|
||||||
|
|
||||||
return (
|
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
|
<ScrollView
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
contentContainerStyle={{ padding: 16, gap: 10 }}
|
contentContainerStyle={{ padding: 16, paddingTop: intro != null ? 10 : 16, gap: 10 }}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
@ -221,5 +242,6 @@ export function SheetFieldStack({ fields, children, onComplete }: Props) {
|
|||||||
{/* Rest des Formulars — sichtbar wenn alle Felder durch */}
|
{/* Rest des Formulars — sichtbar wenn alle Felder durch */}
|
||||||
{allDone && children}
|
{allDone && children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -189,9 +189,9 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onComplete={() => setFieldsComplete(true)}
|
intro={
|
||||||
>
|
<View style={{ gap: 10 }}>
|
||||||
{/* App-Password-Guide — über den Datenschutz-Hinweis */}
|
{/* App-Password-Guide — provider-spezifisch, nicht für 'other' */}
|
||||||
{selectedProvider && selectedProvider.id !== 'other' && (
|
{selectedProvider && selectedProvider.id !== 'other' && (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -202,7 +202,6 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#bfdbfe',
|
borderColor: '#bfdbfe',
|
||||||
marginBottom: 10,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
@ -248,7 +247,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Datenschutz-Hinweis */}
|
{/* Datenschutz-Zusicherung — immer sichtbar */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -258,7 +257,6 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#bbf7d0',
|
borderColor: '#bbf7d0',
|
||||||
marginBottom: 10,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="shield-checkmark" size={16} color="#16a34a" style={{ marginTop: 1 }} />
|
<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')}
|
{t('mail.form_privacy_note')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
onComplete={() => setFieldsComplete(true)}
|
||||||
|
>
|
||||||
{/* Fehler */}
|
{/* Fehler */}
|
||||||
{(formError ?? (connectError ? t(humanizeMailError(connectError)) : null)) && (
|
{(formError ?? (connectError ? t(humanizeMailError(connectError)) : null)) && (
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@ -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 { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useColors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
@ -81,12 +81,10 @@ export function MailEmptyState({ onConnectPress }: Props) {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={onConnectPress}
|
onPress={onConnectPress}
|
||||||
style={({ pressed }) => ({
|
activeOpacity={0.85}
|
||||||
opacity: pressed ? 0.85 : 1,
|
style={{ alignSelf: 'stretch' }}
|
||||||
alignSelf: 'stretch',
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<View style={{
|
<View style={{
|
||||||
backgroundColor: '#007AFF',
|
backgroundColor: '#007AFF',
|
||||||
@ -99,7 +97,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
|
|||||||
{t('mail.empty_state_cta')}
|
{t('mail.empty_state_cta')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user