diff --git a/app/javascript/mastodon/features/donate/api.ts b/app/javascript/mastodon/features/donate/api.ts deleted file mode 100644 index 4ed80e843ff..00000000000 --- a/app/javascript/mastodon/features/donate/api.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect, useState } from 'react'; - -import initialState from '@/mastodon/initial_state'; - -// TODO: Proxy this through at least an env var. -const API_URL = 'https://api.joinmastodon.org/v1/donations/campaigns/active'; - -export const DONATION_FREQUENCIES = ['one_time', 'monthly', 'yearly'] as const; -export type DonationFrequency = (typeof DONATION_FREQUENCIES)[number]; - -export const LOCALE = initialState?.meta.locale ?? 'en'; - -export function useDonateApi() { - const [response, setResponse] = useState(null); - - const [seed, setSeed] = useState(0); - useEffect(() => { - try { - const storedSeed = localStorage.getItem('donate_seed'); - if (storedSeed) { - setSeed(Number.parseInt(storedSeed, 10)); - return; - } - const newSeed = Math.floor(Math.random() * 99) + 1; - localStorage.setItem('donate_seed', newSeed.toString()); - setSeed(newSeed); - } catch { - // No local storage available, just set a seed for this session. - setSeed(Math.floor(Math.random() * 99) + 1); - } - }, []); - - useEffect(() => { - if (!seed) { - return; - } - fetchCampaign({ locale: LOCALE, seed }) - .then((res) => { - setResponse(res); - }) - .catch((reason: unknown) => { - console.warn('Error fetching donation campaign:', reason); - }); - }, [seed]); - - return response; -} - -export interface DonateServerResponse { - id: string; - amounts: Record; - donation_url: string; - banner_message: string; - banner_button_text: string; - donation_message: string; - donation_button_text: string; - donation_success_post: string; - default_currency: string; -} - -export interface DonateCheckoutArgs { - frequency: DonationFrequency; - amount: number; - currency: string; -} - -type DonateAmount = Record; - -async function fetchCampaign( - params: DonateServerRequest, -): Promise { - // Create the URL with query parameters. - const url = new URL(API_URL); - for (const [key, value] of Object.entries(params)) { - // Check to make TS happy. - if (typeof value === 'string' || typeof value === 'number') { - url.searchParams.append(key, value.toString()); - } - } - url.searchParams.append('platform', 'android'); - url.searchParams.append('source', 'menu'); - - const response = await fetch(url); - if (!response.ok) { - return null; - } - return response.json() as Promise; -} - -interface DonateServerRequest { - locale: string; - seed: number; - return_url?: string; -} diff --git a/app/javascript/mastodon/features/donate/checkout.tsx b/app/javascript/mastodon/features/donate/checkout.tsx new file mode 100644 index 00000000000..10da37c6140 --- /dev/null +++ b/app/javascript/mastodon/features/donate/checkout.tsx @@ -0,0 +1,27 @@ +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; + +export const DonateCheckoutHint: FC<{ donateUrl?: string }> = ({ + donateUrl, +}) => { + if (!donateUrl) { + return ; + } + return ( + ( + + {chunks} + + ), + }} + tagName='p' + /> + ); +}; diff --git a/app/javascript/mastodon/features/donate/donate_modal.scss b/app/javascript/mastodon/features/donate/donate_modal.scss index d64a42520da..29eda7aec37 100644 --- a/app/javascript/mastodon/features/donate/donate_modal.scss +++ b/app/javascript/mastodon/features/donate/donate_modal.scss @@ -76,11 +76,18 @@ .submit { padding: 0.6rem 1rem; + + > svg { + height: 1em; + } } button > svg { fill: currentColor; - height: 1em; + } + + .muted { + color: var(--input-placeholder-color); } .footer { @@ -91,15 +98,15 @@ .success { .illustration { + width: 100%; max-width: 350px; margin: 0 auto 1rem; } h2 { - font-size: 16px; - line-height: 24px; + font-size: 1.4rem; + line-height: 1; font-weight: 500; - letter-spacing: 0.15px; } } } diff --git a/app/javascript/mastodon/features/donate/donate_modal.tsx b/app/javascript/mastodon/features/donate/donate_modal.tsx index b6a4751e212..acd30881f47 100644 --- a/app/javascript/mastodon/features/donate/donate_modal.tsx +++ b/app/javascript/mastodon/features/donate/donate_modal.tsx @@ -5,12 +5,12 @@ import { defineMessages, useIntl } from 'react-intl'; import classNames from 'classnames'; +import type { DonationFrequency } from '@/mastodon/api_types/donate'; import { IconButton } from '@/mastodon/components/icon_button'; -import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { useAppSelector } from '@/mastodon/store'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import type { DonateCheckoutArgs } from './api'; -import { useDonateApi } from './api'; +import { DonateCheckoutHint } from './checkout'; import { DonateForm } from './form'; import { DonateSuccess } from './success'; @@ -20,6 +20,12 @@ interface DonateModalProps { onClose: () => void; } +export interface DonateCheckoutArgs { + frequency: DonationFrequency; + amount: number; + currency: string; +} + const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); @@ -31,9 +37,9 @@ const CHECKOUT_URL = 'http://localhost:3001/donate/checkout'; const DonateModal: FC = forwardRef(({ onClose }, ref) => { const intl = useIntl(); - const donationData = useDonateApi() ?? undefined; + const donationData = useAppSelector((state) => state.donate.apiResponse); - const [donateUrl, setDonateUrl] = useState(null); + const [donateUrl, setDonateUrl] = useState(); const handleCheckout = useCallback( ({ frequency, amount, currency }: DonateCheckoutArgs) => { const params = new URLSearchParams({ @@ -85,37 +91,15 @@ const DonateModal: FC = forwardRef(({ onClose }, ref) => { />
- {state === 'start' && - (donationData ? ( - - ) : ( - - ))} - {state === 'checkout' && ( -

- Your session is opened in another tab. - {donateUrl && ( - <> - {' '} - If you don't see it, - {/* eslint-disable-next-line react/jsx-no-target-blank -- We want access to the opener in order to detect success. */} - - click here - - . - - )} -

- )} - {state === 'success' && donationData && ( - - )} + {state === 'start' && } + {state === 'checkout' && } + {state === 'success' && }
diff --git a/app/javascript/mastodon/features/donate/form.tsx b/app/javascript/mastodon/features/donate/form.tsx index 99243b22ba2..4ac541eba31 100644 --- a/app/javascript/mastodon/features/donate/form.tsx +++ b/app/javascript/mastodon/features/donate/form.tsx @@ -5,17 +5,16 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import classNames from 'classnames'; +import type { 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'; import type { SelectItem } from '@/mastodon/components/dropdown_selector'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { useAppSelector } from '@/mastodon/store'; import ExternalLinkIcon from '@/material-icons/400-24px/open_in_new.svg?react'; -import type { - DonateCheckoutArgs, - DonateServerResponse, - DonationFrequency, -} from './api'; +import type { DonateCheckoutArgs } from './donate_modal'; const messages = defineMessages({ one_time: { id: 'donate.frequency.one_time', defaultMessage: 'Just once' }, @@ -24,16 +23,14 @@ const messages = defineMessages({ }); interface DonateFormProps { - data?: DonateServerResponse; onSubmit: (args: DonateCheckoutArgs) => void; } -export const DonateForm: FC> = ({ - data, - onSubmit, -}) => { +export const DonateForm: FC> = ({ onSubmit }) => { const intl = useIntl(); + const donateData = useAppSelector((state) => state.donate.apiResponse); + const [frequency, setFrequency] = useState('one_time'); const handleFrequencyToggle = useCallback((value: DonationFrequency) => { return () => { @@ -41,18 +38,21 @@ export const DonateForm: FC> = ({ }; }, []); - const [currency, setCurrency] = useState(data.default_currency); + const [currency, setCurrency] = useState( + donateData?.default_currency ?? 'EUR', + ); const currencyOptions: SelectItem[] = useMemo( () => - Object.keys(data.amounts.one_time).map((code) => ({ + Object.keys(donateData?.amounts.one_time ?? []).map((code) => ({ value: code, text: code, })), - [data.amounts], + [donateData?.amounts], ); const [amount, setAmount] = useState( - () => data.amounts[frequency][data.default_currency]?.[0] ?? 1000, + () => + donateData?.amounts[frequency][donateData.default_currency]?.[0] ?? 1000, ); const handleAmountChange = useCallback((event: SyntheticEvent) => { let newAmount = 1; @@ -69,29 +69,35 @@ export const DonateForm: FC> = ({ currency, maximumFractionDigits: 0, }); - return Object.values(data.amounts[frequency][currency] ?? {}).map( + return Object.values(donateData?.amounts[frequency][currency] ?? {}).map( (value) => ({ value: value.toString(), text: formatter.format(value / 100), }), ); - }, [currency, data.amounts, frequency]); + }, [currency, donateData?.amounts, frequency]); const handleSubmit = useCallback(() => { onSubmit({ frequency, amount, currency }); }, [amount, currency, frequency, onSubmit]); + if (!donateData) { + return ; + } + return ( <>
- {(Object.keys(data.amounts) as DonationFrequency[]).map((freq) => ( - - ))} + {(Object.keys(donateData.amounts) as DonationFrequency[]).map( + (freq) => ( + + ), + )}
diff --git a/app/javascript/mastodon/features/donate/success.tsx b/app/javascript/mastodon/features/donate/success.tsx index 5c128ce7cad..a9d276937fc 100644 --- a/app/javascript/mastodon/features/donate/success.tsx +++ b/app/javascript/mastodon/features/donate/success.tsx @@ -4,29 +4,23 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; import donateIllustration from '@/images/donation_successful.png'; -import { focusCompose, resetCompose } from '@/mastodon/actions/compose'; +import { composeDonateShare } from '@/mastodon/actions/donate'; import { Button } from '@/mastodon/components/button'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import ShareIcon from '@/material-icons/400-24px/share.svg?react'; -import type { DonateServerResponse } from './api'; - interface DonateSuccessProps { - data: DonateServerResponse; onClose: () => void; } -export const DonateSuccess: FC = ({ data, onClose }) => { +export const DonateSuccess: FC = ({ onClose }) => { const hasComposerContent = useAppSelector( (state) => !!state.compose.get('text'), ); const dispatch = useAppDispatch(); const handleShare = useCallback(() => { - const shareText = data.donation_success_post; - dispatch(resetCompose()); - dispatch(focusCompose(shareText)); - onClose(); - }, [data.donation_success_post, dispatch, onClose]); + dispatch(composeDonateShare()); + }, [dispatch]); return ( <> @@ -41,11 +35,12 @@ export const DonateSuccess: FC = ({ data, onClose }) => { defaultMessage='Thanks for your donation!' tagName='h2' /> - +

+ +