diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c088012471..ff1bb2922a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -816,6 +816,14 @@ "messageformat": "Failed to process some frames during backup import. Please share your logs.", "description": "[Only shown to internal users] An error popup when we failed to process some parts of a backup import." }, + "icu:Toast--ReceiptSaved": { + "messageformat": "Receipt saved", + "description": "Toast message shown when a donation receipt has been successfully saved to disk" + }, + "icu:Toast--ReceiptSaveFailed": { + "messageformat": "Error saving receipt. Please try again.", + "description": "Toast message shown when a donation receipt fails to save to disk" + }, "icu:cannotSelectPhotosAndVideosAlongWithFiles": { "messageformat": "You can't select photos and videos along with files.", "description": "An error popup when the user has attempted to add an attachment" @@ -8778,29 +8786,82 @@ "messageformat": "What's New", "description": "Title for the whats new modal" }, + "icu:PreferencesDonations__title": { + "messageformat": "Privacy over Profit", + "description": "Title shown at the top of the donations preferences page" + }, + "icu:PreferencesDonations__description": { + "messageformat": "Private messaging, funded by you. No ads, no tracking, no compromise. Donate now to support Signal. Learn more", + "description": "Description text explaining Signal's donation model with learn more link" + }, + "icu:PreferencesDonations__donate-button": { + "messageformat": "Donate", + "description": "Button text to make a donation" + }, + "icu:PreferencesDonations__mobile-info": { + "messageformat": "Badges and monthly donations can be managed on your mobile device.", + "description": "Information about donations receipt syncing limitations" + }, + + "icu:PreferencesDonations__receipts": { + "messageformat": "Receipts", + "description": "Menu item to view donation receipts" + }, + + "icu:PreferencesDonations__faqs": { + "messageformat": "Donation FAQs", + "description": "Menu item to view donation FAQs" + }, + + "icu:PreferencesDonations--receiptList__info": { + "messageformat": "Receipts do not sync across devices. If you have reinstalled Signal, receipts from previous donations will not be available.", + "description": "Information about donation receipts syncing limitations" + }, + + "icu:PreferencesDonations--receiptList__empty-title": { + "messageformat": "No Receipts", + "description": "Title shown when there are no donation receipts" + }, + + "icu:PreferencesDonations__ReceiptModal--title": { + "messageformat": "Details", + "description": "Title of the donation receipt details modal" + }, + + "icu:PreferencesDonations__ReceiptModal--download": { + "messageformat": "Download receipt", + "description": "Button text to download a donation receipt" + }, + + "icu:PreferencesDonations__ReceiptModal--type-label": { + "messageformat": "Type", + "description": "Label for the donation type in donation receipt modal" + }, + + "icu:PreferencesDonations__ReceiptModal--date-paid-label": { + "messageformat": "Date paid", + "description": "Label for the payment date in donation receipt modal" + }, + "icu:DonationReceipt__title": { "messageformat": "Donation receipt", "description": "Title shown at the top of donation receipt documents" }, "icu:DonationReceipt__amount-label": { "messageformat": "Amount", - "description": "Label for the donation amount field on receipt" + "description": "Label for the donation amount field on donation receipt" }, "icu:DonationReceipt__type-label": { "messageformat": "Type", - "description": "Label for the donation type field on receipt" + "description": "Label for the donation type field on donation receipt" }, "icu:DonationReceipt__date-paid-label": { "messageformat": "Date paid", - "description": "Label for the payment date field on receipt" + "description": "Label for the payment date field on donation receipt" }, "icu:DonationReceipt__type-value--one-time": { - "messageformat": "One-time", - "description": "Value shown for one-time donations on receipt" - }, - "icu:DonationReceipt__payment-method-label": { - "messageformat": "Payment method", - "description": "(Deleted 2025/07/02) Label for the payment method field on receipt" + "messageformat": "One time", + "description": "Value shown for one-time donations on donation receipt" }, "icu:DonationReceipt__footer-text": { "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 you’re 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.", diff --git a/images/icons/v3/receipt/receipt.svg b/images/icons/v3/receipt/receipt.svg new file mode 100644 index 0000000000..af16158778 --- /dev/null +++ b/images/icons/v3/receipt/receipt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index 9c043ecdda..995dedc046 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -57,6 +57,7 @@ $color-white-alpha-55: rgba($color-white, 0.55); $color-white-alpha-60: rgba($color-white, 0.6); $color-white-alpha-70: rgba($color-white, 0.7); $color-white-alpha-80: rgba($color-white, 0.8); +$color-white-alpha-85: rgba($color-white, 0.85); $color-white-alpha-90: rgba($color-white, 0.9); $color-black-alpha-05: rgba($color-black, 0.05); @@ -75,6 +76,7 @@ $color-black-alpha-50: rgba($color-black, 0.5); $color-black-alpha-60: rgba($color-black, 0.6); $color-black-alpha-70: rgba($color-black, 0.7); $color-black-alpha-80: rgba($color-black, 0.8); +$color-black-alpha-85: rgba($color-black, 0.85); $color-black-alpha-90: rgba($color-black, 0.9); $color-transparent: rgba(0, 0, 0, 0); diff --git a/stylesheets/components/PreferencesDonations.scss b/stylesheets/components/PreferencesDonations.scss new file mode 100644 index 0000000000..b91d305e95 --- /dev/null +++ b/stylesheets/components/PreferencesDonations.scss @@ -0,0 +1,341 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +@use '../mixins'; +@use '../variables'; + +.PreferencesDonations { + display: flex; + flex-direction: column; + align-items: center; + padding-block: 0; + padding-inline: 0; + margin-inline-start: 24px; + margin-inline-end: 24px; + + &__title { + @include mixins.font-title-medium; + margin-bottom: 16px; + } + + &__description { + @include mixins.font-body-2; + text-align: center; + max-width: 320px; + margin-bottom: 24px; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + + &__read-more { + @include mixins.button-reset; + + & { + color: variables.$color-ultramarine; + } + + &:hover { + text-decoration: underline; + } + } + } + + &__donate-button { + margin-bottom: 24px; + } + + &__separator { + width: 100%; + height: 0.5px; + border: none; + margin: 0; + background-color: light-dark( + variables.$color-black-alpha-12, + variables.$color-white-alpha-12 + ); + } + + &__section-header { + @include mixins.font-body-2-bold; + width: 100%; + margin-top: 24px; + margin-bottom: 8px; + color: light-dark( + variables.$color-black-alpha-85, + variables.$color-white-alpha-85 + ); + } + + &__list { + margin-top: 24px; + width: 100%; + } + + &__list-item { + @include mixins.button-reset; + + & { + display: flex; + width: 100%; + align-items: center; + gap: 12px; + padding-block: 12px; + padding-inline: 24px; + border-radius: 5px; + } + + &:hover { + background: light-dark( + variables.$color-gray-02, + variables.$color-gray-80 + ); + } + + @include mixins.keyboard-mode { + &:focus { + outline: 2px solid variables.$color-ultramarine; + } + } + + &__icon { + width: 20px; + height: 20px; + + &--receipts::before { + content: ''; + display: block; + width: 20px; + height: 20px; + @include mixins.color-svg( + '../images/icons/v3/receipt/receipt.svg', + light-dark(variables.$color-gray-75, variables.$color-gray-15) + ); + } + + &--faqs::before { + content: ''; + display: block; + width: 20px; + height: 20px; + @include mixins.color-svg( + '../images/icons/v3/help/help-light.svg', + light-dark(variables.$color-gray-75, variables.$color-gray-15) + ); + } + } + + &__text { + @include mixins.font-body-1; + flex: 1; + color: light-dark(variables.$color-gray-90, variables.$color-gray-05); + } + + &__chevron { + &::before { + content: ''; + display: block; + width: 20px; + height: 20px; + @include mixins.color-svg( + '../images/icons/v3/chevron/chevron-right.svg', + light-dark(variables.$color-gray-45, variables.$color-gray-25) + ); + } + } + } +} + +// Receipts page specific styles +.PreferencesDonations--receiptList { + &__info { + margin-inline: 24px; + margin-bottom: 24px; + + &__text { + @include mixins.font-subtitle; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + } + } + + &-yearContainer { + width: 100%; + } + + &__year-header { + @include mixins.font-body-2-bold; + color: light-dark( + variables.$color-black-alpha-85, + variables.$color-white-alpha-85 + ); + padding-block: 8px; + padding-inline: 24px; + background-color: light-dark( + variables.$color-white, + variables.$color-gray-95 + ); + } + + &__list { + width: 100%; + } + + &__receipt-item { + @include mixins.button-reset; + + & { + display: flex; + align-items: center; + gap: 12px; + padding-block: 8px; + padding-inline: 24px; + border-radius: 5px; + width: 100%; + } + + &:hover { + background-color: light-dark( + variables.$color-gray-02, + variables.$color-gray-80 + ); + } + + // Placeholder for icon depending on receipt type + &__icon { + width: 36px; + height: 36px; + border-radius: 18px; + background-color: variables.$color-ultramarine-pale; + flex-shrink: 0; + } + + &__details { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + } + + &__date { + @include mixins.font-body-1; + color: light-dark( + variables.$color-black-alpha-85, + variables.$color-white-alpha-85 + ); + } + + &__type { + @include mixins.font-subtitle; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + } + + &__amount { + @include mixins.font-body-1; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + } + } + + &__empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 8px; + margin: auto; + + &__title { + @include mixins.font-body-2; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + } + + &__description { + @include mixins.font-caption; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + max-width: 300px; + } + } +} + +.PreferencesDonations__ReceiptModal { + &__content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + &__logo-container { + margin-bottom: 16px; + } + + &__logo { + width: 100px; + height: 28.571px; + margin-bottom: 24px; + background-image: url('../images/signal-logo-and-wordmark.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + } + + &__amount { + font-size: 40px; + letter-spacing: 0.07px; + color: light-dark(variables.$color-gray-90, variables.$color-gray-05); + margin-bottom: 24px; + } + + &__separator { + width: 100%; + height: 0.5px; + border: none; + margin: 0; + background-color: light-dark( + variables.$color-black-alpha-12, + variables.$color-white-alpha-12 + ); + } + + &__details { + width: 100%; + text-align: start; + } + + &__detail-item { + padding-block: 10px; + padding-inline: 0; + } + + &__detail-label { + @include mixins.font-body-1; + color: light-dark( + variables.$color-black-alpha-85, + variables.$color-white-alpha-85 + ); + margin-bottom: 2px; + } + + &__detail-value { + @include mixins.font-subtitle; + color: light-dark( + variables.$color-black-alpha-50, + variables.$color-white-alpha-50 + ); + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 252ed25d79..5924ad6ff2 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -143,6 +143,7 @@ @use 'components/PlaybackButton.scss'; @use 'components/PlaybackRateButton.scss'; @use 'components/Preferences.scss'; +@use 'components/PreferencesDonations.scss'; @use 'components/ProfileEditor.scss'; @use 'components/ProfileMovedModal.scss'; @use 'components/ProfileNameWarningModal.scss'; diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index 273ea03f19..68d9274ac1 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -15,6 +15,7 @@ import { DAY, DurationInSeconds, WEEK } from '../util/durations'; import { DialogUpdate } from './DialogUpdate'; import { DialogType } from '../types/Dialogs'; import { ThemeType } from '../types/Util'; +import type { LocalizerType } from '../types/Util'; import { getDefaultConversation, getDefaultGroup, @@ -30,6 +31,8 @@ import type { WidthBreakpoint } from './_util'; import type { MessageAttributesType } from '../model-types'; import { PreferencesDonations } from './PreferencesDonations'; import { strictAssert } from '../util/assert'; +import type { DonationReceipt } from '../types/Donations'; +import type { AnyToast } from '../types/Toast'; const { i18n } = window.SignalContext; @@ -168,7 +171,20 @@ function RenderProfileEditor(): JSX.Element { ); } -function RenderDonationsPane(): JSX.Element { +function RenderDonationsPane(props: { + me: typeof me; + donationReceipts: ReadonlyArray; + saveAttachmentToDisk: (options: { + data: Uint8Array; + name: string; + baseDir?: string | undefined; + }) => Promise<{ fullPath: string; name: string } | null>; + generateDonationReceiptBlob: ( + receipt: DonationReceipt, + i18n: LocalizerType + ) => Promise; + showToast: (toast: AnyToast) => void; +}): JSX.Element { const contentsRef = useRef(null); return ( ); } @@ -302,7 +326,20 @@ export default { whoCanSeeMe: PhoneNumberSharingMode.Everybody, zoomFactor: 1, - renderDonationsPane: RenderDonationsPane, + renderDonationsPane: () => + RenderDonationsPane({ + me, + donationReceipts: [], + saveAttachmentToDisk: async () => { + action('saveAttachmentToDisk')(); + return { fullPath: '/mock/path/to/file.png', name: 'file.png' }; + }, + generateDonationReceiptBlob: async () => { + action('generateDonationReceiptBlob')(); + return new Blob(); + }, + showToast: action('showToast'), + }), renderProfileEditor: RenderProfileEditor, renderToastManager, renderUpdateDialog, diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index f619c50f39..2184e9d893 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -329,6 +329,7 @@ export enum Page { ChatColor = 'ChatColor', ChatFolders = 'ChatFolders', DonationsDonateFlow = 'DonationsDonateFlow', + DonationsReceiptList = 'DonationsReceiptList', EditChatFolder = 'EditChatFolder', PNP = 'PNP', BackupsDetails = 'BackupsDetails', @@ -338,6 +339,14 @@ export enum Page { LocalBackupsKeyReference = 'LocalBackupsKeyReference', } +function isDonationsPage(page: Page): boolean { + return ( + page === Page.Donations || + page === Page.DonationsDonateFlow || + page === Page.DonationsReceiptList + ); +} + enum LanguageDialog { Selection, Confirmation, @@ -596,10 +605,7 @@ export function Preferences({ if (page === Page.Backups && !shouldShowBackupsPage) { setPage(Page.General); } - if ( - (page === Page.Donations || page === Page.DonationsDonateFlow) && - !donationsFeatureEnabled - ) { + if (isDonationsPage(page) && !donationsFeatureEnabled) { setPage(Page.General); } if (page === Page.Internal && !isInternalUser) { @@ -897,7 +903,7 @@ export function Preferences({ title={i18n('icu:Preferences__button--general')} /> ); - } else if (page === Page.Donations || page === Page.DonationsDonateFlow) { + } else if (isDonationsPage(page)) { content = renderDonationsPane({ contentsRef: settingsPaneRef, page, @@ -2365,9 +2371,7 @@ export function Preferences({ className={classNames({ Preferences__button: true, 'Preferences__button--appearance': true, - 'Preferences__button--selected': - page === Page.Donations || - page === Page.DonationsDonateFlow, + 'Preferences__button--selected': isDonationsPage(page), })} onClick={() => setPage(Page.Donations)} > diff --git a/ts/components/PreferencesDonateFlow.tsx b/ts/components/PreferencesDonateFlow.tsx index dd3c9c52bd..9ed174ee7e 100644 --- a/ts/components/PreferencesDonateFlow.tsx +++ b/ts/components/PreferencesDonateFlow.tsx @@ -3,19 +3,12 @@ import React, { useCallback, useRef, useState } from 'react'; -import type { MutableRefObject } from 'react'; - import type { LocalizerType } from '../types/Util'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; -import { PreferencesContent } from './Preferences'; import { Button, ButtonVariant } from './Button'; import type { CardDetail, DonationWorkflow } from '../types/Donations'; import { Input } from './Input'; -type PropsExternalType = { - contentsRef: MutableRefObject; -}; - export type PropsDataType = { i18n: LocalizerType; workflow: DonationWorkflow | undefined; @@ -23,7 +16,6 @@ export type PropsDataType = { type PropsActionType = { clearWorkflow: () => void; - onBack: () => void; submitDonation: (options: { currencyType: string; paymentAmount: number; @@ -31,14 +23,12 @@ type PropsActionType = { }) => void; }; -export type PropsType = PropsDataType & PropsActionType & PropsExternalType; +export type PropsType = PropsDataType & PropsActionType; export function PreferencesDonateFlow({ - contentsRef, i18n, workflow, clearWorkflow, - onBack, submitDonation, }: PropsType): JSX.Element { const tryClose = useRef<() => void | undefined>(); @@ -81,15 +71,6 @@ export function PreferencesDonateFlow({ const isDonateDisabled = workflow !== undefined; - const backButton = ( - + ); +} + +function DonationsHome({ + i18n, + userAvatarData, + color, + firstName, + profileAvatarUrl, + navigateToPage, + setPage, + isStaging, + donationReceipts, +}: PreferencesHomeProps): JSX.Element { + const avatarData = userAvatarData[0]; + const avatarBuffer = avatarData?.buffer; + const hasReceipts = donationReceipts.length > 0; + + return ( +
+
+ +
+
+ {i18n('icu:PreferencesDonations__title')} +
+
+ +
+ {isStaging && ( + + )} + +
+ + + {hasReceipts && ( + { + navigateToPage(Page.DonationsReceiptList); + }} + > + + + {i18n('icu:PreferencesDonations__receipts')} + + + + )} + { + // TODO: Handle donation FAQs action + }} + > + + + {i18n('icu:PreferencesDonations__faqs')} + + + + +
+ ); +} + +function PreferencesReceiptList({ + i18n, + donationReceipts, + saveAttachmentToDisk, + generateDonationReceiptBlob, + showToast, +}: { + i18n: LocalizerType; + donationReceipts: ReadonlyArray; + saveAttachmentToDisk: (options: { + data: Uint8Array; + name: string; + baseDir?: string | undefined; + }) => Promise<{ fullPath: string; name: string } | null>; + generateDonationReceiptBlob: ( + receipt: DonationReceipt, + i18n: LocalizerType + ) => Promise; + showToast: (toast: AnyToast) => void; +}): JSX.Element { + const [showReceiptModal, setShowReceiptModal] = useState(false); + const [selectedReceipt, setSelectedReceipt] = + useState(null); + const [isDownloading, setIsDownloading] = useState(false); + + const sortedReceipts = sortBy( + donationReceipts, + receipt => -receipt.timestamp + ); + const receiptsByYear = groupBy(sortedReceipts, receipt => + new Date(receipt.timestamp).getFullYear() + ); + + const dateFormatter = getDateTimeFormatter({ + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + const preferredSystemLocales = + window.SignalContext.getPreferredSystemLocales(); + const localeOverride = window.SignalContext.getLocaleOverride(); + const locales = + localeOverride != null ? [localeOverride] : preferredSystemLocales; + + const getCurrencyFormatter = (currencyType: string) => + new Intl.NumberFormat(locales, { + style: 'currency', + currency: currencyType, + }); + + const hasReceipts = Object.keys(receiptsByYear).length > 0; + + const handleDownloadReceipt = useCallback(async () => { + if (!selectedReceipt) { + return; + } + + setIsDownloading(true); + try { + const blob = await generateDonationReceiptBlob(selectedReceipt, i18n); + const buffer = await blob.arrayBuffer(); + + const result = await saveAttachmentToDisk({ + name: `Signal_Receipt_${new Date(selectedReceipt.timestamp).toISOString().split('T')[0]}.png`, + data: new Uint8Array(buffer), + }); + + if (result) { + setShowReceiptModal(false); + showToast({ + toastType: ToastType.ReceiptSaved, + parameters: { fullPath: result.fullPath }, + }); + } + } catch (error) { + log.error('Failed to generate receipt: ', toLogFormat(error)); + showToast({ + toastType: ToastType.ReceiptSaveFailed, + }); + } finally { + setIsDownloading(false); + } + }, [ + selectedReceipt, + generateDonationReceiptBlob, + i18n, + saveAttachmentToDisk, + showToast, + ]); + + return ( +
+ {hasReceipts ? ( + <> +
+
+ {i18n('icu:PreferencesDonations--receiptList__info')} +
+
+ + {Object.entries(receiptsByYear).map(([year, receipts]) => ( +
+
+ {year} +
+ + {receipts.map(receipt => ( + { + setSelectedReceipt(receipt); + setShowReceiptModal(true); + }} + > +
+
+
+ {dateFormatter.format(new Date(receipt.timestamp))} +
+
+ {i18n('icu:DonationReceipt__type-value--one-time')} +
+
+
+ {getCurrencyFormatter(receipt.currencyType).format( + receipt.paymentAmount / 100 + )} +
+ + ))} + +
+ ))} + + ) : ( +
+
+ {i18n('icu:PreferencesDonations--receiptList__empty-title')} +
+
+ {i18n('icu:PreferencesDonations--receiptList__info')} +
+
+ )} + + {showReceiptModal && selectedReceipt && ( + setShowReceiptModal(false)} + modalFooter={ + + } + > +
+
+
+
+
+ {getCurrencyFormatter(selectedReceipt.currencyType).format( + selectedReceipt.paymentAmount / 100 + )} +
+
+
+
+
+ {i18n('icu:PreferencesDonations__ReceiptModal--type-label')} +
+
+ {i18n('icu:DonationReceipt__type-value--one-time')} +
+
+
+
+ {i18n( + 'icu:PreferencesDonations__ReceiptModal--date-paid-label' + )} +
+
+ {dateFormatter.format(new Date(selectedReceipt.timestamp))} +
+
+
+
+ + )} +
+ ); +} + export function PreferencesDonations({ contentsRef, i18n, @@ -43,38 +406,119 @@ export function PreferencesDonations({ clearWorkflow, setPage, submitDonation, -}: PropsType): JSX.Element { + userAvatarData, + color, + firstName, + profileAvatarUrl, + donationReceipts, + saveAttachmentToDisk, + generateDonationReceiptBlob, + showToast, +}: PropsType): JSX.Element | null { + const PAGE_CONFIG = useMemo< + Record + >(() => { + return { + [Page.Donations]: { + title: i18n('icu:Preferences__DonateTitle'), + goBackTo: null, + }, + [Page.DonationsReceiptList]: { + title: i18n('icu:PreferencesDonations__receipts'), + goBackTo: Page.Donations, + }, + [Page.DonationsDonateFlow]: { + title: undefined, + goBackTo: Page.Donations, + }, + } as const; + }, [i18n]); + + const navigateToPage = useCallback( + (newPage: Page) => { + setPage(newPage); + }, + [setPage] + ); + + const handleBack = useCallback(() => { + if (!isDonationPage(page)) { + log.error( + 'Donations page back button tried to go to a non-donations page, ignoring' + ); + return; + } + const { goBackTo } = PAGE_CONFIG[page]; + if (goBackTo) { + setPage(goBackTo); + } + }, [PAGE_CONFIG, page, setPage]); + + if (!isDonationPage(page)) { + return null; + } + + let content; if (page === Page.DonationsDonateFlow) { - return ( + content = ( setPage(Page.Donations)} submitDonation={submitDonation} /> ); } + if (page === Page.Donations) { + content = ( + + ); + } else if (page === Page.DonationsReceiptList) { + content = ( + + ); + } - const content = ( -
- {isStaging && ( - - )} -
- ); + // Show back button based on page configuration + const backButton = PAGE_CONFIG[page].goBackTo ? ( +