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 { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Button } from '@/mastodon/components/button';
|
||||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||||
|
|
||||||
export const DonateCheckoutHint: FC<{ donateUrl?: string }> = ({
|
export const DonateCheckoutHint: FC<{
|
||||||
donateUrl,
|
donateUrl?: string;
|
||||||
}) => {
|
onBack: () => void;
|
||||||
|
}> = ({ donateUrl, onBack }) => {
|
||||||
if (!donateUrl) {
|
if (!donateUrl) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='donate.checkout.instructions'
|
id='donate.checkout.instructions'
|
||||||
defaultMessage="Your checkout session is opened in another tab. If you don't see it, <link>click here</link>."
|
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'
|
tagName='p'
|
||||||
/>
|
/>
|
||||||
|
<Button secondary onClick={onBack}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='donate.checkout.back'
|
||||||
|
defaultMessage='Edit donation'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,13 @@
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkout {
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
.illustration {
|
.illustration {
|
||||||
width: 100%;
|
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
|
// Check response from opened page
|
||||||
const [state, setState] = useState<'start' | 'checkout' | 'success'>('start');
|
const [state, setState] = useState<'start' | 'checkout' | 'success'>('start');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -98,7 +103,12 @@ const DonateModal: FC<DonateModalProps> = forwardRef(({ onClose }, ref) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{state === 'start' && <DonateForm onSubmit={handleCheckout} />}
|
{state === 'start' && <DonateForm onSubmit={handleCheckout} />}
|
||||||
{state === 'checkout' && <DonateCheckoutHint donateUrl={donateUrl} />}
|
{state === 'checkout' && (
|
||||||
|
<DonateCheckoutHint
|
||||||
|
donateUrl={donateUrl}
|
||||||
|
onBack={handleCheckoutBack}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{state === 'success' && <DonateSuccess onClose={onClose} />}
|
{state === 'success' && <DonateSuccess onClose={onClose} />}
|
||||||
</div>
|
</div>
|
||||||
</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 { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
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 type { ButtonProps } from '@/mastodon/components/button';
|
||||||
import { Button } from '@/mastodon/components/button';
|
import { Button } from '@/mastodon/components/button';
|
||||||
import { Dropdown } from '@/mastodon/components/dropdown';
|
import { Dropdown } from '@/mastodon/components/dropdown';
|
||||||
|
|
@ -26,65 +29,84 @@ interface DonateFormProps {
|
||||||
onSubmit: (args: DonateCheckoutArgs) => void;
|
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 intl = useIntl();
|
||||||
|
|
||||||
const donateData = useAppSelector((state) => state.donate.apiResponse);
|
|
||||||
|
|
||||||
const [frequency, setFrequency] = useState<DonationFrequency>('one_time');
|
const [frequency, setFrequency] = useState<DonationFrequency>('one_time');
|
||||||
|
// Nested function to allow passing parameters in onClick.
|
||||||
const handleFrequencyToggle = useCallback((value: DonationFrequency) => {
|
const handleFrequencyToggle = useCallback((value: DonationFrequency) => {
|
||||||
return () => {
|
return () => {
|
||||||
setFrequency(value);
|
setFrequency(value);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [currency, setCurrency] = useState<string>(
|
const [currency, setCurrency] = useState(donateData.default_currency);
|
||||||
donateData?.default_currency ?? 'EUR',
|
|
||||||
);
|
|
||||||
const currencyOptions: SelectItem[] = useMemo(
|
const currencyOptions: SelectItem[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.keys(donateData?.amounts.one_time ?? []).map((code) => ({
|
Object.keys(donateData.amounts.one_time).map((code) => ({
|
||||||
value: code,
|
value: code,
|
||||||
text: code,
|
text: code,
|
||||||
})),
|
})),
|
||||||
[donateData?.amounts],
|
[donateData.amounts],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Amounts handling
|
||||||
const [amount, setAmount] = useState(
|
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) => {
|
const handleAmountChange = useCallback((event: SyntheticEvent) => {
|
||||||
|
// Coerce the value into a valid amount depending on the source of the event.
|
||||||
let newAmount = 1;
|
let newAmount = 1;
|
||||||
if (event.target instanceof HTMLButtonElement) {
|
if (event.target instanceof HTMLButtonElement) {
|
||||||
newAmount = Number.parseInt(event.target.value);
|
newAmount = Number.parseInt(event.target.value);
|
||||||
} else if (event.target instanceof HTMLInputElement) {
|
} else if (event.target instanceof HTMLInputElement) {
|
||||||
newAmount = event.target.valueAsNumber * 100;
|
newAmount = event.target.valueAsNumber * 100;
|
||||||
}
|
}
|
||||||
|
// If invalid, just use the default.
|
||||||
|
if (Number.isNaN(newAmount) || newAmount < 1) {
|
||||||
|
newAmount = DefaultAmount;
|
||||||
|
}
|
||||||
setAmount(newAmount);
|
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 amountOptions: SelectItem[] = useMemo(() => {
|
||||||
const formatter = new Intl.NumberFormat('en', {
|
const formatter = new Intl.NumberFormat('en', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency,
|
currency,
|
||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
});
|
});
|
||||||
return Object.values(donateData?.amounts[frequency][currency] ?? {}).map(
|
return Object.values(donateData.amounts[frequency][currency] ?? {}).map(
|
||||||
(value) => ({
|
(value) => ({
|
||||||
value: value.toString(),
|
value: value.toString(),
|
||||||
text: formatter.format(value / 100),
|
text: formatter.format(value / 100),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [currency, donateData?.amounts, frequency]);
|
}, [currency, donateData.amounts, frequency]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
onSubmit({ frequency, amount, currency });
|
onSubmit({ frequency, amount, currency });
|
||||||
}, [amount, currency, frequency, onSubmit]);
|
}, [amount, currency, frequency, onSubmit]);
|
||||||
|
|
||||||
if (!donateData) {
|
|
||||||
return <LoadingIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
|
|
@ -111,8 +133,8 @@ export const DonateForm: FC<Required<DonateFormProps>> = ({ onSubmit }) => {
|
||||||
type='number'
|
type='number'
|
||||||
min='1'
|
min='1'
|
||||||
step='0.01'
|
step='0.01'
|
||||||
value={(amount / 100).toFixed(2)}
|
|
||||||
onChange={handleAmountChange}
|
onChange={handleAmountChange}
|
||||||
|
onBlur={handleAmountBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user