import React, { useCallback, useContext, useState } from 'react'; import { useSelector } from 'react-redux'; import { sprintf } from 'sprintf-js'; import { VoucherResponse } from '../../shared/daemon-rpc-types'; import { formatRelativeDate } from '../../shared/date-helper'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; import accountActions from '../redux/account/actions'; import { IReduxState } from '../redux/store'; import * as AppButton from './AppButton'; import ImageView from './ImageView'; import { ModalAlert } from './Modal'; import { StyledEmptyResponse, StyledErrorResponse, StyledInput, StyledLabel, StyledProgressResponse, StyledProgressWrapper, StyledSpinner, StyledStatusIcon, StyledTitle, } from './RedeemVoucherStyles'; const MIN_VOUCHER_LENGTH = 16; interface IRedeemVoucherContextValue { onSubmit: () => void; value: string; setValue: (value: string) => void; valueValid: boolean; submitting: boolean; response?: VoucherResponse; } const contextProviderMissingError = new Error(' is missing'); const RedeemVoucherContext = React.createContext({ onSubmit() { throw contextProviderMissingError; }, get value(): string { throw contextProviderMissingError; }, setValue(_) { throw contextProviderMissingError; }, get valueValid(): boolean { throw contextProviderMissingError; }, get submitting(): boolean { throw contextProviderMissingError; }, get response(): VoucherResponse { throw contextProviderMissingError; }, }); interface IRedeemVoucherProps { onSubmit?: () => void; onSuccess?: () => void; onFailure?: () => void; children?: React.ReactNode; } export function RedeemVoucherContainer(props: IRedeemVoucherProps) { const { onSubmit, onSuccess, onFailure } = props; const { submitVoucher } = useAppContext(); const { updateAccountExpiry } = useActions(accountActions); const [value, setValue] = useState(''); const [submitting, setSubmitting] = useState(false); const [response, setResponse] = useState(); const valueValid = value.length >= MIN_VOUCHER_LENGTH; const onSubmitWrapper = useCallback(async () => { if (!valueValid) { return; } const submitTimestamp = Date.now(); setSubmitting(true); onSubmit?.(); const response = await submitVoucher(value); // Show the spinner for at least half a second if it isn't successful. const submitDuration = Date.now() - submitTimestamp; if (response.type !== 'success' && submitDuration < 500) { await new Promise((resolve) => setTimeout(resolve, 500 - submitDuration)); } setSubmitting(false); setResponse(response); if (response.type === 'success') { onSuccess?.(); } else { onFailure?.(); } }, [value, valueValid, onSubmit, submitVoucher, updateAccountExpiry, onSuccess, onFailure]); return ( {props.children} ); } interface IRedeemVoucherInputProps { className?: string; } export function RedeemVoucherInput(props: IRedeemVoucherInputProps) { const { value, setValue, onSubmit, submitting, response } = useContext(RedeemVoucherContext); const disabled = submitting || response?.type === 'success'; const handleChange = useCallback( (value: string) => { setValue(value); }, [setValue], ); const onKeyPress = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Enter') { onSubmit(); } }, [onSubmit], ); return ( ); } export function RedeemVoucherResponse() { const { response, submitting } = useContext(RedeemVoucherContext); if (submitting) { return ( <> {messages.pgettext('redeem-voucher-view', 'Verifying voucher...')} ); } if (response) { switch (response.type) { case 'success': return ; case 'invalid': return ( {messages.pgettext('redeem-voucher-view', 'Voucher code is invalid.')} ); case 'already_used': return ( {messages.pgettext('redeem-voucher-view', 'Voucher code has already been used.')} ); case 'error': return ( {messages.pgettext('redeem-voucher-view', 'An error occurred.')} ); } } return ; } export function RedeemVoucherSubmitButton() { const { valueValid, onSubmit, submitting, response } = useContext(RedeemVoucherContext); const disabled = submitting || response?.type === 'success'; return ( {messages.pgettext('redeem-voucher-view', 'Redeem')} ); } interface IRedeemVoucherAlertProps { onClose?: () => void; } export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) { const { submitting, response } = useContext(RedeemVoucherContext); const accountData = useSelector((state: IReduxState) => state.account); const duration = (accountData.expiry && accountData.previousExpiry && formatRelativeDate(accountData.expiry, accountData.previousExpiry)) ?? ''; if (response?.type === 'success') { return ( {messages.gettext('Got it!')} , ]} close={props.onClose}> {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')} {sprintf(messages.gettext('%(duration)s was added to your account.'), { duration, })} ); } else { return ( , {messages.pgettext('redeem-voucher-alert', 'Cancel')} , ]} close={props.onClose}> {messages.pgettext('redeem-voucher-alert', 'Enter voucher code')} ); } } interface IRedeemVoucherButtonProps { className?: string; } export function RedeemVoucherButton(props: IRedeemVoucherButtonProps) { const [showAlert, setShowAlert] = useState(false); const onClick = useCallback(() => setShowAlert(true), []); const onClose = useCallback(() => setShowAlert(false), []); return ( <> {messages.pgettext('redeem-voucher-alert', 'Redeem voucher')} {showAlert && ( )} ); }