import { useCallback, useEffect, useRef, useState } from 'react' import { reconcileEmailSaved, subscribeEmailSaved } from '@/components/email-customizer/email-persistence' import { defaultSettings as formDefaults, registrationFieldTypes, RegistrationField, RegistrationFieldType, RegistrationFormSettings, createNewField, generateUniqueFieldKey } from '@/features/customization/registration-form/config' import { defaultSettings as accessDefaults, EmailVerificationSettings } from '@/features/spam/email-verification/config' import { parseEmailVerificationSettings } from '@/features/spam/email-verification/hooks/use-email-verification' type Forms = { settings: RegistrationFormSettings; fields: RegistrationField[] } type Access = { settings: EmailVerificationSettings } type Snapshot = { forms: Forms; access: Access } type ReadResult = { success: boolean; forms: Forms & { error?: string }; access: Access & { error?: string }; providers: { review?: boolean; domainRestrictions?: boolean } } type SaveResult = { success: boolean; sections: Partial> } const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b) const normalize = (forms: Forms): Forms => ({ settings: { ...formDefaults, ...forms.settings }, fields: forms.fields.map(field => ({ ...field, validation: { ...field.validation, required: field.validation?.required ?? false } })) }) export function parseRegistrationForms(value: unknown): Forms { const data = value as Forms | null if (!data || !data.settings || typeof data.settings.enabled !== 'boolean' || !Array.isArray(data.fields) || ['applyToWordPress', 'applyToWooCommerce', 'showLabels', 'showPlaceholders'].some(key => typeof (data.settings as unknown as Record)[key] !== 'boolean') || data.fields.some(field => !field || typeof field.id !== 'string' || typeof field.key !== 'string' || typeof field.label !== 'string' || typeof field.enabled !== 'boolean' || !Number.isFinite(field.priority) || !Array.isArray(field.showOn) || !registrationFieldTypes.some(type => type.type === field.type))) throw Error('The saved registration form could not be read. Please retry before editing.') return normalize(data) } export async function registrationAccessRequest(path: string, body?: unknown, signal?: AbortSignal): Promise { const controller = signal ? null : new AbortController() const timeout = controller ? window.setTimeout(() => controller.abort(), 30000) : null try { const response = await fetch(`${window.swiftCommerceData?.apiUrl || '/wp-json/swift-commerce/v1'}/registration-access/${path}`, { method: body === undefined ? 'GET' : 'POST', credentials: 'same-origin', signal: signal || controller?.signal, headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': window.swiftCommerceData?.restNonce || '' }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), }) const data = await response.json() if (!response.ok) throw new Error(data.message || 'The request failed. Please try again.') return data as T } catch (reason) { if (controller?.signal.aborted) throw Error('The request timed out. Reload the saved state before retrying an account decision.') throw reason } finally { if (timeout !== null) window.clearTimeout(timeout) } } /** One workspace draft; legacy owners still persist and sanitize their own settings. */ export function useRegistrationAccess() { const [forms, setForms] = useState({ settings: formDefaults, fields: [] }) const [access, setAccess] = useState({ settings: accessDefaults }) const [baseline, setBaseline] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [providers, setProviders] = useState({}) const originalFields = useRef([]) const lock = useRef(false) const controller = useRef() const retry = useCallback(async () => { controller.current?.abort() const next = new AbortController(); controller.current = next setLoading(true); setError(null) const timeout = window.setTimeout(() => next.abort(), 30000) try { const data = await registrationAccessRequest('settings', undefined, next.signal) if (!data.success || data.forms?.error || data.access?.error || !data.forms?.settings || typeof data.forms.settings.enabled !== 'boolean' || !Array.isArray(data.forms.fields)) throw Error('Saved registration settings are unavailable. Retry before making changes.') const loadedForms = parseRegistrationForms(data.forms) const loadedAccess = { settings: parseEmailVerificationSettings(data.access.settings) } if (next.signal.aborted) return originalFields.current = data.forms.fields setForms(loadedForms); setAccess(loadedAccess); setBaseline({ forms: loadedForms, access: loadedAccess }); setProviders(data.providers || {}) } catch (reason) { if (controller.current === next) setError(next.signal.aborted ? 'Loading took too long. Please retry.' : reason instanceof Error ? reason.message : 'The settings could not be loaded.') } finally { window.clearTimeout(timeout) if (controller.current === next) setLoading(false) } }, []) useEffect(() => { void retry(); return () => controller.current?.abort() }, [retry]) useEffect(() => subscribeEmailSaved('registration-form/settings', saved => { setForms(current => ({ ...current, settings: reconcileEmailSaved(current.settings, saved) })) setBaseline(current => current ? { ...current, forms: { ...current.forms, settings: reconcileEmailSaved(current.forms.settings, saved) } } : current) }), []) useEffect(() => subscribeEmailSaved('email-verification/settings', saved => { setAccess(current => ({ settings: reconcileEmailSaved(current.settings, saved) })) setBaseline(current => current ? { ...current, access: { settings: reconcileEmailSaved(current.access.settings, saved) } } : current) }), []) const canEdit = !!baseline && !loading && !saving && !error const dirtyForms = !!baseline && !same(forms, baseline.forms) const dirtyAccess = !!baseline && !same(access, baseline.access) const hasChanges = dirtyForms || dirtyAccess const updateForms = (updates: Partial) => { if (canEdit) setForms(current => ({ ...current, settings: { ...current.settings, ...updates } })) } const updateAccess = (key: K, value: EmailVerificationSettings[K]) => { if (canEdit) setAccess(current => ({ settings: { ...current.settings, [key]: value } })) } const updateField = (id: string, updates: Partial) => { if (canEdit) setForms(current => ({ ...current, fields: current.fields.map(field => field.id === id ? { ...field, ...updates } : field) })) } const addField = (type: RegistrationFieldType) => { const field = createNewField(type, Math.max(0, ...forms.fields.map(item => item.priority)) + 10, forms.fields) if (canEdit) setForms(current => ({ ...current, fields: [...current.fields, field] })) return field } const save = async () => { if (!baseline || !canEdit || lock.current || !hasChanges) return false lock.current = true; setSaving(true) const patch: Record = {} for (const name of ['forms', 'access'] as const) { const current = name === 'forms' ? forms : access const settings: Record = {}, expected: Record = {} for (const key of Object.keys(current.settings)) { const values = current.settings as unknown as Record const before = baseline[name].settings as unknown as Record if (!same(values[key], before[key])) { settings[key] = values[key]; expected[key] = before[key] } } const fieldsChanged = name === 'forms' && !same(forms.fields, baseline.forms.fields) if (Object.keys(settings).length || fieldsChanged) patch[name] = { settings, expected, ...(fieldsChanged ? { fields: forms.fields, expectedFields: originalFields.current } : {}) } } try { const result = await registrationAccessRequest('settings', patch) const failed: string[] = [] for (const name of Object.keys(patch) as (keyof Snapshot)[]) { const outcome = result.sections?.[name] if (!outcome?.success || !outcome.value) { // An access save can persist Free settings before its Pro owner fails. // Rebase confirmed persisted values, but retain the entire failed draft. if (outcome?.value) { if (name === 'access') { const actual = { settings: parseEmailVerificationSettings(outcome.value.settings) }; setBaseline(current => current ? { ...current, access: actual } : current) } else { const actual = parseRegistrationForms(outcome.value); originalFields.current = (outcome.value as Forms).fields; setBaseline(current => current ? { ...current, forms: actual } : current) } } failed.push(`${name === 'forms' ? 'Forms' : 'Access'}: ${outcome?.error || 'Not saved'}`); continue } if (name === 'forms') { const saved = parseRegistrationForms(outcome.value); originalFields.current = (outcome.value as Forms).fields setForms(current => same(current, forms) ? saved : current) setBaseline(current => current ? { ...current, forms: saved } : current) } else { const saved = { settings: parseEmailVerificationSettings(outcome.value.settings) } setAccess(current => same(current, access) ? saved : current) setBaseline(current => current ? { ...current, access: saved } : current) } } if (failed.length) throw Error(`${failed.join(' ')} Successful sections are saved; unsaved changes are retained.`) return result.success } catch (reason) { setError(reason instanceof Error ? reason.message : 'The settings could not be saved. Your changes are retained.') return false } finally { lock.current = false; setSaving(false) } } // A save failure retains drafts and can be dismissed for a retry, unlike a failed initial load. return { forms, access, loading, saving, error, providers, canEdit, hasChanges, dirtyForms, dirtyAccess, retry, clearSaveError: () => { if (baseline) setError(null) }, updateForms, updateAccess, updateField, addField, deleteField: (id: string) => { if (canEdit) setForms(current => ({ ...current, fields: current.fields.filter(field => field.id !== id) })) }, duplicateField: (id: string) => { const source = forms.fields.find(field => field.id === id); if (source && canEdit) setForms(current => ({ ...current, fields: [...current.fields, { ...source, id: `field_${Date.now()}`, key: generateUniqueFieldKey(source.type, current.fields), label: `${source.label} (Copy)`, isCore: false, priority: Math.max(0, ...current.fields.map(field => field.priority)) + 10 }] })) }, reorderFields: (fields: RegistrationField[]) => { if (canEdit) setForms(current => ({ ...current, fields: fields.map((field, index) => ({ ...field, priority: (index + 1) * 10 })) })) }, discard: () => { if (baseline) { setForms(baseline.forms); setAccess(baseline.access); setError(null) } }, save, } }