Use ErrorBoundary for donations
This commit is contained in:
parent
26b289a4ae
commit
eac9a69e20
11 changed files with 285 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
@include mixins.font-body-2;
|
||||
}
|
||||
|
||||
.DonationProgressModal .SpinnerV2 {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.DonationProgressModal .SpinnerV2__Path {
|
||||
color: variables.$color-ultramarine;
|
||||
}
|
||||
|
|
39
ts/components/DebugLogErrorModal.stories.tsx
Normal file
39
ts/components/DebugLogErrorModal.stories.tsx
Normal 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.',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
57
ts/components/DebugLogErrorModal.tsx
Normal file
57
ts/components/DebugLogErrorModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -34,6 +34,7 @@ export function DonationProgressModal(props: PropsType): JSX.Element {
|
|||
i18n={i18n}
|
||||
moduleClassName="DonationProgressModal"
|
||||
modalName="DonationProgressModal"
|
||||
noEscapeClose
|
||||
noMouseClose
|
||||
onClose={() => undefined}
|
||||
>
|
||||
|
|
73
ts/components/DonationsErrorBoundary.tsx
Normal file
73
ts/components/DonationsErrorBoundary.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue