Use ErrorBoundary for donations

This commit is contained in:
ayumi-signal 2025-08-11 11:44:10 -07:00 committed by GitHub
commit eac9a69e20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 285 additions and 7 deletions

View file

@ -73,7 +73,7 @@ instance while you make changes - they'll run until you stop them:
```
pnpm run dev:transpile # recompiles when you change .ts files
pnpm run dev:sass # recompiles when you change .scss files
pnpm run dev:styles # recompiles when you change .scss files
```
#### Known issues

View file

@ -4684,6 +4684,18 @@
"messageformat": "Please try again or contact support.",
"description": "Description text in popup dialog when user-initiated task has gone wrong"
},
"icu:DebugLogErrorModal__UnexpectedError": {
"messageformat": "An unexpected error occurred",
"description": "Title of the dialog shown when an unexpected user interface or coding bug occurs. The dialog's description text requests the user to submit debug logs."
},
"icu:DebugLogErrorModal__SubmitDebugLog": {
"messageformat": "Submit debug log",
"description": "Primary button text in the dialog shown when an unexpected user interface or coding bug occurs. Clicking the button will open the Debug Log submission dialog."
},
"icu:DebugLogErrorModal__SubmitDebugLog__Cancel": {
"messageformat": "No thanks",
"description": "Secondary button text in the dialog shown when an unexpected user interface or coding bug occurs. Clicking the button will dismiss the error dialog."
},
"icu:Confirmation--confirm": {
"messageformat": "Okay",
"description": "Button to dismiss popup dialog when user-initiated task has gone wrong"
@ -9024,12 +9036,16 @@
},
"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"
"description": "Title of the dialog shown when some unknown error has happened during a user's attempted donation. This will show when we detect an error but haven't designed a string for the error type."
},
"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:DonationsErrorBoundary__DonationUnexpectedError": {
"messageformat": "Try again or submit a debug log to Support for help completing your donation. Debug logs helps us diagnose and fix the issue, and do not contain identifying information.",
"description": "Description of the dialog shown when an unexpected user interface or coding bug occurs while using a donations-related part of the app."
},
"icu:Donations__Processing": {
"messageformat": "Processing donation...",
"description": "Explainer text for donation progress dialog"

View file

@ -21,6 +21,10 @@
@include mixins.font-body-2;
}
.DonationProgressModal .SpinnerV2 {
margin-inline: auto;
}
.DonationProgressModal .SpinnerV2__Path {
color: variables.$color-ultramarine;
}

View file

@ -0,0 +1,39 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import type { PropsType } from './DebugLogErrorModal';
import { DebugLogErrorModal } from './DebugLogErrorModal';
const { i18n } = window.SignalContext;
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
description: overrideProps.description ?? '',
i18n,
onClose: action('onClick'),
onSubmitDebugLog: action('onSubmitDebugLog'),
});
export default {
title: 'Components/DebugLogErrorModal',
argTypes: {},
args: {},
} satisfies Meta<PropsType>;
export function Default(): JSX.Element {
return <DebugLogErrorModal {...createProps()} />;
}
export function Donations(): JSX.Element {
return (
<DebugLogErrorModal
{...createProps({
description:
'Try again or submit a debug log to Support for help completing your donation. Debug logs helps us diagnose and fix the issue, and do not contain identifying information.',
})}
/>
);
}

View file

@ -0,0 +1,57 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { LocalizerType } from '../types/Util';
import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button';
export type PropsType = {
description?: string;
i18n: LocalizerType;
onClose: () => void;
onSubmitDebugLog: () => void;
};
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
export function DebugLogErrorModal(props: PropsType): JSX.Element {
const { description, i18n, onClose, onSubmitDebugLog } = props;
const footer = (
<>
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
{i18n('icu:DebugLogErrorModal__SubmitDebugLog__Cancel')}
</Button>
<Button
onClick={() => {
onSubmitDebugLog();
onClose();
}}
ref={focusRef}
variant={ButtonVariant.Primary}
>
{i18n('icu:DebugLogErrorModal__SubmitDebugLog')}
</Button>
</>
);
return (
<Modal
modalName="DebugLogErrorModal"
i18n={i18n}
onClose={onClose}
title={i18n('icu:DebugLogErrorModal__UnexpectedError')}
modalFooter={footer}
>
<div className="module-error-modal__description">
{description || i18n('icu:ErrorModal--description')}
</div>
</Modal>
);
}

View file

@ -34,6 +34,7 @@ export function DonationProgressModal(props: PropsType): JSX.Element {
i18n={i18n}
moduleClassName="DonationProgressModal"
modalName="DonationProgressModal"
noEscapeClose
noMouseClose
onClose={() => undefined}
>

View file

@ -0,0 +1,73 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode, ErrorInfo } from 'react';
import React, { Component, useCallback } from 'react';
import { createLogger } from '../logging/log';
import * as Errors from '../types/errors';
const log = createLogger('DonationsErrorBoundary');
type ErrorBoundaryProps = Readonly<{
children: ReactNode;
onError: (error: unknown, info: ErrorInfo) => void;
fallback: (error: unknown) => ReactNode;
}>;
type ErrorBoundaryState = {
caught?: { error: unknown };
};
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
// eslint-disable-next-line react/state-in-constructor
override state: ErrorBoundaryState = {};
static getDerivedStateFromError(error: unknown) {
return { caught: { error } };
}
override componentDidCatch(error: unknown, info: ErrorInfo) {
this.props.onError(error, info);
}
override render() {
if (this.state.caught != null) {
return this.props.fallback(this.state.caught.error);
}
return this.props.children;
}
}
export type DonationsErrorBoundaryProps = Readonly<{
children: ReactNode;
}>;
export function DonationsErrorBoundary(
props: DonationsErrorBoundaryProps
): JSX.Element {
const fallback = useCallback(() => {
return <div className="DonationsErrorBoundary" />;
}, []);
const handleError = useCallback((error: unknown, info: ErrorInfo) => {
log.error(
'DonationsErrorBoundary: Caught error',
Errors.toLogFormat(error),
info.componentStack
);
if (window.reduxActions) {
window.reduxActions.globalModals.showDebugLogErrorModal({
description: window.i18n(
'icu:DonationsErrorBoundary__DonationUnexpectedError'
),
});
}
}, []);
return (
<ErrorBoundary fallback={fallback} onError={handleError}>
{props.children}
</ErrorBoundary>
);
}

View file

@ -80,6 +80,13 @@ export type PropsType = {
description?: string;
title?: string | null;
}) => JSX.Element;
// DebugLogErrorModal
debugLogErrorModalProps:
| {
description?: string;
}
| undefined;
renderDebugLogErrorModal: (opts: { description?: string }) => JSX.Element;
// DeleteMessageModal
deleteMessagesProps: DeleteMessagesPropsType | undefined;
renderDeleteMessagesModal: () => JSX.Element;
@ -186,6 +193,9 @@ export function GlobalModalContainer({
// ErrorModal
errorModalProps,
renderErrorModal,
// DebugLogErrorModal
debugLogErrorModalProps,
renderDebugLogErrorModal,
// DeleteMessageModal
deleteMessagesProps,
renderDeleteMessagesModal,
@ -263,6 +273,11 @@ export function GlobalModalContainer({
return renderErrorModal(errorModalProps);
}
// Errors where we want them to submit a debug log
if (debugLogErrorModalProps) {
return renderDebugLogErrorModal(debugLogErrorModalProps);
}
// Safety Number
if (hasSafetyNumberChangeModal || safetyNumberChangedBlockingData) {
return renderSendAnywayDialog();

View file

@ -116,6 +116,9 @@ export type GlobalModalsStateType = ReadonlyDeep<{
criticalIdlePrimaryDeviceModal: boolean;
deleteMessagesProps?: DeleteMessagesPropsType;
draftGifMessageSendModalProps: SmartDraftGifMessageSendModalProps | null;
debugLogErrorModalProps?: {
description?: string;
};
editHistoryMessages?: EditHistoryMessagesType;
editNicknameAndNoteModalProps: EditNicknameAndNoteModalPropsType | null;
errorModalProps?: {
@ -200,6 +203,8 @@ const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW';
const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
export const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL';
const CLOSE_DEBUG_LOG_ERROR_MODAL = 'globalModals/CLOSE_DEBUG_LOG_ERROR_MODAL';
const SHOW_DEBUG_LOG_ERROR_MODAL = 'globalModals/SHOW_DEBUG_LOG_ERROR_MODAL';
const TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL =
'globalModals/TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL';
const TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION =
@ -419,6 +424,17 @@ export type ShowErrorModalActionType = ReadonlyDeep<{
};
}>;
type CloseDebugLogErrorModalActionType = ReadonlyDeep<{
type: typeof CLOSE_DEBUG_LOG_ERROR_MODAL;
}>;
type ShowDebugLogErrorModalActionType = ReadonlyDeep<{
type: typeof SHOW_DEBUG_LOG_ERROR_MODAL;
payload: {
description?: string;
};
}>;
type CloseMediaPermissionsModalActionType = ReadonlyDeep<{
type: typeof CLOSE_MEDIA_PERMISSIONS_MODAL;
}>;
@ -482,6 +498,7 @@ type CloseEditHistoryModalActionType = ReadonlyDeep<{
export type GlobalModalsActionType = ReadonlyDeep<
| CloseEditHistoryModalActionType
| CloseDebugLogErrorModalActionType
| CloseErrorModalActionType
| CloseMediaPermissionsModalActionType
| CloseGV2MigrationDialogActionType
@ -504,6 +521,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowBackfillFailureModalActionType
| ShowCriticalIdlePrimaryDeviceModalActionType
| ShowContactModalActionType
| ShowDebugLogErrorModalActionType
| ShowEditHistoryModalActionType
| ShowErrorModalActionType
| ShowLowDiskSpaceBackupImportModalActionType
@ -538,6 +556,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
// Action Creators
export const actions = {
closeDebugLogErrorModal,
closeEditHistoryModal,
closeErrorModal,
closeGV2MigrationDialog,
@ -560,6 +579,7 @@ export const actions = {
showBlockingSafetyNumberChangeDialog,
showContactModal,
showCriticalIdlePrimaryDeviceModal,
showDebugLogErrorModal,
showEditHistoryModal,
showErrorModal,
showGV2MigrationDialog,
@ -1095,6 +1115,25 @@ function showErrorModal({
};
}
function closeDebugLogErrorModal(): CloseDebugLogErrorModalActionType {
return {
type: CLOSE_DEBUG_LOG_ERROR_MODAL,
};
}
function showDebugLogErrorModal({
description,
}: {
description?: string;
}): ShowDebugLogErrorModalActionType {
return {
type: SHOW_DEBUG_LOG_ERROR_MODAL,
payload: {
description,
},
};
}
function closeMediaPermissionsModal(): CloseMediaPermissionsModalActionType {
return {
type: CLOSE_MEDIA_PERMISSIONS_MODAL,
@ -1591,6 +1630,20 @@ export function reducer(
};
}
if (action.type === CLOSE_DEBUG_LOG_ERROR_MODAL) {
return {
...state,
debugLogErrorModalProps: undefined,
};
}
if (action.type === SHOW_DEBUG_LOG_ERROR_MODAL) {
return {
...state,
debugLogErrorModalProps: action.payload,
};
}
if (action.type === TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL) {
return {
...state,

View file

@ -32,6 +32,7 @@ import { SmartCallLinkPendingParticipantModal } from './CallLinkPendingParticipa
import { SmartAttachmentNotAvailableModal } from './AttachmentNotAvailableModal';
import { SmartProfileNameWarningModal } from './ProfileNameWarningModal';
import { SmartDraftGifMessageSendModal } from './DraftGifMessageSendModal';
import { DebugLogErrorModal } from '../../components/DebugLogErrorModal';
function renderCallLinkAddNameModal(): JSX.Element {
return <SmartCallLinkAddNameModal />;
@ -128,6 +129,7 @@ export const SmartGlobalModalContainer = memo(
confirmLeaveCallModalState,
contactModalState,
criticalIdlePrimaryDeviceModal,
debugLogErrorModalProps,
deleteMessagesProps,
draftGifMessageSendModalProps,
editHistoryMessages,
@ -153,6 +155,7 @@ export const SmartGlobalModalContainer = memo(
} = useSelector(getGlobalModalsState);
const {
closeDebugLogErrorModal,
closeErrorModal,
closeMediaPermissionsModal,
hideCriticalIdlePrimaryDeviceModal,
@ -210,6 +213,18 @@ export const SmartGlobalModalContainer = memo(
[closeErrorModal, i18n]
);
const renderDebugLogErrorModal = useCallback(
({ description }: { description?: string }) => (
<DebugLogErrorModal
description={description}
i18n={i18n}
onClose={closeDebugLogErrorModal}
onSubmitDebugLog={() => window.IPC.showDebugLog()}
/>
),
[closeDebugLogErrorModal, i18n]
);
return (
<GlobalModalContainer
attachmentNotAvailableModalType={attachmentNotAvailableModalType}
@ -225,6 +240,7 @@ export const SmartGlobalModalContainer = memo(
confirmLeaveCallModalState={confirmLeaveCallModalState}
contactModalState={contactModalState}
criticalIdlePrimaryDeviceModal={criticalIdlePrimaryDeviceModal}
debugLogErrorModalProps={debugLogErrorModalProps}
editHistoryMessages={editHistoryMessages}
editNicknameAndNoteModalProps={editNicknameAndNoteModalProps}
errorModalProps={errorModalProps}
@ -263,6 +279,7 @@ export const SmartGlobalModalContainer = memo(
}
renderConfirmLeaveCallModal={renderConfirmLeaveCallModal}
renderContactModal={renderContactModal}
renderDebugLogErrorModal={renderDebugLogErrorModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderEditNicknameAndNoteModal={renderEditNicknameAndNoteModal}
renderErrorModal={renderErrorModal}

View file

@ -84,6 +84,7 @@ import {
resumeBackupMediaDownload,
cancelBackupMediaDownload,
} from '../../util/backupMediaDownload';
import { DonationsErrorBoundary } from '../../components/DonationsErrorBoundary';
const DEFAULT_NOTIFICATION_SETTING = 'message';
@ -115,11 +116,13 @@ function renderDonationsPane({
setPage: (page: SettingsPage) => void;
}): JSX.Element {
return (
<DonationsErrorBoundary>
<SmartPreferencesDonations
contentsRef={contentsRef}
page={page}
setPage={setPage}
/>
</DonationsErrorBoundary>
);
}