@churchapps/apphelper-donations
Version:
Donation components for ChurchApps AppHelper
268 lines • 15.8 kB
JavaScript
"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