diff --git a/resources/js/components/ui/amount-display.tsx b/resources/js/components/ui/amount-display.tsx
index 44f98979..8f88867e 100644
--- a/resources/js/components/ui/amount-display.tsx
+++ b/resources/js/components/ui/amount-display.tsx
@@ -1,4 +1,3 @@
-import { useEncryptionKey } from '@/contexts/encryption-key-context';
import { usePrivacyMode } from '@/contexts/privacy-mode-context';
import { useLocale } from '@/hooks/use-locale';
import { cn } from '@/lib/utils';
@@ -57,38 +56,16 @@ export function AmountDisplay({
monospace = false,
highlightPositive = false,
}: AmountDisplayProps) {
- const { isKeySet } = useEncryptionKey();
const { isPrivacyModeEnabled } = usePrivacyMode();
const locale = useLocale();
const isPositive = amountInCents > 0;
- const shouldHideAmount = !isKeySet;
-
- const displayAmountInCents = useMemo(() => {
- if (shouldHideAmount) {
- const length = Math.max(3, amountInCents.toString().length);
- return parseInt('8'.repeat(length - 2) + '00');
- }
-
- return amountInCents;
- }, [amountInCents, shouldHideAmount]);
-
const formatted = useMemo(() => {
- return formatCurrency(displayAmountInCents, currencyCode, locale, minimumFractionDigits, maximumFractionDigits);
- }, [locale, displayAmountInCents, currencyCode, minimumFractionDigits, maximumFractionDigits]);
+ return formatCurrency(amountInCents, currencyCode, locale, minimumFractionDigits, maximumFractionDigits);
+ }, [locale, amountInCents, currencyCode, minimumFractionDigits, maximumFractionDigits]);
- const getBackgroundClass = (shouldHideAmount: boolean) => {
- if (!highlightPositive && !shouldHideAmount) return '';
-
- if (shouldHideAmount) {
- if (variant === 'positive-highlight' && isPositive) {
- return 'rounded-xs bg-green-400 dark:bg-green-900 text-green-400 dark:text-green-900 opacity-20 dark:opacity-100';
- }
-
- return 'rounded-xs bg-zinc-950 dark:bg-zinc-700 dark:text-zinc-700';
- }
-
- if (variant === 'positive-highlight') {
+ const getBackgroundClass = () => {
+ if (highlightPositive && variant === 'positive-highlight') {
return 'bg-green-100/70 dark:bg-green-900';
}
@@ -103,7 +80,7 @@ export function AmountDisplay({
variantStyles[variant],
size && sizeStyles[size],
weight && weightStyles[weight],
- getBackgroundClass(shouldHideAmount),
+ getBackgroundClass(),
{ 'font-mono tabular-nums': monospace },
className,
)}
diff --git a/resources/js/hooks/use-categorize-transactions.ts b/resources/js/hooks/use-categorize-transactions.ts
index 0ccfb4b9..b23a37e8 100644
--- a/resources/js/hooks/use-categorize-transactions.ts
+++ b/resources/js/hooks/use-categorize-transactions.ts
@@ -1,7 +1,4 @@
import type { AutomateCategorizationCandidate } from '@/components/automation-rules/automate-categorization-dialog';
-import { useEncryptionKey } from '@/contexts/encryption-key-context';
-import { decrypt, importKey } from '@/lib/crypto';
-import { getStoredKey } from '@/lib/key-storage';
import { captureEvent } from '@/lib/posthog';
import { transactionSyncService } from '@/services/transaction-sync';
import { type Account, type Bank } from '@/types/account';
@@ -80,8 +77,6 @@ export function useCategorizeTransactions({
banks,
transactions: initialTransactions,
}: UseCategorizeTransactionsOptions) {
- const { isKeySet } = useEncryptionKey();
-
const [uncategorizedTransactions, setUncategorizedTransactions] = useState<
DecryptedTransaction[]
>([]);
@@ -97,7 +92,6 @@ export function useCategorizeTransactions({
const [automateDialogOpen, setAutomateDialogOpen] = useState(false);
const [automateCandidate, setAutomateCandidate] =
useState
(null);
- const [, setEncryptionKey] = useState(null);
const [categorizedCount, setCategorizedCount] = useState(0);
const commandInputRef = useRef(null);
@@ -112,115 +106,42 @@ export function useCategorizeTransactions({
}, [isLoading, animationState, currentIndex]);
useEffect(() => {
- async function decryptTransactions() {
- setIsLoading(true);
- try {
- const accountsMap = new Map(
- accounts.map((account) => [account.id, account]),
- );
- const banksMap = new Map(banks.map((bank) => [bank.id, bank]));
+ setIsLoading(true);
+ try {
+ const accountsMap = new Map(
+ accounts.map((account) => [account.id, account]),
+ );
+ const banksMap = new Map(banks.map((bank) => [bank.id, bank]));
- const keyString = getStoredKey();
- let key: CryptoKey | null = null;
+ const processed = initialTransactions.map((transaction) => {
+ const account = accountsMap.get(transaction.account_id);
+ const bank = account?.bank?.id
+ ? banksMap.get(account.bank.id)
+ : undefined;
- if (keyString && isKeySet) {
- try {
- key = await importKey(keyString);
- setEncryptionKey(key);
- } catch (error) {
- console.error(
- 'Failed to import encryption key:',
- error,
- );
- }
- }
+ return {
+ ...transaction,
+ decryptedDescription: transaction.description,
+ decryptedNotes: transaction.notes || null,
+ account,
+ category: null,
+ bank,
+ } as DecryptedTransaction;
+ });
- const decrypted = await Promise.all(
- initialTransactions.map(async (transaction) => {
- try {
- let decryptedDescription = '';
- let decryptedNotes: string | null = null;
+ processed.sort((a, b) => {
+ const dateA = parseISO(a.transaction_date).getTime();
+ const dateB = parseISO(b.transaction_date).getTime();
+ return dateB - dateA;
+ });
- if (!transaction.description_iv) {
- decryptedDescription = transaction.description;
- decryptedNotes = transaction.notes || null;
- } else if (key) {
- try {
- decryptedDescription = await decrypt(
- transaction.description,
- key,
- transaction.description_iv,
- );
-
- if (
- transaction.notes &&
- transaction.notes_iv
- ) {
- decryptedNotes = await decrypt(
- transaction.notes,
- key,
- transaction.notes_iv,
- );
- }
- } catch (error) {
- console.error(
- 'Failed to decrypt transaction:',
- transaction.id,
- error,
- );
- }
- }
-
- const account = accountsMap.get(
- transaction.account_id,
- );
- const bank = account?.bank?.id
- ? banksMap.get(account.bank.id)
- : undefined;
-
- return {
- ...transaction,
- decryptedDescription,
- decryptedNotes,
- account,
- category: null,
- bank,
- } as DecryptedTransaction;
- } catch (error) {
- console.error(
- 'Failed to process transaction:',
- transaction.id,
- error,
- );
- return null;
- }
- }),
- );
-
- const validTransactions = decrypted.filter(
- (transaction): transaction is DecryptedTransaction =>
- transaction !== null,
- );
-
- validTransactions.sort((a, b) => {
- const dateA = parseISO(a.transaction_date).getTime();
- const dateB = parseISO(b.transaction_date).getTime();
- return dateB - dateA;
- });
-
- setUncategorizedTransactions(validTransactions);
- } catch (error) {
- console.error(
- 'Failed to load uncategorized transactions:',
- error,
- );
- } finally {
- setIsLoading(false);
- }
+ setUncategorizedTransactions(processed);
+ } catch (error) {
+ console.error('Failed to load uncategorized transactions:', error);
+ } finally {
+ setIsLoading(false);
}
-
- decryptTransactions();
- }, [initialTransactions, accounts, banks, isKeySet]);
+ }, [initialTransactions, accounts, banks]);
const currentTransaction = uncategorizedTransactions[currentIndex];
const remainingCount = uncategorizedTransactions.length - currentIndex;
diff --git a/resources/js/lib/crypto.ts b/resources/js/lib/crypto.ts
index bf49f618..6a120015 100644
--- a/resources/js/lib/crypto.ts
+++ b/resources/js/lib/crypto.ts
@@ -37,31 +37,6 @@ export async function getAESKeyFromPBKDF(
);
}
-export async function encrypt(
- plaintext: string,
- key: CryptoKey,
-): Promise<{ encrypted: string; iv: string }> {
- ensureCryptoAvailable();
- const encoder = new TextEncoder();
- const data = encoder.encode(plaintext);
-
- const iv = window.crypto.getRandomValues(new Uint8Array(12));
-
- const encryptedBuffer = await window.crypto.subtle.encrypt(
- {
- name: 'AES-GCM',
- iv,
- },
- key,
- data,
- );
-
- return {
- encrypted: bufferToBase64(encryptedBuffer),
- iv: bufferToBase64(iv),
- };
-}
-
export async function decrypt(
encrypted: string,
key: CryptoKey,
@@ -84,11 +59,6 @@ export async function decrypt(
return decoder.decode(decryptedBuffer);
}
-export function generateSalt(): Uint8Array {
- ensureCryptoAvailable();
- return window.crypto.getRandomValues(new Uint8Array(16));
-}
-
export function bufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
const bytes =
buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
diff --git a/resources/js/lib/key-storage.ts b/resources/js/lib/key-storage.ts
index f0f442d8..6686ae17 100644
--- a/resources/js/lib/key-storage.ts
+++ b/resources/js/lib/key-storage.ts
@@ -29,9 +29,3 @@ export function clearKey(): void {
sessionStorage.removeItem(ENCRYPTION_KEY_NAME);
localStorage.removeItem(ENCRYPTION_KEY_NAME);
}
-
-export function isKeyPersistent(): boolean {
- if (!isBrowser()) return false;
-
- return localStorage.getItem(ENCRYPTION_KEY_NAME) !== null;
-}
diff --git a/resources/js/pages/Accounts/Show.test.tsx b/resources/js/pages/Accounts/Show.test.tsx
index 98d4ba33..e1de798e 100644
--- a/resources/js/pages/Accounts/Show.test.tsx
+++ b/resources/js/pages/Accounts/Show.test.tsx
@@ -24,10 +24,6 @@ vi.mock('@/actions/App/Http/Controllers/RealEstateDetailController', () => ({
},
}));
-vi.mock('@/contexts/encryption-key-context', () => ({
- useEncryptionKey: () => ({ isKeySet: true }),
-}));
-
vi.mock('@/layouts/app/app-sidebar-layout', () => ({
default: ({ children }: { children: ReactNode }) => <>{children}>,
}));
diff --git a/resources/js/pages/Accounts/Show.tsx b/resources/js/pages/Accounts/Show.tsx
index 92646f91..5abebba9 100644
--- a/resources/js/pages/Accounts/Show.tsx
+++ b/resources/js/pages/Accounts/Show.tsx
@@ -40,7 +40,6 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
-import { useEncryptionKey } from '@/contexts/encryption-key-context';
import { useChartColors } from '@/hooks/use-chart-color-scheme';
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
import { BreadcrumbItem } from '@/types';
@@ -67,7 +66,6 @@ import { Head, router } from '@inertiajs/react';
import { ChevronDown, Pencil, Plus } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { Line, LineChart, ResponsiveContainer, Tooltip } from 'recharts';
-import { toast } from 'sonner';
interface AccountWithDetails extends Account {
real_estate_detail?: RealEstateDetail;
@@ -106,7 +104,6 @@ export default function AccountShow({
const [transactionRefreshKey, setTransactionRefreshKey] = useState(0);
const [chartComputedData, setChartComputedData] =
useState(null);
- const { isKeySet } = useEncryptionKey();
const handleChartDataLoaded = useCallback((data: ChartComputedData) => {
setChartComputedData(data);
@@ -117,13 +114,6 @@ export default function AccountShow({
}
function handleAddTransaction() {
- if (!isKeySet) {
- toast.error(
- __('Please unlock your encryption key to add transactions'),
- );
- return;
- }
-
setCreateTransactionOpen(true);
}
diff --git a/resources/js/pages/auth/setup-encryption.tsx b/resources/js/pages/auth/setup-encryption.tsx
deleted file mode 100644
index be742ec5..00000000
--- a/resources/js/pages/auth/setup-encryption.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import { __ } from '@/utils/i18n';
-import { Head, router } from '@inertiajs/react';
-import axios from 'axios';
-import { useState } from 'react';
-
-import InputError from '@/components/input-error';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { Spinner } from '@/components/ui/spinner';
-import { useEncryptionKey } from '@/contexts/encryption-key-context';
-import AuthLayout from '@/layouts/auth-layout';
-import {
- bufferToBase64,
- encrypt,
- exportKey,
- generateSalt,
- getAESKeyFromPBKDF,
- getKeyFromPassword,
-} from '@/lib/crypto';
-import { storeKey } from '@/lib/key-storage';
-import { dashboard } from '@/routes';
-
-function isCryptoAvailable(): boolean {
- if (typeof window === 'undefined') return true;
- return !!(window.crypto && window.crypto.subtle);
-}
-
-function getInitialErrors(): {
- password?: string;
- confirmPassword?: string;
- general?: string;
-} {
- if (typeof window === 'undefined') return {};
- if (!window.crypto || !window.crypto.subtle) {
- return {
- general:
- 'Web Crypto API is not available. Please ensure you are accessing this page via HTTPS or localhost.',
- };
- }
- return {};
-}
-
-export default function SetupEncryption() {
- const { refreshKeyState } = useEncryptionKey();
- const [password, setPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [storagePreference, setStoragePreference] = useState<
- 'session' | 'persistent'
- >('session');
- const [processing, setProcessing] = useState(false);
- const [cryptoAvailable] = useState(isCryptoAvailable);
- const [errors, setErrors] = useState<{
- password?: string;
- confirmPassword?: string;
- general?: string;
- }>(getInitialErrors);
-
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- setErrors({});
-
- if (password.length < 12) {
- setErrors({
- password: 'Encryption password must be at least 12 characters',
- });
- return;
- }
-
- if (password !== confirmPassword) {
- setErrors({
- confirmPassword: 'Passwords do not match',
- });
- return;
- }
-
- setProcessing(true);
-
- try {
- const salt = generateSalt();
-
- const pbkdfKey = await getKeyFromPassword(password);
-
- const aesKey = await getAESKeyFromPBKDF(pbkdfKey, salt);
-
- const { encrypted, iv } = await encrypt('Hello, world', aesKey);
-
- const exportedKey = await exportKey(aesKey);
-
- await axios.post('/api/encryption/setup', {
- salt: bufferToBase64(salt),
- encrypted_content: encrypted,
- iv: iv,
- });
-
- storeKey(exportedKey, storagePreference === 'persistent');
- refreshKeyState();
-
- router.visit(dashboard().url);
- } catch (error) {
- console.error('Encryption setup error:', error);
- setErrors({
- general:
- 'Failed to setup encryption. Please try again or contact support.',
- });
- setProcessing(false);
- }
- }
-
- return (
-
-
-
-
- );
-}
diff --git a/resources/js/pages/transactions/index.tsx b/resources/js/pages/transactions/index.tsx
index ddfed952..db04b364 100644
--- a/resources/js/pages/transactions/index.tsx
+++ b/resources/js/pages/transactions/index.tsx
@@ -1172,7 +1172,6 @@ export default function Transactions({
categories={categories}
labels={labels}
accounts={accounts}
- isKeySet={true}
enableSavedFilters={true}
actions={
diff --git a/resources/js/services/transaction-sync.ts b/resources/js/services/transaction-sync.ts
index 81c30865..3e395f29 100644
--- a/resources/js/services/transaction-sync.ts
+++ b/resources/js/services/transaction-sync.ts
@@ -1,6 +1,4 @@
-import { importKey } from '@/lib/crypto';
import { db } from '@/lib/dexie-db';
-import { getStoredKey } from '@/lib/key-storage';
import { TransactionSyncManager } from '@/lib/sync-manager';
import type { Transaction } from '@/types/transaction';
import type { UUID } from '@/types/uuid';
@@ -233,42 +231,14 @@ class TransactionSyncService {
return txDate >= minDate && txDate <= maxDate;
});
- const keyString = getStoredKey();
- const key = keyString ? await importKey(keyString) : null;
-
- const decryptedTransactions = await Promise.all(
- transactionsInRange.map(async (t) => {
- try {
- let decryptedDescription: string;
- if (t.description_iv && key) {
- const { decrypt } = await import('@/lib/crypto');
- decryptedDescription = await decrypt(
- t.description,
- key,
- t.description_iv,
- );
- } else if (t.description_iv && !key) {
- return null;
- } else {
- decryptedDescription = t.description;
- }
- return {
- transaction_date: normalizeDate(t.transaction_date),
- amount: parseFloat(t.amount),
- description: decryptedDescription
- .toLowerCase()
- .trim()
- .replace(/\s+/g, ' '),
- };
- } catch {
- return null;
- }
- }),
- );
-
- const validDecryptedTransactions = decryptedTransactions.filter(
- (t) => t !== null,
- );
+ const existingTransactions = transactionsInRange.map((t) => ({
+ transaction_date: normalizeDate(t.transaction_date),
+ amount: parseFloat(t.amount),
+ description: t.description
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, ' '),
+ }));
return transactions.map((importingTx) => {
const normalizedDescription = importingTx.description
@@ -276,7 +246,7 @@ class TransactionSyncService {
.trim()
.replace(/\s+/g, ' ');
- return validDecryptedTransactions.some(
+ return existingTransactions.some(
(existing) =>
existing.transaction_date ===
importingTx.transaction_date &&