import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui"; import { Ionicons } from "@expo/vector-icons"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; import React, { useEffect } from "react"; import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { useGlobalModal } from "@/providers/GlobalModalProvider"; // Option types export type RadioOption = { type: "radio"; label: string; value: T; selected: boolean; onPress: () => void; disabled?: boolean; }; export type ToggleOption = { type: "toggle"; label: string; value: boolean; onToggle: () => void; disabled?: boolean; }; export type ActionOption = { type: "action"; label: string; onPress: () => void; disabled?: boolean; }; export type Option = RadioOption | ToggleOption | ActionOption; // Option group structure export type OptionGroup = { title?: string; options: Option[]; }; interface PlatformDropdownProps { trigger?: React.ReactNode; title?: string; groups: OptionGroup[]; open?: boolean; onOpenChange?: (open: boolean) => void; onOptionSelect?: (value?: any) => void; expoUIConfig?: { hostStyle?: any; }; bottomSheetConfig?: { enableDynamicSizing?: boolean; enablePanDownToClose?: boolean; }; } const ToggleSwitch: React.FC<{ value: boolean }> = ({ value }) => ( ); const OptionItem: React.FC<{ option: Option; isLast?: boolean }> = ({ option, isLast, }) => { const isToggle = option.type === "toggle"; const isAction = option.type === "action"; const handlePress = isToggle ? option.onToggle : (option as RadioOption | ActionOption).onPress; return ( <> {option.label} {isToggle ? ( ) : isAction ? null : (option as RadioOption).selected ? ( ) : ( )} {!isLast && ( )} ); }; const OptionGroupComponent: React.FC<{ group: OptionGroup }> = ({ group }) => ( {group.title && ( {group.title} )} {group.options.map((option, index) => ( ))} ); const BottomSheetContent: React.FC<{ title?: string; groups: OptionGroup[]; onOptionSelect?: (value?: any) => void; onClose?: () => void; }> = ({ title, groups, onOptionSelect, onClose }) => { const insets = useSafeAreaInsets(); // Wrap the groups to call onOptionSelect when an option is pressed const wrappedGroups = groups.map((group) => ({ ...group, options: group.options.map((option) => { if (option.type === "radio") { return { ...option, onPress: () => { option.onPress(); onOptionSelect?.(option.value); onClose?.(); }, }; } if (option.type === "toggle") { return { ...option, onToggle: () => { option.onToggle(); onOptionSelect?.(option.value); }, }; } if (option.type === "action") { return { ...option, onPress: () => { option.onPress(); onClose?.(); }, }; } return option; }), })); return ( {title && {title}} {wrappedGroups.map((group, index) => ( ))} ); }; const PlatformDropdownComponent = ({ trigger, title, groups, open: controlledOpen, onOpenChange: controlledOnOpenChange, onOptionSelect, expoUIConfig, bottomSheetConfig, }: PlatformDropdownProps) => { const { showModal, hideModal, isVisible } = useGlobalModal(); // Handle controlled open state for Android useEffect(() => { if (Platform.OS === "android" && controlledOpen === true) { showModal( { hideModal(); controlledOnOpenChange?.(false); }} />, { snapPoints: ["90%"], enablePanDownToClose: bottomSheetConfig?.enablePanDownToClose ?? true, }, ); } }, [controlledOpen]); // Watch for modal dismissal on Android (e.g., swipe down, backdrop tap) // and sync the controlled open state useEffect(() => { if (Platform.OS === "android" && controlledOpen === true && !isVisible) { controlledOnOpenChange?.(false); } }, [isVisible, controlledOpen, controlledOnOpenChange]); if (Platform.OS === "ios") { return ( {trigger} {groups.flatMap((group, groupIndex) => { // Check if this group has radio options const radioOptions = group.options.filter( (opt) => opt.type === "radio", ) as RadioOption[]; const toggleOptions = group.options.filter( (opt) => opt.type === "toggle", ) as ToggleOption[]; const actionOptions = group.options.filter( (opt) => opt.type === "action", ) as ActionOption[]; const items = []; // Add Picker for radio options ONLY if there's a group title // Otherwise render as individual buttons if (radioOptions.length > 0) { if (group.title) { // Use Picker for grouped options items.push( opt.label)} variant='menu' selectedIndex={radioOptions.findIndex( (opt) => opt.selected, )} onOptionSelected={(event: any) => { const index = event.nativeEvent.index; const selectedOption = radioOptions[index]; selectedOption?.onPress(); onOptionSelect?.(selectedOption?.value); }} />, ); } else { // Render radio options as direct buttons radioOptions.forEach((option, optionIndex) => { items.push( , ); }); } } // Add Buttons for toggle options toggleOptions.forEach((option, optionIndex) => { items.push( , ); }); // Add Buttons for action options (no icon) actionOptions.forEach((option, optionIndex) => { items.push( , ); }); return items; })} ); } // Android: Direct modal trigger const handlePress = () => { showModal( , { snapPoints: ["90%"], enablePanDownToClose: bottomSheetConfig?.enablePanDownToClose ?? true, }, ); }; return ( {trigger || Open Menu} ); }; // Memoize to prevent unnecessary re-renders when parent re-renders export const PlatformDropdown = React.memo( PlatformDropdownComponent, (prevProps, nextProps) => { // Custom comparison - only re-render if these props actually change return ( prevProps.title === nextProps.title && prevProps.open === nextProps.open && prevProps.groups === nextProps.groups && // Reference equality (works because we memoize groups in caller) prevProps.trigger === nextProps.trigger // Reference equality ); }, );