UNPKG

@churchapps/apphelper-donations

Version:

Donation components for ChurchApps AppHelper

268 lines 15.8 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useState } from "react"; import { FormControl, Grid, InputLabel, MenuItem, Select, TextField, Button, CircularProgress, Box, Typography } from "@mui/material"; import { useStripe } from "@stripe/react-stripe-js"; import { InputBox, ErrorMessages } from "@churchapps/apphelper"; import { ApiHelper } from "@churchapps/helpers"; import { Locale } from "../helpers"; export const BankForm = (props) => { const stripe = useStripe(); const [bankAccount, setBankAccount] = useState({ account_holder_name: props.bank.account_holder_name, account_holder_type: props.bank.account_holder_type, country: "US", currency: "usd" }); const [paymentMethod] = useState({ customerId: props.customerId, personId: props.person.id, email: props.person.contactInfo.email, name: props.person.name.display, provider: props.bank.provider || "stripe", gatewayId: props.bank.gatewayId || props.gateway?.id }); const [updateBankData] = useState({ paymentMethodId: props.bank.id, customerId: props.customerId, personId: props.person.id, bankData: { account_holder_name: props.bank.account_holder_name || "", account_holder_type: props.bank.account_holder_type || "individual" } }); const [verifyBankData, setVerifyBankData] = useState({ paymentMethodId: props.bank.id, customerId: props.customerId, amountData: { amounts: [] } }); const [showSave, setShowSave] = useState(true); const [errorMessage, setErrorMessage] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const handleCancel = () => { props.setMode("display"); }; const handleDelete = () => { props.deletePayment(); }; const handleSave = () => { setShowSave(false); if (props.showVerifyForm) verifyBank(); else props.bank.id ? updateBank() : createBank(); }; // New method using Financial Connections (recommended) const createBankWithFinancialConnections = async () => { if (!stripe) { setErrorMessage("Stripe is not available"); setShowSave(true); return; } setIsConnecting(true); setErrorMessage(null); try { // Step 1: Create ACH SetupIntent on the backend const setupIntentResponse = await ApiHelper.post("/paymentmethods/ach-setup-intent", { personId: props.person.id, customerId: props.customerId, email: props.person.contactInfo.email, name: props.person.name.display, gatewayId: props.gateway?.id }, "GivingApi"); if (setupIntentResponse?.error) { setErrorMessage(setupIntentResponse.error); setIsConnecting(false); setShowSave(true); return; } const { clientSecret } = setupIntentResponse; // Step 2: Collect bank account using Financial Connections const { error: collectError, setupIntent: collectedSetupIntent } = await stripe.collectBankAccountForSetup({ clientSecret, params: { payment_method_type: "us_bank_account", payment_method_data: { billing_details: { name: bankAccount.account_holder_name || props.person.name.display, email: props.person.contactInfo.email } } } }); if (collectError) { setErrorMessage(collectError.message || "Failed to connect bank account"); setIsConnecting(false); setShowSave(true); return; } // Check if user closed the modal without completing if (!collectedSetupIntent?.payment_method) { setErrorMessage("Bank account connection was not completed. Please try again."); setIsConnecting(false); setShowSave(true); return; } // Step 3: Confirm the SetupIntent to complete bank account attachment const { error: confirmError, setupIntent } = await stripe.confirmUsBankAccountSetup(clientSecret); if (confirmError) { setErrorMessage(confirmError.message || "Failed to confirm bank account"); setIsConnecting(false); setShowSave(true); return; } if (setupIntent?.status === "succeeded") { props.updateList(Locale.label("donation.bankForm.added")); props.setMode("display"); } else if (setupIntent?.status === "requires_action" || setupIntent?.next_action?.type === "verify_with_microdeposits") { // Bank requires micro-deposit verification (for some banks that don't support instant verification) props.updateList("Bank account added. Please check your bank statement for micro-deposits to verify."); props.setMode("display"); } else { setErrorMessage("Unexpected status: " + setupIntent?.status); } } catch (error) { setErrorMessage(error.message || "Error connecting bank account"); console.error(error); } setIsConnecting(false); setShowSave(true); }; // Legacy method using bank tokens (deprecated - kept for backward compatibility) const createBankLegacy = async () => { if (!stripe) { setErrorMessage("Stripe is not available"); setShowSave(true); return; } if (!bankAccount.routing_number || !bankAccount.account_number) { setErrorMessage(Locale.label("donation.bankForm.validate.accountNumber")); } else { try { const response = await stripe.createToken("bank_account", bankAccount); if (response?.error?.message) { setErrorMessage(response.error.message); } else if (response?.token?.id) { const pm = { ...paymentMethod, id: response.token.id, provider: paymentMethod.provider || "stripe", gatewayId: paymentMethod.gatewayId || props.gateway?.id }; const result = await ApiHelper.post("/paymentmethods/addbankaccount", pm, "GivingApi"); if (result?.raw?.message) { setErrorMessage(result.raw.message); } else { props.updateList(Locale.label("donation.bankForm.added")); props.setMode("display"); } } else { setErrorMessage("Failed to create token"); } } catch (error) { setErrorMessage("Error creating bank token"); console.error(error); } } setShowSave(true); }; // Wrapper function that chooses the appropriate method const createBank = async () => { // Default to Financial Connections (new flow), fall back to legacy if explicitly disabled if (props.useFinancialConnections !== false) { await createBankWithFinancialConnections(); } else { await createBankLegacy(); } }; const updateBank = async () => { if (!bankAccount.account_holder_name || bankAccount.account_holder_name === "") { setErrorMessage(Locale.label("donation.bankForm.validate.holderName")); } else { try { const bank = { ...updateBankData }; bank.bankData.account_holder_name = bankAccount.account_holder_name; bank.bankData.account_holder_type = bankAccount.account_holder_type; const response = await ApiHelper.post("/paymentmethods/updatebank", { ...bank, gatewayId: props.bank.gatewayId || props.gateway?.id, provider: props.bank.provider || "stripe" }, "GivingApi"); if (response?.raw?.message) { setErrorMessage(response.raw.message); } else { props.updateList(Locale.label("donation.bankForm.updated")); props.setMode("display"); } } catch (error) { setErrorMessage("Error updating bank account"); console.error(error); } } setShowSave(true); }; const verifyBank = async () => { const amounts = verifyBankData?.amountData?.amounts; if (amounts && amounts.length === 2 && amounts[0] !== "" && amounts[1] !== "") { try { const response = await ApiHelper.post("/paymentmethods/verifyBank", { ...verifyBankData, gatewayId: props.bank.gatewayId || props.gateway?.id, provider: props.bank.provider || "stripe" }, "GivingApi"); if (response?.raw?.message) { setErrorMessage(response.raw.message); } else { props.updateList(Locale.label("donation.bankForm.verified")); props.setMode("display"); } } catch (error) { setErrorMessage("Error verifying bank account"); console.error(error); } } else { setErrorMessage("Both deposit amounts are required."); } setShowSave(true); }; const getHeaderText = () => props.bank.id ? `${props.bank.name?.toUpperCase() || "BANK"} ****${props.bank.last4 || ""}` : "Add New Bank Account"; const handleChange = (e) => { const bankData = { ...bankAccount }; const inputData = { [e.target.name]: e.target.value }; setBankAccount({ ...bankData, ...inputData }); setShowSave(true); }; const handleKeyPress = (e) => { const pattern = /^\d+$/; if (!pattern.test(e.key)) e.preventDefault(); }; const handleVerify = (e) => { const verifyData = { ...verifyBankData }; if (e.currentTarget.name === "amount1") verifyData.amountData.amounts[0] = e.currentTarget.value; if (e.currentTarget.name === "amount2") verifyData.amountData.amounts[1] = e.currentTarget.value; setVerifyBankData(verifyData); }; const getForm = () => { if (props.showVerifyForm) { return (_jsxs(_Fragment, { children: [_jsx("p", { children: Locale.label("donation.bankForm.twoDeposits") }), _jsxs(Grid, { container: true, spacing: 2, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { fullWidth: true, "aria-label": "amount1", label: Locale.label("donation.bankForm.firstDeposit"), name: "amount1", placeholder: "00", inputProps: { maxLength: 2 }, onChange: handleVerify, onKeyPress: handleKeyPress }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { fullWidth: true, "aria-label": "amount2", label: Locale.label("donation.bankForm.secondDeposit"), name: "amount2", placeholder: "00", inputProps: { maxLength: 2 }, onChange: handleVerify, onKeyPress: handleKeyPress }) })] })] })); } else if (!props.bank.id && props.useFinancialConnections !== false) { // New Financial Connections flow for adding bank accounts return (_jsxs(Box, { sx: { textAlign: "center", py: 2 }, children: [_jsx(Typography, { variant: "body1", sx: { mb: 2 }, children: "Securely connect your bank account using Stripe Financial Connections." }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 3 }, children: "You'll be redirected to log in to your bank and authorize the connection. Your bank credentials are never shared with us." }), isConnecting ? (_jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "center", gap: 2 }, children: [_jsx(CircularProgress, { size: 24 }), _jsx(Typography, { children: "Connecting to your bank..." })] })) : (_jsx(Button, { variant: "contained", color: "primary", onClick: createBankWithFinancialConnections, disabled: !stripe, sx: { minWidth: 200 }, children: "Connect Bank Account" }))] })); } else { // Editing existing bank account or legacy manual entry flow let accountDetails = _jsx(_Fragment, {}); if (!props.bank.id && props.useFinancialConnections === false) { // Legacy manual entry (only if Financial Connections is explicitly disabled) accountDetails = (_jsxs(Grid, { container: true, spacing: 3, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, style: { marginBottom: "20px" }, children: _jsx(TextField, { fullWidth: true, label: Locale.label("donation.bankForm.routingNumber"), type: "number", name: "routing_number", "aria-label": "routing-number", placeholder: "Routing Number", className: "form-control", onChange: handleChange }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, style: { marginBottom: "20px" }, children: _jsx(TextField, { fullWidth: true, label: Locale.label("donation.bankForm.accountNumber"), type: "number", name: "account_number", "aria-label": "account-number", placeholder: "Account Number", className: "form-control", onChange: handleChange }) })] })); } return (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: 3, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, style: { marginBottom: "20px" }, children: _jsx(TextField, { fullWidth: true, label: "Account Holder Name", name: "account_holder_name", required: true, "aria-label": "account-holder-name", placeholder: "Account Holder Name", value: bankAccount.account_holder_name || "", className: "form-control", onChange: handleChange }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, style: { marginBottom: "20px" }, children: _jsxs(FormControl, { fullWidth: true, children: [_jsx(InputLabel, { children: Locale.label("donation.bankForm.name") }), _jsxs(Select, { label: Locale.label("donation.bankForm.name"), name: "account_holder_type", "aria-label": "account-holder-type", value: bankAccount.account_holder_type || "", onChange: handleChange, children: [_jsx(MenuItem, { value: "individual", children: Locale.label("donation.bankForm.individual") }), _jsx(MenuItem, { value: "company", children: Locale.label("donation.bankForm.company") })] })] }) })] }), accountDetails] })); } }; // Determine if we should show the save button // Hide it when using Financial Connections for new accounts (it has its own button) const showSaveButton = props.bank.id || props.showVerifyForm || props.useFinancialConnections === false; return (_jsxs(InputBox, { headerIcon: "volunteer_activism", headerText: getHeaderText(), ariaLabelSave: "save-button", ariaLabelDelete: "delete-button", cancelFunction: handleCancel, saveFunction: showSaveButton && showSave ? handleSave : undefined, deleteFunction: props.bank.id && !props.showVerifyForm ? handleDelete : undefined, children: [errorMessage && _jsx(ErrorMessages, { errors: [errorMessage] }), _jsxs("div", { children: [!props.bank.id && props.useFinancialConnections === false && _jsx("p", { children: Locale.label("donation.bankForm.needVerified") }), getForm()] })] })); }; //# sourceMappingURL=BankForm.js.map