'use client' /** * Settings panel for the whatsapp-inbox extension (Inställningar -> WhatsApp). * * Unlinked: mint a one-time code (10 min TTL) + wa.me deep link; the user * sends the code from their phone and the webhook binds the number. * Linked: masked phone, default-company select (multi-company routing), * revoke. Muted (user sent *stopp* in chat) shows a hint: unmuting happens * in the chat with *start*, not here. */ import { useCallback, useEffect, useState } from 'react' import { useTranslations } from 'next-intl' import { Loader2, ExternalLink } from 'lucide-react' import { WhatsAppMark } from '@/components/extensions/general/WhatsAppMark' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { useCompany } from '@/contexts/CompanyContext' import { SettingsGroup, SettingsRow, SettingsRowNote, SettingsSelect, } from '@/components/settings/SettingsRows' const BASE = '/api/extensions/ext/whatsapp-inbox' interface LinkStatus { linked: boolean phoneMasked?: string defaultCompanyId?: string | null muted?: boolean } interface MintedCode { code: string expiresAt: string waLink: string | null } export function WhatsAppLinkPanel() { const t = useTranslations('settings_whatsapp') const { toast } = useToast() const { companies } = useCompany() const [isLoading, setIsLoading] = useState(true) const [loadFailed, setLoadFailed] = useState(false) const [status, setStatus] = useState(null) const [minted, setMinted] = useState(null) const [isMinting, setIsMinting] = useState(false) const [isSaving, setIsSaving] = useState(false) const [minutesLeft, setMinutesLeft] = useState(null) const fetchStatus = useCallback(async () => { setIsLoading(true) setLoadFailed(false) try { const response = await fetch(`${BASE}/link`) if (!response.ok) throw new Error('load failed') const { data } = (await response.json()) as { data: LinkStatus } setStatus(data) } catch { setLoadFailed(true) } finally { setIsLoading(false) } }, []) useEffect(() => { void fetchStatus() }, [fetchStatus]) // Countdown hint for the minted code (10 min TTL server-side). useEffect(() => { if (!minted) { setMinutesLeft(null) return } const tick = () => { const msLeft = new Date(minted.expiresAt).getTime() - Date.now() setMinutesLeft(msLeft > 0 ? Math.ceil(msLeft / 60_000) : 0) } tick() const interval = setInterval(tick, 15_000) return () => clearInterval(interval) }, [minted]) const startLinking = async () => { setIsMinting(true) try { const response = await fetch(`${BASE}/link/start`, { method: 'POST' }) if (!response.ok) throw new Error('mint failed') const { data } = (await response.json()) as { data: MintedCode } setMinted(data) } catch { toast({ title: t('mint_failed'), variant: 'destructive' }) } finally { setIsMinting(false) } } const saveDefaultCompany = async (companyId: string) => { setIsSaving(true) try { const response = await fetch(`${BASE}/link/default-company`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ companyId: companyId || null }), }) if (!response.ok) throw new Error('save failed') setStatus((prev) => (prev ? { ...prev, defaultCompanyId: companyId || null } : prev)) toast({ title: t('default_company_saved') }) } catch { toast({ title: t('default_company_save_failed'), variant: 'destructive' }) } finally { setIsSaving(false) } } const revoke = async () => { if (!window.confirm(t('revoke_confirm'))) return setIsSaving(true) try { const response = await fetch(`${BASE}/link/revoke`, { method: 'POST' }) if (!response.ok) throw new Error('revoke failed') setStatus({ linked: false }) setMinted(null) toast({ title: t('revoked_toast') }) } catch { toast({ title: t('revoke_failed'), variant: 'destructive' }) } finally { setIsSaving(false) } } if (isLoading) { return (
) } if (loadFailed) { return (
{t('load_failed')}{' '}
) } if (status?.linked) { return ( {status.phoneMasked} {status.muted ? ( {t('muted_hint')} ) : null} void saveDefaultCompany(event.target.value)} > {companies.map(({ company }) => ( ))} {t('revoke_note')} ) } return (

{t('unlinked_intro')}

{minted ? (
{minted.code} {minted.waLink ? ( ) : null}
  1. {t('step_open')}
  2. {t('step_send_code')}
  3. {t('step_confirm')}

{minutesLeft != null && minutesLeft > 0 ? t('expires_in', { minutes: minutesLeft }) : minutesLeft === 0 ? t('code_expired') : t('expires_hint')}

) : ( )}
) } export default WhatsAppLinkPanel