import { reactive, computed, ref, toValue, onBeforeUnmount } from 'vue' import { useForm } from '~/composables/useForm.js' // Assuming useForm handles vForm setup import { FormMode, createFormModeStrategy } from '../FormModeStrategy' import { useFormStructure } from './useFormStructure' import { useFormInitialization } from './useFormInitialization' import { useFormValidation } from './useFormValidation' import { useFormSubmission } from './useFormSubmission' import { useFormPayment } from './useFormPayment' import { useFormTimer } from './useFormTimer' import { usePendingSubmission } from '~/lib/forms/composables/usePendingSubmission.js' import { usePartialSubmission } from '~/composables/forms/usePartialSubmission.js' import { useIsIframe } from '~/composables/useIsIframe' import { useAmplitude } from '~/composables/useAmplitude' import { useConfetti } from '~/composables/useConfetti' import { cloneDeep } from 'lodash' /** * @fileoverview Main orchestrator composable for form operations. * Initializes and coordinates various form composables (Structure, Init, Validation, etc.) * based on the provided form configuration and mode. */ export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options = {}) { // --- Reactive State --- const config = ref(initialFormConfig) // Use ref for potentially replaceable config const form = useForm() // Core vForm instance const strategy = computed(() => createFormModeStrategy(mode)) // Strategy based on mode // Use the passed darkMode ref if it's a ref, otherwise create a new ref const darkMode = options.darkMode && typeof options.darkMode === 'object' && 'value' in options.darkMode ? options.darkMode : ref(options.darkMode || false) const state = reactive({ currentPage: 0, isSubmitted: false, isProcessing: false, // Unified flag for async ops }) // --- Initialize services that depend on config and form data --- // Create a reactive reference to the form data for dependent composables to watch const formDataRef = computed(() => form.data()) // Instantiate pending submission service (handles localStorage saving) const pendingSubmissionService = usePendingSubmission(config, formDataRef) // Instantiate partial submission service (handles server auto-sync) const partialSubmissionService = usePartialSubmission(config, formDataRef, pendingSubmissionService) // --- Instantiate Other Composables (Services) --- const timer = useFormTimer(pendingSubmissionService) const initialization = useFormInitialization(config, form, pendingSubmissionService) const structure = useFormStructure(config, state, form) const validation = useFormValidation(config, form, state) const payment = useFormPayment(config, form) const submission = useFormSubmission(config, form) /** * Updates the form configuration when the entire form reference changes. * @param {Object} newConfig - The new form configuration * @param {Object} options - Optional initialization options * @returns {Promise} */ const updateConfig = async (newConfig, options = {}) => { if (!newConfig) return // Update the config reference config.value = newConfig // Reset form state state.isSubmitted = false state.currentPage = 0 // Reinitialize with new config return initialize(options) } /** * Initializes the form: loads data, resets state, starts timer. * @param {Object} options - Initialization options (passed to useFormInitialization). */ const initialize = async (options = {}) => { state.isProcessing = true state.isSubmitted = false state.currentPage = 0 await initialization.initialize({ ...options }) timer.reset() timer.start() // Start partial submission sync if enabled if (import.meta.client && config.value.enable_partial_submissions) { partialSubmissionService.startSync() } state.isProcessing = false } /** * Navigates to the next page, handling validation and payment intent creation. */ const nextPage = async () => { if (state.isProcessing) return false state.isProcessing = true try { const currentPageFields = structure.getPageFields(state.currentPage) // Use computed isLastPage directly from structure composable const isCurrentlyLastPage = structure.isLastPage.value // 1. Validate current page await validation.validateCurrentPage(currentPageFields, strategy.value) // 2. Process payment (Create Payment Intent if applicable) const paymentBlock = structure.currentPagePaymentBlock.value if (paymentBlock) { // In editor/test mode (not LIVE), skip payment validation const isPaymentRequired = mode === FormMode.LIVE ? !!paymentBlock.required : false // Pass required refs if Stripe needs them now (unlikely for just intent creation) const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired) if (!paymentResult.success) { throw paymentResult.error || new Error('Payment intent creation failed') } } // 3. Move to the next page if not the last if (!isCurrentlyLastPage) { state.currentPage++ } state.isProcessing = false return true } catch { // Use validation composable's failure handler validation.onValidationFailure({ fieldGroups: structure.fieldGroups.value, // Pass reactive groups setPageIndexCallback: (index) => { state.currentPage = index }, timerService: timer // Pass the timer composable instance }) state.isProcessing = false return false } } /** Navigates to the previous page */ const previousPage = () => { if (state.currentPage > 0 && !state.isProcessing) { state.currentPage-- } } /** * Attempts the final form submission. * Handles timer stop, final validation, captcha, and submission call. * @param {Object} submitOptions - Optional extra data for submission. * @returns {Promise} Result from submission composable. */ const submit = async (submitOptions = {}) => { if (state.isProcessing) { return Promise.reject('Processing') } state.isProcessing = true try { // Stop partial submission sync during submission if enabled if (!import.meta.server && toValue(config).enable_partial_submissions) { partialSubmissionService.stopSync() // This will sync immediately before stopping } // 1. Stop Timer & Get Time timer.stop() const completionTime = timer.getCompletionTime() // 2. Process payment if applicable const paymentBlock = structure.currentPagePaymentBlock.value if (paymentBlock) { // In editor/test mode (not LIVE), skip payment validation const isPaymentRequired = mode === FormMode.LIVE ? !!paymentBlock.required : false const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired) if (!paymentResult.success) { // If payment was skipped because it's not required, we continue if (!paymentResult.skipped) { // Payment error - don't proceed with submission state.isProcessing = false throw new Error(paymentResult.error || 'Payment failed') } } } // 3. Get submission hash from partialSubmission if enabled let submissionHash = null if (!import.meta.server && toValue(config).enable_partial_submissions) { submissionHash = partialSubmissionService.getSubmissionHash() } // 4. Perform Submission (using submission composable) const submissionResult = await submission.submit({ formModeStrategy: strategy.value, completionTime: completionTime, submissionHash: submissionHash, ...submitOptions }) // 5. Update State on Success state.isSubmitted = true state.isProcessing = false // 6. Play confetti if enabled in config if (import.meta.client && toValue(config).confetti_on_submission) { useConfetti().play() } // 7. Clear pending submission data on successful submit pendingSubmissionService?.clear() // 8. Handle amplitude logging if (import.meta.client) { const amplitude = useAmplitude() amplitude.logEvent('form_submission', { workspace_id: toValue(config).workspace_id, form_id: toValue(config).id }) } // 9. Handle postMessage communication for iframe integration if (import.meta.client) { const isIframe = useIsIframe() const formConfig = toValue(config) const payload = cloneDeep({ type: 'form-submitted', form: { slug: formConfig.slug, id: formConfig.id, redirect_target_url: (formConfig.is_pro && submissionResult?.redirect && submissionResult?.redirect_url) ? submissionResult.redirect_url : null }, submission_data: form.data(), completion_time: completionTime }) // Send message to parent if in iframe if (isIframe) { window.parent.postMessage(payload, '*') } // Also send to current window for potential internal listeners window.postMessage(payload, '*') } // 10. Handle redirect if server response includes redirect info if (import.meta.client && submissionResult?.redirect && submissionResult?.redirect_url) { window.location.href = submissionResult.redirect_url } return submissionResult // Return result from submission composable } catch (error) { // Restart partial submission sync if there was an error and it's enabled if (!import.meta.server && toValue(config).enable_partial_submissions) { partialSubmissionService.startSync() } // Handle validation or submission errors using validation composable's handler validation.onValidationFailure({ fieldGroups: structure.fieldGroups.value, setPageIndexCallback: (index) => { state.currentPage = index }, timerService: timer }) state.isProcessing = false throw error } } /** Resets the form to its initial state for refilling. */ const restart = async () => { state.isSubmitted = false state.currentPage = 0 state.isProcessing = false form.reset() // Reset vForm data form.errors.clear() // Clear vForm errors timer.reset() // Reset timer via composable timer.start() // Restart timer // Restart partial submission if enabled if (!import.meta.server && toValue(config).enable_partial_submissions) { partialSubmissionService.stopSync() // This will sync immediately before stopping partialSubmissionService.startSync() // Start fresh sync } } // Clean up when component using the manager is unmounted if (import.meta.client) { onBeforeUnmount(() => { if (toValue(config).enable_partial_submissions) { partialSubmissionService.stopSync() } }) } // --- Exposed API --- return { // Reactive State & Config state, // Core state (currentPage, isProcessing, isSubmitted) config, // Form configuration (ref) form, // The vForm instance (from useForm) strategy, // Current mode strategy (computed) pendingSubmission: pendingSubmissionService, // Expose pendingSubmission service partialSubmission: partialSubmissionService, // Expose partialSubmission service with debounced sync // UI-related properties darkMode, // Dark mode setting setDarkMode: (isDark) => { darkMode.value = isDark }, // Method to update dark mode // Composables (Expose if direct access needed, often not necessary) structure, payment, // Expose payment service // Core Methods initialize, updateConfig, // New method to update form config nextPage, previousPage, submit, restart, // Convenience Computed Getters for vForm state data: computed(() => form.data()), errors: computed(() => form.errors), isDirty: computed(() => form.isDirty), busy: computed(() => form.busy), // Or potentially combine with state.isProcessing } }