@churchapps/apphelper 0.4.9 → 0.4.11

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 +21 -21
  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.11",
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",
@@ -34,38 +34,38 @@
34
34
  "peerDependencies": {
35
35
  "react": "^19.1.0",
36
36
  "react-dom": "^19.1.0",
37
- "react-router-dom": "^7.6.2"
37
+ "react-router-dom": "^7.6.3"
38
38
  },
39
39
  "dependencies": {
40
- "@churchapps/helpers": "^1.0.39",
40
+ "@churchapps/helpers": "^1.0.40",
41
41
  "@emotion/cache": "^11.13.5",
42
42
  "@emotion/react": "^11.14.0",
43
- "@emotion/styled": "^11.14.0",
44
- "@lexical/code": "^0.32.1",
45
- "@lexical/link": "^0.32.1",
46
- "@lexical/list": "^0.32.1",
47
- "@lexical/markdown": "^0.32.1",
48
- "@lexical/react": "^0.32.1",
49
- "@lexical/rich-text": "^0.32.1",
50
- "@lexical/selection": "^0.32.1",
51
- "@lexical/table": "^0.32.1",
52
- "@lexical/utils": "^0.32.1",
43
+ "@emotion/styled": "^11.14.1",
44
+ "@lexical/code": "^0.33.0",
45
+ "@lexical/link": "^0.33.0",
46
+ "@lexical/list": "^0.33.0",
47
+ "@lexical/markdown": "^0.33.0",
48
+ "@lexical/react": "^0.33.0",
49
+ "@lexical/rich-text": "^0.33.0",
50
+ "@lexical/selection": "^0.33.0",
51
+ "@lexical/table": "^0.33.0",
52
+ "@lexical/utils": "^0.33.0",
53
53
  "@mui/lab": "^7.0.0-beta.14",
54
- "@mui/material": "^7.1.2",
54
+ "@mui/material": "^7.2.0",
55
55
  "@stripe/react-stripe-js": "^3.7.0",
56
56
  "@stripe/stripe-js": "^7.4.0",
57
57
  "axios": "^1.10.0",
58
58
  "cropperjs": "^2.0.0",
59
59
  "date-fns": "^4.1.0",
60
60
  "flexsearch": "0.8.205",
61
- "i18next": "^25.1.0",
61
+ "i18next": "^25.3.1",
62
62
  "i18next-browser-languagedetector": "^8.1.0",
63
63
  "i18next-chained-backend": "^4.6.2",
64
64
  "i18next-http-backend": "^3.0.2",
65
65
  "jwt-decode": "^4.0.0",
66
- "lexical": "^0.32.1",
67
- "marked": "^15.0.6",
68
- "material-symbols": "^0.31.9",
66
+ "lexical": "^0.33.0",
67
+ "marked": "^16.0.0",
68
+ "material-symbols": "^0.32.0",
69
69
  "mui-tel-input": "^9.0.1",
70
70
  "react-activity": "^2.1.3",
71
71
  "react-cookie": "^8.0.1",
@@ -74,13 +74,13 @@
74
74
  "react-ga4": "^2.1.0",
75
75
  "react-google-charts": "^5.2.1",
76
76
  "react-google-recaptcha": "^3.1.0",
77
- "react-i18next": "^15.5.3",
78
- "react-to-print": "^3.1.0",
77
+ "react-i18next": "^15.6.0",
78
+ "react-to-print": "^3.1.1",
79
79
  "rrule": "^2.8.1",
80
80
  "slug": "^11.0.0"
81
81
  },
82
82
  "devDependencies": {
83
- "@types/node": "^24.0.3",
83
+ "@types/node": "^24.0.10",
84
84
  "@types/react": "^19.1.8",
85
85
  "@types/react-csv": "^1.1.10",
86
86
  "@types/react-dom": "^19.1.6",
@@ -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
+ };