'use client' import { useCallback, useEffect, useState } from 'react' import { useLocale, useTranslations } from 'next-intl' import { useRouter, useSearchParams } from 'next/navigation' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Switch } from '@/components/ui/switch' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { useToast } from '@/components/ui/use-toast' import { useFormat } from '@/lib/hooks/use-format' import { failureDescription } from '@/lib/browser/action-failure' import type { ErrorLocale } from '@/lib/errors/get-error-message' import { KeyRound, Link2, Loader2, RefreshCw, ShoppingCart, Unlink } from 'lucide-react' import { wooRequest, syncSummary, WOO_CONNECT_TIMEOUT_MS, WOO_SYNC_TIMEOUT_MS, type WooSyncPayload, } from '../lib/settings-actions' import type { WooCommerceStatusResponse } from '../types' type ConnectionInfo = NonNullable const STATUS_VARIANT: Record = { active: 'success', pending: 'secondary', revoked: 'warning', error: 'destructive', } export default function WooCommerceSettingsPanel() { const t = useTranslations('woocommerce') const tCommon = useTranslations('common') const locale = useLocale() as ErrorLocale const { toast } = useToast() const router = useRouter() const searchParams = useSearchParams() const { formatDateLong } = useFormat() const [loading, setLoading] = useState(true) const [loadFailed, setLoadFailed] = useState(false) const [configured, setConfigured] = useState(false) const [connection, setConnection] = useState(null) const [storeUrl, setStoreUrl] = useState('') const [manualMode, setManualMode] = useState(false) const [consumerKey, setConsumerKey] = useState('') const [consumerSecret, setConsumerSecret] = useState('') const [connecting, setConnecting] = useState(false) const [disconnecting, setDisconnecting] = useState(false) const [confirmDisconnect, setConfirmDisconnect] = useState(false) const [syncing, setSyncing] = useState(false) const [togglingTransactionSync, setTogglingTransactionSync] = useState(false) const failureCopy = { timeout: t('action_timeout'), network: t('action_network') } const loadStatus = useCallback(async () => { // A failed status read must never render as "not configured" (see the // Stripe panel: that copy sends the user to an administrator for nothing). const result = await wooRequest({ url: '/api/extensions/ext/woocommerce/status', method: 'GET', locale, }) setLoading(false) if (!result.ok || !result.data) { setLoadFailed(true) return } setLoadFailed(false) setConfigured(result.data.configured) setConnection(result.data.connection) }, [locale]) useEffect(() => { void loadStatus() }, [loadStatus]) function retryLoadStatus() { setLoading(true) void loadStatus() } // Consume the one-shot handshake bounce-back params off the render path, // mirroring the Stripe/banking sections. useEffect(() => { const connected = searchParams.get('woocommerce_connected') const error = searchParams.get('woocommerce_error') if (!connected && !error) return let cancelled = false queueMicrotask(() => { if (cancelled) return if (connected) { toast({ title: t('connected_toast_title'), description: t('connected_toast_description') }) } else if (error) { const message = error === 'denied' ? t('error_denied') : t('error_generic') toast({ title: t('connect_failed_title'), description: message, variant: 'destructive' }) } router.replace('/import?mode=woocommerce') }) return () => { cancelled = true } }, [searchParams, router, toast, t]) async function handleConnect() { if (connecting) return setConnecting(true) try { const result = await wooRequest<{ url?: string }>({ url: '/api/extensions/ext/woocommerce/connect', body: { store_url: storeUrl }, locale, timeoutMs: WOO_CONNECT_TIMEOUT_MS, }) if (!result.ok || !result.data?.url) { toast({ title: t('connect_failed_title'), description: result.ok ? t('error_generic') : failureDescription(result, failureCopy), variant: 'destructive', }) return } window.location.href = result.data.url } finally { setConnecting(false) } } async function handleManualConnect() { if (connecting) return setConnecting(true) try { const result = await wooRequest({ url: '/api/extensions/ext/woocommerce/manual-connect', body: { store_url: storeUrl, consumer_key: consumerKey, consumer_secret: consumerSecret, }, locale, timeoutMs: WOO_CONNECT_TIMEOUT_MS, }) if (!result.ok) { toast({ title: t('connect_failed_title'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } toast({ title: t('connected_toast_title'), description: t('connected_toast_description') }) setConsumerKey('') setConsumerSecret('') setManualMode(false) await loadStatus() } finally { setConnecting(false) } } async function handleSyncNow() { if (syncing) return setSyncing(true) try { const result = await wooRequest({ url: '/api/extensions/ext/woocommerce/sync', locale, timeoutMs: WOO_SYNC_TIMEOUT_MS, }) if (!result.ok) { toast({ title: t('sync_failed_title'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } const summary = syncSummary(result.data) if (summary.reason === 'revoked') { toast({ title: t('sync_failed_title'), description: t('sync_revoked'), variant: 'destructive', }) } else if (summary.reason === 'partial') { toast({ title: t('sync_partial_title'), description: t('sync_partial', summary.values) }) } else if (summary.reason === 'empty') { toast({ title: t('sync_done_title'), description: t('sync_done_empty') }) } else if (summary.reason === 'errors') { toast({ title: t('sync_done_title'), description: t('sync_done_feed_errors', summary.values) }) } else if (summary.reason === 'feed') { toast({ title: t('sync_done_title'), description: t('sync_done_feed', summary.values) }) } else { toast({ title: t('sync_done_title') }) } await loadStatus() } finally { setSyncing(false) } } async function handleToggleTransactionSync(enabled: boolean) { if (togglingTransactionSync) return setTogglingTransactionSync(true) try { const result = await wooRequest({ url: '/api/extensions/ext/woocommerce/transaction-sync', body: { enabled }, locale, }) if (!result.ok) { toast({ title: t('transaction_sync_toggle_failed'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } toast({ title: enabled ? t('transaction_sync_enabled_toast') : t('transaction_sync_disabled_toast'), }) await loadStatus() } finally { setTogglingTransactionSync(false) } } async function handleDisconnect() { if (!connection || disconnecting) return setDisconnecting(true) try { const result = await wooRequest({ url: '/api/extensions/ext/woocommerce/disconnect', method: 'DELETE', body: { connection_id: connection.id }, locale, }) if (!result.ok) { toast({ title: t('disconnect_failed_title'), description: failureDescription(result, failureCopy), variant: 'destructive', }) return } // The key still exists in the store's wp-admin; only the merchant can // delete it there, so the toast says so. toast({ title: t('disconnected_toast_title'), description: t('disconnected_toast_description') }) setConfirmDisconnect(false) await loadStatus() } finally { setDisconnecting(false) } } if (loading) { return ( ) } if (loadFailed) { return ( {t('title')}

{t('load_failed')}

) } if (!configured) { return ( {t('title')}

{t('not_configured')}

) } const isActive = connection?.status === 'active' const showConnectForm = !connection || !isActive return ( {t('title')}

{t('description')}

{connection && (
{connection.store_name || connection.store_url || t('unnamed_store')} {t(`status_${connection.status}`)}
{connection.store_name && (

{connection.store_url}

)} {isActive && connection.connected_at && (

{t('connected_since', { date: formatDateLong(connection.connected_at) })}

)} {connection.status === 'pending' && (

{t('pending_note')}

)} {connection.error_message && ( // Shown for active connections too: a sync that cannot run // (e.g. cash-account currency conflict) must not hide // behind a healthy-looking "Ansluten" badge.

{connection.error_message}

)}
{isActive && ( confirmDisconnect ? (
) : (
) )}
)} {showConnectForm && (
setStoreUrl(e.target.value)} disabled={connecting} />
{manualMode ? (

{t('manual_hint')}

setConsumerKey(e.target.value)} disabled={connecting} />
setConsumerSecret(e.target.value)} disabled={connecting} />
) : (

{t('connect_hint')}

)}
)} {isActive && connection && (

{t('transaction_sync_title')}

{t('transaction_sync_description')}

{connection.transaction_sync_enabled ? (

{connection.last_order_synced_at ? t('transaction_sync_last_synced', { date: formatDateLong(connection.last_order_synced_at), }) : t('transaction_sync_never_synced')}

) : (

{t('transaction_sync_backfill_note')}

)}
)}
) }