'use client' import { useCallback, useEffect, useState } from 'react' import { useLocale, useTranslations } from 'next-intl' 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, Loader2, RefreshCw, ShoppingBag, Unlink } from 'lucide-react' import { shopifyRequest, syncSummary, SHOPIFY_CONNECT_TIMEOUT_MS, SHOPIFY_SYNC_TIMEOUT_MS, type ShopifySyncPayload, } from '../lib/settings-actions' import type { ShopifyStatusResponse } from '../types' type ConnectionInfo = NonNullable const STATUS_VARIANT: Record = { active: 'success', pending: 'secondary', revoked: 'warning', error: 'destructive', } export default function ShopifySettingsPanel() { const t = useTranslations('shopify') const tCommon = useTranslations('common') const locale = useLocale() as ErrorLocale const { toast } = useToast() const { formatDateLong } = useFormat() const [loading, setLoading] = useState(true) const [loadFailed, setLoadFailed] = useState(false) const [configured, setConfigured] = useState(false) const [connection, setConnection] = useState(null) const [shopDomain, setShopDomain] = useState('') const [clientId, setClientId] = useState('') const [clientSecret, setClientSecret] = 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 shopifyRequest({ url: '/api/extensions/ext/shopify/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() } async function handleConnect() { if (connecting) return setConnecting(true) try { const result = await shopifyRequest({ url: '/api/extensions/ext/shopify/connect', body: { shop_domain: shopDomain, client_id: clientId, client_secret: clientSecret, }, locale, timeoutMs: SHOPIFY_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') }) setClientId('') setClientSecret('') await loadStatus() } finally { setConnecting(false) } } async function handleSyncNow() { if (syncing) return setSyncing(true) try { const result = await shopifyRequest({ url: '/api/extensions/ext/shopify/sync', locale, timeoutMs: SHOPIFY_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 shopifyRequest({ url: '/api/extensions/ext/shopify/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 shopifyRequest({ url: '/api/extensions/ext/shopify/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 app still exists in the merchant's Shopify Dev Dashboard; only // they 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.shop_name || connection.shop_domain || t('unnamed_store')} {t(`status_${connection.status}`)}
{connection.shop_name && (

{connection.shop_domain}

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

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

)} {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 && (

{t('connect_hint')}

setShopDomain(e.target.value)} disabled={connecting} />
setClientId(e.target.value)} disabled={connecting} />
setClientSecret(e.target.value)} disabled={connecting} />
)} {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')}

)}
)}
) }