diff --git a/src/DonationForms/resources/app/hooks/useDonationSummary.ts b/src/DonationForms/resources/app/hooks/useDonationSummary.ts index 6770683263..bd14bdc6f4 100644 --- a/src/DonationForms/resources/app/hooks/useDonationSummary.ts +++ b/src/DonationForms/resources/app/hooks/useDonationSummary.ts @@ -1,21 +1,26 @@ import { DonationSummaryLineItem, useDonationSummaryContext, - useDonationSummaryDispatch, + useDonationSummaryDispatch } from '@givewp/forms/app/store/donation-summary'; import { addAmountToTotal, addItem, removeAmountFromTotal, - removeItem, + removeItem } from '@givewp/forms/app/store/donation-summary/reducer'; import {useCallback} from '@wordpress/element'; /** + * The donation summary hook is used to interact with the donation summary context which wraps around our donation form. + * It provides methods to add and remove items from the summary, as well as to add and remove amounts from the total. + * It also provides the current items and totals from the context, making it easier to access form values specific to donations. + * + * @unreleased added getTotalSum * @since 3.0.0 */ export default function useDonationSummary() { - const {items, totals} = useDonationSummaryContext(); + const { items, totals } = useDonationSummaryContext(); const dispatch = useDonationSummaryDispatch(); return { @@ -28,5 +33,14 @@ export default function useDonationSummary() { [dispatch] ), removeFromTotal: useCallback((itemId: string) => dispatch(removeAmountFromTotal(itemId)), [dispatch]), + getTotalSum: useCallback((amount: number) => + Number( + Object.values({ + ...totals, + amount + }).reduce((total: number, amount: number) => { + return total + amount; + }, 0) + ), [totals]) }; } diff --git a/src/DonationForms/resources/app/hooks/useFormData.ts b/src/DonationForms/resources/app/hooks/useFormData.ts new file mode 100644 index 0000000000..18e67687ff --- /dev/null +++ b/src/DonationForms/resources/app/hooks/useFormData.ts @@ -0,0 +1,129 @@ +import type {DonationTotals} from '@givewp/forms/app/store/donation-summary'; +import type { + subscriptionPeriod +} from '@givewp/forms/registrars/templates/groups/DonationAmount/subscriptionPeriod'; +import { + useDonationSummaryContext, +} from '@givewp/forms/app/store/donation-summary'; + +/** + * Zero decimal currencies are currencies that do not have a minor unit. + * For example, the Japanese Yen (JPY) does not have a minor unit. + * @unreleased + * + * @see https://stripe.com/docs/currencies#zero-decimal + */ +const zeroDecimalCurrencies = [ + 'BIF', + 'CLP', + 'DJF', + 'GNF', + 'JPY', + 'KMF', + 'KRW', + 'MGA', + 'PYG', + 'RWF', + 'UGX', + 'VND', + 'VUV', + 'XAF', + 'XOF', + 'XPF', +]; + +/** + * Takes in an amount value in dollar units and returns the calculated cents (minor) amount + * + * @unreleased + */ +const amountToMinorUnit = (amount: string, currency: string) => { + if (zeroDecimalCurrencies.includes(currency)) { + return Math.round(parseFloat(amount)); + } + + return Math.round(parseFloat(amount) * 100); +}; + +/** + * Donation total calculation + * + * @unreleased + */ +const getDonationTotal = (totals: DonationTotals, amount: number) => + Number( + Object.values({ + ...totals, + amount, + }).reduce((total: number, amount: number) => { + return total + amount; + }, 0) + ); + +/** + * Subscription total calculation + * TODO: figure out which totals will be included in subscriptions + * + * @unreleased + */ +const getSubscriptionTotal = (totals: DonationTotals, amount: number) => { + let total = 0; + + // Subscriptions currently only support donation amount (TODO: and potentially feeRecovery values) + const allowedKeys = ['feeRecovery']; + + for (const [key, value] of Object.entries(totals)) { + if (allowedKeys.includes(key)) { + total += value; + } + } + + return Number(total + amount); +} +/** + * @unreleased + */ +export default function useFormData() { + const { totals } = useDonationSummaryContext(); + const { useWatch } = window.givewp.form.hooks; + + const firstName = useWatch({ name: 'firstName' }) as string; + const lastName = useWatch({ name: 'lastName' }) as string | undefined; + const email = useWatch({ name: 'email' }) as string; + const billingAddress = { + addressLine1: useWatch({name: 'address1'}) as string | undefined, + addressLine2: useWatch({name: 'address2'}) as string | undefined, + city: useWatch({name: 'city'}) as string | undefined, + state: useWatch({name: 'state'}) as string | undefined, + postalCode: useWatch({name: 'zip'}) as string | undefined, + country: useWatch({name: 'country'}) as string | undefined, + } + const amount = useWatch({ name: 'amount' }) as string; + const currency = useWatch({ name: 'currency' }) as string; + const subscriptionPeriod = useWatch({name: 'subscriptionPeriod'}) as subscriptionPeriod | undefined; + const subscriptionFrequency = useWatch({name: 'subscriptionFrequency'}) as number | undefined; + const subscriptionInstallments = useWatch({name: 'subscriptionInstallments'}); + const donationType = useWatch({name: 'donationType'}) as "single" | "subscription" | undefined; + + const donationAmountTotal = getDonationTotal(totals, Number(amount)); + const subscriptionAmount = getSubscriptionTotal(totals, Number(amount)) + + return { + firstName, + lastName, + email, + billingAddress, + currency, + donationAmount: Number(amount), + donationAmountMinor: amountToMinorUnit(amount, currency), + donationAmountTotal, + donationAmountTotalMinor: amountToMinorUnit(donationAmountTotal.toString(), currency), + subscriptionAmount, + subscriptionAmountMinor: amountToMinorUnit(subscriptionAmount.toString(), currency), + donationIsOneTime: donationType === 'single', + donationIsRecurring: donationType === 'subscription', + subscriptionPeriod, + subscriptionFrequency, + subscriptionInstallments, + }; +} diff --git a/src/DonationForms/resources/registrars/index.ts b/src/DonationForms/resources/registrars/index.ts index 149d647e9d..80de7003f7 100644 --- a/src/DonationForms/resources/registrars/index.ts +++ b/src/DonationForms/resources/registrars/index.ts @@ -5,6 +5,7 @@ import defaultFormTemplates from './templates'; import useCurrencyFormatter from '@givewp/forms/app/hooks/useCurrencyFormatter'; import useDonationSummary from '@givewp/forms/app/hooks/useDonationSummary'; import {useDonationFormSettings} from '@givewp/forms/app/store/form-settings'; +import useFormData from '@givewp/forms/app/hooks/useFormData'; declare global { interface Window { @@ -21,6 +22,7 @@ declare global { useCurrencyFormatter: typeof useCurrencyFormatter; useDonationSummary: typeof useDonationSummary; useDonationFormSettings: typeof useDonationFormSettings; + useFormData: typeof useFormData; }; }; }; diff --git a/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx b/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx index 85a0ea3d32..857d693ec4 100644 --- a/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx +++ b/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx @@ -3,32 +3,23 @@ import {__} from '@wordpress/i18n'; import {isSubscriptionPeriod, SubscriptionPeriod} from '../groups/DonationAmount/subscriptionPeriod'; import {createInterpolateElement} from '@wordpress/element'; -/** - * @since 3.0.0 - */ -const getDonationTotal = (totals: any, amount: any) => - Number( - Object.values({ - ...totals, - amount: Number(amount), - }).reduce((total: number, amount: number) => { - return total + amount; - }, 0) - ); - /** * @since 3.0.0 */ export default function DonationSummary() { const DonationSummaryItemsTemplate = window.givewp.form.templates.layouts.donationSummaryItems; - const {useWatch, useCurrencyFormatter, useDonationSummary} = window.givewp.form.hooks; - const {items, totals} = useDonationSummary(); - const currency = useWatch({name: 'currency'}); - const formatter = useCurrencyFormatter(currency); + const { useCurrencyFormatter, useDonationSummary } = window.givewp.form.hooks; + const { items, getTotalSum } = useDonationSummary(); + const { + currency, + donationAmount, + subscriptionPeriod: period, + subscriptionFrequency: frequency + } = window.givewp.form.hooks.useFormData(); - const amount = useWatch({name: 'amount'}); - const period = useWatch({name: 'subscriptionPeriod'}); - const frequency = useWatch({name: 'subscriptionFrequency'}); + const donationAmountTotal = getTotalSum(donationAmount); + + const formatter = useCurrencyFormatter(currency); const givingFrequency = useMemo(() => { if (isSubscriptionPeriod(period)) { @@ -36,7 +27,7 @@ export default function DonationSummary() { if (frequency > 1) { return createInterpolateElement(__('Every ', 'give'), { - period: {`${frequency} ${subscriptionPeriod.label().plural()}`}, + period: {`${frequency} ${subscriptionPeriod.label().plural()}`} }); } @@ -49,18 +40,18 @@ export default function DonationSummary() { const amountItem = { id: 'amount', label: __('Payment Amount', 'give'), - value: formatter.format(Number(amount)), + value: formatter.format(donationAmount) }; const frequencyItem = { id: 'frequency', label: __('Giving Frequency', 'give'), - value: givingFrequency, + value: givingFrequency }; const donationSummaryItems = [amountItem, frequencyItem, ...Object.values(items)]; - const donationTotal = formatter.format(getDonationTotal(totals, amount)); + const donationTotal = formatter.format(donationAmountTotal); return ( <> diff --git a/src/DonationForms/resources/registrars/templates/layouts/DonationSummaryItems.tsx b/src/DonationForms/resources/registrars/templates/layouts/DonationSummaryItems.tsx index 9b109365c7..6b5231755c 100644 --- a/src/DonationForms/resources/registrars/templates/layouts/DonationSummaryItems.tsx +++ b/src/DonationForms/resources/registrars/templates/layouts/DonationSummaryItems.tsx @@ -32,6 +32,9 @@ const LineItem = ({id, label, value, description, className}: LineItem) => { ); }; +/** + * TODO: account for when the total donation amount is different than subscription amount + */ export default function DonationSummaryItems({items, total}) { return (