import React, { useMemo, useEffect, Fragment, useState } from 'react'; import { View, Linking, Alert } from 'react-native'; import * as Yup from 'yup'; import type { ContentItem, FormProperty } from '../types'; import { FormRenderer } from './FormRenderer'; import { Formik } from 'formik'; import { getLayoutStyles, getViewStyles } from '../utils/styleKeys'; import { ScrollView } from 'react-native'; import { RenderBasicElement } from '../ContentItem/renderers'; import type { ContentViewItem } from '../store/contentModel'; import { getChildren } from '../utils'; import Resync from 'resync-javascript'; interface ContentFormRendererProps { form: ContentItem; contentItem: ContentViewItem; } export const ContentFormRenderer: React.FC = ({ form, contentItem, }) => { const formData = form.data as FormProperty; const [isSubmitted, setIsSubmitted] = useState(false); useEffect(() => { if (form?.event && contentItem?.content?.id) { if (form.eventConfig?.action === 'view') { Resync.logEvent({ eventId: form.event?.eventId || '', logId: form.eventConfig?.logId || '', }); if (contentItem?.logAnalytics && form.eventConfig?.logId) { contentItem.logAnalytics(form.eventConfig?.logId, { itemId: form?.itemId, contentViewId: contentItem?.content?.id, ...form.eventConfig, }); } } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Helper function to render form content items (sections or form elements) const renderFormContentItem = ( element: ContentItem, formValues: Record, formHandlers: { handleChange: (field: string) => (value: any) => void; handleBlur: (field: string) => () => void; handleSubmit: () => void; setFieldTouched: (field: string) => void; }, formState: { errors: Record; touched: Record; isSubmitting: boolean; } ) => { if (element.type === 'section') { // Lazy load RenderSection to avoid circular dependency const { RenderSection } = require('../ContentItem/section'); return ( ); } else { if ( ['text', 'image', 'button', 'divider', 'icon'].includes( element.elementType as string ) ) { return ( ); } // Render form element return ( formHandlers.setFieldTouched(element.name)} onSubmit={formHandlers.handleSubmit} error={formState?.errors?.[element.name] as string} touched={formState?.touched?.[element.name] as boolean | undefined} disabled={formState.isSubmitting} loading={formState.isSubmitting} contentItem={contentItem} /> ); } }; // Get child elements of this form const formElements = getChildren(form.itemId, contentItem?.content); // Helper function to recursively collect all form elements (including those in sections) const getAllFormElements = (items: ContentItem[]): ContentItem[] => { const allElements: ContentItem[] = []; items.forEach((item) => { if (item.type === 'section') { // Get children of section and recursively process them const sectionChildren = getChildren(item.itemId, contentItem?.content); allElements.push(...getAllFormElements(sectionChildren)); } else if ( ['input', 'select', 'checkbox', 'radio', 'textarea'].includes( item.elementType as string ) ) { // This is a form element allElements.push(item); } }); return allElements; }; // Get all form elements including those nested in sections const allFormElements = getAllFormElements(formElements); const handleSubmitSuccess = () => { // Mark as submitted to hide form immediately if hideAfterSubmission is enabled setIsSubmitted(true); const { navigationRegistry } = contentItem; if (formData.submittedSuccessAction) { if ( formData.submittedSuccessAction?.type === 'in_app_navigate' && formData.submittedSuccessAction?.inAppNavigate?.routeName ) { switch (formData.submittedSuccessAction?.inAppNavigate?.type) { case 'navigate': navigationRegistry?.navigate( formData.submittedSuccessAction?.inAppNavigate?.routeName!, formData.submittedSuccessAction?.inAppNavigate?.params ); break; case 'push': navigationRegistry?.push( formData.submittedSuccessAction?.inAppNavigate?.routeName!, formData.submittedSuccessAction?.inAppNavigate?.params ); break; case 'goBack': navigationRegistry?.goBack(); break; default: console.warn( 'Unknown navigation type:', formData.submittedSuccessAction?.inAppNavigate?.type ); } } else if (formData.submittedSuccessAction?.type === 'out_app_navigate') { Linking.canOpenURL( formData.submittedSuccessAction?.outAppNavigate?.url! ).then((supported) => { if (supported) { Linking.openURL( formData.submittedSuccessAction?.outAppNavigate?.url! ); } }); } else if ( formData.submittedSuccessAction?.type === 'alert' && formData.submittedSuccessAction?.alertTitle && formData.submittedSuccessAction?.alertMessage ) { Alert.alert( formData.submittedSuccessAction.alertTitle, formData.submittedSuccessAction.alertMessage ); } } }; const handleSubmitError = () => { const { navigationRegistry } = contentItem; if (formData.submittedErrorAction) { if (formData.submittedErrorAction?.type === 'in_app_navigate') { switch (formData.submittedErrorAction?.inAppNavigate?.type) { case 'navigate': navigationRegistry?.navigate( formData.submittedErrorAction?.inAppNavigate?.routeName!, formData.submittedErrorAction?.inAppNavigate?.params ); break; case 'push': navigationRegistry?.push( formData.submittedErrorAction?.inAppNavigate?.routeName!, formData.submittedErrorAction?.inAppNavigate?.params ); break; case 'goBack': navigationRegistry?.goBack(); break; default: console.warn( 'Unknown navigation type:', formData.submittedErrorAction?.inAppNavigate?.type ); } } else if (formData.submittedErrorAction?.type === 'out_app_navigate') { Linking.canOpenURL( formData.submittedErrorAction?.outAppNavigate?.url! ).then((supported) => { if (supported) { Linking.openURL( formData.submittedErrorAction?.outAppNavigate?.url! ); } }); } else if (formData.submittedErrorAction?.type === 'alert') { Alert.alert( formData.submittedErrorAction?.alertTitle!, formData.submittedErrorAction?.alertMessage! ); } } }; const onSubmit = async (values: Record) => { if (contentItem?.logAnalytics && form.eventConfig?.action === 'submit') { contentItem.logAnalytics(form.eventConfig?.logId || '', { itemId: form?.itemId, ...form.eventConfig, metadata: values, }); } const { functionRegistry } = contentItem; if (!contentItem?.content?.id) { console.warn('Content item ID not found'); return; } if (formData.submissionType === 'function') { const funcName = formData.submissionSettings?.functionName; if (funcName && functionRegistry?.[funcName]) { await (functionRegistry[funcName](values) as unknown as Promise); } else { console.warn(`Function "${funcName}" not found in function registry`); } } else if (formData.submissionType === 'webhook') { const webhookUrl = formData.submitUrl; if (webhookUrl) { try { const response = await fetch(webhookUrl, { method: 'POST', body: JSON.stringify(values), headers: formData.submissionSettings?.webhookHeaders, }); if (response.ok) { handleSubmitSuccess(); } else { handleSubmitError(); } } catch (error) { handleSubmitError(); } } else { console.warn('Webhook URL not found'); } } else if (formData.submissionType === 'internal') { const response = await Resync.submitForm({ contentViewId: contentItem?.content?.id, data: values, }); if (response) { handleSubmitSuccess(); } else { handleSubmitError(); } } }; // Sort form elements by order const sortedElements = (formElements || [])?.sort( (a, b) => a.order - b.order ); const elementWithValidationRules = (allFormElements || [])?.filter( (element) => { const elementData = element.data as FormProperty; return ( elementData?.required || elementData?.minLength || elementData?.maxLength || elementData?.min !== undefined || elementData?.max !== undefined || elementData?.pattern || (elementData?.validationRules && elementData.validationRules.length > 0) ); } ); const initialValues = useMemo(() => { return allFormElements .filter((element) => ['input', 'select', 'checkbox', 'radio', 'textarea'].includes( element.elementType as string ) ) .reduce( (acc, element) => { const elementData = element.data as FormProperty; // Set appropriate default values based on element type if ( element.elementType === 'checkbox' || element.elementType === 'radio' ) { acc[element.name] = elementData?.defaultValue || false; } else if (element.elementType === 'select') { acc[element.name] = elementData?.defaultValue || ''; } else { acc[element.name] = elementData?.defaultValue || ''; } return acc; }, {} as Record ); }, [allFormElements]); const validationSchema = useMemo(() => { return elementWithValidationRules.reduce( (acc, element) => { const elementData = element.data as FormProperty; if (!element.name) return acc; // Determine base schema type based on input mode let schema: any; const isNumeric = elementData?.inputMode === 'numeric' || elementData?.inputMode === 'decimal'; if (isNumeric) { schema = Yup.number(); } else { schema = Yup.string(); } // Apply required validation if (elementData?.required) { schema = schema.required('This field is required'); } // Apply legacy validations (for backward compatibility) if (!isNumeric) { if (elementData?.minLength) { schema = schema.min( elementData.minLength, `Minimum length is ${elementData.minLength}` ); } if (elementData?.maxLength) { schema = schema.max( elementData.maxLength, `Maximum length is ${elementData.maxLength}` ); } if (elementData?.pattern) { schema = schema.matches( new RegExp(elementData.pattern), `Must match pattern ${elementData.pattern}` ); } } else { if (elementData?.min !== undefined) { schema = schema.min( elementData.min, `Must be greater than or equal to ${elementData.min}` ); } if (elementData?.max !== undefined) { schema = schema.max( elementData.max, `Must be less than or equal to ${elementData.max}` ); } } // Apply validation rules from array // Normalize validationRules to always be an array (handle undefined, {}, null, etc.) const validationRules = Array.isArray(elementData?.validationRules) ? elementData.validationRules : []; if (validationRules.length > 0) { validationRules.forEach((rule: any) => { const errorMessage = rule.message || `Validation failed for ${rule.type}`; switch (rule.type) { case 'email': schema = schema.email(errorMessage); break; case 'url': schema = schema.url(errorMessage); break; case 'minLength': if (rule.value) { const minVal = parseInt(rule.value, 10); schema = schema.min(minVal, errorMessage); } else { console.warn(`[${element.name}] minLength rule has no value`); } break; case 'maxLength': if (rule.value) { const maxVal = parseInt(rule.value, 10); schema = schema.max(maxVal, errorMessage); } else { console.warn(`[${element.name}] maxLength rule has no value`); } break; case 'length': if (rule.value) { const len = parseInt(rule.value, 10); schema = schema.min(len, errorMessage).max(len, errorMessage); } break; case 'min': if (rule.value !== undefined) { schema = schema.min(parseFloat(rule.value), errorMessage); } break; case 'max': if (rule.value !== undefined) { schema = schema.max(parseFloat(rule.value), errorMessage); } break; case 'greaterThan': if (rule.value !== undefined) { schema = schema.moreThan( parseFloat(rule.value), errorMessage ); } break; case 'lessThan': if (rule.value !== undefined) { schema = schema.lessThan( parseFloat(rule.value), errorMessage ); } break; case 'positive': schema = schema.positive(errorMessage); break; case 'negative': schema = schema.negative(errorMessage); break; case 'pattern': if (rule.value) { try { schema = schema.matches( new RegExp(rule.value), errorMessage ); } catch (e) { console.warn( `Invalid regex pattern for ${element.name}:`, rule.value ); } } break; default: console.warn(`Unknown validation rule type: ${rule.type}`); } }); } acc[element.name] = schema; return acc; }, {} as Record ); }, [elementWithValidationRules]); // Hide form immediately after successful submission if hideAfterSubmission is enabled if (formData?.hideAfterSubmission && isSubmitted) { return null; } return ( {({ handleSubmit, handleChange, handleBlur, setFieldTouched, values, errors, touched, isSubmitting, }) => { return true ? ( {sortedElements?.map((element) => { return ( {renderFormContentItem( element, values, { handleChange: (field: string) => (value: any) => handleChange(field)(value), handleBlur: (field: string) => () => handleBlur(field), handleSubmit, setFieldTouched, }, { errors, touched, isSubmitting, } )} ); })} ) : ( {sortedElements.map((element) => renderFormContentItem( element, values, { handleChange: (field: string) => (value: any) => handleChange(field)(value), handleBlur: (field: string) => () => handleBlur(field), handleSubmit, setFieldTouched, }, { errors, touched, isSubmitting, } ) )} ); }} ); };