import { Ionicons } from "@expo/vector-icons"; import React, { ReactNode, useEffect, useState } from "react"; import { ActivityIndicator, Dimensions, Pressable, Modal as RNModal, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; // Types export interface ModalProps { /** Whether the modal dialog is visible or not */ open?: boolean; /** The modal dialog's title */ title?: ReactNode; /** Whether a close (x) button is visible on top right or not */ closable?: boolean; /** Custom close icon */ closeIcon?: ReactNode; /** Whether to close the modal dialog when the mask (area outside the modal) is clicked */ maskClosable?: boolean; /** Centered Modal */ centered?: boolean; /** Width of the modal dialog */ width?: number | string; /** Whether to apply loading visual effect for OK button or not */ confirmLoading?: boolean; /** Text of the OK button */ okText?: string; /** Text of the Cancel button */ cancelText?: string; /** Button type of the OK button */ okType?: "primary" | "default" | "dashed" | "text" | "link"; /** Footer content, set as footer={null} when you don't need default buttons */ footer?: ReactNode | null; /** Whether show mask or not */ mask?: boolean; /** The z-index of the Modal */ zIndex?: number; /** Specify a function that will be called when a user clicks the OK button */ onOk?: (e?: any) => void | Promise; /** Specify a function that will be called when a user clicks mask, close button on top right or Cancel button */ onCancel?: (e?: any) => void; /** Callback when the animation ends when Modal is turned on and off */ afterOpenChange?: (open: boolean) => void; /** Specify a function that will be called when modal is closed completely */ afterClose?: () => void; /** Custom className */ className?: string; /** Modal body content */ children?: ReactNode; /** Whether to unmount child components on close */ destroyOnClose?: boolean; /** The ok button props */ okButtonProps?: any; /** The cancel button props */ cancelButtonProps?: any; /** Whether support press esc to close */ keyboard?: boolean; } export interface ConfirmModalProps extends Omit { /** Type of the confirm modal */ type?: "info" | "success" | "error" | "warning" | "confirm"; /** Content */ content?: ReactNode; /** Custom icon */ icon?: ReactNode; } // Modal Component const Modal: React.FC & { info: (props: ConfirmModalProps) => ModalInstance; success: (props: ConfirmModalProps) => ModalInstance; error: (props: ConfirmModalProps) => ModalInstance; warning: (props: ConfirmModalProps) => ModalInstance; confirm: (props: ConfirmModalProps) => ModalInstance; useModal: () => [ { info: (props: ConfirmModalProps) => ModalInstance; success: (props: ConfirmModalProps) => ModalInstance; error: (props: ConfirmModalProps) => ModalInstance; warning: (props: ConfirmModalProps) => ModalInstance; confirm: (props: ConfirmModalProps) => ModalInstance; }, ReactNode ]; } = ({ open = false, title, closable = true, closeIcon, maskClosable = true, centered = false, width = 520, confirmLoading = false, okText = "OK", cancelText = "Cancel", okType = "primary", footer, mask = true, zIndex = 1000, onOk, onCancel, afterOpenChange, afterClose, className, children, destroyOnClose = false, okButtonProps, cancelButtonProps, keyboard = true, }) => { const [visible, setVisible] = useState(open); const [loading, setLoading] = useState(false); useEffect(() => { setVisible(open); if (afterOpenChange) { afterOpenChange(open); } }, [open, afterOpenChange]); const handleOk = async () => { if (onOk) { setLoading(true); try { await onOk(); // Không tự động đóng modal - để parent component quyết định } catch (error) { console.error("Modal onOk error:", error); } finally { setLoading(false); } } else { setVisible(false); } }; const handleCancel = () => { if (onCancel) { onCancel(); } else { // Nếu không có onCancel, tự động đóng modal setVisible(false); } }; const handleMaskPress = () => { if (maskClosable) { handleCancel(); } }; const handleRequestClose = () => { if (keyboard) { handleCancel(); } }; useEffect(() => { if (!visible && afterClose) { const timer = setTimeout(() => { afterClose(); }, 300); // Wait for animation to complete return () => clearTimeout(timer); } }, [visible, afterClose]); const renderFooter = () => { if (footer === null) { return null; } if (footer !== undefined) { return {footer}; } return ( {cancelText} {loading || confirmLoading ? ( ) : ( {okText} )} ); }; const modalWidth = typeof width === "number" ? width : Dimensions.get("window").width * 0.9; return ( e.stopPropagation()} > {/* Header */} {(title || closable) && ( {title && ( {title} )} {closable && ( {closeIcon || ( )} )} )} {/* Body */} {(!destroyOnClose || visible) && children} {/* Footer */} {renderFooter()} ); }; // Confirm Modal Component const ConfirmModal: React.FC< ConfirmModalProps & { visible: boolean; onClose: () => void } > = ({ visible, onClose, type = "confirm", title, content, icon, okText = "OK", cancelText = "Cancel", onOk, onCancel, ...restProps }) => { const [loading, setLoading] = useState(false); const getIcon = () => { if (icon !== undefined) return icon; const iconProps = { size: 24, style: { marginRight: 12 } }; switch (type) { case "info": return ( ); case "success": return ( ); case "error": return ; case "warning": return ; default: return ; } }; const handleOk = async () => { if (onOk) { setLoading(true); try { await onOk(); onClose(); } catch (error) { console.error("Confirm modal onOk error:", error); } finally { setLoading(false); } } else { onClose(); } }; const handleCancel = () => { if (onCancel) { onCancel(); } onClose(); }; return ( {loading ? ( ) : ( {okText} )} ) } {...restProps} > {getIcon()} {content} ); }; // Modal Instance export interface ModalInstance { destroy: () => void; update: (config: ConfirmModalProps) => void; } // Container for imperatively created modals - Not used in React Native // Static methods will return instance but won't render imperatively // Use Modal.useModal() hook for proper context support const createConfirmModal = (config: ConfirmModalProps): ModalInstance => { console.warn( "Modal static methods are not fully supported in React Native. Please use Modal.useModal() hook for better context support." ); return { destroy: () => { console.warn( "Modal.destroy() called but static modals are not supported in React Native" ); }, update: (newConfig: ConfirmModalProps) => { console.warn( "Modal.update() called but static modals are not supported in React Native" ); }, }; }; // Static methods Modal.info = (props: ConfirmModalProps) => createConfirmModal({ ...props, type: "info" }); Modal.success = (props: ConfirmModalProps) => createConfirmModal({ ...props, type: "success" }); Modal.error = (props: ConfirmModalProps) => createConfirmModal({ ...props, type: "error" }); Modal.warning = (props: ConfirmModalProps) => createConfirmModal({ ...props, type: "warning" }); Modal.confirm = (props: ConfirmModalProps) => createConfirmModal({ ...props, type: "confirm" }); // useModal hook Modal.useModal = () => { const [modals, setModals] = useState([]); const createModal = ( config: ConfirmModalProps, type: ConfirmModalProps["type"] ) => { const id = `modal-${Date.now()}-${Math.random()}`; const destroy = () => { setModals((prev) => prev.filter((modal: any) => modal.key !== id)); }; const update = (newConfig: ConfirmModalProps) => { setModals((prev) => prev.map((modal: any) => modal.key === id ? ( ) : ( modal ) ) ); }; const modalElement = ( ); setModals((prev) => [...prev, modalElement]); return { destroy, update }; }; const modalMethods = { info: (props: ConfirmModalProps) => createModal(props, "info"), success: (props: ConfirmModalProps) => createModal(props, "success"), error: (props: ConfirmModalProps) => createModal(props, "error"), warning: (props: ConfirmModalProps) => createModal(props, "warning"), confirm: (props: ConfirmModalProps) => createModal(props, "confirm"), }; const contextHolder = <>{modals}; return [modalMethods, contextHolder]; }; // Styles const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.45)", justifyContent: "flex-start", paddingTop: 100, alignItems: "center", }, centered: { justifyContent: "center", paddingTop: 0, }, noMask: { backgroundColor: "transparent", }, modal: { backgroundColor: "#fff", borderRadius: 8, shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 12, elevation: 8, maxHeight: "90%", }, header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 24, paddingTop: 20, paddingBottom: 16, borderBottomWidth: 1, borderBottomColor: "#f0f0f0", }, title: { fontSize: 18, fontWeight: "600", color: "#000", flex: 1, }, closeButton: { padding: 4, marginLeft: 12, }, body: { paddingHorizontal: 24, paddingVertical: 20, }, footer: { flexDirection: "row", justifyContent: "flex-end", alignItems: "center", paddingHorizontal: 24, paddingBottom: 20, paddingTop: 12, gap: 8, }, button: { paddingVertical: 8, paddingHorizontal: 16, borderRadius: 6, minWidth: 70, alignItems: "center", justifyContent: "center", height: 36, }, cancelButton: { backgroundColor: "#fff", borderWidth: 1, borderColor: "#d9d9d9", }, cancelButtonText: { color: "#000", fontSize: 14, fontWeight: "500", }, okButton: { backgroundColor: "#1890ff", }, primaryButton: { backgroundColor: "#1890ff", }, okButtonText: { color: "#fff", fontSize: 14, fontWeight: "500", }, disabledButton: { opacity: 0.6, }, confirmContent: { flexDirection: "row", alignItems: "flex-start", }, confirmText: { flex: 1, fontSize: 14, color: "#000", lineHeight: 22, }, }); export default Modal;