338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
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<void>}
|
|
*/
|
|
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<Object>} 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
|
|
}
|
|
}
|