diff --git a/islands/PaymentMethodsList.tsx b/islands/PaymentMethodsList.tsx new file mode 100644 index 0000000..d355062 --- /dev/null +++ b/islands/PaymentMethodsList.tsx @@ -0,0 +1,300 @@ +// islands/PaymentMethodsList.tsx +import { PaymentMethod } from "../services/paymentService.ts"; +import { useEffect, useState } from "preact/hooks"; +import { PopAddPaymentMethodDialog } from "./dialogs/PopAddPaymentMethodDialog.tsx"; +import { + decryptDataWithStoredPassword, + getEncryptionPassword, +} from "../helpers.ts"; + +interface Props { + paymentMethods: PaymentMethod[]; + userId?: string; + openModal?: boolean; +} + +interface ProcessedPaymentMethod extends PaymentMethod { + maskedAccountNumber?: string; + maskedRoutingNumber?: string; +} + +export default function PaymentMethodsList( + { paymentMethods, userId, openModal = false }: Props, +) { + const [popAddPaymentMethod, popupAddPaymentMethodDialog] = + PopAddPaymentMethodDialog({ + userId, + defaultOpen: openModal, + }); + const [processedMethods, setProcessedMethods] = useState< + ProcessedPaymentMethod[] + >([]); + const [isProcessing, setIsProcessing] = useState(false); + const [isOpeningModal, setIsOpeningModal] = useState(openModal); + + // Process payment methods on component mount and when paymentMethods or userId change + useEffect(() => { + setTimeout(() => { + const lockRoute = localStorage.getItem("lock-route"); + const url = new URL(globalThis.location.href); + const currentRoute = url.pathname + url.search; + + if (lockRoute && lockRoute === currentRoute) { + localStorage.removeItem("lock-route"); + } + }, 100); + + if (!userId) { + setProcessedMethods(paymentMethods as ProcessedPaymentMethod[]); + return; + } + + const processMethods = async () => { + setIsProcessing(true); + const encryptionPassword = getEncryptionPassword(userId); + + if (!encryptionPassword) { + // No encryption key available, use methods as-is + setProcessedMethods(paymentMethods as ProcessedPaymentMethod[]); + setIsProcessing(false); + return; + } + + const updatedMethods: ProcessedPaymentMethod[] = []; + + for (const method of paymentMethods) { + const processedMethod = { ...method }; + + // Decrypt and mask account number + if (method.account_number_encrypted) { + try { + const decryptedData = await decryptDataWithStoredPassword( + userId, + method.account_number_encrypted, + ); + if (decryptedData) { + const accountNumber = decryptedData; + // Show only last 4 digits + processedMethod.maskedAccountNumber = + "****" + accountNumber.slice(-4); + } + } catch (error) { + console.error("Failed to decrypt account number:", error); + } + } + + // Decrypt and mask routing number + if (method.routing_number_encrypted) { + try { + const decryptedData = await decryptDataWithStoredPassword( + userId, + method.routing_number_encrypted, + ); + if (decryptedData) { + const routingNumber = decryptedData; + // Show only last 4 digits + processedMethod.maskedRoutingNumber = + "****" + routingNumber.slice(-4); + } + } catch (error) { + console.error("Failed to decrypt routing number:", error); + } + } + + updatedMethods.push(processedMethod); + } + + setProcessedMethods(updatedMethods); + setIsProcessing(false); + }; + + processMethods(); + }, [paymentMethods, userId]); + + const handleSetDefault = async (methodId: string) => { + try { + const response = await fetch("/api/update/payment_method", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: methodId, is_default: true }), + }); + + const data = await response.json(); + + if (data.success) { + // Refresh the page to show updated default + window.location.reload(); + } else { + console.error("Failed to set default payment method:", data.error); + alert("Failed to set default payment method"); + } + } catch (error) { + console.error("Error setting default payment method:", error); + alert("Failed to set default payment method"); + } + }; + + if (isProcessing) { + return ( +
+ Add a payment method to get started with rent payments. +
+ +