Donations: Show toasts when resuming after startup

Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
Scott Nonnenberg 2025-07-17 04:33:41 +10:00 committed by GitHub
parent ea3a7f70b6
commit e17bcb2409
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 496 additions and 314 deletions

View file

@ -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 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__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"

View file

@ -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<void> {
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<void> {
drop(ReleaseNotesFetcher.init(window.Whisper.events, newVersion));
drop(runDonationWorkflow());
drop(initializeDonationService());
if (isFromMessageReceiver) {
drop(

View file

@ -151,12 +151,15 @@ export function DebugLogWindow({
<Button onClick={copyLog}>{i18n('icu:debugLogCopy')}</Button>
</div>
<ToastManager
changeLocation={shouldNeverBeCalled}
clearDonation={shouldNeverBeCalled}
OS="unused"
hideToast={closeToast}
i18n={i18n}
onShowDebugLog={shouldNeverBeCalled}
onUndoArchive={shouldNeverBeCalled}
openFileInFolder={shouldNeverBeCalled}
setDidResumeDonation={shouldNeverBeCalled}
showAttachmentNotAvailableModal={shouldNeverBeCalled}
toast={toast}
containerWidthBreakpoint={null}
@ -209,12 +212,15 @@ export function DebugLogWindow({
</Button>
</div>
<ToastManager
changeLocation={shouldNeverBeCalled}
clearDonation={shouldNeverBeCalled}
OS="unused"
hideToast={closeToast}
i18n={i18n}
onShowDebugLog={shouldNeverBeCalled}
onUndoArchive={shouldNeverBeCalled}
openFileInFolder={shouldNeverBeCalled}
setDidResumeDonation={shouldNeverBeCalled}
showAttachmentNotAvailableModal={shouldNeverBeCalled}
toast={toast}
containerWidthBreakpoint={null}

View file

@ -284,12 +284,15 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
),
renderToastManager: ({ containerWidthBreakpoint }) => (
<ToastManager
changeLocation={action('changeLocation')}
clearDonation={action('clearDonation')}
OS="unused"
hideToast={action('hideToast')}
i18n={i18n}
onShowDebugLog={action('onShowDebugLog')}
onUndoArchive={action('onUndoArchive')}
openFileInFolder={action('openFileInFolder')}
setDidResumeDonation={action('setDidResumeDonation')}
showAttachmentNotAvailableModal={action(
'showAttachmentNotAvailableModal'
)}

View file

@ -58,10 +58,8 @@ import type { UnreadStats } from '../util/countUnreadStats';
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
import type { ServerAlertsType } from '../util/handleServerAlerts';
import { getServerAlertDialog } from './ServerAlerts';
import { NavTab } from '../state/ducks/nav';
import type { Location } from '../state/ducks/nav';
import { Page } from './Preferences';
import { EditState } from './ProfileEditor';
import { NavTab, SettingsPage, ProfileEditorPage } from '../types/Nav';
import type { Location } from '../types/Nav';
export type PropsType = {
backupMediaDownloadProgress: {
@ -674,8 +672,8 @@ export function LeftPane({
changeLocation({
tab: NavTab.Settings,
details: {
page: Page.Profile,
state: EditState.Username,
page: SettingsPage.Profile,
state: ProfileEditorPage.Username,
},
});
}}
@ -691,8 +689,8 @@ export function LeftPane({
changeLocation({
tab: NavTab.Settings,
details: {
page: Page.Profile,
state: EditState.UsernameLink,
page: SettingsPage.Profile,
state: ProfileEditorPage.UsernameLink,
},
});
}}

View file

@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import type { NavTabsProps } from './NavTabs';
import { NavTabs } from './NavTabs';
import { NavTab } from '../state/ducks/nav';
import { NavTab } from '../types/Nav';
import { getDefaultConversation } from '../test-helpers/getDefaultConversation';
import { ThemeType } from '../types/Util';

View file

@ -9,13 +9,11 @@ import { Avatar, AvatarSize } from './Avatar';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { BadgeType } from '../badges/types';
import { NavTab } from '../state/ducks/nav';
import type { Location } from '../state/ducks/nav';
import { NavTab, ProfileEditorPage, SettingsPage } from '../types/Nav';
import type { Location } from '../types/Nav';
import { Tooltip, TooltipPlacement } from './Tooltip';
import { Theme } from '../util/theme';
import type { UnreadStats } from '../util/countUnreadStats';
import { Page } from './Preferences';
import { EditState } from './ProfileEditor';
import { ProfileMovedModal } from './ProfileMovedModal';
type NavTabsItemBadgesProps = Readonly<{
@ -248,8 +246,8 @@ export function NavTabs({
onChangeLocation({
tab: NavTab.Settings,
details: {
page: Page.Profile,
state: EditState.None,
page: SettingsPage.Profile,
state: ProfileEditorPage.None,
},
});
} else {

View file

@ -6,7 +6,7 @@ import React, { useRef, useState } from 'react';
import { action } from '@storybook/addon-actions';
import { shuffle } from 'lodash';
import { Page, Preferences } from './Preferences';
import { Preferences } from './Preferences';
import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
@ -15,22 +15,23 @@ 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,
} from '../test-helpers/getDefaultConversation';
import { EditState, ProfileEditor } from './ProfileEditor';
import { ProfileEditor } from './ProfileEditor';
import {
UsernameEditState,
UsernameLinkState,
} from '../state/ducks/usernameEnums';
import { ProfileEditorPage, SettingsPage } from '../types/Nav';
import { PreferencesDonations } from './PreferencesDonations';
import { strictAssert } from '../util/assert';
import type { LocalizerType } from '../types/Util';
import type { PropsType } from './Preferences';
import type { WidthBreakpoint } from './_util';
import type { MessageAttributesType } from '../model-types';
import { PreferencesDonations } from './PreferencesDonations';
import { strictAssert } from '../util/assert';
import type {
DonationReceipt,
OneTimeDonationHumanAmounts,
@ -160,7 +161,7 @@ function RenderProfileEditor(): JSX.Element {
firstName={me.firstName ?? ''}
hasCompletedUsernameLinkOnboarding={false}
i18n={i18n}
editState={EditState.None}
editState={ProfileEditorPage.None}
markCompletedUsernameLinkOnboarding={action(
'markCompletedUsernameLinkOnboarding'
)}
@ -212,7 +213,7 @@ function RenderDonationsPane(props: {
contentsRef={contentsRef}
clearWorkflow={action('clearWorkflow')}
isStaging={false}
page={Page.Donations}
page={SettingsPage.Donations}
setPage={action('setPage')}
submitDonation={action('submitDonation')}
workflow={undefined}
@ -331,7 +332,7 @@ export default {
unreadMentionsCount: 0,
markedUnread: false,
},
page: Page.Profile,
page: SettingsPage.Profile,
preferredSystemLocales: ['en'],
preferredWidthFromStorage: 300,
resolvedLocale: 'en',
@ -476,86 +477,86 @@ export const _Preferences = Template.bind({});
export const General = Template.bind({});
General.args = {
page: Page.General,
page: SettingsPage.General,
};
export const Appearance = Template.bind({});
Appearance.args = {
page: Page.Appearance,
page: SettingsPage.Appearance,
};
export const Chats = Template.bind({});
Chats.args = {
page: Page.Chats,
page: SettingsPage.Chats,
};
export const ChatFolders = Template.bind({});
ChatFolders.args = {
page: Page.ChatFolders,
page: SettingsPage.ChatFolders,
};
export const EditChatFolder = Template.bind({});
EditChatFolder.args = {
page: Page.EditChatFolder,
page: SettingsPage.EditChatFolder,
};
export const Calls = Template.bind({});
Calls.args = {
page: Page.Calls,
page: SettingsPage.Calls,
};
export const Notifications = Template.bind({});
Notifications.args = {
page: Page.Notifications,
page: SettingsPage.Notifications,
};
export const Privacy = Template.bind({});
Privacy.args = {
page: Page.Privacy,
page: SettingsPage.Privacy,
};
export const DataUsage = Template.bind({});
DataUsage.args = {
page: Page.DataUsage,
page: SettingsPage.DataUsage,
};
export const Donations = Template.bind({});
Donations.args = {
donationsFeatureEnabled: true,
page: Page.Donations,
page: SettingsPage.Donations,
};
export const Internal = Template.bind({});
Internal.args = {
page: Page.Internal,
page: SettingsPage.Internal,
isInternalUser: true,
};
export const Blocked1 = Template.bind({});
Blocked1.args = {
blockedCount: 1,
page: Page.Privacy,
page: SettingsPage.Privacy,
};
export const BlockedMany = Template.bind({});
BlockedMany.args = {
blockedCount: 55,
page: Page.Privacy,
page: SettingsPage.Privacy,
};
export const CustomUniversalExpireTimer = Template.bind({});
CustomUniversalExpireTimer.args = {
universalExpireTimer: DurationInSeconds.fromSeconds(9000),
page: Page.Privacy,
page: SettingsPage.Privacy,
};
export const PNPSharingDisabled = Template.bind({});
PNPSharingDisabled.args = {
whoCanSeeMe: PhoneNumberSharingMode.Nobody,
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
page: Page.PNP,
page: SettingsPage.PNP,
};
export const PNPDiscoverabilityDisabled = Template.bind({});
PNPDiscoverabilityDisabled.args = {
whoCanSeeMe: PhoneNumberSharingMode.Nobody,
whoCanFindMe: PhoneNumberDiscoverability.NotDiscoverable,
page: Page.PNP,
page: SettingsPage.PNP,
};
export const BackupsMediaDownloadActive = Template.bind({});
BackupsMediaDownloadActive.args = {
page: Page.BackupsDetails,
page: SettingsPage.BackupsDetails,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
cloudBackupStatus: {
@ -579,7 +580,7 @@ BackupsMediaDownloadActive.args = {
};
export const BackupsMediaDownloadPaused = Template.bind({});
BackupsMediaDownloadPaused.args = {
page: Page.BackupsDetails,
page: SettingsPage.BackupsDetails,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
cloudBackupStatus: {
@ -604,7 +605,7 @@ BackupsMediaDownloadPaused.args = {
export const BackupsPaidActive = Template.bind({});
BackupsPaidActive.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
cloudBackupStatus: {
@ -623,7 +624,7 @@ BackupsPaidActive.args = {
export const BackupsPaidCancelled = Template.bind({});
BackupsPaidCancelled.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
cloudBackupStatus: {
@ -642,7 +643,7 @@ BackupsPaidCancelled.args = {
export const BackupsFree = Template.bind({});
BackupsFree.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
backupSubscriptionStatus: {
@ -653,21 +654,21 @@ BackupsFree.args = {
export const BackupsOff = Template.bind({});
BackupsOff.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
};
export const BackupsLocalBackups = Template.bind({});
BackupsLocalBackups.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
};
export const BackupsSubscriptionNotFound = Template.bind({});
BackupsSubscriptionNotFound.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
backupSubscriptionStatus: {
@ -681,7 +682,7 @@ BackupsSubscriptionNotFound.args = {
export const BackupsSubscriptionExpired = Template.bind({});
BackupsSubscriptionExpired.args = {
page: Page.Backups,
page: SettingsPage.Backups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
backupSubscriptionStatus: {
@ -691,7 +692,7 @@ BackupsSubscriptionExpired.args = {
export const LocalBackups = Template.bind({});
LocalBackups.args = {
page: Page.LocalBackups,
page: SettingsPage.LocalBackups,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
backupKeyViewed: true,
@ -700,14 +701,14 @@ LocalBackups.args = {
export const LocalBackupsSetupChooseFolder = Template.bind({});
LocalBackupsSetupChooseFolder.args = {
page: Page.LocalBackupsSetupFolder,
page: SettingsPage.LocalBackupsSetupFolder,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
};
export const LocalBackupsSetupViewBackupKey = Template.bind({});
LocalBackupsSetupViewBackupKey.args = {
page: Page.LocalBackupsSetupKey,
page: SettingsPage.LocalBackupsSetupKey,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
localBackupFolder: '/home/signaluser/Signal Backups/',

View file

@ -50,6 +50,7 @@ import { PreferencesInternal } from './PreferencesInternal';
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider';
import { Avatar, AvatarSize } from './Avatar';
import { NavSidebar } from './NavSidebar';
import { SettingsPage } from '../types/Nav';
import type { MediaDeviceSettings } from '../types/Calling';
import type { ValidationResultType as BackupValidationResultType } from '../services/backups';
@ -153,7 +154,7 @@ export type PropsDataType = {
hasStoriesDisabled: boolean;
hasTextFormatting: boolean;
hasTypingIndicators: boolean;
page: Page;
page: SettingsPage;
lastSyncTime?: number;
notificationContent: NotificationSettingType;
phoneNumber: string | undefined;
@ -208,8 +209,8 @@ type PropsFunctionType = {
// Render props
renderDonationsPane: (options: {
contentsRef: MutableRefObject<HTMLDivElement | null>;
page: Page;
setPage: (page: Page) => void;
page: SettingsPage;
setPage: (page: SettingsPage) => void;
}) => JSX.Element;
renderProfileEditor: (options: {
contentsRef: MutableRefObject<HTMLDivElement | null>;
@ -254,7 +255,7 @@ type PropsFunctionType = {
value: CustomColorType;
}
) => unknown;
setPage: (page: Page) => unknown;
setPage: (page: SettingsPage) => unknown;
showToast: (toast: AnyToast) => unknown;
validateBackup: () => Promise<BackupValidationResultType>;
@ -318,39 +319,11 @@ export type PropsType = PropsDataType & PropsFunctionType;
export type PropsPreloadType = Omit<PropsType, 'i18n'>;
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 = (
<>
<SettingsRow>
@ -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={
<div
@ -1140,7 +1113,7 @@ export function Preferences({
title={i18n('icu:Preferences__button--appearance')}
/>
);
} 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)}
/>
</SettingsRow>
)}
@ -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 = (
<>
<SettingsRow title={i18n('icu:calling')}>
@ -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 = (
<>
<SettingsRow>
@ -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({
)}
>
<Button
onClick={() => setPage(Page.PNP)}
onClick={() => setPage(SettingsPage.PNP)}
variant={ButtonVariant.Secondary}
>
{i18n('icu:Preferences__pnp__row--button')}
@ -1813,7 +1786,7 @@ export function Preferences({
title={i18n('icu:Preferences__button--privacy')}
/>
);
} else if (page === Page.DataUsage) {
} else if (page === SettingsPage.DataUsage) {
const pageContents = (
<>
<SettingsRow title={i18n('icu:Preferences__media-auto-download')}>
@ -1925,12 +1898,12 @@ export function Preferences({
title={i18n('icu:Preferences__button--data-usage')}
/>
);
} else if (page === Page.ChatColor) {
} else if (page === SettingsPage.ChatColor) {
const backButton = (
<button
aria-label={i18n('icu:goBack')}
className="Preferences__back-icon"
onClick={() => setPage(Page.Appearance)}
onClick={() => setPage(SettingsPage.Appearance)}
type="button"
/>
);
@ -1961,18 +1934,18 @@ export function Preferences({
title={i18n('icu:ChatColorPicker__menu-title')}
/>
);
} else if (page === Page.ChatFolders) {
} else if (page === SettingsPage.ChatFolders) {
content = (
<ChatFoldersPage
i18n={i18n}
settingsPaneRef={settingsPaneRef}
onBack={() => setPage(Page.Chats)}
onBack={() => setPage(SettingsPage.Chats)}
onOpenEditChatFoldersPage={handleOpenEditChatFoldersPage}
chatFolders={chatFolders}
onCreateChatFolder={handleCreateChatFolder}
/>
);
} else if (page === Page.EditChatFolder) {
} else if (page === SettingsPage.EditChatFolder) {
let initChatFolderParam: ChatFolderParams;
if (editChatFolderPageId != null) {
const found = chatFolders.find(chatFolder => {
@ -1999,7 +1972,7 @@ export function Preferences({
onDeleteChatFolder={handleDeleteChatFolder}
/>
);
} else if (page === Page.PNP) {
} else if (page === SettingsPage.PNP) {
let sharingDescription: string;
if (whoCanSeeMe === PhoneNumberSharingMode.Everybody) {
@ -2020,7 +1993,7 @@ export function Preferences({
<button
aria-label={i18n('icu:goBack')}
className="Preferences__back-icon"
onClick={() => setPage(Page.Privacy)}
onClick={() => setPage(SettingsPage.Privacy)}
type="button"
/>
);
@ -2142,18 +2115,18 @@ export function Preferences({
);
} else if (isBackupPage(page)) {
let pageTitle: string | undefined;
if (page === Page.Backups || page === Page.BackupsDetails) {
if (page === SettingsPage.Backups || page === SettingsPage.BackupsDetails) {
pageTitle = i18n('icu:Preferences__button--backups');
} else if (page === Page.LocalBackups) {
} else if (page === SettingsPage.LocalBackups) {
pageTitle = i18n('icu:Preferences__local-backups');
}
// Local backups setup page titles intentionally left blank
let backPage: PreferencesBackupPage | undefined;
if (page === Page.LocalBackupsKeyReference) {
backPage = Page.LocalBackups;
} else if (page !== Page.Backups) {
backPage = Page.Backups;
if (page === SettingsPage.LocalBackupsKeyReference) {
backPage = SettingsPage.LocalBackups;
} else if (page !== SettingsPage.Backups) {
backPage = SettingsPage.Backups;
}
let backButton: JSX.Element | undefined;
if (backPage) {
@ -2197,7 +2170,7 @@ export function Preferences({
title={pageTitle}
/>
);
} else if (page === Page.Internal) {
} else if (page === SettingsPage.Internal) {
content = (
<PreferencesContent
contents={
@ -2249,9 +2222,10 @@ export function Preferences({
type="button"
className={classNames({
'Preferences__profile-chip': true,
'Preferences__profile-chip--selected': page === Page.Profile,
'Preferences__profile-chip--selected':
page === SettingsPage.Profile,
})}
onClick={() => setPage(Page.Profile)}
onClick={() => setPage(SettingsPage.Profile)}
>
<div className="Preferences__profile-chip__avatar">
<Avatar
@ -2293,9 +2267,10 @@ export function Preferences({
className={classNames({
Preferences__button: true,
'Preferences__button--general': true,
'Preferences__button--selected': page === Page.General,
'Preferences__button--selected':
page === SettingsPage.General,
})}
onClick={() => setPage(Page.General)}
onClick={() => setPage(SettingsPage.General)}
>
{i18n('icu:Preferences__button--general')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>
@ -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')}
</button>

View file

@ -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<boolean>(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 (
<PreferencesLocalBackups
@ -193,7 +193,7 @@ export function PreferencesBackups({
)}
>
<Button
onClick={() => setPage(Page.BackupsDetails)}
onClick={() => setPage(SettingsPage.BackupsDetails)}
variant={ButtonVariant.Secondary}
>
{i18n('icu:Preferences__button--manage')}
@ -246,7 +246,7 @@ export function PreferencesBackups({
}
}
setPage(Page.LocalBackups);
setPage(SettingsPage.LocalBackups);
}}
variant={ButtonVariant.Secondary}
>

View file

@ -9,7 +9,8 @@ import { ListBox, ListBoxItem } from 'react-aria-components';
import { getDateTimeFormatter } from '../util/formatTimestamp';
import type { LocalizerType } from '../types/Util';
import { Page, PreferencesContent } from './Preferences';
import { PreferencesContent } from './Preferences';
import { SettingsPage } from '../types/Nav';
import { PreferencesDonateFlow } from './PreferencesDonateFlow';
import type {
DonationWorkflow,
@ -39,7 +40,7 @@ type PropsExternalType = {
export type PropsDataType = {
i18n: LocalizerType;
isStaging: boolean;
page: Page;
page: SettingsPage;
workflow: DonationWorkflow | undefined;
userAvatarData: ReadonlyArray<AvatarDataType>;
color?: AvatarColorType;
@ -62,26 +63,26 @@ export type PropsDataType = {
type PropsActionType = {
clearWorkflow: () => void;
setPage: (page: Page) => void;
setPage: (page: SettingsPage) => void;
submitDonation: (payload: SubmitDonationType) => void;
};
export type PropsType = PropsDataType & PropsActionType & PropsExternalType;
type DonationPage =
| Page.Donations
| Page.DonationsDonateFlow
| Page.DonationsReceiptList;
| SettingsPage.Donations
| SettingsPage.DonationsDonateFlow
| SettingsPage.DonationsReceiptList;
type PreferencesHomeProps = PropsType & {
navigateToPage: (newPage: Page) => void;
navigateToPage: (newPage: SettingsPage) => void;
};
function isDonationPage(page: Page): page is DonationPage {
function isDonationPage(page: SettingsPage): page is DonationPage {
return (
page === Page.Donations ||
page === Page.DonationsDonateFlow ||
page === Page.DonationsReceiptList
page === SettingsPage.Donations ||
page === SettingsPage.DonationsDonateFlow ||
page === SettingsPage.DonationsReceiptList
);
}
@ -147,7 +148,7 @@ function DonationsHome({
variant={ButtonVariant.Primary}
size={ButtonSize.Medium}
onClick={() => {
setPage(Page.DonationsDonateFlow);
setPage(SettingsPage.DonationsDonateFlow);
}}
>
{i18n('icu:PreferencesDonations__donate-button')}
@ -161,7 +162,7 @@ function DonationsHome({
<ListBoxItem
className="PreferencesDonations__list-item"
onAction={() => {
navigateToPage(Page.DonationsReceiptList);
navigateToPage(SettingsPage.DonationsReceiptList);
}}
>
<span className="PreferencesDonations__list-item__icon PreferencesDonations__list-item__icon--receipts" />
@ -418,7 +419,7 @@ export function PreferencesDonations({
showToast,
}: PropsType): JSX.Element | null {
const navigateToPage = useCallback(
(newPage: Page) => {
(newPage: SettingsPage) => {
setPage(newPage);
},
[setPage]
@ -429,7 +430,7 @@ export function PreferencesDonations({
}
let content;
if (page === Page.DonationsDonateFlow) {
if (page === SettingsPage.DonationsDonateFlow) {
// DonateFlow has to control Back button to switch between CC form and Amount picker
return (
<PreferencesDonateFlow
@ -440,11 +441,11 @@ export function PreferencesDonations({
workflow={workflow}
clearWorkflow={clearWorkflow}
submitDonation={submitDonation}
onBack={() => setPage(Page.Donations)}
onBack={() => setPage(SettingsPage.Donations)}
/>
);
}
if (page === Page.Donations) {
if (page === SettingsPage.Donations) {
content = (
<DonationsHome
contentsRef={contentsRef}
@ -468,7 +469,7 @@ export function PreferencesDonations({
submitDonation={submitDonation}
/>
);
} else if (page === Page.DonationsReceiptList) {
} else if (page === SettingsPage.DonationsReceiptList) {
content = (
<PreferencesReceiptList
i18n={i18n}
@ -482,15 +483,15 @@ export function PreferencesDonations({
let title: string | undefined;
let backButton: JSX.Element | undefined;
if (page === Page.Donations) {
if (page === SettingsPage.Donations) {
title = i18n('icu:Preferences__DonateTitle');
} else if (page === Page.DonationsReceiptList) {
} else if (page === SettingsPage.DonationsReceiptList) {
title = i18n('icu:PreferencesDonations__receipts');
backButton = (
<button
aria-label={i18n('icu:goBack')}
className="Preferences__back-icon"
onClick={() => setPage(Page.Donations)}
onClick={() => setPage(SettingsPage.Donations)}
type="button"
/>
);

View file

@ -24,7 +24,7 @@ import {
} from './PreferencesBackups';
import { I18n } from './I18n';
import type { PreferencesBackupPage } from '../types/PreferencesBackupPage';
import { Page } from './Preferences';
import { SettingsPage } from '../types/Nav';
import { ToastType } from '../types/Toast';
import type { ShowToastAction } from '../state/ducks/toast';
import { Modal } from './Modal';
@ -73,7 +73,7 @@ export function PreferencesLocalBackups({
);
}
const isReferencingBackupKey = page === Page.LocalBackupsKeyReference;
const isReferencingBackupKey = page === SettingsPage.LocalBackupsKeyReference;
if (!backupKeyViewed || isReferencingBackupKey) {
strictAssert(accountEntropyPool, 'AEP is required for backup key viewer');
@ -84,7 +84,7 @@ export function PreferencesLocalBackups({
isReferencing={isReferencingBackupKey}
onBackupKeyViewed={() => {
if (backupKeyViewed) {
setPage(Page.LocalBackups);
setPage(SettingsPage.LocalBackups);
} else {
onBackupKeyViewedChange(true);
}
@ -158,7 +158,7 @@ export function PreferencesLocalBackups({
setIsAuthPending(true);
const result = await promptOSAuth('view-aep');
if (result === 'success' || result === 'unsupported') {
setPage(Page.LocalBackupsKeyReference);
setPage(SettingsPage.LocalBackupsKeyReference);
} else {
setAuthError(result);
}

View file

@ -8,7 +8,9 @@ import casual from 'casual';
import { v4 as generateUuid } from 'uuid';
import type { PropsType } from './ProfileEditor';
import { EditState, ProfileEditor } from './ProfileEditor';
import { ProfileEditorPage } from '../types/Nav';
import { ProfileEditor } from './ProfileEditor';
import { UsernameEditor } from './UsernameEditor';
import {
UsernameEditState,
@ -51,7 +53,7 @@ export default {
conversationId: generateUuid(),
color: getRandomColor(),
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
editState: EditState.None,
editState: ProfileEditorPage.None,
familyName: casual.last_name,
firstName: casual.first_name,
i18n,

View file

@ -51,6 +51,7 @@ import { FunEmojiPickerButton } from './fun/FunButton';
import { isFunPickerEnabled } from './fun/isFunPickerEnabled';
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer';
import { PreferencesContent } from './Preferences';
import { ProfileEditorPage } from '../types/Nav';
import type { AvatarColorType } from '../types/Colors';
import type {
@ -73,15 +74,6 @@ import type { EmojiVariantKey } from './fun/data/emojis';
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
export enum EditState {
None = 'None',
BetterAvatar = 'BetterAvatar',
ProfileName = 'ProfileName',
Bio = 'Bio',
Username = 'Username',
UsernameLink = 'UsernameLink',
}
type PropsExternalType = {
onProfileChanged: (
profileData: ProfileDataType,
@ -100,7 +92,7 @@ export type PropsDataType = {
firstName: string;
hasCompletedUsernameLinkOnboarding: boolean;
i18n: LocalizerType;
editState: EditState;
editState: ProfileEditorPage;
profileAvatarUrl?: string;
userAvatarData: ReadonlyArray<AvatarDataType>;
username?: string;
@ -123,7 +115,7 @@ type PropsActionType = {
setUsernameLinkColor: (color: number) => void;
resetUsernameLink: () => void;
deleteUsername: () => void;
setEditState: (editState: EditState) => void;
setEditState: (editState: ProfileEditorPage) => void;
showToast: ShowToastAction;
openUsernameReservationModal: () => void;
};
@ -219,13 +211,13 @@ export function ProfileEditor({
tryClose,
});
const TITLES_BY_EDIT_STATE: Record<EditState, string | undefined> = {
[EditState.BetterAvatar]: i18n('icu:ProfileEditorModal--avatar'),
[EditState.Bio]: i18n('icu:ProfileEditorModal--about'),
[EditState.None]: i18n('icu:ProfileEditorModal--profile'),
[EditState.ProfileName]: i18n('icu:ProfileEditorModal--name'),
[EditState.Username]: i18n('icu:ProfileEditorModal--username'),
[EditState.UsernameLink]: i18n('icu:ProfileEditorModal--sharing'),
const TITLES_BY_EDIT_STATE: Record<ProfileEditorPage, string | undefined> = {
[ProfileEditorPage.BetterAvatar]: i18n('icu:ProfileEditorModal--avatar'),
[ProfileEditorPage.Bio]: i18n('icu:ProfileEditorModal--about'),
[ProfileEditorPage.None]: i18n('icu:ProfileEditorModal--profile'),
[ProfileEditorPage.ProfileName]: i18n('icu:ProfileEditorModal--name'),
[ProfileEditorPage.Username]: i18n('icu:ProfileEditorModal--username'),
[ProfileEditorPage.UsernameLink]: i18n('icu:ProfileEditorModal--sharing'),
};
// This is here to avoid component re-render jitters in the time it takes
@ -275,7 +267,7 @@ export function ProfileEditor({
// To make AvatarEditor re-render less often
const handleBack = useCallback(() => {
setEditState(EditState.None);
setEditState(ProfileEditorPage.None);
}, [setEditState]);
const handleEmojiPickerOpenChange = useCallback((open: boolean) => {
@ -379,7 +371,7 @@ export function ProfileEditor({
let content: JSX.Element;
if (editState === EditState.BetterAvatar) {
if (editState === ProfileEditorPage.BetterAvatar) {
content = (
<AvatarEditor
avatarColor={color || AvatarColors[0]}
@ -396,7 +388,7 @@ export function ProfileEditor({
saveAvatarToDisk={saveAvatarToDisk}
/>
);
} else if (editState === EditState.ProfileName) {
} else if (editState === ProfileEditorPage.ProfileName) {
const shouldDisableSave =
!stagedProfile.firstName ||
(stagedProfile.firstName === fullName.firstName &&
@ -458,7 +450,7 @@ export function ProfileEditor({
</div>
</>
);
} 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({
</div>
</>
);
} 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 = (
<UsernameLinkEditor
i18n={i18n}
@ -604,10 +596,10 @@ export function ProfileEditor({
resetUsernameLink={resetUsernameLink}
saveAttachment={saveAttachment}
showToast={showToast}
onBack={() => 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({
<div className="ProfileEditor__EditPhotoContainer">
<Button
onClick={() => {
setEditState(EditState.BetterAvatar);
setEditState(ProfileEditorPage.BetterAvatar);
}}
variant={ButtonVariant.Secondary}
className="ProfileEditor__EditPhoto"
@ -783,7 +775,7 @@ export function ProfileEditor({
}
label={<UserText text={getFullNameText()} />}
onClick={() => {
setEditState(EditState.ProfileName);
setEditState(ProfileEditorPage.ProfileName);
}}
/>
<PanelRow
@ -805,7 +797,7 @@ export function ProfileEditor({
/>
}
onClick={() => {
setEditState(EditState.Bio);
setEditState(ProfileEditorPage.Bio);
}}
/>
<div className="ProfileEditor__info">
@ -819,7 +811,7 @@ export function ProfileEditor({
}
const backButton =
editState !== EditState.None ? (
editState !== ProfileEditorPage.None ? (
<button
aria-label={i18n('icu:goBack')}
className="Preferences__back-icon"
@ -862,7 +854,7 @@ export function ProfileEditor({
{
action: () => {
setIsResettingUsernameLink(false);
setEditState(EditState.UsernameLink);
setEditState(ProfileEditorPage.UsernameLink);
},
style: 'affirmative',
text: i18n('icu:UsernameLinkModalBody__error__fix-now'),
@ -885,7 +877,7 @@ export function ProfileEditor({
style: 'affirmative',
action: () => {
openUsernameReservationModal();
setEditState(EditState.Username);
setEditState(ProfileEditorPage.Username);
},
},
]}

View file

@ -100,6 +100,10 @@ function getToast(toastType: ToastType): AnyToast {
};
case ToastType.DeleteForEveryoneFailed:
return { toastType: ToastType.DeleteForEveryoneFailed };
case ToastType.DonationCompleted:
return { toastType: ToastType.DonationCompleted };
case ToastType.DonationProcessing:
return { toastType: ToastType.DonationProcessing };
case ToastType.Error:
return { toastType: ToastType.Error };
case ToastType.Expired:
@ -252,6 +256,8 @@ export default {
},
},
args: {
changeLocation: action('changeLocation'),
clearDonation: action('clearDonation'),
hideToast: action('hideToast'),
openFileInFolder: action('openFileInFolder'),
onShowDebugLog: action('onShowDebugLog'),

View file

@ -5,20 +5,25 @@ import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import type { LocalizerType } from '../types/Util';
import { SECOND } from '../util/durations';
import { Toast } from './Toast';
import { WidthBreakpoint } from './_util';
import { UsernameMegaphone } from './UsernameMegaphone';
import { assertDev } from '../util/assert';
import { missingCaseError } from '../util/missingCaseError';
import type { AnyToast } from '../types/Toast';
import { ToastType } from '../types/Toast';
import type { AnyActionableMegaphone } from '../types/Megaphone';
import { MegaphoneType } from '../types/Megaphone';
import { AttachmentNotAvailableModalType } from './AttachmentNotAvailableModal';
import { NavTab, SettingsPage } from '../types/Nav';
import type { LocalizerType } from '../types/Util';
import type { AnyToast } from '../types/Toast';
import type { AnyActionableMegaphone } from '../types/Megaphone';
import type { Location } from '../types/Nav';
export type PropsType = {
changeLocation: (newLocation: Location) => unknown;
clearDonation: () => unknown;
hideToast: () => unknown;
i18n: LocalizerType;
openFileInFolder: (target: string) => unknown;
@ -28,6 +33,7 @@ export type PropsType = {
conversationId: string,
options?: { wasPinned?: boolean }
) => unknown;
setDidResumeDonation: (didResume: boolean) => unknown;
showAttachmentNotAvailableModal: (
type: AttachmentNotAvailableModalType
) => void;
@ -42,11 +48,14 @@ export type PropsType = {
const SHORT_TIMEOUT = 3 * SECOND;
export function renderToast({
changeLocation,
clearDonation,
hideToast,
i18n,
openFileInFolder,
onShowDebugLog,
onUndoArchive,
setDidResumeDonation,
showAttachmentNotAvailableModal,
OS,
toast,
@ -271,6 +280,54 @@ export function renderToast({
);
}
if (toastType === ToastType.DonationCompleted) {
return (
<Toast
autoDismissDisabled
onClose={() => {
clearDonation();
hideToast();
}}
toastAction={{
label: i18n('icu:view'),
onClick: () =>
changeLocation({
tab: NavTab.Settings,
details: {
page: SettingsPage.Donations,
},
}),
}}
>
{i18n('icu:Donations__Toast__Completed')}
</Toast>
);
}
if (toastType === ToastType.DonationProcessing) {
return (
<Toast
onClose={() => {
setDidResumeDonation(false);
hideToast();
}}
toastAction={{
label: i18n('icu:view'),
onClick: () => {
changeLocation({
tab: NavTab.Settings,
details: {
page: SettingsPage.DonationsDonateFlow,
},
});
},
}}
>
{i18n('icu:Donations__Toast__Processing')}
</Toast>
);
}
if (toastType === ToastType.Error) {
return (
<Toast

View file

@ -16,7 +16,7 @@ import { getDefaultConversation } from '../../../test-helpers/getDefaultConversa
import { makeFakeLookupConversationWithoutServiceId } from '../../../test-helpers/fakeLookupConversationWithoutServiceId';
import { ThemeType } from '../../../types/Util';
import { DurationInSeconds } from '../../../util/durations';
import { NavTab } from '../../../state/ducks/nav';
import { NavTab } from '../../../types/Nav';
import { getFakeCallHistoryGroup } from '../../../test-helpers/getFakeCallHistoryGroup';
const { i18n } = window.SignalContext;

View file

@ -53,7 +53,7 @@ import { isConversationMuted } from '../../../util/isConversationMuted';
import { ConversationDetailsGroups } from './ConversationDetailsGroups';
import { PanelType } from '../../../types/Panels';
import { type CallHistoryGroup } from '../../../types/CallDisposition';
import { NavTab } from '../../../state/ducks/nav';
import { NavTab } from '../../../types/Nav';
import { ContextMenu } from '../../ContextMenu';
import { canHaveNicknameAndNote } from '../../../util/nicknames';
import { CallHistoryGroupPanelSection } from './CallHistoryGroupPanelSection';

View file

@ -3,10 +3,11 @@
import { createLogger } from '../logging/log';
import type { Location } from '../state/ducks/nav';
import { SECOND } from '../util/durations';
import { sleep } from '../util/sleep';
import type { Location } from '../types/Nav';
const log = createLogger('BeforeNavigate');
export enum BeforeNavigateResponse {

View file

@ -13,10 +13,7 @@ import {
getDistributionListsForRedux,
loadDistributionLists,
} from './distributionListLoader';
import {
getDonationReceiptsForRedux,
loadDonationReceipts,
} from './donationReceiptsLoader';
import { getDonationsForRedux, loadDonationReceipts } from './donationsLoader';
import { getStoriesForRedux, loadStories } from './storyLoader';
import { getUserDataForRedux, loadUserData } from './userLoader';
import {
@ -67,7 +64,7 @@ export function getParametersForRedux(): ReduxInitData {
callHistory: getCallsHistoryForRedux(),
callHistoryUnreadCount: getCallsHistoryUnreadCountForRedux(),
callLinks: getCallLinksForRedux(),
donations: getDonationReceiptsForRedux(),
donations: getDonationsForRedux(),
gifs: getGifsStateForRedux(),
mainWindowStats,
menuOptions,

View file

@ -40,6 +40,8 @@ import type {
ReceiptContext,
StripeDonationAmount,
} from '../types/Donations';
import { ToastType } from '../types/Toast';
import { NavTab, SettingsPage } from '../types/Nav';
const { createDonationReceipt } = DataWriter;
@ -63,9 +65,36 @@ const MAX_CREDENTIAL_EXPIRATION_IN_DAYS = 90;
let runDonationAbortController: AbortController | undefined;
let isInternalDonationInProgress = false;
let isDonationInProgress = false;
let isInitialized = false;
// Public API
// Starting everything up
export async function initialize(): Promise<void> {
if (isInitialized) {
return;
}
isInitialized = true;
const workflow = _getWorkflowFromRedux();
if (!workflow) {
return;
}
if (didResumeWorkflowAtStartup() && !isDonationPageVisible()) {
log.info(
'initialize: We resumed at startup and donation page not visible. Showing processing toast.'
);
window.reduxActions.toast.showToast({
toastType: ToastType.DonationProcessing,
});
}
await _runDonationWorkflow();
}
// These are the four moments the user provides input to the donation workflow. So,
// UI calls these methods directly; everything else happens automatically.
@ -190,13 +219,11 @@ export async function _saveAndRunWorkflow(
log.info(`${logId}: No need to start workflow; it's been cleared`);
}
await runDonationWorkflow();
await _runDonationWorkflow();
}
// There's one place where this is called outside this file - when starting up, in the
// onEmpty handler in background.ts.
export async function runDonationWorkflow(): Promise<void> {
let logId = 'runDonationWorkflow';
export async function _runDonationWorkflow(): Promise<void> {
let logId = '_runDonationWorkflow';
let totalCount = 0;
let backoffCount = 0;
@ -283,6 +310,15 @@ export async function runDonationWorkflow(): Promise<void> {
updated = await _redeemReceipt(existing);
// continuing
} else if (type === donationStateSchema.Enum.DONE) {
if (!isDonationPageVisible()) {
log.info(
`${logId}: Donation page not visible. Showing complete toast.`
);
window.reduxActions.toast.showToast({
toastType: ToastType.DonationCompleted,
});
}
log.info(`${logId}: Workflow is complete. Returning.`);
return;
} else {
@ -789,6 +825,19 @@ async function saveReceipt(workflow: DonationWorkflow, logId: string) {
log.info(`${logId}: Successfully saved receipt`);
}
function didResumeWorkflowAtStartup() {
return window.reduxStore.getState().donations.didResumeWorkflowAtStartup;
}
function isDonationPageVisible() {
const { selectedLocation } = window.reduxStore.getState().nav;
return (
selectedLocation.tab === NavTab.Settings &&
(selectedLocation.details.page === SettingsPage.DonationsDonateFlow ||
selectedLocation.details.page === SettingsPage.DonationsReceiptList)
);
}
// Working with zkgroup receipts
function getServerPublicParams(): ServerPublicParams {

View file

@ -15,13 +15,16 @@ export async function loadDonationReceipts(): Promise<void> {
donationReceipts = await DataReader.getAllDonationReceipts();
}
export function getDonationReceiptsForRedux(): DonationsStateType {
export function getDonationsForRedux(): DonationsStateType {
strictAssert(
donationReceipts != null,
'donation receipts have not been loaded'
);
const currentWorkflow = _getWorkflowFromStorage();
return {
currentWorkflow: _getWorkflowFromStorage(),
currentWorkflow,
didResumeWorkflowAtStartup: Boolean(currentWorkflow),
lastError: undefined,
receipts: donationReceipts,
};

View file

@ -180,12 +180,8 @@ import {
MESSAGE_MAX_EDIT_COUNT,
} from '../../util/canEditMessage';
import type { ChangeLocationAction } from './nav';
import {
CHANGE_LOCATION,
NavTab,
changeLocation,
actions as navActions,
} from './nav';
import { CHANGE_LOCATION, changeLocation, actions as navActions } from './nav';
import { NavTab, ProfileEditorPage, SettingsPage } from '../../types/Nav';
import { sortByMessageOrder } from '../../types/ForwardDraft';
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
import {
@ -221,8 +217,6 @@ import { markFailed } from '../../test-node/util/messageFailures';
import { cleanupMessages } from '../../util/cleanup';
import { MessageModel } from '../../models/messages';
import type { ConversationModel } from '../../models/conversations';
import { EditState } from '../../components/ProfileEditor';
import { Page } from '../../components/Preferences';
import { MessageRequestResponseSource } from '../../types/MessageRequestResponseEvent';
const log = createLogger('conversations');
@ -2268,8 +2262,8 @@ function myProfileChanged(
changeLocation({
tab: NavTab.Settings,
details: {
page: Page.Profile,
state: EditState.None,
page: SettingsPage.Profile,
state: ProfileEditorPage.None,
},
});
}

View file

@ -8,6 +8,8 @@ import { useBoundActions } from '../../hooks/useBoundActions';
import { createLogger } from '../../logging/log';
import * as Errors from '../../types/errors';
import { isStagingServer } from '../../util/isStagingServer';
import { DataWriter } from '../../sql/Client';
import * as donations from '../../services/donations';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type {
@ -18,8 +20,7 @@ import type {
StripeDonationAmount,
} from '../../types/Donations';
import type { StateType as RootStateType } from '../reducer';
import { DataWriter } from '../../sql/Client';
import * as donations from '../../services/donations';
import { drop } from '../../util/drop';
const log = createLogger('donations');
@ -29,6 +30,7 @@ export type DonationsStateType = ReadonlyDeep<{
currentWorkflow: DonationWorkflow | undefined;
lastError: DonationErrorType | undefined;
receipts: Array<DonationReceipt>;
didResumeWorkflowAtStartup: boolean;
}>;
// Actions
@ -37,12 +39,18 @@ export const ADD_RECEIPT = 'donations/ADD_RECEIPT';
export const SUBMIT_DONATION = 'donations/SUBMIT_DONATION';
export const UPDATE_WORKFLOW = 'donations/UPDATE_WORKFLOW';
export const UPDATE_LAST_ERROR = 'donations/UPDATE_LAST_ERROR';
export const SET_DID_RESUME = 'donations/SET_DID_RESUME';
export type AddReceiptAction = ReadonlyDeep<{
type: typeof ADD_RECEIPT;
payload: { receipt: DonationReceipt };
}>;
export type SetDidResumeAction = ReadonlyDeep<{
type: typeof SET_DID_RESUME;
payload: boolean;
}>;
export type SubmitDonationAction = ReadonlyDeep<{
type: typeof SUBMIT_DONATION;
payload: SubmitDonationType;
@ -60,6 +68,7 @@ export type UpdateWorkflowAction = ReadonlyDeep<{
export type DonationsActionType = ReadonlyDeep<
| AddReceiptAction
| SetDidResumeAction
| SubmitDonationAction
| UpdateLastErrorAction
| UpdateWorkflowAction
@ -97,6 +106,13 @@ function internalAddDonationReceipt(
};
}
function setDidResume(didResume: boolean): SetDidResumeAction {
return {
type: SET_DID_RESUME,
payload: didResume,
};
}
export type SubmitDonationType = ReadonlyDeep<{
currencyType: string;
paymentAmount: StripeDonationAmount;
@ -132,6 +148,8 @@ function submitDonation({
}
function clearWorkflow(): UpdateWorkflowAction {
drop(donations.clearDonation());
return {
type: UPDATE_WORKFLOW,
payload: { nextWorkflow: undefined },
@ -160,6 +178,7 @@ export const actions = {
addReceipt,
clearWorkflow,
internalAddDonationReceipt,
setDidResume,
submitDonation,
updateLastError,
updateWorkflow,
@ -174,6 +193,7 @@ export const useDonationsActions = (): BoundActionCreatorsMapObject<
export function getEmptyState(): DonationsStateType {
return {
currentWorkflow: undefined,
didResumeWorkflowAtStartup: false,
lastError: undefined,
receipts: [],
};
@ -190,6 +210,13 @@ export function reducer(
};
}
if (action.type === SET_DID_RESUME) {
return {
...state,
didResumeWorkflowAtStartup: action.payload,
};
}
if (action.type === UPDATE_LAST_ERROR) {
return {
...state,
@ -198,9 +225,11 @@ export function reducer(
}
if (action.type === UPDATE_WORKFLOW) {
const { nextWorkflow } = action.payload;
return {
...state,
currentWorkflow: action.payload.nextWorkflow,
currentWorkflow: nextWorkflow,
};
}

View file

@ -6,38 +6,19 @@ import type { ThunkAction } from 'redux-thunk';
import { createLogger } from '../../logging/log';
import { useBoundActions } from '../../hooks/useBoundActions';
import { Page } from '../../components/Preferences';
import { NavTab, SettingsPage } from '../../types/Nav';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { StateType as RootStateType } from '../reducer';
import type { EditState } from '../../components/ProfileEditor';
import type { Location } from '../../types/Nav';
const log = createLogger('nav');
// Types
export enum NavTab {
Chats = 'Chats',
Calls = 'Calls',
Stories = 'Stories',
Settings = 'Settings',
}
export type Location = ReadonlyDeep<
| {
tab: NavTab.Settings;
details:
| {
page: Page.Profile;
state: EditState;
}
| { page: Exclude<Page, Page.Profile> };
}
| { tab: Exclude<NavTab, NavTab.Settings> }
>;
function printLocation(location: Location): string {
if (location.tab === NavTab.Settings) {
if (location.details.page === Page.Profile) {
if (location.details.page === SettingsPage.Profile) {
return `${location.tab}/${location.details.page}/${location.details.state}`;
}
return `${location.tab}/${location.details.page}`;

View file

@ -2,12 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import { NavTab, type NavStateType } from '../ducks/nav';
import { getAllConversationsUnreadStats } from './conversations';
import { getStoriesNotificationCount } from './stories';
import type { UnreadStats } from '../../util/countUnreadStats';
import { getCallHistoryUnreadCount } from './callHistory';
import { NavTab } from '../../types/Nav';
import type { StateType } from '../reducer';
import type { NavStateType } from '../ducks/nav';
import type { UnreadStats } from '../../util/countUnreadStats';
function getNav(state: StateType): NavStateType {
return state.nav;

View file

@ -1,8 +1,10 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { memo, useCallback } from 'react';
import type { ReactNode } from 'react';
import { useSelector } from 'react-redux';
import { NavTabs } from '../../components/NavTabs';
import { getIntl, getTheme, getIsNightly } from '../selectors/user';
@ -20,13 +22,14 @@ import {
getStoriesEnabled,
} from '../selectors/items';
import { getSelectedNavTab } from '../selectors/nav';
import type { Location } from '../ducks/nav';
import { useNavActions } from '../ducks/nav';
import { getHasPendingUpdate } from '../selectors/updates';
import { getCallHistoryUnreadCount } from '../selectors/callHistory';
import { Environment } from '../../environment';
import { useItemsActions } from '../ducks/items';
import type { Location } from '../../types/Nav';
export type SmartNavTabsProps = Readonly<{
navTabsCollapsed: boolean;
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;

View file

@ -55,15 +55,15 @@ import { waitForEvent } from '../../shims/events';
import { MINUTE } from '../../util/durations';
import { sendSyncRequests } from '../../textsecure/syncRequests';
import { SmartUpdateDialog } from './UpdateDialog';
import { Page, Preferences } from '../../components/Preferences';
import { Preferences } from '../../components/Preferences';
import { useUpdatesActions } from '../ducks/updates';
import { getUpdateDialogType } from '../selectors/updates';
import { getHasAnyFailedStorySends } from '../selectors/stories';
import { getOtherTabsUnreadStats, getSelectedLocation } from '../selectors/nav';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { SmartProfileEditor } from './ProfileEditor';
import { NavTab, useNavActions } from '../ducks/nav';
import { EditState } from '../../components/ProfileEditor';
import { useNavActions } from '../ducks/nav';
import { NavTab, ProfileEditorPage, SettingsPage } from '../../types/Nav';
import { SmartToastManager } from './ToastManager';
import { useToastActions } from '../ducks/toast';
import { DataReader } from '../../sql/Client';
@ -111,8 +111,8 @@ function renderDonationsPane({
setPage,
}: {
contentsRef: MutableRefObject<HTMLDivElement | null>;
page: Page;
setPage: (page: Page) => void;
page: SettingsPage;
setPage: (page: SettingsPage) => void;
}): JSX.Element {
return (
<SmartPreferencesDonations
@ -687,13 +687,13 @@ export function SmartPreferences(): JSX.Element | null {
}
const { page } = currentLocation.details;
const setPage = (newPage: Page, editState?: EditState) => {
if (newPage === Page.Profile) {
const setPage = (newPage: SettingsPage, editState?: ProfileEditorPage) => {
if (newPage === SettingsPage.Profile) {
changeLocation({
tab: NavTab.Settings,
details: {
page: newPage,
state: editState || EditState.None,
state: editState || ProfileEditorPage.None,
},
});
return;

View file

@ -9,7 +9,7 @@ import type { MutableRefObject } from 'react';
import { getIntl } from '../selectors/user';
import { getMe } from '../selectors/conversations';
import { PreferencesDonations } from '../../components/PreferencesDonations';
import type { Page } from '../../components/Preferences';
import type { SettingsPage } from '../../types/Nav';
import { useDonationsActions } from '../ducks/donations';
import type { StateType } from '../reducer';
import { isStagingServer } from '../../util/isStagingServer';
@ -26,8 +26,8 @@ export const SmartPreferencesDonations = memo(
setPage,
}: {
contentsRef: MutableRefObject<HTMLDivElement | null>;
page: Page;
setPage: (page: Page) => void;
page: SettingsPage;
setPage: (page: SettingsPage) => void;
}) {
const [validCurrencies, setValidCurrencies] = useState<
ReadonlyArray<string>

View file

@ -27,10 +27,10 @@ import {
} from '../selectors/username';
import { SmartUsernameEditor } from './UsernameEditor';
import { getSelectedLocation } from '../selectors/nav';
import { NavTab, useNavActions } from '../ducks/nav';
import { Page } from '../../components/Preferences';
import { useNavActions } from '../ducks/nav';
import { NavTab, SettingsPage } from '../../types/Nav';
import type { EditState } from '../../components/ProfileEditor';
import type { ProfileEditorPage } from '../../types/Nav';
import type { SmartUsernameEditorProps } from './UsernameEditor';
import { ConfirmationDialog } from '../../components/ConfirmationDialog';
@ -103,17 +103,17 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: {
if (
selectedLocation.tab !== NavTab.Settings ||
selectedLocation.details.page !== Page.Profile
selectedLocation.details.page !== SettingsPage.Profile
) {
return null;
}
const editState = selectedLocation.details.state;
const setEditState = (newState: EditState) => {
const setEditState = (newState: ProfileEditorPage) => {
changeLocation({
tab: NavTab.Settings,
details: {
page: Page.Profile,
page: SettingsPage.Profile,
state: newState,
},
});

View file

@ -21,11 +21,13 @@ import { getMe, getSelectedConversationId } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast';
import { useGlobalModalActions } from '../ducks/globalModals';
import { NavTab } from '../ducks/nav';
import { useNavActions } from '../ducks/nav';
import { NavTab } from '../../types/Nav';
import { getHasCompletedUsernameOnboarding } from '../selectors/items';
import { ToastManager } from '../../components/ToastManager';
import type { WidthBreakpoint } from '../../components/_util';
import { getToast } from '../selectors/toast';
import { useDonationsActions } from '../ducks/donations';
export type SmartPropsType = Readonly<{
disableMegaphone?: boolean;
@ -53,6 +55,8 @@ export const SmartToastManager = memo(function SmartToastManager({
const { username } = useSelector(getMe);
const selectedNavTab = useSelector(getSelectedNavTab);
const selectedConversationId = useSelector(getSelectedConversationId);
const { changeLocation } = useNavActions();
const { clearWorkflow, setDidResume } = useDonationsActions();
const { onUndoArchive } = useConversationsActions();
const { openFileInFolder, hideToast } = useToastActions();
@ -86,6 +90,8 @@ export const SmartToastManager = memo(function SmartToastManager({
return (
<ToastManager
changeLocation={changeLocation}
clearDonation={clearWorkflow}
i18n={i18n}
OS={OS.getName()}
toast={toast}
@ -94,6 +100,7 @@ export const SmartToastManager = memo(function SmartToastManager({
onUndoArchive={onUndoArchive}
openFileInFolder={openFileInFolder}
hideToast={hideToast}
setDidResumeDonation={setDidResume}
showAttachmentNotAvailableModal={showAttachmentNotAvailableModal}
centerToast={centerToast}
containerWidthBreakpoint={containerWidthBreakpoint}

View file

@ -8,9 +8,8 @@ import { UsernameOnboardingModal } from '../../components/UsernameOnboardingModa
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useUsernameActions } from '../ducks/username';
import { NavTab, useNavActions } from '../ducks/nav';
import { Page } from '../../components/Preferences';
import { EditState } from '../../components/ProfileEditor';
import { useNavActions } from '../ducks/nav';
import { NavTab, SettingsPage, ProfileEditorPage } from '../../types/Nav';
export const SmartUsernameOnboardingModal = memo(
function SmartUsernameOnboardingModal(): JSX.Element {
@ -25,8 +24,8 @@ export const SmartUsernameOnboardingModal = memo(
changeLocation({
tab: NavTab.Settings,
details: {
page: Page.Profile,
state: EditState.Username,
page: SettingsPage.Profile,
state: ProfileEditorPage.Username,
},
});
toggleUsernameOnboarding();

61
ts/types/Nav.ts Normal file
View file

@ -0,0 +1,61 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
export type Location = ReadonlyDeep<
| {
tab: NavTab.Settings;
details:
| {
page: SettingsPage.Profile;
state: ProfileEditorPage;
}
| { page: Exclude<SettingsPage, SettingsPage.Profile> };
}
| { tab: Exclude<NavTab, NavTab.Settings> }
>;
export enum NavTab {
Chats = 'Chats',
Calls = 'Calls',
Stories = 'Stories',
Settings = 'Settings',
}
export enum SettingsPage {
// 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',
}
export enum ProfileEditorPage {
None = 'None',
BetterAvatar = 'BetterAvatar',
ProfileName = 'ProfileName',
Bio = 'Bio',
Username = 'Username',
UsernameLink = 'UsernameLink',
}

View file

@ -1,25 +1,27 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Page } from '../components/Preferences';
import { SettingsPage } from './Nav';
// Should be in sync with isBackupPage()
export type PreferencesBackupPage =
| Page.Backups
| Page.BackupsDetails
| Page.LocalBackups
| Page.LocalBackupsKeyReference
| Page.LocalBackupsSetupFolder
| Page.LocalBackupsSetupKey;
| SettingsPage.Backups
| SettingsPage.BackupsDetails
| SettingsPage.LocalBackups
| SettingsPage.LocalBackupsKeyReference
| SettingsPage.LocalBackupsSetupFolder
| SettingsPage.LocalBackupsSetupKey;
// Should be in sync with PreferencesBackupPage
export function isBackupPage(page: Page): page is PreferencesBackupPage {
export function isBackupPage(
page: SettingsPage
): page is PreferencesBackupPage {
return (
page === Page.Backups ||
page === Page.BackupsDetails ||
page === Page.LocalBackups ||
page === Page.LocalBackupsSetupFolder ||
page === Page.LocalBackupsSetupKey ||
page === Page.LocalBackupsKeyReference
page === SettingsPage.Backups ||
page === SettingsPage.BackupsDetails ||
page === SettingsPage.LocalBackups ||
page === SettingsPage.LocalBackupsSetupFolder ||
page === SettingsPage.LocalBackupsSetupKey ||
page === SettingsPage.LocalBackupsKeyReference
);
}

View file

@ -31,6 +31,8 @@ export enum ToastType {
DecryptionError = 'DecryptionError',
DebugLogError = 'DebugLogError',
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
DonationCompleted = 'DonationCompleted',
DonationProcessing = 'DonationProcessing',
Error = 'Error',
Expired = 'Expired',
FailedToDeleteUsername = 'FailedToDeleteUsername',
@ -120,6 +122,8 @@ export type AnyToast =
| { toastType: ToastType.DangerousFileType }
| { toastType: ToastType.DebugLogError }
| { toastType: ToastType.DeleteForEveryoneFailed }
| { toastType: ToastType.DonationCompleted }
| { toastType: ToastType.DonationProcessing }
| { toastType: ToastType.Error }
| { toastType: ToastType.Expired }
| { toastType: ToastType.FailedToDeleteUsername }

View file

@ -126,6 +126,7 @@ window.testUtilities = {
stories: [],
storyDistributionLists: [],
donations: {
didResumeWorkflowAtStartup: false,
currentWorkflow: undefined,
lastError: undefined,
receipts: [],