diff --git a/.gitignore b/.gitignore index ce2e628f..6d63ef03 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,4 @@ supabase/.temp/ # out of the box without running the generator. supabase/.branches/ -scripts\remap-krister-bas96-to-bas2025.ts +/scripts \ No newline at end of file diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index a5ac505f..cb12e080 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -4,6 +4,9 @@ import { headers } from 'next/headers' import DashboardNav from '@/components/dashboard/DashboardNav' import { MainContainer } from '@/components/dashboard/MainContainer' import CompanyTabSync from '@/components/dashboard/CompanyTabSync' +import { RecaptIdentify } from '@/components/RecaptIdentify' +import { RecaptLoader } from '@/components/RecaptLoader' +import { RecaptHideWidget } from '@/components/RecaptHideWidget' import { AgentSheetProvider } from '@/components/agent/AgentSheetProvider' import AgentTrigger from '@/components/agent/AgentTrigger' import CommandPalette from '@/components/common/CommandPalette' @@ -278,6 +281,17 @@ export default async function DashboardLayout({ + {!isSandbox && ( + <> + + + + + )} ) diff --git a/app/(dashboard)/settings/account/page.tsx b/app/(dashboard)/settings/account/page.tsx index a6f7d50f..1c701b60 100644 --- a/app/(dashboard)/settings/account/page.tsx +++ b/app/(dashboard)/settings/account/page.tsx @@ -13,6 +13,7 @@ import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings import { AccountDangerZone } from '@/components/settings/AccountDangerZone' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { useSettings } from '@/components/settings/useSettings' +import { clearRecaptIdentity } from '@/lib/recapt' import { useToast } from '@/components/ui/use-toast' import { SUPPORTED_LOCALES, type Locale } from '@/i18n/config' @@ -32,6 +33,7 @@ export default function AccountSettingsPage() { useEffect(() => { setMounted(true) }, []) async function handleLogout() { + clearRecaptIdentity() await supabase.auth.signOut() router.push('/login') } diff --git a/app/(dashboard)/skattekonto/page.tsx b/app/(dashboard)/skattekonto/page.tsx index e1c11308..d735e38f 100644 --- a/app/(dashboard)/skattekonto/page.tsx +++ b/app/(dashboard)/skattekonto/page.tsx @@ -445,7 +445,7 @@ function BalanceHero({ {saldo.lastSyncedAt && (

- Senast synkad{' '} + Synkas automatiskt varje natt. Senast synkad{' '} {new Date(saldo.lastSyncedAt).toLocaleString('sv-SE')} diff --git a/app/(dashboard)/transactions/page.tsx b/app/(dashboard)/transactions/page.tsx index 7dd0aee2..fde5f858 100644 --- a/app/(dashboard)/transactions/page.tsx +++ b/app/(dashboard)/transactions/page.tsx @@ -24,6 +24,7 @@ import { ChevronDown, Search, Trash2, X } from 'lucide-react' import TransactionForm from '@/components/transactions/TransactionForm' import BatchCategorySelector from '@/components/transactions/BatchCategorySelector' import TransactionStatusBar from '@/components/transactions/TransactionStatusBar' +import BankSyncStatusChip from '@/components/transactions/BankSyncStatusChip' import TransactionInboxCard from '@/components/transactions/TransactionInboxCard' import TransactionHistoryList from '@/components/transactions/TransactionHistoryList' import InboxZeroState from '@/components/transactions/InboxZeroState' @@ -1513,6 +1514,8 @@ export default function TransactionsPage() { onToggleBatchMode={() => (isBatchMode ? exitBatchMode() : setIsBatchMode(true))} /> + + {/* Search + view dropdown */}

diff --git a/app/(public)/privacy/page.tsx b/app/(public)/privacy/page.tsx index d6395319..7c81d92d 100644 --- a/app/(public)/privacy/page.tsx +++ b/app/(public)/privacy/page.tsx @@ -18,7 +18,7 @@ export default function PrivacyPolicyPage() { Integritetspolicy

- Senast uppdaterad: 2026-03-05 + Senast uppdaterad: 2026-05-28

@@ -124,6 +124,16 @@ export default function PrivacyPolicyPage() { USA SCCs (standardavtalsklausuler) + + Recapt + + Produktanalys och användarfeedback. Laddas endast för + inloggade användare (ej sandbox/demo). Överförda + uppgifter: användar-ID, e-postadress och företagsnamn. + + EU + SCCs vid eventuella underbiträden utanför EES +
diff --git a/components/RecaptHideWidget.tsx b/components/RecaptHideWidget.tsx new file mode 100644 index 00000000..9cc17c87 --- /dev/null +++ b/components/RecaptHideWidget.tsx @@ -0,0 +1,38 @@ +'use client' + +import { useEffect } from 'react' + +/** + * Hides Recapt's floating feedback bubble while keeping the SDK active so + * `window.recapt('identify', ...)` and programmatic `window.recapt('feedback', + * { message })` calls continue to work. Mounted globally in the root layout. + */ +export function RecaptHideWidget() { + useEffect(() => { + let attempts = 0 + const maxAttempts = 50 + + const hide = (): boolean => { + if (typeof window.recapt !== 'function') return false + try { + window.recapt('feedback', { widget: 'hide' }) + } catch { + // best-effort + } + return true + } + + if (hide()) return + + const interval = setInterval(() => { + attempts++ + if (hide() || attempts >= maxAttempts) { + clearInterval(interval) + } + }, 100) + + return () => clearInterval(interval) + }, []) + + return null +} diff --git a/components/RecaptIdentify.tsx b/components/RecaptIdentify.tsx new file mode 100644 index 00000000..90d98103 --- /dev/null +++ b/components/RecaptIdentify.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useEffect } from 'react' + +export function RecaptIdentify({ + userId, + email, + displayName, +}: { + userId: string + email?: string + displayName?: string +}) { + useEffect(() => { + let attempts = 0 + const maxAttempts = 50 + const interval = setInterval(() => { + if (typeof window.recapt === 'function') { + window.recapt('identify', { + uid: userId, + email, + nickname: displayName, + }) + clearInterval(interval) + return + } + attempts++ + if (attempts >= maxAttempts) { + clearInterval(interval) + } + }, 100) + + return () => clearInterval(interval) + }, [userId, email, displayName]) + + return null +} diff --git a/components/RecaptLoader.tsx b/components/RecaptLoader.tsx new file mode 100644 index 00000000..5c56e962 --- /dev/null +++ b/components/RecaptLoader.tsx @@ -0,0 +1,31 @@ +'use client' + +import Script from 'next/script' + +/** + * Loads the Recapt SDK for authenticated dashboard users only. + * + * Privacy guard rails: + * - Mounted inside the dashboard layout, so the script never loads on + * public pages (login, register, privacy policy, marketing). This + * prevents pre-consent IP/fingerprint collection on those routes. + * - The public key is sourced from NEXT_PUBLIC_RECAPT_PUBLIC_KEY so + * hosted and self-hosted deployments can each supply their own key + * (or disable Recapt entirely by leaving it unset). + * - data-persist / data-enable-user-comments are intentionally omitted + * from the default tag. Persistent cross-session tracking and + * unstructured free-text capture are opt-in product decisions, not + * defaults (GDPR Art. 25 — privacy by default). + */ +export function RecaptLoader() { + const publicKey = process.env.NEXT_PUBLIC_RECAPT_PUBLIC_KEY + if (!publicKey) return null + + return ( +