mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-27 18:10:58 +00:00
allow uncontrolled input and add a back button on checkout.
This commit is contained in:
parent
a3cf9c669d
commit
caec59c46d
|
|
@ -2,15 +2,18 @@ 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 <LoadingIndicator />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='donate.checkout.instructions'
|
||||
defaultMessage="Your checkout session is opened in another tab. If you don't see it, <link>click here</link>."
|
||||
|
|
@ -23,5 +26,12 @@ export const DonateCheckoutHint: FC<{ donateUrl?: string }> = ({
|
|||
}}
|
||||
tagName='p'
|
||||
/>
|
||||
<Button secondary onClick={onBack}>
|
||||
<FormattedMessage
|
||||
id='donate.checkout.back'
|
||||
defaultMessage='Edit donation'
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,6 +137,13 @@
|
|||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.checkout {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
.illustration {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ const DonateModal: FC<DonateModalProps> = 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<DonateModalProps> = forwardRef(({ onClose }, ref) => {
|
|||
})}
|
||||
>
|
||||
{state === 'start' && <DonateForm onSubmit={handleCheckout} />}
|
||||
{state === 'checkout' && <DonateCheckoutHint donateUrl={donateUrl} />}
|
||||
{state === 'checkout' && (
|
||||
<DonateCheckoutHint
|
||||
donateUrl={donateUrl}
|
||||
onBack={handleCheckoutBack}
|
||||
/>
|
||||
)}
|
||||
{state === 'success' && <DonateSuccess onClose={onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<Required<DonateFormProps>> = ({ onSubmit }) => {
|
||||
const DefaultAmount = 1000; // 10.00
|
||||
|
||||
export const DonateForm: FC<DonateFormProps> = (props) => {
|
||||
const donateData = useAppSelector((state) => state.donate.apiResponse);
|
||||
if (!donateData) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
return <DonateFormInner {...props} data={donateData} />;
|
||||
};
|
||||
|
||||
export const DonateFormInner: FC<
|
||||
DonateFormProps & { data: DonateServerResponse }
|
||||
> = ({ onSubmit, data: donateData }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const donateData = useAppSelector((state) => state.donate.apiResponse);
|
||||
|
||||
const [frequency, setFrequency] = useState<DonationFrequency>('one_time');
|
||||
// Nested function to allow passing parameters in onClick.
|
||||
const handleFrequencyToggle = useCallback((value: DonationFrequency) => {
|
||||
return () => {
|
||||
setFrequency(value);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [currency, setCurrency] = useState<string>(
|
||||
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<HTMLInputElement> = 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 <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='row'>
|
||||
|
|
@ -111,8 +133,8 @@ export const DonateForm: FC<Required<DonateFormProps>> = ({ onSubmit }) => {
|
|||
type='number'
|
||||
min='1'
|
||||
step='0.01'
|
||||
value={(amount / 100).toFixed(2)}
|
||||
onChange={handleAmountChange}
|
||||
onBlur={handleAmountBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user