// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { MutableRefObject } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import classNames from 'classnames'; import type { LocalizerType } from '../types/Util'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; import { Button, ButtonVariant } from './Button'; import type { DonationErrorType, HumanDonationAmount, } from '../types/Donations'; import { ONE_TIME_DONATION_CONFIG_ID, type DonationWorkflow, type OneTimeDonationHumanAmounts, } from '../types/Donations'; import type { CardCvcError, CardExpirationError, CardNumberError, } from '../types/DonationsCardForm'; import { cardFormToCardDetail, getCardFormSettings, getPossibleCardFormats, parseCardCvc, parseCardExpiration, parseCardForm, parseCardNumber, } from '../types/DonationsCardForm'; import { brandHumanDonationAmount, parseCurrencyString, toHumanCurrencyString, toStripeDonationAmount, } from '../util/currency'; import { Input } from './Input'; import { PreferencesContent } from './Preferences'; import type { SubmitDonationType } from '../state/ducks/donations'; import { Select } from './Select'; import { DonateInputCardNumber, getCardNumberErrorMessage, } from './preferences/donations/DonateInputCardNumber'; import { DonateInputCardExp, getCardExpirationErrorMessage, } from './preferences/donations/DonateInputCardExp'; import { DonateInputCardCvc, getCardCvcErrorMessage, } from './preferences/donations/DonateInputCardCvc'; import { I18n } from './I18n'; const SUPPORT_URL = 'https://support.signal.org/hc/requests/new?desktop'; export type PropsDataType = { i18n: LocalizerType; donationAmountsConfig: OneTimeDonationHumanAmounts | undefined; lastError: DonationErrorType | undefined; validCurrencies: ReadonlyArray; workflow: DonationWorkflow | undefined; renderDonationHero: () => JSX.Element; }; type PropsHousekeepingType = { contentsRef: MutableRefObject; }; type PropsActionType = { clearWorkflow: () => void; submitDonation: (payload: SubmitDonationType) => void; onBack: () => void; }; export type PropsType = PropsDataType & PropsActionType & PropsHousekeepingType; export function PreferencesDonateFlow({ contentsRef, i18n, donationAmountsConfig, lastError, validCurrencies, workflow, clearWorkflow, renderDonationHero, submitDonation, onBack, }: PropsType): JSX.Element { const tryClose = useRef<() => void | undefined>(); const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ i18n, name: 'PreferencesDonateFlow', tryClose, }); const [step, setStep] = useState<'amount' | 'paymentDetails'>('amount'); const [amount, setAmount] = useState(); const [currency, setCurrency] = useState(); const [cardExpiration, setCardExpiration] = useState(''); const [cardNumber, setCardNumber] = useState(''); const [cardCvc, setCardCvc] = useState(''); const [isDonateDisabled, setIsDonateDisabled] = useState(false); const [cardNumberError, setCardNumberError] = useState(null); const [cardExpirationError, setCardExpirationError] = useState(null); const [cardCvcError, setCardCvcError] = useState(null); const possibleCardFormats = useMemo(() => { return getPossibleCardFormats(cardNumber); }, [cardNumber]); const cardFormSettings = useMemo(() => { return getCardFormSettings(possibleCardFormats); }, [possibleCardFormats]); const handleCardNumberChange = useCallback((value: string) => { setCardNumber(value); setCardNumberError(null); }, []); const handleCardNumberBlur = useCallback(() => { if (cardNumber !== '') { const result = parseCardNumber(cardNumber); setCardNumberError(result.error ?? null); } }, [cardNumber]); const handleCardExpirationChange = useCallback((value: string) => { setCardExpiration(value); setCardExpirationError(null); }, []); const handleCardExpirationBlur = useCallback(() => { if (cardExpiration !== '') { const result = parseCardExpiration(cardExpiration); setCardExpirationError(result.error ?? null); } }, [cardExpiration]); const handleCardCvcChange = useCallback((value: string) => { setCardCvc(value); setCardCvcError(null); }, []); const handleCardCvcBlur = useCallback(() => { if (cardCvc !== '') { const result = parseCardCvc(cardCvc, possibleCardFormats); setCardCvcError(result.error ?? null); } }, [cardCvc, possibleCardFormats]); const formattedCurrencyAmount = useMemo(() => { return toHumanCurrencyString({ amount, currency }); }, [amount, currency]); const handleAmountPickerResult = useCallback((result: AmountPickerResult) => { const { currency: pickedCurrency, amount: pickedAmount } = result; setAmount(pickedAmount); setCurrency(pickedCurrency); setStep('paymentDetails'); }, []); const handleDonateClicked = useCallback(() => { if (amount == null || currency == null) { return; } const paymentAmount = toStripeDonationAmount({ amount, currency }); const formResult = parseCardForm({ cardNumber, cardExpiration, cardCvc }); setCardNumberError(formResult.cardNumber.error ?? null); setCardExpirationError(formResult.cardExpiration.error ?? null); setCardCvcError(formResult.cardCvc.error ?? null); const cardDetail = cardFormToCardDetail(formResult); if (cardDetail == null) { return; } setIsDonateDisabled(true); submitDonation({ currencyType: currency, paymentAmount, paymentDetail: cardDetail, }); }, [ amount, cardCvc, cardExpiration, cardNumber, currency, setIsDonateDisabled, submitDonation, ]); useEffect(() => { if (!workflow || lastError) { setIsDonateDisabled(false); } }, [lastError, setIsDonateDisabled, workflow]); const onTryClose = useCallback(() => { const onDiscard = () => { clearWorkflow(); }; const isDirty = Boolean( (cardExpiration || cardNumber || cardCvc) && !isDonateDisabled ); confirmDiscardIf(isDirty, onDiscard); }, [ cardCvc, cardExpiration, cardNumber, clearWorkflow, confirmDiscardIf, isDonateDisabled, ]); tryClose.current = onTryClose; let innerContent: JSX.Element; let handleBack: () => void; if (step === 'amount') { innerContent = ( <> {renderDonationHero()} ); // Dismiss DonateFlow and return to Donations home handleBack = () => onBack(); } else { innerContent = (
{workflow && (

Current Workflow

{JSON.stringify(workflow)}
)}
          {amount} {currency}
        
{cardNumberError != null && ( {getCardNumberErrorMessage(i18n, cardNumberError)} )} {cardExpirationError && ( {getCardExpirationErrorMessage(i18n, cardExpirationError)} )} {cardCvcError && ( {getCardCvcErrorMessage(i18n, cardCvcError)} )}
); handleBack = () => { setStep('amount'); }; } const backButton = ( ); } type HelpFooterProps = { i18n: LocalizerType; showOneTimeOnlyNotice?: boolean; }; function HelpFooter({ i18n, showOneTimeOnlyNotice, }: HelpFooterProps): JSX.Element { const contactSupportLink = (parts: Array) => ( {parts} ); return (
{showOneTimeOnlyNotice && (
{i18n('icu:DonateFlow__desktop-one-time-only-notice')}
)}
); }