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..8744cf4c97 --- /dev/null +++ b/src/DonationForms/resources/app/hooks/useFormData.ts @@ -0,0 +1,132 @@ +import type {DonationTotals} from '@givewp/forms/app/store/donation-summary'; +import {useDonationSummaryContext} from '@givewp/forms/app/store/donation-summary'; +import type {subscriptionPeriod} from '@givewp/forms/registrars/templates/groups/DonationAmount/subscriptionPeriod'; + +/** + * 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 getAmountTotal = (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 phone = useWatch({name: 'phone'}) as string | undefined; + 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 amountTotal = getAmountTotal(totals, Number(amount)); + const amountTotalInMinorUnits = amountToMinorUnit(amountTotal.toString(), currency); + const subscriptionAmountTotal = getSubscriptionTotal(totals, Number(amount)); + const subscriptionAmountTotalInMinorUnits = amountToMinorUnit(subscriptionAmountTotal.toString(), currency); + + const donationAmount = Number(amount); + const donationAmountMinor = amountToMinorUnit(amount, currency); + + const isOneTime = donationType === 'single'; + const isRecurring = donationType === 'subscription'; + + return { + firstName, + lastName, + email, + phone, + currency, + billingAddress, + amount: isOneTime ? amountTotal : subscriptionAmountTotal, + amountInMinorUnits: isOneTime ? amountTotalInMinorUnits : subscriptionAmountTotalInMinorUnits, + isOneTime, + isRecurring, + subscriptionPeriod, + subscriptionFrequency, + subscriptionInstallments, + }; +} diff --git a/src/DonationForms/resources/app/utilities/mountWindowData.ts b/src/DonationForms/resources/app/utilities/mountWindowData.ts index 84dd508185..dd424b0910 100644 --- a/src/DonationForms/resources/app/utilities/mountWindowData.ts +++ b/src/DonationForms/resources/app/utilities/mountWindowData.ts @@ -2,6 +2,7 @@ import {useFormContext, useFormState, useWatch} from 'react-hook-form'; 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'; /** * @@ -17,5 +18,6 @@ export default function mountWindowData(): void { useCurrencyFormatter, useDonationSummary, useDonationFormSettings, + useFormData }; } 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..4b36b7a9bc 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, useWatch } = window.givewp.form.hooks; + const donationAmount = Number(useWatch({name: 'amount'})); + const { items, getTotalSum } = useDonationSummary(); + const { + currency, + 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 (