@churchapps/apphelper 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/components/ImageEditor.d.ts +0 -1
  2. package/dist/components/ImageEditor.d.ts.map +1 -1
  3. package/dist/components/ImageEditor.js +1 -1
  4. package/dist/components/ImageEditor.js.map +1 -1
  5. package/dist/donationComponents/components/BankForm.d.ts +14 -0
  6. package/dist/donationComponents/components/BankForm.d.ts.map +1 -0
  7. package/dist/donationComponents/components/BankForm.js +126 -0
  8. package/dist/donationComponents/components/BankForm.js.map +1 -0
  9. package/dist/donationComponents/components/CardForm.d.ts +13 -0
  10. package/dist/donationComponents/components/CardForm.d.ts.map +1 -0
  11. package/dist/donationComponents/components/CardForm.js +122 -0
  12. package/dist/donationComponents/components/CardForm.js.map +1 -0
  13. package/dist/donationComponents/components/DonationForm.d.ts +15 -0
  14. package/dist/donationComponents/components/DonationForm.d.ts.map +1 -0
  15. package/dist/donationComponents/components/DonationForm.js +199 -0
  16. package/dist/donationComponents/components/DonationForm.js.map +1 -0
  17. package/dist/donationComponents/components/FundDonation.d.ts +12 -0
  18. package/dist/donationComponents/components/FundDonation.d.ts.map +1 -0
  19. package/dist/donationComponents/components/FundDonation.js +32 -0
  20. package/dist/donationComponents/components/FundDonation.js.map +1 -0
  21. package/dist/donationComponents/components/FundDonations.d.ts +11 -0
  22. package/dist/donationComponents/components/FundDonations.d.ts.map +1 -0
  23. package/dist/donationComponents/components/FundDonations.js +33 -0
  24. package/dist/donationComponents/components/FundDonations.js.map +1 -0
  25. package/dist/donationComponents/components/PaymentMethods.d.ts +14 -0
  26. package/dist/donationComponents/components/PaymentMethods.d.ts.map +1 -0
  27. package/dist/donationComponents/components/PaymentMethods.js +84 -0
  28. package/dist/donationComponents/components/PaymentMethods.js.map +1 -0
  29. package/dist/donationComponents/components/RecurringDonations.d.ts +10 -0
  30. package/dist/donationComponents/components/RecurringDonations.d.ts.map +1 -0
  31. package/dist/donationComponents/components/RecurringDonations.js +93 -0
  32. package/dist/donationComponents/components/RecurringDonations.js.map +1 -0
  33. package/dist/donationComponents/components/RecurringDonationsEdit.d.ts +11 -0
  34. package/dist/donationComponents/components/RecurringDonationsEdit.d.ts.map +1 -0
  35. package/dist/donationComponents/components/RecurringDonationsEdit.js +66 -0
  36. package/dist/donationComponents/components/RecurringDonationsEdit.js.map +1 -0
  37. package/dist/donationComponents/components/index.d.ts +9 -0
  38. package/dist/donationComponents/components/index.d.ts.map +1 -0
  39. package/dist/donationComponents/components/index.js +20 -0
  40. package/dist/donationComponents/components/index.js.map +1 -0
  41. package/dist/donationComponents/index.d.ts +3 -0
  42. package/dist/donationComponents/index.d.ts.map +1 -0
  43. package/dist/donationComponents/index.js +21 -0
  44. package/dist/donationComponents/index.js.map +1 -0
  45. package/dist/donationComponents/modals/DonationPreviewModal.d.ts +15 -0
  46. package/dist/donationComponents/modals/DonationPreviewModal.d.ts.map +1 -0
  47. package/dist/donationComponents/modals/DonationPreviewModal.js +33 -0
  48. package/dist/donationComponents/modals/DonationPreviewModal.js.map +1 -0
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/components/ImageEditor.tsx +1 -1
  55. package/src/donationComponents/components/BankForm.tsx +163 -0
  56. package/src/donationComponents/components/CardForm.tsx +104 -0
  57. package/src/donationComponents/components/DonationForm.tsx +260 -0
  58. package/src/donationComponents/components/FundDonation.tsx +59 -0
  59. package/src/donationComponents/components/FundDonations.tsx +44 -0
  60. package/src/donationComponents/components/PaymentMethods.tsx +133 -0
  61. package/src/donationComponents/components/RecurringDonations.tsx +117 -0
  62. package/src/donationComponents/components/RecurringDonationsEdit.tsx +96 -0
  63. package/src/donationComponents/components/index.tsx +8 -0
  64. package/src/donationComponents/index.ts +2 -0
  65. package/src/donationComponents/modals/DonationPreviewModal.tsx +70 -0
  66. package/src/index.ts +1 -0
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export * from "./components";
3
3
  export * from "@churchapps/helpers";
4
4
  export * from "./pageComponents";
5
5
  export * from "./hooks";
6
+ export * from "./donationComponents";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -19,4 +19,5 @@ __exportStar(require("./components"), exports);
19
19
  __exportStar(require("@churchapps/helpers"), exports);
20
20
  __exportStar(require("./pageComponents"), exports);
21
21
  __exportStar(require("./hooks"), exports);
22
+ __exportStar(require("./donationComponents"), exports);
22
23
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4CAA0B;AAC1B,+CAA6B;AAC7B,sDAAoC;AACpC,mDAAiC;AACjC,0CAAwB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4CAA0B;AAC1B,+CAA6B;AAC7B,sDAAoC;AACpC,mDAAiC;AACjC,0CAAwB;AACxB,uDAAqC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@churchapps/apphelper",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Library of helper functions for React and NextJS ChurchApps",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { useState, useEffect, useRef } from "react";
4
4
  import Cropper from "react-cropper";
5
- import "cropperjs/dist/cropper.min.css";
5
+ // import "cropperjs/dist/cropper.css"; // CSS import removed due to Next.js compatibility issues
6
6
  import { InputBox, SmallButton } from ".";
7
7
  import { Locale } from "../helpers";
8
8
 
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { FormControl, Grid, InputLabel, MenuItem, Select, TextField } from "@mui/material";
5
+ import type { SelectChangeEvent } from "@mui/material";
6
+ import { useStripe } from "@stripe/react-stripe-js";
7
+ import { InputBox, ErrorMessages } from "../../components";
8
+ import { ApiHelper, Locale } from "../../helpers";
9
+ import { PersonInterface, StripePaymentMethod, PaymentMethodInterface, StripeBankAccountInterface, StripeBankAccountUpdateInterface, StripeBankAccountVerifyInterface } from "@churchapps/helpers";
10
+
11
+ interface Props { bank: StripePaymentMethod, showVerifyForm: boolean, customerId: string, person: PersonInterface, setMode: any, deletePayment: any, updateList: (message?: string) => void }
12
+
13
+ export const BankForm: React.FC<Props> = (props) => {
14
+ const stripe = useStripe();
15
+ const [bankAccount, setBankAccount] = React.useState<StripeBankAccountInterface>({ account_holder_name: props.bank.account_holder_name, account_holder_type: props.bank.account_holder_type, country: "US", currency: "usd" } as StripeBankAccountInterface);
16
+ const [paymentMethod] = React.useState<PaymentMethodInterface>({ customerId: props.customerId, personId: props.person.id, email: props.person.contactInfo.email, name: props.person.name.display });
17
+ const [updateBankData] = React.useState<StripeBankAccountUpdateInterface>({ 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 } } as StripeBankAccountUpdateInterface);
18
+ const [verifyBankData, setVerifyBankData] = React.useState<StripeBankAccountVerifyInterface>({ paymentMethodId: props.bank.id, customerId: props.customerId, amountData: { amounts: [] } });
19
+ const [showSave, setShowSave] = React.useState<boolean>(true);
20
+ const [errorMessage, setErrorMessage] = React.useState<string>(null);
21
+ const saveDisabled = () => { };
22
+ const handleCancel = () => { props.setMode("display"); };
23
+ const handleDelete = () => { props.deletePayment(); };
24
+ const handleSave = () => {
25
+ setShowSave(false);
26
+ if (props.showVerifyForm) verifyBank();
27
+ else props.bank.id ? updateBank() : createBank();
28
+ };
29
+
30
+ const createBank = async () => {
31
+ if (!bankAccount.routing_number || !bankAccount.account_number) setErrorMessage(Locale.label("donation.bankForm.validate.accountNumber"));
32
+ else {
33
+ await stripe.createToken("bank_account", bankAccount).then(response => {
34
+ if (response?.error?.message) setErrorMessage(response.error.message);
35
+ else {
36
+ const pm = { ...paymentMethod };
37
+ pm.id = response.token.id;
38
+ ApiHelper.post("/paymentmethods/addbankaccount", pm, "GivingApi").then(result => {
39
+ if (result?.raw?.message) setErrorMessage(result.raw.message);
40
+ else {
41
+ props.updateList(Locale.label("donation.bankForm.added"));
42
+ props.setMode("display");
43
+ }
44
+ });
45
+ }
46
+ });
47
+ }
48
+ setShowSave(true);
49
+ };
50
+
51
+ const updateBank = () => {
52
+ if (bankAccount.account_holder_name === "") setErrorMessage(Locale.label("donation.bankForm.validate.holderName"));
53
+ else {
54
+ const bank = { ...updateBankData };
55
+ bank.bankData.account_holder_name = bankAccount.account_holder_name;
56
+ bank.bankData.account_holder_type = bankAccount.account_holder_type;
57
+ ApiHelper.post("/paymentmethods/updatebank", bank, "GivingApi").then(response => {
58
+ if (response?.raw?.message) setErrorMessage(response.raw.message);
59
+ else {
60
+ props.updateList(Locale.label("donation.bankForm.updated"));
61
+ props.setMode("display");
62
+ }
63
+ });
64
+ }
65
+ setShowSave(true);
66
+ };
67
+
68
+ const verifyBank = () => {
69
+ const amounts = verifyBankData?.amountData?.amounts;
70
+ if (amounts && amounts.length === 2 && amounts[0] !== "" && amounts[1] !== "") {
71
+ ApiHelper.post("/paymentmethods/verifyBank", verifyBankData, "GivingApi").then(response => {
72
+ if (response?.raw?.message) setErrorMessage(response.raw.message);
73
+ else {
74
+ props.updateList(Locale.label("donation.bankForm.verified"));
75
+ props.setMode("display");
76
+ }
77
+ });
78
+ } else setErrorMessage("Both deposit amounts are required.");
79
+ setShowSave(true);
80
+ };
81
+
82
+ const getHeaderText = () => props.bank.id
83
+ ? `${props.bank.name.toUpperCase()} ****${props.bank.last4}`
84
+ : "Add New Bank Account";
85
+
86
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string>) => {
87
+ const bankData = { ...bankAccount };
88
+ const inputData = { [e.target.name]: e.target.value };
89
+ setBankAccount({ ...bankData, ...inputData });
90
+ setShowSave(true);
91
+ };
92
+
93
+ const handleKeyPress = (e: React.KeyboardEvent<any>) => {
94
+ const pattern = /^\d+$/;
95
+ if (!pattern.test(e.key)) e.preventDefault();
96
+ };
97
+
98
+ const handleVerify = (e: React.ChangeEvent<HTMLInputElement>) => {
99
+ const verifyData = { ...verifyBankData };
100
+ if (e.currentTarget.name === "amount1") verifyData.amountData.amounts[0] = e.currentTarget.value;
101
+ if (e.currentTarget.name === "amount2") verifyData.amountData.amounts[1] = e.currentTarget.value;
102
+ setVerifyBankData(verifyData);
103
+ };
104
+
105
+ const getForm = () => {
106
+ if (props.showVerifyForm) {
107
+ return (<>
108
+ <p>{Locale.label("donation.bankForm.twoDeposits")}</p>
109
+ <Grid container columnSpacing={2}>
110
+ <Grid size={{ xs: 12, md: 6 }}>
111
+ <TextField fullWidth aria-label="amount1" label={Locale.label("donation.bankForm.firstDeposit")} name="amount1" placeholder="00" inputProps={{ maxLength: 2 }} onChange={handleVerify} onKeyPress={handleKeyPress} />
112
+ </Grid>
113
+ <Grid size={{ xs: 12, md: 6 }}>
114
+ <TextField fullWidth aria-label="amount2" label={Locale.label("donation.bankForm.secondDeposit")} name="amount2" placeholder="00" inputProps={{ maxLength: 2 }} onChange={handleVerify} onKeyPress={handleKeyPress} />
115
+ </Grid>
116
+ </Grid>
117
+ </>);
118
+
119
+ } else {
120
+ let accountDetails = <></>;
121
+ if (!props.bank.id) {
122
+ accountDetails = (
123
+ <Grid container spacing={3}>
124
+ <Grid size={{ xs: 12, md: 6 }} style={{ marginBottom: "20px" }}>
125
+ <TextField fullWidth label={Locale.label("donation.bankForm.routingNumber")} type="number" name="routing_number" aria-label="routing-number" placeholder="Routing Number" className="form-control" onChange={handleChange} />
126
+ </Grid>
127
+ <Grid size={{ xs: 12, md: 6 }} style={{ marginBottom: "20px" }}>
128
+ <TextField fullWidth label={Locale.label("donation.bankForm.accountNumber")} type="number" name="account_number" aria-label="account-number" placeholder="Account Number" className="form-control" onChange={handleChange} />
129
+ </Grid>
130
+ </Grid>
131
+ );
132
+ }
133
+ return (<>
134
+ <Grid container spacing={3}>
135
+ <Grid size={{ xs: 12, md: 6 }} style={{ marginBottom: "20px" }}>
136
+ <TextField fullWidth label="Account Holder Name" name="account_holder_name" required aria-label="account-holder-name" placeholder="Account Holder Name" value={bankAccount.account_holder_name} className="form-control" onChange={handleChange} />
137
+ </Grid>
138
+ <Grid size={{ xs: 12, md: 6 }} style={{ marginBottom: "20px" }}>
139
+ <FormControl fullWidth>
140
+ <InputLabel>{Locale.label("donation.bankForm.name")}</InputLabel>
141
+ <Select label={Locale.label("donation.bankForm.name")} name="account_holder_type" aria-label="account-holder-type" value={bankAccount.account_holder_type} onChange={handleChange}>
142
+ <MenuItem value="individual">{Locale.label("donation.bankForm.individual")}</MenuItem>
143
+ <MenuItem value="company">{Locale.label("donation.bankForm.company")}</MenuItem>
144
+ </Select>
145
+ </FormControl>
146
+ </Grid>
147
+ </Grid>
148
+ {accountDetails}
149
+ </>);
150
+ }
151
+ };
152
+
153
+ return (
154
+ <InputBox headerIcon="volunteer_activism" headerText={getHeaderText()} ariaLabelSave="save-button" ariaLabelDelete="delete-button" cancelFunction={handleCancel} saveFunction={showSave ? handleSave : saveDisabled} deleteFunction={props.bank.id && !props.showVerifyForm ? handleDelete : undefined}>
155
+ {errorMessage && <ErrorMessages errors={[errorMessage]}></ErrorMessages>}
156
+ <div>
157
+ {!props.bank.id && <p>{Locale.label("donation.bankForm.needVerified")}</p>}
158
+ {getForm()}
159
+ </div>
160
+ </InputBox>
161
+ );
162
+
163
+ };
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import React, { useEffect } from "react";
4
+ import { Grid, TextField } from "@mui/material";
5
+ import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
6
+ import { InputBox, ErrorMessages } from "../../components";
7
+ import { ApiHelper, Locale } from "../../helpers";
8
+ import { PersonInterface, StripePaymentMethod, PaymentMethodInterface, StripeCardUpdateInterface } from "@churchapps/helpers";
9
+
10
+ interface Props { card: StripePaymentMethod, customerId: string, person: PersonInterface, setMode: any, deletePayment: any, updateList: (message: string) => void }
11
+
12
+ export const CardForm: React.FC<Props> = (props) => {
13
+ const stripe = useStripe();
14
+ const elements = useElements();
15
+ const formStyling = { style: { base: { fontSize: "18px" } } };
16
+ const [showSave, setShowSave] = React.useState(true);
17
+ const [paymentMethod] = React.useState<PaymentMethodInterface>({ id: props.card.id, customerId: props.customerId, personId: props.person.id, email: props.person.contactInfo.email, name: props.person.name.display });
18
+ const [cardUpdate, setCardUpdate] = React.useState<StripeCardUpdateInterface>({ personId: props.person.id, paymentMethodId: props.card.id, cardData: { card: {} } } as StripeCardUpdateInterface);
19
+ const [errorMessage, setErrorMessage] = React.useState<string>(null);
20
+ const handleCancel = () => { props.setMode("display"); };
21
+ const handleSave = () => { setShowSave(false); props.card.id ? updateCard() : createCard(); };
22
+ const saveDisabled = () => { };
23
+ const handleDelete = () => { props.deletePayment(); };
24
+
25
+ const handleKeyPress = (e: React.KeyboardEvent<any>) => {
26
+ const pattern = /^\d+$/;
27
+ if (!pattern.test(e.key)) e.preventDefault();
28
+ };
29
+
30
+ useEffect(() => {
31
+ setCardUpdate({ ...cardUpdate, cardData: { card: { exp_year: props.card?.exp_year?.toString().slice(2) || "", exp_month: props.card?.exp_month || "" } } });
32
+ }, []) //eslint-disable-line
33
+
34
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35
+ const card = { ...cardUpdate };
36
+ if (e.currentTarget.name === "exp_month") card.cardData.card.exp_month = e.currentTarget.value;
37
+ if (e.currentTarget.name === "exp_year") card.cardData.card.exp_year = e.currentTarget.value;
38
+ setCardUpdate(card);
39
+ setShowSave(true);
40
+ };
41
+
42
+ const createCard = async () => {
43
+ const cardData = elements.getElement(CardElement);
44
+ const stripePM = await stripe.createPaymentMethod({
45
+ type: "card",
46
+ card: cardData
47
+ });
48
+ if (stripePM.error) {
49
+ setErrorMessage(stripePM.error.message);
50
+ setShowSave(true);
51
+ } else {
52
+ const pm = { ...paymentMethod };
53
+ pm.id = stripePM.paymentMethod.id;
54
+ await ApiHelper.post("/paymentmethods/addcard", pm, "GivingApi").then(result => {
55
+ if (result?.raw?.message) {
56
+ setErrorMessage(result.raw.message);
57
+ setShowSave(true);
58
+ } else {
59
+ props.updateList(Locale.label("donation.cardForm.added"));
60
+ props.setMode("display");
61
+ }
62
+ });
63
+ }
64
+ };
65
+
66
+ const updateCard = async () => {
67
+ if (!cardUpdate.cardData.card.exp_month || !cardUpdate.cardData.card.exp_year) setErrorMessage("Expiration month and year cannot be blank.");
68
+ else {
69
+ await ApiHelper.post("/paymentmethods/updatecard", cardUpdate, "GivingApi").then(result => {
70
+ if (result?.raw?.message) {
71
+ setErrorMessage(result.raw.message);
72
+ setShowSave(true);
73
+ } else {
74
+ props.updateList(Locale.label("donation.cardForm.updated"));
75
+ props.setMode("display");
76
+ }
77
+ });
78
+ }
79
+ };
80
+
81
+ const getHeaderText = () => props.card.id
82
+ ? `${props.card.name.toUpperCase()} ****${props.card.last4}`
83
+ : Locale.label("donation.cardForm.addNew");
84
+
85
+ return (
86
+ <InputBox headerIcon="volunteer_activism" headerText={getHeaderText()} ariaLabelSave="save-button" ariaLabelDelete="delete-button" cancelFunction={handleCancel} saveFunction={showSave ? handleSave : saveDisabled} deleteFunction={props.card.id ? handleDelete : undefined}>
87
+ {errorMessage && <ErrorMessages errors={[errorMessage]}></ErrorMessages>}
88
+ <div>
89
+ {!props.card.id
90
+ ? <CardElement options={formStyling} />
91
+ : <Grid container spacing={3}>
92
+ <Grid size={{ xs: 12, md: 6 }}>
93
+ <TextField fullWidth aria-label="card-exp-month" label={Locale.label("donation.cardForm.expirationMonth")} name="exp_month" value={cardUpdate.cardData.card.exp_month} placeholder="MM" inputProps={{ maxLength: 2 }} onChange={handleChange} onKeyPress={handleKeyPress} />
94
+ </Grid>
95
+ <Grid size={{ xs: 12, md: 6 }}>
96
+ <TextField fullWidth aria-label="card-exp-year" label={Locale.label("donation.cardForm.expirationYear")} name="exp_year" value={cardUpdate.cardData.card.exp_year} placeholder="YY" inputProps={{ maxLength: 2 }} onChange={handleChange} onKeyPress={handleKeyPress} />
97
+ </Grid>
98
+ </Grid>
99
+ }
100
+ </div>
101
+ </InputBox>
102
+ );
103
+
104
+ };
@@ -0,0 +1,260 @@
1
+ "use client";
2
+
3
+
4
+ import React from "react";
5
+ import type { Stripe } from "@stripe/stripe-js";
6
+ import { InputBox, ErrorMessages } from "../../components";
7
+ import { FundDonations } from ".";
8
+ import { DonationPreviewModal } from "../modals/DonationPreviewModal";
9
+ import { ApiHelper, CurrencyHelper, DateHelper, Locale } from "../../helpers";
10
+ import { PersonInterface, StripePaymentMethod, StripeDonationInterface, FundDonationInterface, FundInterface, ChurchInterface } from "@churchapps/helpers";
11
+ import {
12
+ Grid, InputLabel, MenuItem, Select, TextField, FormControl, Button, FormControlLabel, Checkbox, FormGroup, Typography
13
+ } from "@mui/material";
14
+ import type { SelectChangeEvent } from "@mui/material";
15
+ import { DonationHelper } from "../../helpers";
16
+
17
+ interface Props { person: PersonInterface, customerId: string, paymentMethods: StripePaymentMethod[], stripePromise: Promise<Stripe>, donationSuccess: (message: string) => void, church?: ChurchInterface, churchLogo?: string }
18
+
19
+ export const DonationForm: React.FC<Props> = (props) => {
20
+ const [errorMessage, setErrorMessage] = React.useState<string>();
21
+ const [fundDonations, setFundDonations] = React.useState<FundDonationInterface[]>();
22
+ const [funds, setFunds] = React.useState<FundInterface[]>([]);
23
+ const [fundsTotal, setFundsTotal] = React.useState<number>(0);
24
+ const [transactionFee, setTransactionFee] = React.useState<number>(0);
25
+ const [payFee, setPayFee] = React.useState<number>(0);
26
+ const [total, setTotal] = React.useState<number>(0);
27
+ const [paymentMethodName, setPaymentMethodName] = React.useState<string>(`${props?.paymentMethods[0]?.name} ****${props?.paymentMethods[0]?.last4}`);
28
+ const [donationType, setDonationType] = React.useState<string>();
29
+ const [showDonationPreviewModal, setShowDonationPreviewModal] = React.useState<boolean>(false);
30
+ const [interval, setInterval] = React.useState("one_month");
31
+ const [gateway, setGateway] = React.useState(null);
32
+ const [donation, setDonation] = React.useState<StripeDonationInterface>({
33
+ id: props?.paymentMethods[0]?.id,
34
+ type: props?.paymentMethods[0]?.type,
35
+ customerId: props.customerId,
36
+ person: {
37
+ id: props.person?.id,
38
+ email: props.person?.contactInfo.email,
39
+ name: props.person?.name.display
40
+ },
41
+ amount: 0,
42
+ billing_cycle_anchor: + new Date(),
43
+ interval: {
44
+ interval_count: 1,
45
+ interval: "month"
46
+ },
47
+ funds: []
48
+ });
49
+
50
+ const loadData = () => {
51
+ ApiHelper.get("/funds", "GivingApi").then(data => {
52
+ setFunds(data);
53
+ if (data.length) setFundDonations([{ fundId: data[0].id }]);
54
+ });
55
+ ApiHelper.get("/gateways", "GivingApi").then((data) => {
56
+ if (data.length !== 0) setGateway(data[0]);
57
+ });
58
+ };
59
+
60
+ const handleKeyDown = (e: React.KeyboardEvent<any>) => { if (e.key === "Enter") { e.preventDefault(); handleSave(); } };
61
+
62
+ const handleCheckChange = (e: React.SyntheticEvent<Element, Event>, checked: boolean) => {
63
+ const d = { ...donation } as StripeDonationInterface;
64
+ d.amount = checked ? fundsTotal + transactionFee : fundsTotal;
65
+ const showFee = checked ? transactionFee : 0;
66
+ setTotal(d.amount);
67
+ setPayFee(showFee);
68
+ setDonation(d);
69
+ };
70
+
71
+ const handleAutoPayFee = () => {
72
+ const d = { ...donation } as StripeDonationInterface;
73
+ d.amount = fundsTotal + transactionFee;
74
+ const showFee = transactionFee;
75
+ setTotal(d.amount);
76
+ setPayFee(showFee);
77
+ setDonation(d);
78
+ };
79
+
80
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> | SelectChangeEvent<string>) => {
81
+ setErrorMessage(null);
82
+ const d = { ...donation } as StripeDonationInterface;
83
+ const value = e.target.value;
84
+ switch (e.target.name) {
85
+ case "method":
86
+ d.id = value;
87
+ const pm = props.paymentMethods.find(pm => pm.id === value);
88
+ d.type = pm.type;
89
+ setPaymentMethodName(`${pm.name} ****${pm.last4}`);
90
+ break;
91
+ case "type": setDonationType(value); break;
92
+ case "date": d.billing_cycle_anchor = + new Date(value); break;
93
+ case "interval":
94
+ setInterval(value);
95
+ d.interval = DonationHelper.getInterval(value);
96
+ break;
97
+ case "notes": d.notes = value; break;
98
+ case "transaction-fee":
99
+ const element = e.target as HTMLInputElement;
100
+ d.amount = element.checked ? fundsTotal + transactionFee : fundsTotal;
101
+ const showFee = element.checked ? transactionFee : 0;
102
+ setTotal(d.amount);
103
+ setPayFee(showFee);
104
+ }
105
+ setDonation(d);
106
+ };
107
+
108
+ const handleCancel = () => { setDonationType(null); };
109
+ const handleSave = () => {
110
+ if (donation.amount < .5) setErrorMessage(Locale.label("donation.donationForm.tooLow"));
111
+ else setShowDonationPreviewModal(true);
112
+ };
113
+ const handleDonationSelect = (type: string) => {
114
+ const dt = donationType === type ? null : type;
115
+ setDonationType(dt);
116
+ };
117
+
118
+ const makeDonation = async (message: string) => {
119
+ let results;
120
+
121
+ const churchObj = {
122
+ name: props?.church?.name,
123
+ subDomain: props?.church?.subDomain,
124
+ churchURL: typeof window !== "undefined" && window.location.origin,
125
+ logo: props?.churchLogo
126
+ };
127
+
128
+ if (donationType === "once") results = await ApiHelper.post("/donate/charge/", { ...donation, church: churchObj }, "GivingApi");
129
+ if (donationType === "recurring") results = await ApiHelper.post("/donate/subscribe/", { ...donation, church: churchObj }, "GivingApi");
130
+
131
+ if (results?.status === "succeeded" || results?.status === "pending" || results?.status === "active") {
132
+ setShowDonationPreviewModal(false);
133
+ setDonationType(null);
134
+ props.donationSuccess(message);
135
+ }
136
+ if (results?.raw?.message) {
137
+ setShowDonationPreviewModal(false);
138
+ setErrorMessage(Locale.label("donation.common.error") + ": " + results?.raw?.message);
139
+ }
140
+ };
141
+
142
+ const handleFundDonationsChange = async (fd: FundDonationInterface[]) => {
143
+ setErrorMessage(null);
144
+ setFundDonations(fd);
145
+ let totalAmount = 0;
146
+ const selectedFunds: any = [];
147
+ for (const fundDonation of fd) {
148
+ totalAmount += fundDonation.amount || 0;
149
+ const fund = funds.find((fund: FundInterface) => fund.id === fundDonation.fundId);
150
+ selectedFunds.push({ id: fundDonation.fundId, amount: fundDonation.amount || 0, name: fund.name });
151
+ }
152
+ const d = { ...donation };
153
+ d.amount = totalAmount;
154
+ d.funds = selectedFunds;
155
+ setFundsTotal(totalAmount);
156
+
157
+ const fee = await getTransactionFee(totalAmount);
158
+ setTransactionFee(fee);
159
+
160
+ if (gateway && gateway.payFees === true) {
161
+ d.amount = totalAmount + fee;
162
+ setPayFee(fee);
163
+ }
164
+ setTotal(d.amount);
165
+ setDonation(d);
166
+ };
167
+
168
+ const getTransactionFee = async (amount: number) => {
169
+ if (amount > 0) {
170
+ let dt: string = "";
171
+ if (donation.type === "card") dt = "creditCard";
172
+ if (donation.type === "bank") dt = "ach";
173
+ try {
174
+ const response = await ApiHelper.post("/donate/fee?churchId=" + props?.church?.id, { type: dt, amount }, "GivingApi");
175
+ return response.calculatedFee;
176
+ } catch (error) {
177
+ console.log("Error calculating transaction fee: ", error);
178
+ return 0;
179
+ }
180
+ } else {
181
+ return 0;
182
+ }
183
+ };
184
+
185
+ React.useEffect(loadData, [props.person?.id]);
186
+
187
+ // React.useEffect(() => { gateway && gateway.payFees === true && handleAutoPayFee() }, [fundDonations]);
188
+
189
+ if (!funds.length || !props?.paymentMethods[0]?.id) return null;
190
+ else {
191
+ return (
192
+ <>
193
+ <DonationPreviewModal show={showDonationPreviewModal} onHide={() => setShowDonationPreviewModal(false)} handleDonate={makeDonation} donation={donation} donationType={donationType} payFee={payFee} paymentMethodName={paymentMethodName} funds={funds} />
194
+ <InputBox id="donationBox" 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")}>
195
+ <Grid container spacing={3}>
196
+ <Grid size={{ xs: 12, md: 6 }}>
197
+ <Button aria-label="single-donation" size="small" fullWidth style={{ minHeight: "50px" }} variant={donationType === "once" ? "contained" : "outlined"} onClick={() => handleDonationSelect("once")}>{Locale.label("donation.donationForm.make")}</Button>
198
+ </Grid>
199
+ <Grid size={{ xs: 12, md: 6 }}>
200
+ <Button aria-label="recurring-donation" size="small" fullWidth style={{ minHeight: "50px" }} variant={donationType === "recurring" ? "contained" : "outlined"} onClick={() => handleDonationSelect("recurring")}>{Locale.label("donation.donationForm.makeRecurring")}</Button>
201
+ </Grid>
202
+ </Grid>
203
+ {donationType
204
+ && <div style={{ marginTop: "20px" }}>
205
+ <Grid container spacing={3}>
206
+ <Grid size={12}>
207
+ <FormControl fullWidth>
208
+ <InputLabel>{Locale.label("donation.donationForm.method")}</InputLabel>
209
+ <Select label={Locale.label("donation.donationForm.method")} name="method" aria-label="method" value={donation.id} className="capitalize" onChange={handleChange}>
210
+ {props.paymentMethods.map((paymentMethod: any, i: number) => <MenuItem key={i} value={paymentMethod.id}>{paymentMethod.name} ****{paymentMethod.last4}</MenuItem>)}
211
+ </Select>
212
+ </FormControl>
213
+ </Grid>
214
+ </Grid>
215
+ {donationType === "recurring"
216
+ && <Grid container spacing={3} style={{ marginTop:10 }}>
217
+ <Grid size={{ xs: 12, md: 6 }}>
218
+ <TextField fullWidth name="date" type="date" aria-label="date" label={Locale.label("donation.donationForm.startDate")} value={DateHelper.formatHtml5Date(new Date(donation.billing_cycle_anchor))} onChange={handleChange} onKeyDown={handleKeyDown} />
219
+ </Grid>
220
+ <Grid size={{ xs: 12, md: 6 }}>
221
+ <FormControl fullWidth>
222
+ <InputLabel>{Locale.label("donation.donationForm.frequency")}</InputLabel>
223
+ <Select label={Locale.label("donation.donationForm.frequency")} name="interval" aria-label="interval" value={interval} onChange={handleChange}>
224
+ <MenuItem value="one_week">{Locale.label("donation.donationForm.weekly")}</MenuItem>
225
+ <MenuItem value="two_week">{Locale.label("donation.donationForm.biWeekly")}</MenuItem>
226
+ <MenuItem value="one_month">{Locale.label("donation.donationForm.monthly")}</MenuItem>
227
+ <MenuItem value="three_month">{Locale.label("donation.donationForm.quarterly")}</MenuItem>
228
+ <MenuItem value="one_year">{Locale.label("donation.donationForm.annually")}</MenuItem>
229
+ </Select>
230
+ </FormControl>
231
+ </Grid>
232
+ </Grid>
233
+ }
234
+ <div className="form-group">
235
+ {funds && fundDonations
236
+ && <>
237
+ <h4>{Locale.label("donation.donationForm.fund")}</h4>
238
+ <FundDonations fundDonations={fundDonations} funds={funds} updatedFunction={handleFundDonationsChange} />
239
+ </>
240
+ }
241
+ {fundsTotal > 0
242
+ && <>
243
+ {(gateway && gateway.payFees === true) ? <Typography fontSize={14} fontStyle="italic">*{Locale.label("donation.donationForm.fees").replace("{}", CurrencyHelper.formatCurrency(transactionFee))}</Typography> : (
244
+ <FormGroup>
245
+ <FormControlLabel control={<Checkbox />} name="transaction-fee" label={Locale.label("donation.donationForm.cover").replace("{}", CurrencyHelper.formatCurrency(transactionFee))} onChange={handleCheckChange} />
246
+ </FormGroup>
247
+ )}
248
+ <p>{Locale.label("donation.donationForm.total")}: ${total}</p>
249
+ </>
250
+ }
251
+ <TextField fullWidth label={Locale.label("donation.donationForm.notes")} multiline aria-label="note" name="notes" value={donation.notes || ""} onChange={handleChange} onKeyDown={handleKeyDown} />
252
+ </div>
253
+ {errorMessage && <ErrorMessages errors={[errorMessage]}></ErrorMessages>}
254
+ </div>
255
+ }
256
+ </InputBox>
257
+ </>
258
+ );
259
+ }
260
+ };
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { FundDonationInterface, FundInterface } from "@churchapps/helpers";
5
+ import { FormControl, Grid, InputLabel, MenuItem, Select, TextField } from "@mui/material";
6
+ import type { SelectChangeEvent } from "@mui/material";
7
+ import { Locale } from "../../helpers";
8
+
9
+ interface Props {
10
+ fundDonation: FundDonationInterface,
11
+ funds: FundInterface[],
12
+ index: number,
13
+ updatedFunction: (fundDonation: FundDonationInterface, index: number) => void,
14
+ params?: any,
15
+ }
16
+
17
+ export const FundDonation: React.FC<Props> = (props) => {
18
+
19
+ const getOptions = () => {
20
+ const result = [];
21
+ for (let i = 0; i < props.funds.length; i++) {
22
+ const getDisabled = (props?.params?.fundId && props.params.fundId !== "") ? props.params.fundId !== props.funds[i].id : false;
23
+ result.push(<MenuItem key={i} value={props.funds[i].id} disabled={getDisabled}>{props.funds[i].name}</MenuItem>);
24
+ }
25
+ return result;
26
+ };
27
+
28
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> | SelectChangeEvent<string>) => {
29
+ const fd = { ...props.fundDonation };
30
+ switch (e.target.name) {
31
+ case "amount":
32
+ fd.amount = parseFloat(e.target.value.replace("$", "").replace(",", ""));
33
+ break;
34
+ case "fund":
35
+ fd.fundId = e.target.value;
36
+ break;
37
+ }
38
+ props.updatedFunction(fd, props.index);
39
+ };
40
+
41
+ return (
42
+ <>
43
+ <Grid container spacing={3}>
44
+ <Grid size={{ xs: 12, md: 6 }}>
45
+ <TextField fullWidth name="amount" label={Locale.label("donation.fundDonations.amount")} type="number" disabled={props.params?.amount && props.params.amount !== ""} aria-label="amount" lang="en-150" value={props.fundDonation.amount || ""} onChange={handleChange} />
46
+ </Grid>
47
+ <Grid size={{ xs: 12, md: 6 }}>
48
+ <FormControl fullWidth>
49
+ <InputLabel>{Locale.label("donation.fundDonations.fund")}</InputLabel>
50
+ <Select fullWidth label={Locale.label("donation.fundDonations.fund")} name="fund" aria-label="fund" value={props.fundDonation.fundId} onChange={handleChange}>
51
+ {getOptions()}
52
+ </Select>
53
+ </FormControl>
54
+ </Grid>
55
+ </Grid>
56
+ </>
57
+ );
58
+ };
59
+