UNPKG

@churchapps/apphelper-donations

Version:

Donation components for ChurchApps AppHelper

309 lines 18.2 kB
"use client"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useRef, useEffect } from "react"; import ReCAPTCHA from "react-google-recaptcha"; import { ErrorMessages, InputBox } from "@churchapps/apphelper"; import { FundDonations } from "."; // PayPal Hosted Fields for secure card entry import { PayPalHostedFields } from "./PayPalHostedFields"; import { ApiHelper, DateHelper, CurrencyHelper } from "@churchapps/helpers"; import { Locale, DonationHelper } from "../helpers"; import { Grid, Alert, TextField, Button, FormControl, InputLabel, Select, MenuItem, FormGroup, FormControlLabel, Checkbox, Typography } from "@mui/material"; export const PayPalNonAuthDonationInner = ({ mainContainerCssProps, showHeader = true, ...props }) => { const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [email, setEmail] = useState(""); const [fundsTotal, setFundsTotal] = useState(0); const [transactionFee, setTransactionFee] = useState(0); const [total, setTotal] = useState(0); const [errors, setErrors] = useState([]); const [fundDonations, setFundDonations] = useState([]); const [funds, setFunds] = useState([]); const [donationComplete, setDonationComplete] = useState(false); const [processing, setProcessing] = useState(false); const [donationType, setDonationType] = useState("once"); const [interval, setInterval] = useState("one_month"); const [startDate, setStartDate] = useState(new Date().toDateString()); const [_captchaResponse, setCaptchaResponse] = useState(""); // Keep church for potential future metadata usage const [_church, _setChurch] = useState(); const [gateway, setGateway] = useState(null); const [searchParams, setSearchParams] = useState(null); const [notes, setNotes] = useState(""); const [coverFees, setCoverFees] = useState(false); const hostedFieldsRef = useRef(null); const [hostedValid, setHostedValid] = useState(false); const [useHostedFields, setUseHostedFields] = useState(true); const captchaRef = useRef(null); const getUrlParam = (param) => { if (typeof window === "undefined") return null; const urlParams = new URLSearchParams(window.location.search); return urlParams.get(param); }; const init = () => { const fundId = getUrlParam("fundId"); const amount = getUrlParam("amount"); setSearchParams({ fundId, amount }); ApiHelper.get("/funds/churchId/" + props.churchId, "GivingApi").then((data) => { setFunds(data); if (fundId && fundId !== "") { const selectedFund = data.find((f) => f.id === fundId); if (selectedFund) { setFundDonations([{ fundId: selectedFund.id, amount: (amount && amount !== "") ? parseFloat(amount) : 0 }]); } } else if (data.length) { setFundDonations([{ fundId: data[0].id }]); } }); ApiHelper.get("/churches/" + props.churchId, "MembershipApi").then((_data) => { _setChurch(_data); }); ApiHelper.get(`/donate/gateways/${props.churchId}`, "GivingApi").then((response) => { const gateways = Array.isArray(response?.gateways) ? response.gateways : []; const paypalGateway = DonationHelper.findGatewayByProvider(gateways, "paypal"); if (paypalGateway) setGateway(paypalGateway); }); }; const handleCaptchaChange = (value) => { if (value) { ApiHelper.postAnonymous("/donate/captcha-verify", { token: value }, "GivingApi") .then((data) => { // Check for various success indicators if (data.response === "success" || data.response === "human" || data.success === true || data.score >= 0.5) { setCaptchaResponse("success"); } else { setCaptchaResponse(data.response || "robot"); } }) .catch((error) => { console.error("Error verifying captcha:", error); setCaptchaResponse("error"); }); } else { setCaptchaResponse(""); } }; const handleCheckChange = (_e, checked) => { setCoverFees(checked); const totalPayAmount = checked ? fundsTotal + transactionFee : fundsTotal; setTotal(totalPayAmount); }; const handleSave = async () => { if (validate()) { // CAPTCHA TEMPORARILY DISABLED - Remove this bypass in production setProcessing(true); ApiHelper.post("/users/loadOrCreate", { userEmail: email, firstName, lastName }, "MembershipApi") .catch((ex) => { setErrors([ex.toString()]); setProcessing(false); }) .then(async (userData) => { const personData = { churchId: props.churchId, firstName, lastName, email }; const person = await ApiHelper.post("/people/loadOrCreate", personData, "MembershipApi"); await savePayPalDonation(userData, person); }); } }; const savePayPalDonation = async (_user, person) => { // Try Hosted Fields first if client ID provided let hostedOrderId; if (props.paypalClientId && useHostedFields) { try { const payload = await hostedFieldsRef.current?.submit(); hostedOrderId = payload?.orderId || payload?.id; } catch (e) { console.warn("PayPal Hosted Fields submit failed or not ready. Falling back to manual card.", e); } } if (!hostedOrderId) { setErrors(["PayPal card fields are unavailable. Ensure HTTPS and that PayPal Hosted Fields are enabled."]); setProcessing(false); return; } const donation = { id: "", // PayPal will generate this customerId: "", // Will be set by backend type: "paypal", provider: "paypal", gatewayId: gateway?.id, churchId: props.churchId, amount: total, funds: [], person: { id: person?.id || "", email: person?.contactInfo?.email || "", name: person?.name?.display || "" }, notes: notes }; // Attach hosted order id when available for backend capture if (hostedOrderId) donation.paypalOrderId = hostedOrderId; if (donationType === "recurring") { donation.billing_cycle_anchor = startDate ? +new Date(startDate) : +new Date(); donation.interval = DonationHelper.getInterval(interval); } for (const fundDonation of fundDonations) { if (donation.funds) { donation.funds.push({ id: fundDonation.fundId || "", amount: fundDonation.amount || 0 }); } } // Church object is no longer required for unified PayPal capture. // Capture via existing /donate/charge endpoint (PayPal) const compactFunds = (donation.funds || []).map(f => ({ id: f.id, amount: f.amount })); const results = await ApiHelper.post("/donate/charge", { provider: "paypal", gatewayId: gateway?.id, id: hostedOrderId, churchId: props.churchId, amount: total, funds: compactFunds, person: donation.person, notes }, "GivingApi"); if (results?.status === "COMPLETED" || results?.status === "APPROVED" || results?.status === "CREATED") { setDonationComplete(true); } if (results?.message || results?.error) { setErrors([results?.message || results?.error || "Payment processing failed"]); setProcessing(false); } setProcessing(false); }; const validate = () => { const result = []; if (!firstName) result.push(Locale.label("donation.donationForm.validate.firstName")); if (!lastName) result.push(Locale.label("donation.donationForm.validate.lastName")); if (!email) result.push(Locale.label("donation.donationForm.validate.email")); if (fundsTotal === 0) result.push(Locale.label("donation.donationForm.validate.amount")); if (props.paypalClientId && useHostedFields) { if (!hostedValid) result.push("Please provide valid card information"); } else result.push("PayPal Hosted Fields not available"); if (result.length === 0) { if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) result.push(Locale.label("donation.donationForm.validate.validEmail")); } setErrors(result); return result.length === 0; }; const handleChange = (e) => { const val = e.currentTarget.value; switch (e.currentTarget.name) { case "firstName": setFirstName(val); break; case "lastName": setLastName(val); break; case "email": setEmail(val); break; case "startDate": setStartDate(val); break; case "interval": setInterval(val); break; case "notes": setNotes(val); break; } }; const handleFundDonationsChange = async (fd) => { 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); selectedFunds.push({ id: fundDonation.fundId, amount: fundDonation.amount || 0, name: fund?.name || "" }); } setFundsTotal(totalAmount); const fee = await getTransactionFee(totalAmount); setTransactionFee(fee); if (gateway?.payFees === true) { setTotal(totalAmount + fee); } else { // If the checkbox is checked, include the fee in the total setTotal(coverFees ? totalAmount + fee : totalAmount); } }; const getTransactionFee = async (amount) => { if (amount > 0) { try { const response = await ApiHelper.post("/donate/fee?churchId=" + props.churchId, { amount, provider: "paypal", gatewayId: gateway?.id }, "GivingApi"); return response.calculatedFee; } catch (error) { console.log("Error calculating transaction fee: ", error); return 0; } } else { return 0; } }; const getFundList = () => { if (funds) { return (_jsxs(_Fragment, { children: [_jsx("hr", {}), _jsx("h4", { children: Locale.label("donation.donationForm.funds") }), _jsx(FundDonations, { fundDonations: fundDonations, funds: funds, params: searchParams, updatedFunction: handleFundDonationsChange })] })); } }; useEffect(init, []); if (donationComplete) return _jsx(Alert, { severity: "success", children: Locale.label("donation.donationForm.thankYou") }); else { return (_jsxs(InputBox, { headerIcon: showHeader ? "volunteer_activism" : "", headerText: showHeader ? "Donate with PayPal" : "", saveFunction: handleSave, saveText: "Donate", isSubmitting: processing, mainContainerCssProps: mainContainerCssProps, children: [_jsx(ErrorMessages, { errors: errors }), _jsxs(Grid, { container: true, spacing: 3, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(Button, { "aria-label": "single-donation", size: "small", fullWidth: true, style: { minHeight: "50px" }, variant: donationType === "once" ? "contained" : "outlined", onClick: () => setDonationType("once"), children: Locale.label("donation.donationForm.make") }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(Button, { "aria-label": "recurring-donation", size: "small", fullWidth: true, style: { minHeight: "50px" }, variant: donationType === "recurring" ? "contained" : "outlined", onClick: () => setDonationType("recurring"), children: Locale.label("donation.donationForm.makeRecurring") }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { fullWidth: true, label: Locale.label("person.firstName"), name: "firstName", value: firstName, onChange: handleChange }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { fullWidth: true, label: Locale.label("person.lastName"), name: "lastName", value: lastName, onChange: handleChange }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { fullWidth: true, label: Locale.label("person.email"), name: "email", value: email, onChange: handleChange }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(ReCAPTCHA, { sitekey: props.recaptchaSiteKey, ref: captchaRef, onChange: handleCaptchaChange, onExpired: () => { console.log("Captcha expired"); setCaptchaResponse(""); }, onErrored: () => { console.log("Captcha error"); setCaptchaResponse("error"); } }) })] }), props.paypalClientId && useHostedFields ? (_jsx(PayPalHostedFields, { ref: hostedFieldsRef, clientId: props.paypalClientId, getClientToken: async () => { try { const resp = await ApiHelper.post("/donate/client-token", { churchId: props.churchId, provider: "paypal", gatewayId: gateway?.id }, "GivingApi"); const token = resp?.clientToken || resp?.token || resp?.result || resp; return typeof token === "string" && token.length > 0 ? token : ""; } catch { return ""; } }, onValidityChange: setHostedValid, onIneligible: () => setUseHostedFields(false), createOrder: async () => { // Create order on backend if supported; fallback to simple legacy flow try { const fundsPayload = (fundDonations || []) .filter(fd => (fd.amount || 0) > 0 && fd.fundId) .map(fd => ({ id: fd.fundId, amount: fd.amount || 0 })); const response = await ApiHelper.post("/donate/create-order", { churchId: props.churchId, provider: "paypal", gatewayId: gateway?.id, amount: total, currency: "USD", funds: fundsPayload, notes }, "GivingApi"); return response?.id || response?.orderId || ""; } catch (e) { console.warn("Create PayPal order failed; Hosted Fields may not be enabled on backend.", e); return ""; } } })) : (_jsx(Alert, { severity: "error", sx: { mb: 1 }, children: "PayPal card fields are unavailable. This requires HTTPS and an enabled PayPal merchant." })), donationType === "recurring" && _jsxs(Grid, { container: true, spacing: 3, style: { marginTop: 0 }, children: [_jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsxs(FormControl, { fullWidth: true, children: [_jsx(InputLabel, { children: Locale.label("donation.donationForm.frequency") }), _jsxs(Select, { label: "Frequency", name: "interval", "aria-label": "interval", value: interval, onChange: (e) => { setInterval(e.target.value); }, 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") })] })] }) }), _jsx(Grid, { size: { xs: 12, md: 6 }, children: _jsx(TextField, { fullWidth: true, name: "startDate", type: "date", "aria-label": "startDate", label: Locale.label("donation.donationForm.startDate"), value: DateHelper.formatHtml5Date(startDate ? new Date(startDate) : new Date()), onChange: handleChange }) })] }), getFundList(), _jsx(TextField, { fullWidth: true, label: "Memo (optional)", multiline: true, "aria-label": "note", name: "notes", value: notes, onChange: handleChange, style: { marginTop: 10, marginBottom: 10 } }), _jsx("div", { children: fundsTotal > 0 && _jsxs(_Fragment, { children: [(gateway?.payFees === true) ? _jsxs(Typography, { fontSize: 14, fontStyle: "italic", children: ["*", Locale.label("donation.donationForm.fees").replace("{}", CurrencyHelper.formatCurrency(transactionFee))] }) : (_jsx(FormGroup, { children: _jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: coverFees }), name: "transaction-fee", label: Locale.label("donation.donationForm.cover").replace("{}", CurrencyHelper.formatCurrency(transactionFee)), onChange: handleCheckChange }) })), _jsxs("p", { children: ["Total Donation Amount: $", total] })] }) })] })); } }; //# sourceMappingURL=PayPalNonAuthDonationInner.js.map