diff --git a/src/DonationForms/resources/app/hooks/useDonationSummary.ts b/src/DonationForms/resources/app/hooks/useDonationSummary.ts index 6770683263..c6ddbe4488 100644 --- a/src/DonationForms/resources/app/hooks/useDonationSummary.ts +++ b/src/DonationForms/resources/app/hooks/useDonationSummary.ts @@ -1,22 +1,118 @@ import { - DonationSummaryLineItem, + DonationSummaryLineItem, DonationTotals, 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'; +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 dollarsToCents = (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); +} +/** + * 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. + * + * Although the initial intent for this hook was to be used in the DonationSummary component for visual reasons, it is also recommended to be used in others + * areas like gateways to get the total donation amount and currency. + * + * @unreleased added state object * @since 3.0.0 */ export default function useDonationSummary() { - const {items, totals} = useDonationSummaryContext(); + const { items, totals } = useDonationSummaryContext(); const dispatch = useDonationSummaryDispatch(); + const { useWatch } = window.givewp.form.hooks; + + 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 { items, @@ -28,5 +124,19 @@ export default function useDonationSummary() { [dispatch] ), removeFromTotal: useCallback((itemId: string) => dispatch(removeAmountFromTotal(itemId)), [dispatch]), + state: { + currency, + donationAmount: Number(amount), + donationAmountMinor: dollarsToCents(amount, currency), + donationAmountTotal, + donationAmountTotalMinor: dollarsToCents(donationAmountTotal.toString(), currency), + subscriptionAmount, + subscriptionAmountMinor: dollarsToCents(subscriptionAmount.toString(), currency), + donationIsOneTime: donationType === 'single', + donationIsRecurring: donationType === 'subscription', + subscriptionPeriod, + subscriptionFrequency, + subscriptionInstallments, + }, }; } diff --git a/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx b/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx index 85a0ea3d32..78ce91744d 100644 --- a/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx +++ b/src/DonationForms/resources/registrars/templates/elements/DonationSummary.tsx @@ -3,40 +3,29 @@ 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 { useCurrencyFormatter, useDonationSummary } = window.givewp.form.hooks; + const { items } = useDonationSummary(); + const {state: { + currency, + donationAmount, + donationAmountTotal, + subscriptionPeriod: period, + subscriptionFrequency: frequency, + }} = window.givewp.form.hooks.useDonationSummary(); const formatter = useCurrencyFormatter(currency); - const amount = useWatch({name: 'amount'}); - const period = useWatch({name: 'subscriptionPeriod'}); - const frequency = useWatch({name: 'subscriptionFrequency'}); - const givingFrequency = useMemo(() => { if (isSubscriptionPeriod(period)) { const subscriptionPeriod = new SubscriptionPeriod(period); if (frequency > 1) { return createInterpolateElement(__('Every ', 'give'), { - period: {`${frequency} ${subscriptionPeriod.label().plural()}`}, + period: {`${frequency} ${subscriptionPeriod.label().plural()}`} }); } @@ -49,18 +38,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 (