From f24c364c81d813a0993a4fe99131a0b76cf1b4d7 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Tue, 12 May 2026 22:12:29 +0200 Subject: [PATCH] feat(rebreak-native): KeyboardAwareScreen composable + full-screen form migration (phase 3A) New component/KeyboardAwareScreen.tsx encapsulates the standard KeyboardAvoidingView pattern for full-screen forms: - iOS behavior="padding", Android no-op (adjustResize covers it) - scrollable prop: ScrollView with keyboardShouldPersistTaps="handled" - non-scrollable: TouchableWithoutFeedback+View for tap-to-dismiss - headerOffset prop for screens owning their own header padding Migrated to KeyboardAwareScreen: signin, signup, forgot-password, confirm-otp (SafeAreaView-wrapped, no headerOffset needed) and profile/edit (KAV wrapper only, explicit ScrollView retained). Co-Authored-By: Claude Sonnet 4.6 --- .../rebreak-native/app/(auth)/confirm-otp.tsx | 12 +--- .../app/(auth)/forgot-password.tsx | 12 +--- apps/rebreak-native/app/(auth)/signin.tsx | 18 ++--- apps/rebreak-native/app/(auth)/signup.tsx | 18 ++--- apps/rebreak-native/app/profile/edit.tsx | 10 +-- .../components/KeyboardAwareScreen.tsx | 65 +++++++++++++++++++ 6 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 apps/rebreak-native/components/KeyboardAwareScreen.tsx diff --git a/apps/rebreak-native/app/(auth)/confirm-otp.tsx b/apps/rebreak-native/app/(auth)/confirm-otp.tsx index 0b84132..d959567 100644 --- a/apps/rebreak-native/app/(auth)/confirm-otp.tsx +++ b/apps/rebreak-native/app/(auth)/confirm-otp.tsx @@ -4,14 +4,13 @@ import { Text, TextInput, TouchableOpacity, - KeyboardAvoidingView, - Platform, ActivityIndicator, } from 'react-native'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../stores/auth'; +import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen'; const OTP_LENGTH = 6; @@ -111,11 +110,7 @@ export default function ConfirmOtpScreen() { return ( - - + {/* Header */} @@ -203,8 +198,7 @@ export default function ConfirmOtpScreen() { > {t('auth.backToSignup')} - - + ); } diff --git a/apps/rebreak-native/app/(auth)/forgot-password.tsx b/apps/rebreak-native/app/(auth)/forgot-password.tsx index da69940..6055c43 100644 --- a/apps/rebreak-native/app/(auth)/forgot-password.tsx +++ b/apps/rebreak-native/app/(auth)/forgot-password.tsx @@ -4,14 +4,13 @@ import { Text, TextInput, TouchableOpacity, - KeyboardAvoidingView, - Platform, ActivityIndicator, } from 'react-native'; import { useRouter } from 'expo-router'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../stores/auth'; +import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen'; const INPUT_STYLE = { fontSize: 16, @@ -47,11 +46,7 @@ export default function ForgotPasswordScreen() { return ( - - + {t('auth.resetPasswordTitle')} {t('auth.resetPasswordSubtitle')} @@ -108,8 +103,7 @@ export default function ForgotPasswordScreen() { > {t('auth.backToLogin')} - - + ); } diff --git a/apps/rebreak-native/app/(auth)/signin.tsx b/apps/rebreak-native/app/(auth)/signin.tsx index 23b5eea..d8b7d32 100644 --- a/apps/rebreak-native/app/(auth)/signin.tsx +++ b/apps/rebreak-native/app/(auth)/signin.tsx @@ -4,9 +4,6 @@ import { Text, TextInput, TouchableOpacity, - KeyboardAvoidingView, - Platform, - ScrollView, ActivityIndicator, } from 'react-native'; import { useRouter } from 'expo-router'; @@ -14,6 +11,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Path } from 'react-native-svg'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../stores/auth'; +import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen'; function GoogleIcon() { return ( @@ -85,15 +83,10 @@ export default function SignInScreen() { return ( - - {t('auth.welcomeBack')} {t('auth.signinSubtitle')} @@ -197,8 +190,7 @@ export default function SignInScreen() { {t('auth.signup')} - - + ); } diff --git a/apps/rebreak-native/app/(auth)/signup.tsx b/apps/rebreak-native/app/(auth)/signup.tsx index 111bd42..45d5428 100644 --- a/apps/rebreak-native/app/(auth)/signup.tsx +++ b/apps/rebreak-native/app/(auth)/signup.tsx @@ -4,9 +4,6 @@ import { Text, TextInput, TouchableOpacity, - KeyboardAvoidingView, - Platform, - ScrollView, Image, ActivityIndicator, } from 'react-native'; @@ -16,6 +13,7 @@ import Svg, { Path } from 'react-native-svg'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../stores/auth'; import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars'; +import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen'; function GoogleIcon() { return ( @@ -109,15 +107,10 @@ export default function SignUpScreen() { return ( - - {t('auth.signupTitle')} {t('auth.signupSubtitle')} @@ -300,8 +293,7 @@ export default function SignUpScreen() { {t('auth.signin')} - - + ); } diff --git a/apps/rebreak-native/app/profile/edit.tsx b/apps/rebreak-native/app/profile/edit.tsx index ca83526..3b868ac 100644 --- a/apps/rebreak-native/app/profile/edit.tsx +++ b/apps/rebreak-native/app/profile/edit.tsx @@ -7,8 +7,6 @@ import { ScrollView, Image, ActivityIndicator, - KeyboardAvoidingView, - Platform, Alert, } from 'react-native'; import { useRouter } from 'expo-router'; @@ -24,6 +22,7 @@ import { resolveAvatar } from '../../lib/resolveAvatar'; import { apiFetch } from '../../lib/api'; import { useMe } from '../../hooks/useMe'; import { AvatarCropSheet } from '../../components/profile/AvatarCropSheet'; +import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen'; export default function ProfileEditScreen() { const router = useRouter(); @@ -133,10 +132,7 @@ export default function ProfileEditScreen() { avatarId !== me?.avatar; return ( - + - + ); } diff --git a/apps/rebreak-native/components/KeyboardAwareScreen.tsx b/apps/rebreak-native/components/KeyboardAwareScreen.tsx new file mode 100644 index 0000000..410dcf0 --- /dev/null +++ b/apps/rebreak-native/components/KeyboardAwareScreen.tsx @@ -0,0 +1,65 @@ +import { ReactNode } from 'react'; +import { + Keyboard, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleProp, + TouchableWithoutFeedback, + View, + ViewStyle, +} from 'react-native'; + +export interface KeyboardAwareScreenProps { + children: ReactNode; + /** + * Extra offset for `keyboardVerticalOffset` on iOS. For screens wrapped in + * `` this should be 0 (default) — the SafeAreaView already + * absorbs the top inset. For screens that own their header with + * `paddingTop: insets.top` baked in (e.g. profile/edit), pass the full + * header height so iOS computes the correct push distance. + */ + headerOffset?: number; + /** + * When true, wraps children in a `ScrollView`. Use for long forms (sign-up, + * profile-edit). When false (default), a plain `View` fills the remaining + * space — tap anywhere outside an input dismisses the keyboard. + */ + scrollable?: boolean; + /** Style applied to the outer KeyboardAvoidingView. */ + style?: StyleProp; + /** Style applied to the inner container (ScrollView or View). */ + contentContainerStyle?: StyleProp; +} + +export function KeyboardAwareScreen({ + children, + headerOffset = 0, + scrollable = false, + style, + contentContainerStyle, +}: KeyboardAwareScreenProps) { + return ( + + {scrollable ? ( + + {children} + + ) : ( + + {children} + + )} + + ); +}