@churchapps/apphelper-donations
Version:
Donation components for ChurchApps AppHelper
222 lines • 14.9 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useState, useEffect } from "react";
import { useStripe } from "@stripe/react-stripe-js";
import { InputBox, ErrorMessages } from "@churchapps/apphelper";
import { FundDonations } from ".";
import { DonationPreviewModal } from "../modals/DonationPreviewModal";
import { ApiHelper, CurrencyHelper, DateHelper } from "@churchapps/helpers";
import { Locale, DonationHelper } from "../helpers";
import { Grid, InputLabel, MenuItem, Select, TextField, FormControl, Button, FormControlLabel, Checkbox, FormGroup, Typography } from "@mui/material";
export const DonationForm = ({ currency = "usd", ...props }) => {
const stripe = useStripe();
const [errorMessage, setErrorMessage] = useState();
const [fundDonations, setFundDonations] = useState();
const [funds, setFunds] = useState([]);
const [fundsTotal, setFundsTotal] = useState(0);
const [transactionFee, setTransactionFee] = useState(0);
const [payFee, setPayFee] = useState(0);
const [total, setTotal] = useState(0);
const [paymentMethodName, setPaymentMethodName] = useState(props?.paymentMethods?.length > 0 ? `${props.paymentMethods[0].name} ****${props.paymentMethods[0].last4}` : "");
const [donationType, setDonationType] = useState("once");
const [showDonationPreviewModal, setShowDonationPreviewModal] = useState(false);
const [interval, setInterval] = useState("one_month");
const [gateway, setGateway] = useState(null);
const [donation, setDonation] = useState({
id: props?.paymentMethods?.length > 0 ? props.paymentMethods[0].id : "",
type: props?.paymentMethods?.length > 0 ? props.paymentMethods[0].type : "",
customerId: props.customerId,
person: {
id: props.person?.id || "",
email: props.person?.contactInfo?.email || "",
name: props.person?.name?.display || ""
},
amount: 0,
billing_cycle_anchor: +new Date(),
interval: {
interval_count: 1,
interval: "month"
},
funds: []
});
const loadData = useCallback(() => {
ApiHelper.get("/funds", "GivingApi").then((data) => {
setFunds(data);
if (data.length)
setFundDonations([{ fundId: data[0].id }]);
});
ApiHelper.get(`/donate/gateways/${props?.church?.id || ""}`, "GivingApi").then((response) => {
const gateways = Array.isArray(response?.gateways) ? response.gateways : [];
const stripeGateway = gateways.find((g) => DonationHelper.isProvider(g.provider, "stripe"));
if (stripeGateway)
setGateway(stripeGateway);
});
}, []);
const handleSave = useCallback(() => {
if ((donation.amount ?? 0) < .5)
setErrorMessage(Locale.label("donation.donationForm.tooLow"));
else
setShowDonationPreviewModal(true);
}, [donation.amount]);
const handleKeyDown = useCallback((e) => { if (e.key === "Enter") {
e.preventDefault();
handleSave();
} }, [handleSave]);
const handleCheckChange = useCallback((_e, checked) => {
const d = { ...donation };
d.amount = checked ? fundsTotal + transactionFee : fundsTotal;
const showFee = checked ? transactionFee : 0;
setTotal(d.amount);
setPayFee(showFee);
setDonation(d);
}, [donation, fundsTotal, transactionFee]);
const handleChange = useCallback((e) => {
setErrorMessage(undefined);
const d = { ...donation };
const value = e.target.value;
switch (e.target.name) {
case "method":
d.id = value;
const pm = props.paymentMethods.find(pm => pm.id === value);
if (pm) {
d.type = pm.type;
setPaymentMethodName(`${pm.name} ****${pm.last4}`);
}
break;
case "type":
setDonationType(value);
break;
case "date":
d.billing_cycle_anchor = value ? +new Date(value) : +new Date();
break;
case "interval":
setInterval(value);
d.interval = DonationHelper.getInterval(value);
break;
case "notes":
d.notes = value;
break;
case "transaction-fee":
const element = e.target;
d.amount = element.checked ? fundsTotal + transactionFee : fundsTotal;
const showFee = element.checked ? transactionFee : 0;
setTotal(d.amount);
setPayFee(showFee);
}
setDonation(d);
}, [donation, props.paymentMethods, fundsTotal, transactionFee, gateway?.id]);
const handleCancel = useCallback(() => { setDonationType(undefined); }, []);
const handleDonationSelect = useCallback((type) => {
const dt = donationType === type ? undefined : type;
setDonationType(dt);
}, [donationType]);
const handleSingleDonationClick = useCallback(() => handleDonationSelect("once"), [handleDonationSelect]);
const handleRecurringDonationClick = useCallback(() => handleDonationSelect("recurring"), [handleDonationSelect]);
const makeDonation = useCallback(async (message) => {
let results;
const churchObj = {
name: props?.church?.name || "",
subDomain: props?.church?.subDomain || "",
churchURL: typeof window !== "undefined" ? window.location.origin : "",
logo: props?.churchLogo || ""
};
const selectedPaymentMethod = props.paymentMethods.find(pm => pm.id === donation.id);
const payload = {
...donation,
provider: selectedPaymentMethod?.provider || "stripe",
gatewayId: selectedPaymentMethod?.gatewayId || gateway?.id,
church: churchObj
};
if (donationType === "once")
results = await ApiHelper.post("/donate/charge", payload, "GivingApi");
if (donationType === "recurring")
results = await ApiHelper.post("/donate/subscribe", payload, "GivingApi");
// Handle 3D Secure authentication if required
const threeDSResult = await DonationHelper.handle3DSIfRequired(results, stripe);
if (threeDSResult.requiresAction) {
setShowDonationPreviewModal(false);
if (threeDSResult.success) {
setDonationType(undefined);
props.donationSuccess(message);
}
else {
setErrorMessage(Locale.label("donation.common.error") + ": " + threeDSResult.error);
}
return;
}
if (results?.status === "succeeded" || results?.status === "pending" || results?.status === "active" || results?.status === "processing") {
setShowDonationPreviewModal(false);
setDonationType(undefined);
props.donationSuccess(message);
}
else {
// Handle any error case
setShowDonationPreviewModal(false);
if (results?.raw?.message) {
setErrorMessage(Locale.label("donation.common.error") + ": " + results?.raw?.message);
}
else if (results?.error) {
setErrorMessage(Locale.label("donation.common.error") + ": " + results.error);
}
else {
setErrorMessage(Locale.label("donation.common.error") + ": An unexpected error occurred. Please try again.");
}
}
}, [
donation, donationType, gateway?.id, props.church?.name, props.church?.subDomain, props.churchLogo, props.donationSuccess, stripe, props.paymentMethods
]);
const handleFundDonationsChange = useCallback(async (fd) => {
setErrorMessage(undefined);
setFundDonations(fd);
let totalAmount = 0;
const selectedFunds = [];
for (const fundDonation of fd) {
totalAmount += fundDonation.amount || 0;
const fund = funds.find((fund) => fund.id === fundDonation.fundId);
if (fund) {
selectedFunds.push({ id: fundDonation.fundId, amount: fundDonation.amount || 0, name: fund.name });
}
}
const d = { ...donation };
d.amount = totalAmount;
d.funds = selectedFunds;
setFundsTotal(totalAmount);
const selectedPm = props.paymentMethods.find(pm => pm.id === d.id);
const fee = await getTransactionFee(totalAmount, (selectedPm?.gatewayId || gateway?.id), selectedPm?.provider || "stripe");
setTransactionFee(fee);
if (gateway && gateway.payFees === true) {
d.amount = totalAmount + fee;
setPayFee(fee);
}
setTotal(d.amount ?? 0);
setDonation(d);
}, [donation, funds, gateway]);
const getTransactionFee = useCallback(async (amount, activeGatewayId, provider = "stripe") => {
if (amount > 0) {
try {
const response = await ApiHelper.post("/donate/fee?churchId=" + (props?.church?.id || ""), { amount, provider, gatewayId: activeGatewayId }, "GivingApi");
return response.calculatedFee;
}
catch (error) {
console.log("Error calculating transaction fee: ", error);
return 0;
}
}
else {
return 0;
}
}, [props?.church?.id]);
useEffect(() => {
loadData();
}, [loadData, props.person?.id]);
useEffect(() => {
if (gateway?.id) {
// Gateway is stored at the component level, not in the donation object
setGateway(gateway);
}
}, [gateway?.id]);
if (!funds.length || !props?.paymentMethods?.length)
return null;
return (_jsxs(_Fragment, { children: [_jsx(DonationPreviewModal, { show: showDonationPreviewModal, onHide: () => setShowDonationPreviewModal(false), handleDonate: makeDonation, donation: donation, donationType: donationType || "", payFee: payFee, paymentMethodName: paymentMethodName, funds: funds }), _jsxs(InputBox, { id: "donation-form", "aria-label": "donation-box", headerIcon: "volunteer_activism", headerText: Locale.label("donation.donationForm.donate"), ariaLabelSave: "save-button", cancelFunction: donationType ? handleCancel : undefined, saveFunction: donationType ? handleSave : undefined, saveText: Locale.label("donation.donationForm.preview"), children: [_jsxs(Grid, { id: "donation-type-selector", container: true, spacing: 3, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(Button, { id: "single-donation-button", "aria-label": "single-donation", size: "small", fullWidth: true, style: { minHeight: "50px" }, variant: donationType === "once" ? "contained" : "outlined", onClick: handleSingleDonationClick, children: Locale.label("donation.donationForm.make") }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(Button, { id: "recurring-donation-button", "aria-label": "recurring-donation", size: "small", fullWidth: true, style: { minHeight: "50px" }, variant: donationType === "recurring" ? "contained" : "outlined", onClick: handleRecurringDonationClick, children: Locale.label("donation.donationForm.makeRecurring") }) })] }), donationType && (_jsxs("div", { id: "donation-details", style: { marginTop: "20px" }, children: [_jsx(Grid, { container: true, spacing: 3, children: _jsx(Grid, { size: { xs: 12 }, children: _jsxs(FormControl, { fullWidth: true, children: [_jsx(InputLabel, { children: Locale.label("donation.donationForm.method") }), _jsx(Select, { id: "payment-method-select", label: Locale.label("donation.donationForm.method"), name: "method", "aria-label": "method", value: donation.id, className: "capitalize", onChange: handleChange, children: props.paymentMethods.map((paymentMethod) => _jsxs(MenuItem, { value: paymentMethod.id, children: [paymentMethod.name, " ****", paymentMethod.last4] }, paymentMethod.id)) })] }) }) }), donationType === "recurring" && (_jsxs(Grid, { container: true, spacing: 3, style: { marginTop: 10 }, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { id: "start-date-field", fullWidth: true, name: "date", type: "date", "aria-label": "date", label: Locale.label("donation.donationForm.startDate"), value: DateHelper.formatHtml5Date(new Date(donation.billing_cycle_anchor || Date.now())), onChange: handleChange, onKeyDown: handleKeyDown }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsxs(FormControl, { fullWidth: true, children: [_jsx(InputLabel, { children: Locale.label("donation.donationForm.frequency") }), _jsxs(Select, { id: "frequency-select", label: Locale.label("donation.donationForm.frequency"), name: "interval", "aria-label": "interval", value: interval, onChange: handleChange, children: [_jsx(MenuItem, { value: "one_week", children: Locale.label("donation.donationForm.weekly") }), _jsx(MenuItem, { value: "two_week", children: Locale.label("donation.donationForm.biWeekly") }), _jsx(MenuItem, { value: "one_month", children: Locale.label("donation.donationForm.monthly") }), _jsx(MenuItem, { value: "three_month", children: Locale.label("donation.donationForm.quarterly") }), _jsx(MenuItem, { value: "one_year", children: Locale.label("donation.donationForm.annually") })] })] }) })] })), _jsxs("div", { id: "fund-selection", className: "form-group", children: [funds && fundDonations && (_jsxs(_Fragment, { children: [_jsx("h4", { children: Locale.label("donation.donationForm.fund") }), _jsx(FundDonations, { fundDonations: fundDonations, funds: funds, updatedFunction: handleFundDonationsChange })] })), fundsTotal > 0 && (_jsxs(_Fragment, { children: [(gateway && gateway.payFees === true) ? _jsxs(Typography, { fontSize: 14, fontStyle: "italic", children: ["*", Locale.label("donation.donationForm.fees").replace("{}", CurrencyHelper.formatCurrencyWithLocale(transactionFee, currency))] }) : (_jsx(FormGroup, { children: _jsx(FormControlLabel, { control: _jsx(Checkbox, {}), name: "transaction-fee", label: Locale.label("donation.donationForm.cover").replace("{}", CurrencyHelper.formatCurrencyWithLocale(transactionFee, currency)), onChange: handleCheckChange }) })), _jsxs("p", { children: [Locale.label("donation.donationForm.total"), ": ", CurrencyHelper.getCurrencySymbol(currency), total] })] })), _jsx(TextField, { id: "donation-notes", fullWidth: true, label: "Memo (optional)", multiline: true, "aria-label": "note", name: "notes", value: donation.notes || "", onChange: handleChange, onKeyDown: handleKeyDown })] }), errorMessage && _jsx(ErrorMessages, { errors: [errorMessage] })] }))] })] }));
};
//# sourceMappingURL=DonationForm.js.map