From e17bcb2409198630da81ac1e66a5e590bf9f3e6d Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 17 Jul 2025 04:33:41 +1000 Subject: [PATCH] Donations: Show toasts when resuming after startup Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- _locales/en/messages.json | 8 + ts/background.ts | 12 +- ts/components/DebugLogWindow.tsx | 6 + ts/components/LeftPane.stories.tsx | 3 + ts/components/LeftPane.tsx | 14 +- ts/components/NavTabs.stories.tsx | 2 +- ts/components/NavTabs.tsx | 10 +- ts/components/Preferences.stories.tsx | 73 +++++---- ts/components/Preferences.tsx | 155 ++++++++---------- ts/components/PreferencesBackups.tsx | 22 +-- ts/components/PreferencesDonations.tsx | 43 ++--- ts/components/PreferencesLocalBackups.tsx | 8 +- ts/components/ProfileEditor.stories.tsx | 6 +- ts/components/ProfileEditor.tsx | 62 +++---- ts/components/ToastManager.stories.tsx | 6 + ts/components/ToastManager.tsx | 63 ++++++- .../ConversationDetails.stories.tsx | 2 +- .../ConversationDetails.tsx | 2 +- ts/services/BeforeNavigate.ts | 3 +- ts/services/allLoaders.ts | 7 +- ts/services/donations.ts | 59 ++++++- ...onReceiptsLoader.ts => donationsLoader.ts} | 7 +- ts/state/ducks/conversations.ts | 14 +- ts/state/ducks/donations.ts | 35 +++- ts/state/ducks/nav.ts | 25 +-- ts/state/selectors/nav.ts | 8 +- ts/state/smart/NavTabs.tsx | 7 +- ts/state/smart/Preferences.tsx | 16 +- ts/state/smart/PreferencesDonations.tsx | 6 +- ts/state/smart/ProfileEditor.tsx | 12 +- ts/state/smart/ToastManager.tsx | 9 +- ts/state/smart/UsernameOnboardingModal.tsx | 9 +- ts/types/Nav.ts | 61 +++++++ ts/types/PreferencesBackupPage.ts | 30 ++-- ts/types/Toast.tsx | 4 + ts/windows/main/preload_test.ts | 1 + 36 files changed, 496 insertions(+), 314 deletions(-) rename ts/services/{donationReceiptsLoader.ts => donationsLoader.ts} (78%) create mode 100644 ts/types/Nav.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cbbf130f3d..cc36b1f492 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -8886,6 +8886,14 @@ "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.", "description": "Footer text shown on donation receipts explaining tax deductibility and Signal's mission" }, + "icu:Donations__Toast__Completed": { + "messageformat": "Donation completed", + "description": "Toast shown when a donation started processing after resuming on startup, and it completed successfully when the user is not on the Preferences/Donations screen" + }, + "icu:Donations__Toast__Processing": { + "messageformat": "Processing donation", + "description": "Toast shown when a donation starts processing again after resuming on startup" + }, "icu:Donations__PaymentMethodDeclined": { "messageformat": "Payment method declined", "description": "Title of the dialog shown with the user's provided payment method has not worked" diff --git a/ts/background.ts b/ts/background.ts index e454b8b59c..e5fc5980b1 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -216,10 +216,8 @@ import { waitForEvent } from './shims/events'; import { sendSyncRequests } from './textsecure/syncRequests'; import { handleServerAlerts } from './util/handleServerAlerts'; import { isLocalBackupsEnabled } from './util/isLocalBackupsEnabled'; -import { NavTab } from './state/ducks/nav'; -import { Page } from './components/Preferences'; -import { EditState } from './components/ProfileEditor'; -import { runDonationWorkflow } from './services/donations'; +import { NavTab, SettingsPage, ProfileEditorPage } from './types/Nav'; +import { initialize as initializeDonationService } from './services/donations'; import { MessageRequestResponseSource } from './types/MessageRequestResponseEvent'; const log = createLogger('background'); @@ -1373,8 +1371,8 @@ export async function startApp(): Promise { window.reduxActions.nav.changeLocation({ tab: NavTab.Settings, details: { - page: Page.Profile, - state: EditState.None, + page: SettingsPage.Profile, + state: ProfileEditorPage.None, }, }); }); @@ -2198,7 +2196,7 @@ export async function startApp(): Promise { drop(ReleaseNotesFetcher.init(window.Whisper.events, newVersion)); - drop(runDonationWorkflow()); + drop(initializeDonationService()); if (isFromMessageReceiver) { drop( diff --git a/ts/components/DebugLogWindow.tsx b/ts/components/DebugLogWindow.tsx index a64c64a4f6..97cdc63f2f 100644 --- a/ts/components/DebugLogWindow.tsx +++ b/ts/components/DebugLogWindow.tsx @@ -151,12 +151,15 @@ export function DebugLogWindow({ { ), renderToastManager: ({ containerWidthBreakpoint }) => ( ; - page: Page; - setPage: (page: Page) => void; + page: SettingsPage; + setPage: (page: SettingsPage) => void; }) => JSX.Element; renderProfileEditor: (options: { contentsRef: MutableRefObject; @@ -254,7 +255,7 @@ type PropsFunctionType = { value: CustomColorType; } ) => unknown; - setPage: (page: Page) => unknown; + setPage: (page: SettingsPage) => unknown; showToast: (toast: AnyToast) => unknown; validateBackup: () => Promise; @@ -318,39 +319,11 @@ export type PropsType = PropsDataType & PropsFunctionType; export type PropsPreloadType = Omit; -export enum Page { - // Accessible through left nav - Profile = 'Profile', - General = 'General', - Donations = 'Donations', - Appearance = 'Appearance', - Chats = 'Chats', - Calls = 'Calls', - Notifications = 'Notifications', - Privacy = 'Privacy', - DataUsage = 'DataUsage', - Backups = 'Backups', - Internal = 'Internal', - - // Sub pages - ChatColor = 'ChatColor', - ChatFolders = 'ChatFolders', - DonationsDonateFlow = 'DonationsDonateFlow', - DonationsReceiptList = 'DonationsReceiptList', - EditChatFolder = 'EditChatFolder', - PNP = 'PNP', - BackupsDetails = 'BackupsDetails', - LocalBackups = 'LocalBackups', - LocalBackupsSetupFolder = 'LocalBackupsSetupFolder', - LocalBackupsSetupKey = 'LocalBackupsSetupKey', - LocalBackupsKeyReference = 'LocalBackupsKeyReference', -} - -function isDonationsPage(page: Page): boolean { +function isDonationsPage(page: SettingsPage): boolean { return ( - page === Page.Donations || - page === Page.DonationsDonateFlow || - page === Page.DonationsReceiptList + page === SettingsPage.Donations || + page === SettingsPage.DonationsDonateFlow || + page === SettingsPage.DonationsReceiptList ); } @@ -567,14 +540,14 @@ export function Preferences({ const handleOpenEditChatFoldersPage = useCallback( (chatFolderId: ChatFolderId | null) => { - setPage(Page.EditChatFolder); + setPage(SettingsPage.EditChatFolder); setEditChatFolderPageId(chatFolderId); }, [setPage] ); const handleCloseEditChatFoldersPage = useCallback(() => { - setPage(Page.ChatFolders); + setPage(SettingsPage.ChatFolders); setEditChatFolderPageId(null); }, [setPage]); @@ -613,14 +586,14 @@ export function Preferences({ const shouldShowBackupsPage = backupFeatureEnabled || backupLocalBackupsEnabled; - if (page === Page.Backups && !shouldShowBackupsPage) { - setPage(Page.General); + if (page === SettingsPage.Backups && !shouldShowBackupsPage) { + setPage(SettingsPage.General); } if (isDonationsPage(page) && !donationsFeatureEnabled) { - setPage(Page.General); + setPage(SettingsPage.General); } - if (page === Page.Internal && !isInternalUser) { - setPage(Page.General); + if (page === SettingsPage.Internal && !isInternalUser) { + setPage(SettingsPage.General); } let maybeUpdateDialog: JSX.Element | undefined; @@ -782,11 +755,11 @@ export function Preferences({ let content: JSX.Element | undefined; - if (page === Page.Profile) { + if (page === SettingsPage.Profile) { content = renderProfileEditor({ contentsRef: settingsPaneRef, }); - } else if (page === Page.General) { + } else if (page === SettingsPage.General) { const pageContents = ( <> @@ -920,7 +893,7 @@ export function Preferences({ page, setPage, }); - } else if (page === Page.Appearance) { + } else if (page === SettingsPage.Appearance) { let zoomFactors = DEFAULT_ZOOM_FACTORS; if ( @@ -1101,7 +1074,7 @@ export function Preferences({ icon left={i18n('icu:showChatColorEditor')} onClick={() => { - setPage(Page.ChatColor); + setPage(SettingsPage.ChatColor); }} right={
); - } else if (page === Page.Chats) { + } else if (page === SettingsPage.Chats) { let spellCheckDirtyText: string | undefined; if ( hasSpellCheck !== undefined && @@ -1231,7 +1204,7 @@ export function Preferences({ } right={null} - onClick={() => setPage(Page.ChatFolders)} + onClick={() => setPage(SettingsPage.ChatFolders)} /> )} @@ -1298,7 +1271,7 @@ export function Preferences({ title={i18n('icu:Preferences__button--chats')} /> ); - } else if (page === Page.Calls) { + } else if (page === SettingsPage.Calls) { const pageContents = ( <> @@ -1447,7 +1420,7 @@ export function Preferences({ title={i18n('icu:Preferences__button--calls')} /> ); - } else if (page === Page.Notifications) { + } else if (page === SettingsPage.Notifications) { const pageContents = ( <> @@ -1535,7 +1508,7 @@ export function Preferences({ title={i18n('icu:Preferences__button--notifications')} /> ); - } else if (page === Page.Privacy) { + } else if (page === SettingsPage.Privacy) { const isCustomDisappearingMessageValue = !DEFAULT_DURATIONS_SET.has(universalExpireTimer); const pageContents = ( @@ -1562,7 +1535,7 @@ export function Preferences({ )} > @@ -2305,9 +2280,10 @@ export function Preferences({ Preferences__button: true, 'Preferences__button--appearance': true, 'Preferences__button--selected': - page === Page.Appearance || page === Page.ChatColor, + page === SettingsPage.Appearance || + page === SettingsPage.ChatColor, })} - onClick={() => setPage(Page.Appearance)} + onClick={() => setPage(SettingsPage.Appearance)} > {i18n('icu:Preferences__button--appearance')} @@ -2316,9 +2292,9 @@ export function Preferences({ className={classNames({ Preferences__button: true, 'Preferences__button--chats': true, - 'Preferences__button--selected': page === Page.Chats, + 'Preferences__button--selected': page === SettingsPage.Chats, })} - onClick={() => setPage(Page.Chats)} + onClick={() => setPage(SettingsPage.Chats)} > {i18n('icu:Preferences__button--chats')} @@ -2327,9 +2303,9 @@ export function Preferences({ className={classNames({ Preferences__button: true, 'Preferences__button--calls': true, - 'Preferences__button--selected': page === Page.Calls, + 'Preferences__button--selected': page === SettingsPage.Calls, })} - onClick={() => setPage(Page.Calls)} + onClick={() => setPage(SettingsPage.Calls)} > {i18n('icu:Preferences__button--calls')} @@ -2338,9 +2314,10 @@ export function Preferences({ className={classNames({ Preferences__button: true, 'Preferences__button--notifications': true, - 'Preferences__button--selected': page === Page.Notifications, + 'Preferences__button--selected': + page === SettingsPage.Notifications, })} - onClick={() => setPage(Page.Notifications)} + onClick={() => setPage(SettingsPage.Notifications)} > {i18n('icu:Preferences__button--notifications')} @@ -2350,9 +2327,9 @@ export function Preferences({ Preferences__button: true, 'Preferences__button--privacy': true, 'Preferences__button--selected': - page === Page.Privacy || page === Page.PNP, + page === SettingsPage.Privacy || page === SettingsPage.PNP, })} - onClick={() => setPage(Page.Privacy)} + onClick={() => setPage(SettingsPage.Privacy)} > {i18n('icu:Preferences__button--privacy')} @@ -2361,9 +2338,10 @@ export function Preferences({ className={classNames({ Preferences__button: true, 'Preferences__button--data-usage': true, - 'Preferences__button--selected': page === Page.DataUsage, + 'Preferences__button--selected': + page === SettingsPage.DataUsage, })} - onClick={() => setPage(Page.DataUsage)} + onClick={() => setPage(SettingsPage.DataUsage)} > {i18n('icu:Preferences__button--data-usage')} @@ -2375,7 +2353,7 @@ export function Preferences({ 'Preferences__button--backups': true, 'Preferences__button--selected': isBackupPage(page), })} - onClick={() => setPage(Page.Backups)} + onClick={() => setPage(SettingsPage.Backups)} > {i18n('icu:Preferences__button--backups')} @@ -2388,7 +2366,7 @@ export function Preferences({ 'Preferences__button--appearance': true, 'Preferences__button--selected': isDonationsPage(page), })} - onClick={() => setPage(Page.Donations)} + onClick={() => setPage(SettingsPage.Donations)} > {i18n('icu:Preferences__button--donate')} @@ -2399,9 +2377,10 @@ export function Preferences({ className={classNames({ Preferences__button: true, 'Preferences__button--internal': true, - 'Preferences__button--selected': page === Page.Internal, + 'Preferences__button--selected': + page === SettingsPage.Internal, })} - onClick={() => setPage(Page.Internal)} + onClick={() => setPage(SettingsPage.Internal)} > {i18n('icu:Preferences__button--internal')} diff --git a/ts/components/PreferencesBackups.tsx b/ts/components/PreferencesBackups.tsx index 85ed487ee5..608db96671 100644 --- a/ts/components/PreferencesBackups.tsx +++ b/ts/components/PreferencesBackups.tsx @@ -20,7 +20,7 @@ import { import { missingCaseError } from '../util/missingCaseError'; import { Button, ButtonVariant } from './Button'; import type { PreferencesBackupPage } from '../types/PreferencesBackupPage'; -import { Page } from './Preferences'; +import { SettingsPage } from '../types/Nav'; import { I18n } from './I18n'; import { PreferencesLocalBackups } from './PreferencesLocalBackups'; import type { ShowToastAction } from '../state/ducks/toast'; @@ -82,17 +82,17 @@ export function PreferencesBackups({ const [isAuthPending, setIsAuthPending] = useState(false); useEffect(() => { - if (page === Page.Backups) { + if (page === SettingsPage.Backups) { refreshBackupSubscriptionStatus(); - } else if (page === Page.BackupsDetails) { + } else if (page === SettingsPage.BackupsDetails) { refreshBackupSubscriptionStatus(); refreshCloudBackupStatus(); } }, [page, refreshBackupSubscriptionStatus, refreshCloudBackupStatus]); - if (page === Page.BackupsDetails) { + if (page === SettingsPage.BackupsDetails) { if (backupSubscriptionStatus.status === 'off') { - setPage(Page.Backups); + setPage(SettingsPage.Backups); return null; } return ( @@ -110,10 +110,10 @@ export function PreferencesBackups({ } if ( - page === Page.LocalBackups || - page === Page.LocalBackupsKeyReference || - page === Page.LocalBackupsSetupFolder || - page === Page.LocalBackupsSetupKey + page === SettingsPage.LocalBackups || + page === SettingsPage.LocalBackupsKeyReference || + page === SettingsPage.LocalBackupsSetupFolder || + page === SettingsPage.LocalBackupsSetupKey ) { return (
); - } else if (editState === EditState.Bio) { + } else if (editState === ProfileEditorPage.Bio) { const shouldDisableSave = stagedProfile.aboutText === fullBio.aboutText && stagedProfile.aboutEmoji === fullBio.aboutEmoji; @@ -587,11 +579,11 @@ export function ProfileEditor({ ); - } else if (editState === EditState.Username) { + } else if (editState === ProfileEditorPage.Username) { content = renderUsernameEditor({ onClose: handleBack, }); - } else if (editState === EditState.UsernameLink) { + } else if (editState === ProfileEditorPage.UsernameLink) { content = ( setEditState(EditState.None)} + onBack={() => setEditState(ProfileEditorPage.None)} /> ); - } else if (editState === EditState.None) { + } else if (editState === ProfileEditorPage.None) { let actions: JSX.Element | undefined; let alwaysShowActions = false; @@ -696,7 +688,7 @@ export function ProfileEditor({ return; } - setEditState(EditState.UsernameLink); + setEditState(ProfileEditorPage.UsernameLink); }} alwaysShowActions actions={linkActions} @@ -734,7 +726,7 @@ export function ProfileEditor({ } openUsernameReservationModal(); - setEditState(EditState.Username); + setEditState(ProfileEditorPage.Username); }} alwaysShowActions={alwaysShowActions} actions={actions} @@ -758,7 +750,7 @@ export function ProfileEditor({ i18n={i18n} onAvatarLoaded={handleAvatarLoaded} onClick={() => { - setEditState(EditState.BetterAvatar); + setEditState(ProfileEditorPage.BetterAvatar); }} style={{ height: 80, @@ -768,7 +760,7 @@ export function ProfileEditor({