import { __, sprintf } from '@wordpress/i18n'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { CheckCircle2, CreditCard, Package, ShoppingCart, Settings2, X, } from 'lucide-react'; import { Alert, Button, Select, Spinner, Toggle } from './UI/index.jsx'; import { api } from '../api/index.js'; import useSettingsMeta from '../hooks/useSettingsMeta.js'; import SetupSteps from './SetupSteps.jsx'; import PluginNotificationPreferences from './PluginNotificationPreferences.jsx'; import { canManagePluginNotifications, DEFAULT_PLUGIN_NOTIFICATION_SETTINGS, getPluginNotificationQuestion, } from '../lib/plugin-notifications.js'; import '../../styles/easy-setup.css'; const MODULES = [ { key: 'arrayfunnel_side_cart_enabled', label: __( 'Side Cart', 'arrayfunnel' ), description: __( 'Let customers review their cart without leaving the page.', 'arrayfunnel' ), icon: ShoppingCart, path: '/side-cart', }, { key: 'arrayfunnel_checkout_optimized', label: __( 'Checkout', 'arrayfunnel' ), description: __( 'Add connection badges and refine your checkout experience.', 'arrayfunnel' ), icon: CreditCard, path: '/checkout', }, { key: 'arrayfunnel_order_bumps_enabled', label: __( 'Order Bumps', 'arrayfunnel' ), description: __( 'Offer relevant add-ons at checkout. Create your offers after setup.', 'arrayfunnel' ), icon: Package, path: '/order-bumps', }, ]; const EXPERIENCE_FIELDS = [ { key: 'arrayfunnel_side_cart_theme', label: __( 'Cart Theme', 'arrayfunnel' ), options: [ { value: 'light', label: __( 'Light', 'arrayfunnel' ) }, { value: 'graphite', label: __( 'Graphite', 'arrayfunnel' ) }, ], fallback: 'light', }, { key: 'arrayfunnel_side_cart_radius', label: __( 'Cart Corners', 'arrayfunnel' ), options: [ { value: 'soft', label: __( 'Soft', 'arrayfunnel' ) }, { value: 'balanced', label: __( 'Balanced', 'arrayfunnel' ) }, { value: 'expressive', label: __( 'Expressive', 'arrayfunnel' ) }, ], fallback: 'balanced', }, { key: 'arrayfunnel_side_cart_accent', label: __( 'Cart Accent', 'arrayfunnel' ), options: [ { value: 'emerald', label: __( 'Emerald', 'arrayfunnel' ) }, { value: 'black', label: __( 'Black', 'arrayfunnel' ) }, { value: 'sky', label: __( 'Sky', 'arrayfunnel' ) }, ], fallback: 'emerald', }, ]; const CHECKOUT_FIELDS = [ { key: 'arrayfunnel_checkout_trust_badges', label: __( 'Connection Badges', 'arrayfunnel' ), fallback: 'yes', }, ]; const CAN_MANAGE_NOTIFICATIONS = canManagePluginNotifications(); const NOTIFICATIONS_STEP = 2; const STEPS = [ __( 'Modules', 'arrayfunnel' ), __( 'Store Experience', 'arrayfunnel' ), ...( CAN_MANAGE_NOTIFICATIONS ? [ __( 'Notifications', 'arrayfunnel' ) ] : [] ), __( 'Review & Apply', 'arrayfunnel' ), ]; const REVIEW_STEP = STEPS.length - 1; const NOTIFICATION_LABELS = { security_updates: __( 'Security & Updates', 'arrayfunnel' ), mega_offers: __( 'Mega Offer Notifications', 'arrayfunnel' ), marketing_articles: __( 'Article Notifications', 'arrayfunnel' ), }; function normalizeSettings( settings ) { const normalized = Object.fromEntries( MODULES.map( ( module ) => [ module.key, module.key === 'arrayfunnel_order_bumps_enabled' ? settings[ module.key ] === 'no' ? 'no' : 'yes' : settings[ module.key ] === 'yes' ? 'yes' : 'no', ] ) ); for ( const field of EXPERIENCE_FIELDS ) { normalized[ field.key ] = field.options.some( ( option ) => option.value === settings[ field.key ] ) ? settings[ field.key ] : field.fallback; } for ( const field of CHECKOUT_FIELDS ) { normalized[ field.key ] = [ 'yes', 'no' ].includes( settings[ field.key ] ) ? settings[ field.key ] : field.fallback; } return normalized; } export default function EasySetupWizard() { const queryClient = useQueryClient(); const location = useLocation(); const navigate = useNavigate(); const dialogRef = useRef( null ); const headingRef = useRef( null ); const bodyRef = useRef( null ); const launcherRef = useRef( null ); const loadedRef = useRef( {} ); const savingRef = useRef( false ); const initialNotificationsRef = useRef( null ); const [ isOpen, setOpen ] = useState( false ); const { data: settingsMeta } = useSettingsMeta( isOpen ); const [ loading, setLoading ] = useState( false ); const [ saving, setSaving ] = useState( false ); const [ complete, setComplete ] = useState( false ); const [ step, setStep ] = useState( 0 ); const [ loadAttempt, setLoadAttempt ] = useState( 0 ); const [ loadError, setLoadError ] = useState( '' ); const [ saveError, setSaveError ] = useState( '' ); const [ original, setOriginal ] = useState( {} ); const [ answers, setAnswers ] = useState( {} ); const [ notifications, setNotifications ] = useState( DEFAULT_PLUGIN_NOTIFICATION_SETTINGS ); const [ notificationDefaults, setNotificationDefaults ] = useState( {} ); const [ editingNotifications, setEditingNotifications ] = useState( false ); const [ notificationError, setNotificationError ] = useState( '' ); useEffect( () => { const open = () => { if ( dialogRef.current?.open || savingRef.current ) return; launcherRef.current = document.activeElement; setStep( 0 ); setEditingNotifications( false ); setNotificationError( '' ); setComplete( false ); setSaveError( '' ); setLoadError( '' ); setLoading( true ); setOpen( true ); }; window.addEventListener( 'arrayfunnel:open-setup', open ); return () => window.removeEventListener( 'arrayfunnel:open-setup', open ); }, [] ); // Activation redirects here once; consume the flag before opening so refresh // and Back do not launch onboarding again. useEffect( () => { const query = new window.URLSearchParams( location.search ); if ( location.pathname !== '/settings' || query.get( 'setup' ) !== '1' ) return; query.delete( 'setup' ); const search = query.toString(); navigate( { pathname: location.pathname, search: search ? `?${ search }` : '', }, { replace: true } ); window.dispatchEvent( new window.CustomEvent( 'arrayfunnel:open-setup' ) ); }, [ location.pathname, location.search, navigate ] ); useEffect( () => { if ( ! isOpen ) return; const dialog = dialogRef.current; const bodyOverflow = document.body.style.getPropertyValue( 'overflow' ); const bodyPriority = document.body.style.getPropertyPriority( 'overflow' ); document.body.style.setProperty( 'overflow', 'hidden', 'important' ); if ( ! dialog.open ) dialog.showModal(); const updateScrollbar = () => { const body = bodyRef.current; dialog.style.setProperty( '--arrayfunnel-setup-scrollbar-width', `${ body ? body.offsetWidth - body.clientWidth : 0 }px` ); }; const observer = new window.ResizeObserver( updateScrollbar ); observer.observe( bodyRef.current ); updateScrollbar(); return () => { observer.disconnect(); if ( dialog.open ) dialog.close(); if ( bodyOverflow ) document.body.style.setProperty( 'overflow', bodyOverflow, bodyPriority ); else document.body.style.removeProperty( 'overflow' ); }; }, [ isOpen ] ); useEffect( () => { if ( ! isOpen ) return; let active = true; setLoading( true ); setLoadError( '' ); Promise.all( [ api.getSettings(), CAN_MANAGE_NOTIFICATIONS ? api.getPluginNotifications() : null, ] ) .then( ( [ settings, notificationResult ] ) => { if ( ! active ) return; if ( CAN_MANAGE_NOTIFICATIONS ) { if ( ! notificationResult?.settings || ! notificationResult?.defaults ) throw new Error( __( 'Could not load notification preferences. Please try again.', 'arrayfunnel' ) ); const preferences = { ...DEFAULT_PLUGIN_NOTIFICATION_SETTINGS, ...notificationResult.settings, }; initialNotificationsRef.current = preferences; setNotifications( preferences ); setNotificationDefaults( notificationResult.defaults ); } loadedRef.current = settings; const values = normalizeSettings( settings ); setOriginal( values ); setAnswers( values ); } ) .catch( ( error ) => { if ( active ) setLoadError( error.message || __( 'Could not load settings. Please try again.', 'arrayfunnel' ) ); } ) .finally( () => { if ( active ) setLoading( false ); } ); return () => { active = false; }; }, [ isOpen, loadAttempt ] ); useEffect( () => { if ( ! isOpen || loading ) return; const frame = window.requestAnimationFrame( () => { if ( bodyRef.current ) bodyRef.current.scrollTop = 0; headingRef.current?.focus( { preventScroll: true } ); } ); return () => window.cancelAnimationFrame( frame ); }, [ isOpen, loading, step, complete ] ); const close = useCallback( () => { if ( ! savingRef.current ) dialogRef.current?.close(); }, [] ); const onClose = () => { setOpen( false ); window.requestAnimationFrame( () => { if ( launcherRef.current?.isConnected ) launcherRef.current.focus( { preventScroll: true } ); } ); }; const set = ( key, value ) => setAnswers( ( current ) => ( { ...current, [ key ]: value } ) ); const sideCartEnabled = answers.arrayfunnel_side_cart_enabled === 'yes'; const checkoutEnabled = answers.arrayfunnel_checkout_optimized === 'yes'; const relevantKeys = [ ...MODULES.map( ( module ) => module.key ), ...( sideCartEnabled ? EXPERIENCE_FIELDS.map( ( field ) => field.key ) : [] ), ...( checkoutEnabled ? CHECKOUT_FIELDS.map( ( field ) => field.key ) : [] ), ]; const changes = Object.fromEntries( relevantKeys .filter( ( key ) => answers[ key ] !== original[ key ] ) .map( ( key ) => [ key, answers[ key ] ] ) ); useEffect( () => { if ( step !== NOTIFICATIONS_STEP || ! CAN_MANAGE_NOTIFICATIONS ) setEditingNotifications( false ); }, [ step ] ); const validateNotifications = () => { if ( ! CAN_MANAGE_NOTIFICATIONS ) return true; const error = getPluginNotificationQuestion( notificationDefaults ).validate( notifications ); setNotificationError( error || '' ); return ! error && ! editingNotifications; }; const next = () => { if ( step === NOTIFICATIONS_STEP && ! validateNotifications() ) return; setStep( ( current ) => current + 1 ); setSaveError( '' ); }; const finish = async () => { if ( loading || savingRef.current || loadError ) return; if ( ! validateNotifications() ) { setStep( NOTIFICATIONS_STEP ); return; } savingRef.current = true; setSaving( true ); setSaveError( '' ); try { if ( Object.keys( changes ).length ) { const result = await api.saveSettings( changes ); loadedRef.current = { ...loadedRef.current, ...( result?.saved || changes ), }; setOriginal( normalizeSettings( loadedRef.current ) ); queryClient.setQueryData( [ 'settings' ], ( current ) => ( { ...current, ...loadedRef.current, } ) ); } if ( CAN_MANAGE_NOTIFICATIONS ) { const result = await api.savePluginNotifications( { settings: notifications, initial_settings: initialNotificationsRef.current, } ); if ( ! result?.settings ) throw new Error( __( 'Could not save notification preferences. Please try again.', 'arrayfunnel' ) ); initialNotificationsRef.current = result.settings; setNotifications( result.settings ); } setComplete( true ); } catch ( error ) { setSaveError( error.message || __( 'Could not apply your setup. Please try again.', 'arrayfunnel' ) ); } finally { savingRef.current = false; setSaving( false ); } }; return ( ); } function ReviewRow( { label, value } ) { return (