From f2134e791ebc1447c13d0dd32d6a2a34375985b4 Mon Sep 17 00:00:00 2001 From: vladimir Date: Fri, 12 Jun 2026 11:15:48 +0200 Subject: [PATCH 1/9] feat: replace country/currency inputs with searchable dropdown Replace separate Country and Currency text inputs with a single country dropdown that automatically sets the currency. The dropdown displays full country names with currency codes (e.g., "Canada - CAD") and opens in a full-screen modal with a search bar for easy filtering. - Add COUNTRY_DATA mapping with 37 country-currency pairs in assets - Enhance FormDropdown with labelExtractor and fullScreen props - Add search functionality to filter options by name or currency - Add full-screen modal styles with header and search bar --- example/src/assets/countries.ts | 61 ++++++++ .../src/components/Settings/SettingsView.tsx | 23 +-- .../src/components/common/FormDropdown.tsx | 138 +++++++++++++----- example/src/components/common/Styles.ts | 36 +++++ 4 files changed, 212 insertions(+), 46 deletions(-) create mode 100644 example/src/assets/countries.ts diff --git a/example/src/assets/countries.ts b/example/src/assets/countries.ts new file mode 100644 index 000000000..a07515ad8 --- /dev/null +++ b/example/src/assets/countries.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ + +export type CountryInfo = { + name: string; + currency: string; +}; + +export const COUNTRY_DATA: Record = { + AE: { name: 'United Arab Emirates', currency: 'AED' }, + AT: { name: 'Austria', currency: 'EUR' }, + AU: { name: 'Australia', currency: 'AUD' }, + BE: { name: 'Belgium', currency: 'EUR' }, + BR: { name: 'Brazil', currency: 'BRL' }, + CA: { name: 'Canada', currency: 'CAD' }, + CH: { name: 'Switzerland', currency: 'CHF' }, + CN: { name: 'China', currency: 'CNY' }, + CZ: { name: 'Czech Republic', currency: 'CZK' }, + DE: { name: 'Germany', currency: 'EUR' }, + DK: { name: 'Denmark', currency: 'DKK' }, + ES: { name: 'Spain', currency: 'EUR' }, + FI: { name: 'Finland', currency: 'EUR' }, + FR: { name: 'France', currency: 'EUR' }, + GB: { name: 'United Kingdom', currency: 'GBP' }, + HK: { name: 'Hong Kong', currency: 'HKD' }, + ID: { name: 'Indonesia', currency: 'IDR' }, + IN: { name: 'India', currency: 'INR' }, + IT: { name: 'Italy', currency: 'EUR' }, + JP: { name: 'Japan', currency: 'JPY' }, + KE: { name: 'Kenya', currency: 'KES' }, + KR: { name: 'South Korea', currency: 'KRW' }, + MX: { name: 'Mexico', currency: 'MXN' }, + MY: { name: 'Malaysia', currency: 'MYR' }, + NL: { name: 'Netherlands', currency: 'EUR' }, + NO: { name: 'Norway', currency: 'NOK' }, + NZ: { name: 'New Zealand', currency: 'NZD' }, + PH: { name: 'Philippines', currency: 'PHP' }, + PL: { name: 'Poland', currency: 'PLN' }, + PT: { name: 'Portugal', currency: 'EUR' }, + RU: { name: 'Russia', currency: 'RUB' }, + SE: { name: 'Sweden', currency: 'SEK' }, + SG: { name: 'Singapore', currency: 'SGD' }, + TH: { name: 'Thailand', currency: 'THB' }, + US: { name: 'United States', currency: 'USD' }, + VN: { name: 'Vietnam', currency: 'VND' }, + ZA: { name: 'South Africa', currency: 'ZAR' }, +}; + +export const COUNTRY_CODES = Object.keys(COUNTRY_DATA); + +export const getCountryLabel = (countryCode: string): string => { + const data = COUNTRY_DATA[countryCode]; + return data ? `${data.name} - ${data.currency}` : countryCode; +}; + +export const getCurrency = (countryCode: string): string | undefined => { + return COUNTRY_DATA[countryCode]?.currency; +}; diff --git a/example/src/components/Settings/SettingsView.tsx b/example/src/components/Settings/SettingsView.tsx index 931a9fb8f..8288c5551 100644 --- a/example/src/components/Settings/SettingsView.tsx +++ b/example/src/components/Settings/SettingsView.tsx @@ -3,9 +3,15 @@ import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import { Button, ScrollView, TouchableOpacity, View } from 'react-native'; import FormTextInput from '../common/FormTextInput'; +import FormDropdown from '../common/FormDropdown'; import AdaptiveText from '../common/AdaptiveText'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { SettingsStackParamList } from './SettingsNavigator'; +import { + COUNTRY_CODES, + getCountryLabel, + getCurrency, +} from '../../assets/countries'; type Props = NativeStackScreenProps; @@ -14,7 +20,6 @@ const SettingView = ({ navigation }: Props) => { const [countryCode, setCountryCode] = useState(configuration.countryCode); const [amount, setAmount] = useState(String(configuration.amount)); - const [currency, setCurrency] = useState(configuration.currency); const [merchantAccount, setMerchantAccount] = useState( configuration.merchantAccount ); @@ -25,6 +30,8 @@ const SettingView = ({ navigation }: Props) => { configuration.shopperLocale ); + const currency = getCurrency(countryCode) ?? configuration.currency; + const settings = useMemo( () => ({ ...configuration, @@ -53,17 +60,13 @@ const SettingView = ({ navigation }: Props) => { return ( - - = { value: T; options: T[]; onChange: (value: T) => void; + labelExtractor?: (value: T) => string; + fullScreen?: boolean; }; const FormDropdown = ({ @@ -22,15 +26,57 @@ const FormDropdown = ({ value, options, onChange, + labelExtractor = (v) => v, + fullScreen = false, }: FormDropdownProps) => { const [visible, setVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); const isDarkMode = useColorScheme() === 'dark'; const backgroundColor = isDarkMode ? Colors.textBackgroundDark : Colors.textBackgroundLight; + const modalBackground = isDarkMode ? '#1c1c1e' : '#f2f2f7'; const textColor = isDarkMode ? Colors.textDark : Colors.textLight; + const filteredOptions = useMemo(() => { + if (!searchQuery) return options; + const query = searchQuery.toLowerCase(); + return options.filter((item) => + labelExtractor(item).toLowerCase().includes(query) + ); + }, [options, searchQuery, labelExtractor]); + + const handleClose = () => { + setVisible(false); + setSearchQuery(''); + }; + + const handleSelect = (item: T) => { + onChange(item); + handleClose(); + }; + + const renderItem = ({ item }: { item: T }) => ( + handleSelect(item)} + > + + {labelExtractor(item)} + + + ); + return ( {title} @@ -38,48 +84,68 @@ const FormDropdown = ({ style={[Styles.dropdown, { backgroundColor }]} onPress={() => setVisible(true)} > - {value} + + {labelExtractor(value)} + setVisible(false)} + transparent={!fullScreen} + animationType={fullScreen ? 'slide' : 'fade'} + onRequestClose={handleClose} > - setVisible(false)} - > - + {fullScreen ? ( + + + + + Cancel + + + + {title} + + + + + + item} - renderItem={({ item }) => ( - { - onChange(item); - setVisible(false); - }} - > - - {item} - - - )} + renderItem={renderItem} + style={Styles.fullScreenList} + keyboardShouldPersistTaps="handled" /> - - + + ) : ( + + + item} + renderItem={renderItem} + /> + + + )} ); diff --git a/example/src/components/common/Styles.ts b/example/src/components/common/Styles.ts index 805f2ec1f..a5fa29478 100644 --- a/example/src/components/common/Styles.ts +++ b/example/src/components/common/Styles.ts @@ -164,6 +164,42 @@ const Styles = StyleSheet.create({ color: '#007AFF', fontWeight: '600', }, + fullScreenModal: { + flex: 1, + }, + fullScreenHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: 'rgba(0, 0, 0, 0.1)', + }, + fullScreenTitle: { + fontSize: 17, + fontWeight: '600', + }, + fullScreenCancel: { + fontSize: 17, + color: '#007AFF', + }, + fullScreenHeaderSpacer: { + width: 60, + }, + searchContainer: { + marginHorizontal: 16, + marginVertical: 8, + borderRadius: 10, + paddingHorizontal: 12, + }, + searchInput: { + fontSize: 16, + paddingVertical: 10, + }, + fullScreenList: { + flex: 1, + }, }); export default Styles; From ef81fc557f5776181fdca6ec57b9888cefce6cf4 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 18 Jun 2026 16:29:55 +0200 Subject: [PATCH 2/9] fix: prevent dropdown modal closing on inner whitespace tap, add chevron indicator --- example/src/components/common/FormDropdown.tsx | 11 +++++++++-- example/src/components/common/Styles.ts | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/example/src/components/common/FormDropdown.tsx b/example/src/components/common/FormDropdown.tsx index f5684ce1b..369b1fac9 100644 --- a/example/src/components/common/FormDropdown.tsx +++ b/example/src/components/common/FormDropdown.tsx @@ -87,6 +87,9 @@ const FormDropdown = ({ {labelExtractor(value)} + + {'\u203a'} + ({ activeOpacity={1} onPress={handleClose} > - + {}} + style={[Styles.dropdownMenu, { backgroundColor }]} + > item} renderItem={renderItem} /> - + )} diff --git a/example/src/components/common/Styles.ts b/example/src/components/common/Styles.ts index a5fa29478..6ac602bcd 100644 --- a/example/src/components/common/Styles.ts +++ b/example/src/components/common/Styles.ts @@ -133,6 +133,9 @@ const Styles = StyleSheet.create({ marginHorizontal: 16, padding: 12, borderRadius: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', }, dropdownText: { fontSize: 16, From cc8e901043aa56c6af58ba0080e5e86110dc5088 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 18 Jun 2026 16:36:07 +0200 Subject: [PATCH 3/9] chore: add dynamic bottom paddings --- .../Checkout/components/PaymentMethodsView.tsx | 9 ++++----- example/src/components/Settings/SettingsView.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/example/src/components/Checkout/components/PaymentMethodsView.tsx b/example/src/components/Checkout/components/PaymentMethodsView.tsx index 88d2b4a0f..662d736b5 100644 --- a/example/src/components/Checkout/components/PaymentMethodsView.tsx +++ b/example/src/components/Checkout/components/PaymentMethodsView.tsx @@ -1,6 +1,6 @@ import { useAdyenCheckout } from '@adyen/react-native'; -import { View, Text, ScrollView, ActivityIndicator } from 'react-native'; -import Styles from '../../common/Styles'; +import { Text, ScrollView, ActivityIndicator } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import PaymentMethodsList from './PaymentMethodsList'; import DropInButton from './DropInButton'; import PlatformPayButton from './PlatformPayButton'; @@ -21,6 +21,7 @@ export type PaymentMethodsProps = NativeStackScreenProps< const PaymentMethods = (prop: PaymentMethodsProps) => { const { isReady, paymentMethods } = useAdyenCheckout(); + const insets = useSafeAreaInsets(); const showDropIn = prop.route.params?.showDropIn ?? false; const showEmbeddedComponents = @@ -37,7 +38,7 @@ const PaymentMethods = (prop: PaymentMethodsProps) => { } return ( - + {showDropIn && } {showEmbeddedComponents && ( @@ -54,8 +55,6 @@ const PaymentMethods = (prop: PaymentMethodsProps) => { {showDropinBasedComponents && ( )} - - ); }; diff --git a/example/src/components/Settings/SettingsView.tsx b/example/src/components/Settings/SettingsView.tsx index 8288c5551..76d35f4de 100644 --- a/example/src/components/Settings/SettingsView.tsx +++ b/example/src/components/Settings/SettingsView.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo, useState } from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import { Button, ScrollView, TouchableOpacity, View } from 'react-native'; @@ -53,13 +54,18 @@ const SettingView = ({ navigation }: Props) => { ] ); + const insets = useSafeAreaInsets(); + const saveAndClose = useCallback(() => { save(settings); navigateToRoot(); }, [settings, save, navigateToRoot]); return ( - + Date: Thu, 18 Jun 2026 17:06:25 +0200 Subject: [PATCH 4/9] refactor: split FormDropdown into separate compact and search variants, move to Settings/common --- .../Settings/ApplePaySettingsView.tsx | 2 +- .../components/Settings/CardSettingsView.tsx | 2 +- .../Settings/GooglePaySettingsView.tsx | 2 +- .../src/components/Settings/SettingsView.tsx | 5 +- .../Settings/common/DropdownTrigger.tsx | 42 +++++ .../Settings/common/FormDropdown.tsx | 97 +++++++++++ .../Settings/common/FormSearchDropdown.tsx | 126 ++++++++++++++ .../src/components/common/FormDropdown.tsx | 161 ------------------ 8 files changed, 270 insertions(+), 167 deletions(-) create mode 100644 example/src/components/Settings/common/DropdownTrigger.tsx create mode 100644 example/src/components/Settings/common/FormDropdown.tsx create mode 100644 example/src/components/Settings/common/FormSearchDropdown.tsx delete mode 100644 example/src/components/common/FormDropdown.tsx diff --git a/example/src/components/Settings/ApplePaySettingsView.tsx b/example/src/components/Settings/ApplePaySettingsView.tsx index 8690b3f7f..ae163fe5e 100644 --- a/example/src/components/Settings/ApplePaySettingsView.tsx +++ b/example/src/components/Settings/ApplePaySettingsView.tsx @@ -4,7 +4,7 @@ import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import FormToggle from '../common/FormToggle'; import FormTextInput from '../common/FormTextInput'; -import FormDropdown from '../common/FormDropdown'; +import FormDropdown from './common/FormDropdown'; import { ENVIRONMENT } from '../../Configuration'; import type { ApplePaySettings } from '../../settings/types'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/example/src/components/Settings/CardSettingsView.tsx b/example/src/components/Settings/CardSettingsView.tsx index b6a627adf..6a741718d 100644 --- a/example/src/components/Settings/CardSettingsView.tsx +++ b/example/src/components/Settings/CardSettingsView.tsx @@ -3,7 +3,7 @@ import { Button, ScrollView, View } from 'react-native'; import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import FormToggle from '../common/FormToggle'; -import FormDropdown from '../common/FormDropdown'; +import FormDropdown from './common/FormDropdown'; import type { CardSettings } from '../../settings/types'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { SettingsStackParamList } from './SettingsNavigator'; diff --git a/example/src/components/Settings/GooglePaySettingsView.tsx b/example/src/components/Settings/GooglePaySettingsView.tsx index 2dba9fe68..4759a27e1 100644 --- a/example/src/components/Settings/GooglePaySettingsView.tsx +++ b/example/src/components/Settings/GooglePaySettingsView.tsx @@ -3,7 +3,7 @@ import { Button, ScrollView, View } from 'react-native'; import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import FormToggle from '../common/FormToggle'; -import FormDropdown from '../common/FormDropdown'; +import FormDropdown from './common/FormDropdown'; import type { GooglePaySettings } from '../../settings/types'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { SettingsStackParamList } from './SettingsNavigator'; diff --git a/example/src/components/Settings/SettingsView.tsx b/example/src/components/Settings/SettingsView.tsx index 76d35f4de..6b44e835f 100644 --- a/example/src/components/Settings/SettingsView.tsx +++ b/example/src/components/Settings/SettingsView.tsx @@ -4,7 +4,7 @@ import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import { Button, ScrollView, TouchableOpacity, View } from 'react-native'; import FormTextInput from '../common/FormTextInput'; -import FormDropdown from '../common/FormDropdown'; +import FormSearchDropdown from './common/FormSearchDropdown'; import AdaptiveText from '../common/AdaptiveText'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { SettingsStackParamList } from './SettingsNavigator'; @@ -66,13 +66,12 @@ const SettingView = ({ navigation }: Props) => { style={Styles.page} contentContainerStyle={{ paddingBottom: insets.bottom }} > - void; +}; + +const DropdownTrigger = ({ + title, + label, + onPress, + children, +}: PropsWithChildren) => { + const isDarkMode = useColorScheme() === 'dark'; + const backgroundColor = isDarkMode + ? Colors.textBackgroundDark + : Colors.textBackgroundLight; + const textColor = isDarkMode ? Colors.textDark : Colors.textLight; + + return ( + + {title} + + {label} + + {'›'} + + + {children} + + ); +}; + +export default DropdownTrigger; diff --git a/example/src/components/Settings/common/FormDropdown.tsx b/example/src/components/Settings/common/FormDropdown.tsx new file mode 100644 index 000000000..6fb47def3 --- /dev/null +++ b/example/src/components/Settings/common/FormDropdown.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react'; +import { + TouchableOpacity, + Modal, + FlatList, + useColorScheme, +} from 'react-native'; +import Styles from '../../common/Styles'; +import AdaptiveText from '../../common/AdaptiveText'; +import Colors from '../../common/Assets'; +import DropdownTrigger from './DropdownTrigger'; + +type FormDropdownProps = { + title: string; + value: T; + options: T[]; + onChange: (value: T) => void; + labelExtractor?: (value: T) => string; +}; + +const FormDropdown = ({ + title, + value, + options, + onChange, + labelExtractor = (v) => v, +}: FormDropdownProps) => { + const [visible, setVisible] = useState(false); + const isDarkMode = useColorScheme() === 'dark'; + + const backgroundColor = isDarkMode + ? Colors.textBackgroundDark + : Colors.textBackgroundLight; + const textColor = isDarkMode ? Colors.textDark : Colors.textLight; + + const handleClose = () => setVisible(false); + + const handleSelect = (item: T) => { + onChange(item); + handleClose(); + }; + + const renderItem = ({ item }: { item: T }) => ( + handleSelect(item)} + > + + {labelExtractor(item)} + + + ); + + return ( + setVisible(true)} + > + + + {}} + style={[Styles.dropdownMenu, { backgroundColor }]} + > + item} + renderItem={renderItem} + /> + + + + + ); +}; + +export default FormDropdown; diff --git a/example/src/components/Settings/common/FormSearchDropdown.tsx b/example/src/components/Settings/common/FormSearchDropdown.tsx new file mode 100644 index 000000000..b1f2aa7dd --- /dev/null +++ b/example/src/components/Settings/common/FormSearchDropdown.tsx @@ -0,0 +1,126 @@ +import { useMemo, useState } from 'react'; +import { + TouchableOpacity, + Modal, + FlatList, + TextInput, + SafeAreaView, + View, + useColorScheme, +} from 'react-native'; +import Styles from '../../common/Styles'; +import AdaptiveText from '../../common/AdaptiveText'; +import Colors from '../../common/Assets'; +import DropdownTrigger from './DropdownTrigger'; + +type FormSearchDropdownProps = { + title: string; + value: T; + options: T[]; + onChange: (value: T) => void; + labelExtractor?: (value: T) => string; +}; + +const FormSearchDropdown = ({ + title, + value, + options, + onChange, + labelExtractor = (v) => v, +}: FormSearchDropdownProps) => { + const [visible, setVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const isDarkMode = useColorScheme() === 'dark'; + + const backgroundColor = isDarkMode + ? Colors.textBackgroundDark + : Colors.textBackgroundLight; + const modalBackground = isDarkMode ? '#1c1c1e' : '#f2f2f7'; + const textColor = isDarkMode ? Colors.textDark : Colors.textLight; + + const filteredOptions = useMemo(() => { + if (!searchQuery) return options; + const query = searchQuery.toLowerCase(); + return options.filter((item) => + labelExtractor(item).toLowerCase().includes(query) + ); + }, [options, searchQuery, labelExtractor]); + + const handleClose = () => { + setVisible(false); + setSearchQuery(''); + }; + + const handleSelect = (item: T) => { + onChange(item); + handleClose(); + }; + + const renderItem = ({ item }: { item: T }) => ( + handleSelect(item)} + > + + {labelExtractor(item)} + + + ); + + return ( + setVisible(true)} + > + + + + + + Cancel + + + {title} + + + + + + item} + renderItem={renderItem} + style={Styles.fullScreenList} + keyboardShouldPersistTaps="handled" + /> + + + + ); +}; + +export default FormSearchDropdown; diff --git a/example/src/components/common/FormDropdown.tsx b/example/src/components/common/FormDropdown.tsx deleted file mode 100644 index 369b1fac9..000000000 --- a/example/src/components/common/FormDropdown.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useMemo, useState } from 'react'; -import { - View, - TouchableOpacity, - Modal, - FlatList, - TextInput, - SafeAreaView, - useColorScheme, -} from 'react-native'; -import Styles from './Styles'; -import AdaptiveText from './AdaptiveText'; -import Colors from './Assets'; - -type FormDropdownProps = { - title: string; - value: T; - options: T[]; - onChange: (value: T) => void; - labelExtractor?: (value: T) => string; - fullScreen?: boolean; -}; - -const FormDropdown = ({ - title, - value, - options, - onChange, - labelExtractor = (v) => v, - fullScreen = false, -}: FormDropdownProps) => { - const [visible, setVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const isDarkMode = useColorScheme() === 'dark'; - - const backgroundColor = isDarkMode - ? Colors.textBackgroundDark - : Colors.textBackgroundLight; - const modalBackground = isDarkMode ? '#1c1c1e' : '#f2f2f7'; - const textColor = isDarkMode ? Colors.textDark : Colors.textLight; - - const filteredOptions = useMemo(() => { - if (!searchQuery) return options; - const query = searchQuery.toLowerCase(); - return options.filter((item) => - labelExtractor(item).toLowerCase().includes(query) - ); - }, [options, searchQuery, labelExtractor]); - - const handleClose = () => { - setVisible(false); - setSearchQuery(''); - }; - - const handleSelect = (item: T) => { - onChange(item); - handleClose(); - }; - - const renderItem = ({ item }: { item: T }) => ( - handleSelect(item)} - > - - {labelExtractor(item)} - - - ); - - return ( - - {title} - setVisible(true)} - > - - {labelExtractor(value)} - - - {'\u203a'} - - - - {fullScreen ? ( - - - - - Cancel - - - - {title} - - - - - - - item} - renderItem={renderItem} - style={Styles.fullScreenList} - keyboardShouldPersistTaps="handled" - /> - - ) : ( - - {}} - style={[Styles.dropdownMenu, { backgroundColor }]} - > - item} - renderItem={renderItem} - /> - - - )} - - - ); -}; - -export default FormDropdown; From 22849dc83821dc5636235a34f051bd7c7cd00d39 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 18 Jun 2026 17:25:40 +0200 Subject: [PATCH 5/9] fix: address code review - Pressable, SafeAreaView, useCallback, min bottom padding --- .../components/PaymentMethodsView.tsx | 4 +- .../src/components/Settings/SettingsView.tsx | 2 +- .../Settings/common/FormDropdown.tsx | 18 +++--- .../Settings/common/FormSearchDropdown.tsx | 56 +++++++++++-------- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/example/src/components/Checkout/components/PaymentMethodsView.tsx b/example/src/components/Checkout/components/PaymentMethodsView.tsx index 662d736b5..6983bd580 100644 --- a/example/src/components/Checkout/components/PaymentMethodsView.tsx +++ b/example/src/components/Checkout/components/PaymentMethodsView.tsx @@ -38,7 +38,9 @@ const PaymentMethods = (prop: PaymentMethodsProps) => { } return ( - + {showDropIn && } {showEmbeddedComponents && ( diff --git a/example/src/components/Settings/SettingsView.tsx b/example/src/components/Settings/SettingsView.tsx index 6b44e835f..ecb5965a7 100644 --- a/example/src/components/Settings/SettingsView.tsx +++ b/example/src/components/Settings/SettingsView.tsx @@ -64,7 +64,7 @@ const SettingView = ({ navigation }: Props) => { return ( v; + type FormDropdownProps = { title: string; value: T; @@ -23,7 +26,7 @@ const FormDropdown = ({ value, options, onChange, - labelExtractor = (v) => v, + labelExtractor = defaultLabelExtractor, }: FormDropdownProps) => { const [visible, setVisible] = useState(false); const isDarkMode = useColorScheme() === 'dark'; @@ -72,13 +75,8 @@ const FormDropdown = ({ animationType="fade" onRequestClose={handleClose} > - - + {}} style={[Styles.dropdownMenu, { backgroundColor }]} > @@ -87,8 +85,8 @@ const FormDropdown = ({ keyExtractor={(item) => item} renderItem={renderItem} /> - - + + ); diff --git a/example/src/components/Settings/common/FormSearchDropdown.tsx b/example/src/components/Settings/common/FormSearchDropdown.tsx index b1f2aa7dd..fe15759de 100644 --- a/example/src/components/Settings/common/FormSearchDropdown.tsx +++ b/example/src/components/Settings/common/FormSearchDropdown.tsx @@ -1,18 +1,20 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { TouchableOpacity, Modal, FlatList, TextInput, - SafeAreaView, View, useColorScheme, } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import Styles from '../../common/Styles'; import AdaptiveText from '../../common/AdaptiveText'; import Colors from '../../common/Assets'; import DropdownTrigger from './DropdownTrigger'; +const defaultLabelExtractor = (v: string): string => v; + type FormSearchDropdownProps = { title: string; value: T; @@ -26,7 +28,7 @@ const FormSearchDropdown = ({ value, options, onChange, - labelExtractor = (v) => v, + labelExtractor = defaultLabelExtractor, }: FormSearchDropdownProps) => { const [visible, setVisible] = useState(false); const [searchQuery, setSearchQuery] = useState(''); @@ -46,34 +48,40 @@ const FormSearchDropdown = ({ ); }, [options, searchQuery, labelExtractor]); - const handleClose = () => { + const handleClose = useCallback(() => { setVisible(false); setSearchQuery(''); - }; + }, []); - const handleSelect = (item: T) => { - onChange(item); - handleClose(); - }; + const handleSelect = useCallback( + (item: T) => { + onChange(item); + handleClose(); + }, + [onChange, handleClose] + ); - const renderItem = ({ item }: { item: T }) => ( - handleSelect(item)} - > - ( + handleSelect(item)} > - {labelExtractor(item)} - - + + {labelExtractor(item)} + + + ), + [value, textColor, labelExtractor, handleSelect] ); return ( From a608e49e7355025201d49de8d38f693fee0f276c Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 18 Jun 2026 17:42:27 +0200 Subject: [PATCH 6/9] fix: add dynamic safe area bottom padding to all settings views --- example/src/components/Settings/ApplePaySettingsView.tsx | 7 ++++++- example/src/components/Settings/CardSettingsView.tsx | 7 ++++++- example/src/components/Settings/DropInSettingsView.tsx | 7 ++++++- example/src/components/Settings/GooglePaySettingsView.tsx | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/example/src/components/Settings/ApplePaySettingsView.tsx b/example/src/components/Settings/ApplePaySettingsView.tsx index ae163fe5e..2072e30d1 100644 --- a/example/src/components/Settings/ApplePaySettingsView.tsx +++ b/example/src/components/Settings/ApplePaySettingsView.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react'; import { Button, ScrollView, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import FormToggle from '../common/FormToggle'; @@ -21,6 +22,7 @@ const shippingTypes = [ const ApplePaySettingsView = ({ navigation }: Props) => { const { configuration, update } = useAppContext(); + const insets = useSafeAreaInsets(); const existing = configuration.applePaySettings ?? {}; const [merchantID, setMerchantID] = useState( @@ -54,7 +56,10 @@ const ApplePaySettingsView = ({ navigation }: Props) => { ]); return ( - + { const { configuration, update } = useAppContext(); + const insets = useSafeAreaInsets(); const existing = configuration.cardSettings ?? {}; const [holderNameRequired, setHolderNameRequired] = useState( @@ -73,7 +75,10 @@ const CardSettingsView = ({ navigation }: Props) => { ]); return ( - + ; const DropInSettingsView = ({ navigation }: Props) => { const { configuration, update } = useAppContext(); + const insets = useSafeAreaInsets(); const existing = configuration.dropInSettings ?? {}; const [showPreselected, setShowPreselected] = useState( @@ -44,7 +46,10 @@ const DropInSettingsView = ({ navigation }: Props) => { ]); return ( - + { const { configuration, update } = useAppContext(); + const insets = useSafeAreaInsets(); const existing = configuration.googlePaySettings ?? {}; const [allowPrepaidCards, setAllowPrepaidCards] = useState( @@ -70,7 +72,10 @@ const GooglePaySettingsView = ({ navigation }: Props) => { ]); return ( - + Date: Thu, 18 Jun 2026 17:52:25 +0200 Subject: [PATCH 7/9] refactor: extract PageScrollView to remove safe area padding duplication in settings --- .../Checkout/components/PaymentMethodsView.tsx | 11 ++++------- .../Settings/ApplePaySettingsView.tsx | 12 ++++-------- .../components/Settings/CardSettingsView.tsx | 12 ++++-------- .../components/Settings/DropInSettingsView.tsx | 12 ++++-------- .../Settings/GooglePaySettingsView.tsx | 12 ++++-------- .../src/components/Settings/SettingsView.tsx | 13 ++++--------- .../src/components/common/PageScrollView.tsx | 18 ++++++++++++++++++ 7 files changed, 42 insertions(+), 48 deletions(-) create mode 100644 example/src/components/common/PageScrollView.tsx diff --git a/example/src/components/Checkout/components/PaymentMethodsView.tsx b/example/src/components/Checkout/components/PaymentMethodsView.tsx index 6983bd580..68ef7875f 100644 --- a/example/src/components/Checkout/components/PaymentMethodsView.tsx +++ b/example/src/components/Checkout/components/PaymentMethodsView.tsx @@ -1,6 +1,6 @@ import { useAdyenCheckout } from '@adyen/react-native'; -import { Text, ScrollView, ActivityIndicator } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Text, ActivityIndicator } from 'react-native'; +import PageScrollView from '../../common/PageScrollView'; import PaymentMethodsList from './PaymentMethodsList'; import DropInButton from './DropInButton'; import PlatformPayButton from './PlatformPayButton'; @@ -21,7 +21,6 @@ export type PaymentMethodsProps = NativeStackScreenProps< const PaymentMethods = (prop: PaymentMethodsProps) => { const { isReady, paymentMethods } = useAdyenCheckout(); - const insets = useSafeAreaInsets(); const showDropIn = prop.route.params?.showDropIn ?? false; const showEmbeddedComponents = @@ -38,9 +37,7 @@ const PaymentMethods = (prop: PaymentMethodsProps) => { } return ( - + {showDropIn && } {showEmbeddedComponents && ( @@ -57,7 +54,7 @@ const PaymentMethods = (prop: PaymentMethodsProps) => { {showDropinBasedComponents && ( )} - + ); }; diff --git a/example/src/components/Settings/ApplePaySettingsView.tsx b/example/src/components/Settings/ApplePaySettingsView.tsx index 2072e30d1..85e66ea6f 100644 --- a/example/src/components/Settings/ApplePaySettingsView.tsx +++ b/example/src/components/Settings/ApplePaySettingsView.tsx @@ -1,11 +1,11 @@ import { useCallback, useState } from 'react'; -import { Button, ScrollView, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button, View } from 'react-native'; import { useAppContext } from '../../hooks/useAppContext'; import Styles from '../common/Styles'; import FormToggle from '../common/FormToggle'; import FormTextInput from '../common/FormTextInput'; import FormDropdown from './common/FormDropdown'; +import PageScrollView from '../common/PageScrollView'; import { ENVIRONMENT } from '../../Configuration'; import type { ApplePaySettings } from '../../settings/types'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; @@ -22,7 +22,6 @@ const shippingTypes = [ const ApplePaySettingsView = ({ navigation }: Props) => { const { configuration, update } = useAppContext(); - const insets = useSafeAreaInsets(); const existing = configuration.applePaySettings ?? {}; const [merchantID, setMerchantID] = useState( @@ -56,10 +55,7 @@ const ApplePaySettingsView = ({ navigation }: Props) => { ]); return ( - + {