Introduce new DonationErrorModal component

Co-authored-by: Scott Nonnenberg <scott@signal.org>
Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2025-07-15 11:58:17 -05:00 committed by GitHub
parent abd3afaaeb
commit 0a2f341602
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 180 additions and 6 deletions

View file

@ -8882,6 +8882,38 @@
"messageformat": "Thank you for supporting Signal. Your contribution helps fuel the mission of protecting free expression and enabling secure global communication for millions around the world, through open source privacy technology. If youre a resident of the United States, please retain this receipt for your tax records. Signal Technology Foundation is a tax-exempt nonprofit organization in the United States under section 501c3 of the Internal Revenue Code. Our Federal Tax ID is 82-4506840.",
"description": "Footer text shown on donation receipts explaining tax deductibility and Signal's mission"
},
"icu:Donations__PaymentMethodDeclined": {
"messageformat": "Payment method declined",
"description": "Title of the dialog shown with the user's provided payment method has not worked"
},
"icu:Donations__PaymentMethodDeclined__Description": {
"messageformat": "Try another payment method or contact your bank for more information.",
"description": "An explanation for the 'payment declined' dialog"
},
"icu:Donations__ErrorProcessingDonation": {
"messageformat": "Error processing donation",
"description": "Title of the dialog shown when a user's donation didn't seem to complete"
},
"icu:Donations__ErrorProcessingDonation__Description": {
"messageformat": "Try another payment method or contact your bank for more information.",
"description": "An explanation for the 'error processing' dialog"
},
"icu:Donations__Failed3dsValidation": {
"messageformat": "Verification Failed",
"description": "Title of the dialog shown when something went wrong processing a user's 3ds verification with their bank"
},
"icu:Donations__Failed3dsValidation__Description": {
"messageformat": "Additional verification step failed. Please try again.",
"description": "An explanation for the 'verification failed' dialog"
},
"icu:Donations__GenericError": {
"messageformat": "An error occurred with your donation",
"description": "Title of the dialog shown when some unknown error has happened during a user's attempted donation"
},
"icu:Donations__GenericError__Description": {
"messageformat": "Your donation might not have been processed. Click on “Donate to Signal” and then “Donation Receipts” to check your receipts and confirm.",
"description": "An explanation for the 'error occurred' dialog"
},
"icu:Donations__Processing": {
"messageformat": "Processing donation...",
"description": "Explainer text for donation progress dialog"

View file

@ -0,0 +1,17 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
.DonationErrorModal__width-container {
max-width: 420px;
}
// We include both for specificity
.module-Modal__title.DonationErrorModal__title {
@include mixins.font-title-medium;
}
.DonationErrorModal__body_inner {
@include mixins.font-body-2;
}

View file

@ -96,6 +96,7 @@
@use 'components/DeleteMessagesModal.scss';
@use 'components/DisappearingTimeDialog.scss';
@use 'components/DisappearingTimerSelect.scss';
@use 'components/DonationErrorModal.scss';
@use 'components/DonationProgressModal.scss';
@use 'components/DonationStillProcessingModal.scss';
@use 'components/DonationVerificationModal.scss';

View file

@ -0,0 +1,58 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import type { PropsType } from './DonationErrorModal';
import { DonationErrorModal } from './DonationErrorModal';
import { donationErrorTypeSchema } from '../types/Donations';
const { i18n } = window.SignalContext;
export default {
title: 'Components/DonationErrorModal',
} satisfies Meta<PropsType>;
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export function DonationProcessingError(): JSX.Element {
return (
<DonationErrorModal
{...defaultProps}
errorType={donationErrorTypeSchema.Enum.DonationProcessingError}
/>
);
}
export function Failed3dsValidation(): JSX.Element {
return (
<DonationErrorModal
{...defaultProps}
errorType={donationErrorTypeSchema.Enum.Failed3dsValidation}
/>
);
}
export function GeneralError(): JSX.Element {
return (
<DonationErrorModal
{...defaultProps}
errorType={donationErrorTypeSchema.Enum.GeneralError}
/>
);
}
export function PaymentDeclined(): JSX.Element {
return (
<DonationErrorModal
{...defaultProps}
errorType={donationErrorTypeSchema.Enum.PaymentDeclined}
/>
);
}

View file

@ -0,0 +1,68 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { ReactNode } from 'react';
import { missingCaseError } from '../util/missingCaseError';
import { donationErrorTypeSchema } from '../types/Donations';
import type { LocalizerType } from '../types/Util';
import type { DonationErrorType } from '../types/Donations';
import { Button } from './Button';
import { Modal } from './Modal';
export type PropsType = {
i18n: LocalizerType;
onClose: () => void;
errorType: DonationErrorType;
};
export function DonationErrorModal(props: PropsType): JSX.Element {
const { i18n, onClose } = props;
let title: string;
let body: ReactNode;
switch (props.errorType) {
case donationErrorTypeSchema.Enum.DonationProcessingError: {
title = i18n('icu:Donations__ErrorProcessingDonation');
body = i18n('icu:Donations__ErrorProcessingDonation__Description');
break;
}
case donationErrorTypeSchema.Enum.Failed3dsValidation: {
title = i18n('icu:Donations__Failed3dsValidation');
body = i18n('icu:Donations__Failed3dsValidation__Description');
break;
}
case donationErrorTypeSchema.Enum.GeneralError: {
title = i18n('icu:Donations__GenericError');
body = i18n('icu:Donations__GenericError__Description');
break;
}
case donationErrorTypeSchema.Enum.PaymentDeclined: {
title = i18n('icu:Donations__PaymentMethodDeclined');
body = i18n('icu:Donations__PaymentMethodDeclined__Description');
break;
}
default:
throw missingCaseError(props.errorType);
}
return (
<Modal
i18n={i18n}
modalFooter={
<Button onClick={onClose}>{i18n('icu:Confirmation--confirm')}</Button>
}
hasXButton
moduleClassName="DonationErrorModal"
modalName="DonationErrorModal"
onClose={onClose}
title={title}
>
{body}
</Modal>
);
}

View file

@ -13,16 +13,14 @@ export const donationStateSchema = z.enum([
]);
export const donationErrorTypeSchema = z.enum([
// Any 4xx error when adding payment method or confirming intent
'PaymentDeclined',
// Only used if we can't support 3DS validation for our first release
'CardNotSupported',
// Used if the user is redirected back from validation, but continuing forward fails
'Failed3dsValidation',
// Any other HTTPError during the process
'DonationProcessingError',
// Used if the user is redirected back from validation, but continuing forward fails
'Failed3dsValidation',
// Any other error
'GeneralError',
// Any 4xx error when adding payment method or confirming intent
'PaymentDeclined',
]);
export type DonationErrorType = z.infer<typeof donationErrorTypeSchema>;