From caec59c46d3ec75c5ca3ff4c58240ed978fbe032 Mon Sep 17 00:00:00 2001 From: ChaosExAnima Date: Fri, 12 Sep 2025 19:28:18 +0200 Subject: [PATCH] allow uncontrolled input and add a back button on checkout. --- .../mastodon/features/donate/checkout.tsx | 40 ++++++++----- .../features/donate/donate_modal.scss | 7 +++ .../mastodon/features/donate/donate_modal.tsx | 12 +++- .../mastodon/features/donate/form.tsx | 58 +++++++++++++------ 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/app/javascript/mastodon/features/donate/checkout.tsx b/app/javascript/mastodon/features/donate/checkout.tsx index 10da37c6140..73e1d171d2f 100644 --- a/app/javascript/mastodon/features/donate/checkout.tsx +++ b/app/javascript/mastodon/features/donate/checkout.tsx @@ -2,26 +2,36 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; +import { Button } from '@/mastodon/components/button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; -export const DonateCheckoutHint: FC<{ donateUrl?: string }> = ({ - donateUrl, -}) => { +export const DonateCheckoutHint: FC<{ + donateUrl?: string; + onBack: () => void; +}> = ({ donateUrl, onBack }) => { if (!donateUrl) { return ; } return ( - ( - - {chunks} - - ), - }} - tagName='p' - /> + <> + ( + + {chunks} + + ), + }} + tagName='p' + /> + + ); }; diff --git a/app/javascript/mastodon/features/donate/donate_modal.scss b/app/javascript/mastodon/features/donate/donate_modal.scss index ad2bd7d8a22..fc98b239dfc 100644 --- a/app/javascript/mastodon/features/donate/donate_modal.scss +++ b/app/javascript/mastodon/features/donate/donate_modal.scss @@ -137,6 +137,13 @@ font-size: 0.9em; } + .checkout { + a { + text-decoration: underline; + color: inherit; + } + } + .success { .illustration { width: 100%; diff --git a/app/javascript/mastodon/features/donate/donate_modal.tsx b/app/javascript/mastodon/features/donate/donate_modal.tsx index acd30881f47..44e288e8f28 100644 --- a/app/javascript/mastodon/features/donate/donate_modal.tsx +++ b/app/javascript/mastodon/features/donate/donate_modal.tsx @@ -61,6 +61,11 @@ const DonateModal: FC = forwardRef(({ onClose }, ref) => { [], ); + const handleCheckoutBack = useCallback(() => { + setState('start'); + setDonateUrl(undefined); + }, []); + // Check response from opened page const [state, setState] = useState<'start' | 'checkout' | 'success'>('start'); useEffect(() => { @@ -98,7 +103,12 @@ const DonateModal: FC = forwardRef(({ onClose }, ref) => { })} > {state === 'start' && } - {state === 'checkout' && } + {state === 'checkout' && ( + + )} {state === 'success' && } diff --git a/app/javascript/mastodon/features/donate/form.tsx b/app/javascript/mastodon/features/donate/form.tsx index 4ac541eba31..442389d3388 100644 --- a/app/javascript/mastodon/features/donate/form.tsx +++ b/app/javascript/mastodon/features/donate/form.tsx @@ -1,11 +1,14 @@ -import type { FC, SyntheticEvent } from 'react'; +import type { FC, FocusEventHandler, SyntheticEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import classNames from 'classnames'; -import type { DonationFrequency } from '@/mastodon/api_types/donate'; +import type { + DonateServerResponse, + DonationFrequency, +} from '@/mastodon/api_types/donate'; import type { ButtonProps } from '@/mastodon/components/button'; import { Button } from '@/mastodon/components/button'; import { Dropdown } from '@/mastodon/components/dropdown'; @@ -26,65 +29,84 @@ interface DonateFormProps { onSubmit: (args: DonateCheckoutArgs) => void; } -export const DonateForm: FC> = ({ onSubmit }) => { +const DefaultAmount = 1000; // 10.00 + +export const DonateForm: FC = (props) => { + const donateData = useAppSelector((state) => state.donate.apiResponse); + if (!donateData) { + return ; + } + return ; +}; + +export const DonateFormInner: FC< + DonateFormProps & { data: DonateServerResponse } +> = ({ onSubmit, data: donateData }) => { const intl = useIntl(); - const donateData = useAppSelector((state) => state.donate.apiResponse); - const [frequency, setFrequency] = useState('one_time'); + // Nested function to allow passing parameters in onClick. const handleFrequencyToggle = useCallback((value: DonationFrequency) => { return () => { setFrequency(value); }; }, []); - const [currency, setCurrency] = useState( - donateData?.default_currency ?? 'EUR', - ); + const [currency, setCurrency] = useState(donateData.default_currency); const currencyOptions: SelectItem[] = useMemo( () => - Object.keys(donateData?.amounts.one_time ?? []).map((code) => ({ + Object.keys(donateData.amounts.one_time).map((code) => ({ value: code, text: code, })), - [donateData?.amounts], + [donateData.amounts], ); + // Amounts handling const [amount, setAmount] = useState( () => - donateData?.amounts[frequency][donateData.default_currency]?.[0] ?? 1000, + donateData.amounts[frequency][donateData.default_currency]?.[0] ?? + DefaultAmount, ); const handleAmountChange = useCallback((event: SyntheticEvent) => { + // Coerce the value into a valid amount depending on the source of the event. let newAmount = 1; if (event.target instanceof HTMLButtonElement) { newAmount = Number.parseInt(event.target.value); } else if (event.target instanceof HTMLInputElement) { newAmount = event.target.valueAsNumber * 100; } + // If invalid, just use the default. + if (Number.isNaN(newAmount) || newAmount < 1) { + newAmount = DefaultAmount; + } setAmount(newAmount); }, []); + // The input field is uncontrolled to not interfere with user input, but set the value to the state on blue. + const handleAmountBlur: FocusEventHandler = useCallback( + (event) => { + event.target.value = (amount / 100).toFixed(2); + }, + [amount], + ); const amountOptions: SelectItem[] = useMemo(() => { const formatter = new Intl.NumberFormat('en', { style: 'currency', currency, maximumFractionDigits: 0, }); - return Object.values(donateData?.amounts[frequency][currency] ?? {}).map( + return Object.values(donateData.amounts[frequency][currency] ?? {}).map( (value) => ({ value: value.toString(), text: formatter.format(value / 100), }), ); - }, [currency, donateData?.amounts, frequency]); + }, [currency, donateData.amounts, frequency]); const handleSubmit = useCallback(() => { onSubmit({ frequency, amount, currency }); }, [amount, currency, frequency, onSubmit]); - if (!donateData) { - return ; - } - return ( <>
@@ -111,8 +133,8 @@ export const DonateForm: FC> = ({ onSubmit }) => { type='number' min='1' step='0.01' - value={(amount / 100).toFixed(2)} onChange={handleAmountChange} + onBlur={handleAmountBlur} />