Change PIN for standalone

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
ayumi-signal
2026-09-01 21:30:44 +00:00
committed by GitHub
co-authored by Scott Nonnenberg
parent 01411b3a3d
commit ecaaa6ee65
17 changed files with 425 additions and 1 deletions
+52
View File
@@ -8216,6 +8216,14 @@
"messageformat": "Advanced",
"description": "Title for advanced settings"
},
"icu:Preferences--change-signal-pin": {
"messageformat": "Change your PIN",
"description": "Title for changing Signal PIN in settings"
},
"icu:Preferences--change-signal-pin-button": {
"messageformat": "Change",
"description": "Button for changing Signal PIN in settings"
},
"icu:Preferences--signal-pin": {
"messageformat": "Signal PIN",
"description": "Title for Signal PIN settings"
@@ -10910,6 +10918,10 @@
"messageformat": "Incorrect PIN. Try again",
"description": "Error text shown in PIN input box in Signal PIN reminder dialog, when the input was incorrect"
},
"icu:PinReminderModal__forgot": {
"messageformat": "Forgot PIN",
"description": "Forgot PIN button in Signal PIN reminder dialog"
},
"icu:PinReminderModal__confirm": {
"messageformat": "Confirm",
"description": "Confirm button in Signal PIN reminder dialog"
@@ -10918,6 +10930,46 @@
"messageformat": "PIN correct. Well remind you again later.",
"description": "Toast message when Signal PIN reminder was completed successfully."
},
"icu:PinChangeModal__title--create": {
"messageformat": "Change your Signal PIN",
"description": "Title of Signal PIN change dialog when creating a new PIN"
},
"icu:PinChangeModal__body--create": {
"messageformat": "You can change your PIN as long as this device is registered. <learnMoreLink>Learn more</learnMoreLink>",
"description": "Body of Signal PIN change dialog when creating a new PIN"
},
"icu:PinChangeModal__title--confirm": {
"messageformat": "Confirm your Signal PIN",
"description": "Title of Signal PIN change dialog when confirming a new PIN"
},
"icu:PinChangeModal__body--confirm": {
"messageformat": "Enter the PIN you just created.",
"description": "Body of Signal PIN change dialog when confirming a new PIN"
},
"icu:PinChangeModal__pin-input-placeholder--create": {
"messageformat": "Enter a new PIN",
"description": "Placeholder text of PIN input box in Signal PIN change dialog when creating a new PIN"
},
"icu:PinChangeModal__pin-input-placeholder--confirm": {
"messageformat": "Confirm PIN",
"description": "Placeholder text of PIN input box in Signal PIN change dialog when confirming PIN"
},
"icu:PinChangeModal__cancel": {
"messageformat": "Cancel",
"description": "Cancel button in Signal PIN change dialog"
},
"icu:PinChangeModal__continue": {
"messageformat": "Continue",
"description": "Continue button in Signal PIN change dialog, to go to the Confirm step"
},
"icu:PinChangeModal__confirm": {
"messageformat": "Confirm",
"description": "Confirm button in Signal PIN change dialog"
},
"icu:PinChangeCompletedToast": {
"messageformat": "New PIN created.",
"description": "Toast message when Signal PIN was changed successfully."
},
"icu:UnsupportedOSWarningDialog__body": {
"messageformat": "Signal Desktop will no longer support your computers version of {OS} soon. To keep using Signal, update your computers operating system by {expirationDate}. <learnMoreLink>Learn more</learnMoreLink>",
"description": "Body of a dialog displayed on unsupported operating systems"
@@ -181,6 +181,9 @@ export type PropsType = {
// TerminateGroupFailedModal
terminateGroupFailedModal: { conversationId: string } | null;
renderTerminateGroupFailedModal: () => JSX.Element | null;
// PinChangeModal
isPinChangeModalVisible: boolean;
renderPinChangeModal: () => JSX.Element | null;
// PinReminderModal
pinReminderState: PinReminderState;
renderPinReminderModal: () => JSX.Element | null;
@@ -249,6 +252,9 @@ export function GlobalModalContainer({
// PinMessageDialog
pinMessageDialogData,
renderPinMessageDialog,
// PinChangeModal
isPinChangeModalVisible,
renderPinChangeModal,
// PinReminderModal
pinReminderState,
renderPinReminderModal,
@@ -420,6 +426,12 @@ export function GlobalModalContainer({
return renderPinMessageDialog();
}
// PIN change renders with precedence over the reminder modal, since
// the reminder modal can open the PIN change modal.
if (isPinChangeModalVisible) {
return renderPinChangeModal();
}
if (pinReminderState === PinReminderState.Modal) {
return renderPinReminderModal();
}
@@ -0,0 +1,24 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { JSX } from 'react';
import { action } from '@storybook/addon-actions';
import { type Meta } from '@storybook/react';
import { PinChangeModal } from './PinChangeModal.dom.tsx';
const { i18n } = window.SignalContext;
export default {
title: 'Components/PinChangeModal',
} satisfies Meta;
export function Default(): JSX.Element {
return (
<PinChangeModal
onCancel={action('onCancel')}
onSubmit={action('onSubmit')}
i18n={i18n}
/>
);
}
+184
View File
@@ -0,0 +1,184 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useCallback, useState, type JSX } from 'react';
import type { LocalizerType } from '../types/Util.std.ts';
import { AxoDialog } from '../axo/AxoDialog.dom.tsx';
import { tw } from '../axo/tw.dom.tsx';
import { AxoPasswordField } from '../axo/fields/AxoPasswordField.dom.tsx';
import {
InputContainer,
PIN_ARTICLE_ON_SUPPORT,
Spacer,
} from './standaloneRegistration/util/StepComponents.dom.tsx';
import {
PIN_LENGTH_MINIMUM,
PIN_MAX_BYTES,
PIN_MAX_GRAPHEMES,
} from './standaloneRegistration/stages/VerifyPIN.dom.tsx';
import { missingCaseError } from '../util/missingCaseError.std.ts';
import { I18n } from './I18n.dom.tsx';
const learnMoreLink = (parts: Array<JSX.Element | string>) => (
<a
className={tw('text-primary')}
href={PIN_ARTICLE_ON_SUPPORT}
target="_blank"
rel="noreferrer"
>
{parts}
</a>
);
export function PinChangeModal({
i18n,
onCancel,
onSubmit,
}: {
i18n: LocalizerType;
onCancel: () => void;
onSubmit: (pin: string) => void;
}): JSX.Element {
const [pin, setPin] = useState('');
const [stagedPin, setStagedPin] = useState('');
const [isValidPIN, setIsValidPIN] = useState(false);
const [step, setStep] = useState<'create' | 'confirm'>('create');
const handlePinInputChange = useCallback(
(value: string) => {
setPin(value);
const isValid =
value.length >= PIN_LENGTH_MINIMUM &&
(step === 'confirm' ? value === stagedPin : true);
setIsValidPIN(isValid);
},
[stagedPin, step, setIsValidPIN, setPin]
);
const handleCancel = useCallback(() => {
if (step === 'create') {
onCancel();
} else if (step === 'confirm') {
setPin(stagedPin);
setStagedPin('');
setIsValidPIN(true);
setStep('create');
} else {
throw missingCaseError(step);
}
}, [stagedPin, step, onCancel, setStep]);
const handleSubmit = useCallback(() => {
if (!isValidPIN) {
return;
}
if (step === 'create') {
setStagedPin(pin);
setPin('');
setIsValidPIN(false);
setStep('confirm');
} else if (step === 'confirm') {
onSubmit(pin);
} else {
throw missingCaseError(step);
}
}, [isValidPIN, pin, step, onSubmit]);
return (
<AxoDialog.Root
open
onOpenChange={isOpen => {
if (!isOpen) {
onCancel();
}
}}
>
<AxoDialog.Content
size="sm"
escape="cancel-is-noop"
disableMissingAriaDescriptionWarning
>
<form
onSubmit={event => {
event.preventDefault();
handleSubmit();
}}
>
<AxoDialog.Header>
{step === 'confirm' && <AxoDialog.Back onClick={handleCancel} />}
<AxoDialog.Title>
{step === 'create'
? i18n('icu:PinChangeModal__title--create')
: i18n('icu:PinChangeModal__title--confirm')}
</AxoDialog.Title>
<AxoDialog.Close />
</AxoDialog.Header>
<AxoDialog.Body>
<div className={tw('mb-10 items-center')}>
<div
className={tw(
'mb-6 min-h-9 text-center type-body-medium text-secondary'
)}
>
{step === 'create' ? (
<I18n
id="icu:PinChangeModal__body--create"
i18n={i18n}
components={{
learnMoreLink,
}}
/>
) : (
<I18n id="icu:PinChangeModal__body--confirm" i18n={i18n} />
)}
</div>
<InputContainer helperElement={<Spacer className={tw('h-8')} />}>
<AxoPasswordField.Root
placeholder={
step === 'create'
? i18n(
'icu:PinChangeModal__pin-input-placeholder--create'
)
: i18n(
'icu:PinChangeModal__pin-input-placeholder--confirm'
)
}
autoFocus
maxBytes={PIN_MAX_BYTES}
maxGraphemes={PIN_MAX_GRAPHEMES}
onValueChange={handlePinInputChange}
value={pin}
autoComplete="current-password"
/>
</InputContainer>
</div>
</AxoDialog.Body>
<AxoDialog.Footer>
<AxoDialog.Actions>
<AxoDialog.Action
variant="subtle-secondary"
onClick={handleCancel}
>
{i18n('icu:PinChangeModal__cancel')}
</AxoDialog.Action>
<AxoDialog.Action
variant="strong-primary"
onClick={handleSubmit}
disabled={!isValidPIN}
>
{step === 'create'
? i18n('icu:PinChangeModal__continue')
: i18n('icu:PinChangeModal__confirm')}
</AxoDialog.Action>
{/* This is used so the Enter key submits the form. */}
{/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */}
<button type="submit" hidden />
</AxoDialog.Actions>
</AxoDialog.Footer>
</form>
</AxoDialog.Content>
</AxoDialog.Root>
);
}
@@ -18,6 +18,7 @@ export function Default(): JSX.Element {
<PinReminderModal
open
onCancel={action('onCancel')}
onForgotPin={action('onForgotPin')}
onPinEntry={() => Math.random() > 0.5}
i18n={i18n}
/>
@@ -30,6 +31,7 @@ export function WrongPin(): JSX.Element {
internalHasValidationError
open
onCancel={action('onCancel')}
onForgotPin={action('onForgotPin')}
onPinEntry={() => Math.random() > 0.5}
i18n={i18n}
/>
+8
View File
@@ -21,12 +21,14 @@ export function PinReminderModal({
internalHasValidationError,
open,
onCancel,
onForgotPin,
onPinEntry,
}: {
internalHasValidationError?: boolean;
i18n: LocalizerType;
open: boolean;
onCancel: () => void;
onForgotPin: () => void;
onPinEntry: (pin: string, ignoreWrongGuess?: boolean) => boolean;
}): JSX.Element {
const [pin, setPin] = useState('');
@@ -124,6 +126,12 @@ export function PinReminderModal({
</AxoDialog.Body>
<AxoDialog.Footer>
<AxoDialog.Actions>
<AxoDialog.Action
variant="subtle-secondary"
onClick={onForgotPin}
>
{i18n('icu:PinReminderModal__forgot')}
</AxoDialog.Action>
<AxoDialog.Action
variant="strong-primary"
onClick={handleSubmit}
@@ -667,6 +667,7 @@ export default {
'setGlobalDefaultConversationColor'
),
setSettingsLocation: action('setSettingsLocation'),
showPinChangeModal: action('showPinChangeModal'),
showToast: action('showToast'),
startLocalBackupExport: action('startLocalBackupExport'),
startPlaintextExport: action('startPlaintextExport'),
+13
View File
@@ -312,6 +312,7 @@ type PropsFunctionType = {
}
) => unknown;
setSettingsLocation: (settingsLocation: SettingsLocation) => unknown;
showPinChangeModal: () => void;
showToast: (toast: AnyToast) => unknown;
startLocalBackupExport: () => void;
startPlaintextExport: () => unknown;
@@ -616,6 +617,7 @@ export function Preferences({
setGlobalDefaultConversationColor,
setSettingsLocation,
shouldShowUpdateDialog,
showPinChangeModal,
showToast,
startLocalBackupExport,
startPlaintextExport,
@@ -888,6 +890,17 @@ export function Preferences({
</List>
{weArePrimaryDevice && (
<List label={i18n('icu:Preferences--signal-pin')}>
<ItemWithAction
label={i18n('icu:Preferences--change-signal-pin')}
action={
<AxoItem.Action
variant="subtle-secondary"
onClick={showPinChangeModal}
>
{i18n('icu:Preferences--change-signal-pin-button')}
</AxoItem.Action>
}
/>
<AxoSwitchItem.Root
label={i18n('icu:Preferences--pin-reminders--header')}
description={i18n('icu:Preferences--pin-reminders--description')}
@@ -216,6 +216,8 @@ function getToast(toastType: ToastType): AnyToast {
};
case ToastType.PinnedMessageNotFound:
return { toastType: ToastType.PinnedMessageNotFound };
case ToastType.PinChangeCompleted:
return { toastType: ToastType.PinChangeCompleted };
case ToastType.PinReminderCompleted:
return { toastType: ToastType.PinReminderCompleted };
case ToastType.PollNotFound:
+6
View File
@@ -798,6 +798,12 @@ function renderToast({
);
}
if (toastType === ToastType.PinChangeCompleted) {
return (
<Toast onClose={hideToast}>{i18n('icu:PinChangeCompletedToast')}</Toast>
);
}
if (toastType === ToastType.PinReminderCompleted) {
return (
<Toast onClose={hideToast}>{i18n('icu:PinReminderCompletedToast')}</Toast>
+6
View File
@@ -91,6 +91,12 @@ class PinReminderService {
}
}
async resetPinReminderTimes(): Promise<void> {
await itemStorage.put(STORAGE_KEY_LAST_REMINDER_TIME, Date.now());
await itemStorage.put(STORAGE_KEY_NEXT_INTERVAL, this.#defaultInterval);
window.reduxActions.globalModals.togglePinReminder(PinReminderState.None);
}
handleSkipReminder(): void {
drop(this.#resolveReminder('skip'));
}
+79
View File
@@ -4,6 +4,7 @@
import type { CallSummary } from '@signalapp/ringrtc';
import type { ThunkAction } from 'redux-thunk';
import type { ReadonlyDeep } from 'type-fest';
import { v7 as generateUuid } from 'uuid';
import OS from '../../util/os/osMain.node.ts';
import type { ExplodePromiseResultType } from '../../util/explodePromise.std.ts';
import type {
@@ -71,6 +72,12 @@ import type { ErrorModalDataProps } from '../../components/ErrorModal.dom.tsx';
import { isDownloadableOrBackfillable } from '../../util/downloadAttachment.preload.ts';
import { backupsService } from '../../services/backups/index.preload.ts';
import { getHasMediaBackups } from '../selectors/items.dom.ts';
import { registrationJobQueue } from '../../jobs/registrationJobQueue.preload.ts';
import { toLogFormat } from '../../types/errors.std.ts';
import { pinReminderService } from '../../services/pinReminder.preload.ts';
import { drop } from '../../util/drop.std.ts';
import { showToast, type ToastActionType } from './toast.preload.ts';
import { ToastType } from '../../types/Toast.dom.tsx';
const log = createLogger('globalModals');
@@ -150,6 +157,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
gv2MigrationProps?: MigrateToGV2PropsType;
groupMemberLabelInfoModalState?: GroupMemberLabelInfoPropsType;
hasConfirmationModal: boolean;
isPinChangeModalVisible: boolean;
isProfileNameWarningModalVisible: boolean;
profileNameWarningModalConversationType?: string;
isShortcutGuideModalVisible: boolean;
@@ -227,6 +235,8 @@ const TOGGLE_CALL_LINK_PENDING_PARTICIPANT_MODAL =
export const SHOW_CALL_QUALITY_SURVEY = 'globalModals/SHOW_CALL_QUALITY_SURVEY';
export const HIDE_CALL_QUALITY_SURVEY = 'globalModals/HIDE_CALL_QUALITY_SURVEY';
const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL';
const SHOW_PIN_CHANGE_MODAL = 'globalModals/SHOW_PIN_CHANGE_MODAL';
const HIDE_PIN_CHANGE_MODAL = 'globalModals/HIDE_PIN_CHANGE_MODAL';
const TOGGLE_PIN_REMINDER = 'globalModals/TOGGLE_PIN_REMINDER';
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
@@ -560,6 +570,14 @@ type TogglePinMessageDialogActionType = ReadonlyDeep<{
payload: PinMessageDialogData | null;
}>;
type HidePinChangeModalActionType = ReadonlyDeep<{
type: typeof HIDE_PIN_CHANGE_MODAL;
}>;
type ShowPinChangeModalActionType = ReadonlyDeep<{
type: typeof SHOW_PIN_CHANGE_MODAL;
}>;
// Not to be confused with pinned messages
type TogglePinReminderActionType = ReadonlyDeep<{
type: typeof TOGGLE_PIN_REMINDER;
@@ -592,6 +610,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| HideKeyTransparencyErrorDialogActionType
| HideKeyTransparencyOnboardingDialogActionType
| HideLowDiskSpaceBackupImportModalActionType
| HidePinChangeModalActionType
| HideSendAnywayDialogActiontype
| HideStoriesSettingsActionType
| HideTapToViewNotAvailableModalActionType
@@ -611,6 +630,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowKeyTransparencyOnboardingDialogActionType
| ShowLowDiskSpaceBackupImportModalActionType
| ShowMediaPermissionsModalActionType
| ShowPinChangeModalActionType
| ShowSendAnywayDialogActionType
| ShowShortcutGuideModalActionType
| ShowStickerPackPreviewActionType
@@ -663,6 +683,7 @@ export const actions = {
hideKeyTransparencyErrorDialog,
hideKeyTransparencyOnboardingDialog,
hideLowDiskSpaceBackupImportModal,
hidePinChangeModal,
hideStoriesSettings,
hideTapToViewNotAvailableModal,
hideTerminateGroupFailedModal,
@@ -681,6 +702,7 @@ export const actions = {
showKeyTransparencyErrorDialog,
showKeyTransparencyOnboardingDialog,
showLowDiskSpaceBackupImportModal,
showPinChangeModal,
showShareCallLinkViaSignal,
showShortcutGuideModal,
showStickerPackPreview,
@@ -689,6 +711,7 @@ export const actions = {
showTerminateGroupFailedModal,
showUserNotFoundModal,
showWhatsNewModal,
submitPinChangeModal,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleCallLinkAddNameModal,
@@ -1198,6 +1221,47 @@ function toggleConfirmationModal(
};
}
function hidePinChangeModal(): HidePinChangeModalActionType {
return {
type: HIDE_PIN_CHANGE_MODAL,
};
}
function showPinChangeModal(): ShowPinChangeModalActionType {
return {
type: SHOW_PIN_CHANGE_MODAL,
};
}
function submitPinChangeModal(
pin: string
): ThunkAction<
void,
RootStateType,
unknown,
HidePinChangeModalActionType | ToastActionType
> {
return async dispatch => {
try {
await itemStorage.put('svrPin', pin);
await registrationJobQueue.add({
type: 'StoreSVR',
id: generateUuid(),
reason: 'submitPinChangeModal',
});
drop(pinReminderService.resetPinReminderTimes());
dispatch(showToast({ toastType: ToastType.PinChangeCompleted }));
} catch (error) {
log.error(`submitPinChangeModal: error changing PIN`, toLogFormat(error));
}
dispatch({
type: HIDE_PIN_CHANGE_MODAL,
});
};
}
function maybeShowPinReminder(): ThunkAction<
void,
RootStateType,
@@ -1604,6 +1668,7 @@ export function getEmptyState(): GlobalModalsStateType {
draftGifMessageSendModalProps: null,
editNicknameAndNoteModalProps: null,
errorModalProps: null,
isPinChangeModalVisible: false,
isProfileNameWarningModalVisible: false,
profileNameWarningModalConversationType: undefined,
isShortcutGuideModalVisible: false,
@@ -1858,6 +1923,20 @@ export function reducer(
};
}
if (action.type === HIDE_PIN_CHANGE_MODAL) {
return {
...state,
isPinChangeModalVisible: false,
};
}
if (action.type === SHOW_PIN_CHANGE_MODAL) {
return {
...state,
isPinChangeModalVisible: true,
};
}
if (action.type === TOGGLE_PIN_REMINDER) {
return {
...state,
@@ -47,6 +47,7 @@ import { SmartPinMessageDialog } from './PinMessageDialog.preload.tsx';
import { SmartGroupMemberLabelInfoModal } from './GroupMemberLabelInfoModal.preload.tsx';
import { SmartTerminateGroupFailedModal } from './TerminateGroupFailedModal.preload.tsx';
import { SmartPinReminderModal } from './PinReminderModal.preload.tsx';
import { SmartPinChangeModal } from './PinChangeModal.preload.tsx';
function renderCallLinkAddNameModal(): JSX.Element {
return <SmartCallLinkAddNameModal />;
@@ -124,6 +125,10 @@ function renderPinMessageDialog(): JSX.Element {
return <SmartPinMessageDialog />;
}
function renderPinChangeModal(): JSX.Element {
return <SmartPinChangeModal />;
}
function renderPinReminderModal(): JSX.Element {
return <SmartPinReminderModal />;
}
@@ -192,6 +197,7 @@ export const SmartGlobalModalContainer = memo(
notePreviewModalProps,
pinMessageDialogData,
pinReminderState,
isPinChangeModalVisible,
isProfileNameWarningModalVisible,
profileNameWarningModalConversationType,
isShortcutGuideModalVisible,
@@ -333,6 +339,7 @@ export const SmartGlobalModalContainer = memo(
isAboutContactModalVisible={aboutContactModalState != null}
isKeyTransparencyErrorVisible={isKeyTransparencyErrorVisible}
isKeyTransparencyOnboardingVisible={isKeyTransparencyOnboardingVisible}
isPinChangeModalVisible={isPinChangeModalVisible}
isProfileNameWarningModalVisible={isProfileNameWarningModalVisible}
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
isSignalConnectionsVisible={isSignalConnectionsVisible}
@@ -361,6 +368,7 @@ export const SmartGlobalModalContainer = memo(
renderMessageRequestActionsConfirmation
}
renderNotePreviewModal={renderNotePreviewModal}
renderPinChangeModal={renderPinChangeModal}
renderPinMessageDialog={renderPinMessageDialog}
renderPinReminderModal={renderPinReminderModal}
renderPlaintextExportWorkflow={renderPlaintextExportWorkflow}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { getIntl } from '../selectors/user.std.ts';
import { useGlobalModalActions } from '../ducks/globalModals.preload.ts';
import { PinChangeModal } from '../../components/PinChangeModal.dom.tsx';
export const SmartPinChangeModal = memo(function SmartPinChangeModal() {
const i18n = useSelector(getIntl);
const { hidePinChangeModal, submitPinChangeModal } = useGlobalModalActions();
return (
<PinChangeModal
i18n={i18n}
onCancel={hidePinChangeModal}
onSubmit={submitPinChangeModal}
/>
);
});
+2 -1
View File
@@ -13,7 +13,7 @@ import { PinReminderModal } from '../../components/PinReminderModal.dom.tsx';
export const SmartPinReminderModal = memo(function SmartPinReminderModal() {
const i18n = useSelector(getIntl);
const pinReminderState = useSelector(getPinReminderModalProps);
const { togglePinReminder } = useGlobalModalActions();
const { showPinChangeModal, togglePinReminder } = useGlobalModalActions();
const handleCancel = useCallback(() => {
togglePinReminder(PinReminderState.Megaphone);
@@ -35,6 +35,7 @@ export const SmartPinReminderModal = memo(function SmartPinReminderModal() {
i18n={i18n}
open
onCancel={handleCancel}
onForgotPin={showPinChangeModal}
onPinEntry={handlePinEntry}
/>
);
+3
View File
@@ -129,6 +129,7 @@ import {
} from '../../types/StorageKeys.std.ts';
import type { BlockedConversation } from '../../components/Preferences.dom.tsx';
import { pinReminderService } from '../../services/pinReminder.preload.ts';
import { useGlobalModalActions } from '../ducks/globalModals.preload.ts';
function renderUpdateDialog(
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
@@ -240,6 +241,7 @@ export function SmartPreferences(): JSX.Element | null {
const { internalAddDonationReceipt } = useDonationsActions();
const { startPlaintextExport, startLocalBackupExport } = useBackupActions();
const { addVisibleMegaphone } = useMegaphonesActions();
const { showPinChangeModal } = useGlobalModalActions();
// Selectors
@@ -1200,6 +1202,7 @@ export function SmartPreferences(): JSX.Element | null {
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
setSettingsLocation={setSettingsLocation}
shouldShowUpdateDialog={shouldShowUpdateDialog}
showPinChangeModal={showPinChangeModal}
showToast={showToast}
startLocalBackupExport={startLocalBackupExport}
startPlaintextExport={startPlaintextExport}
+2
View File
@@ -73,6 +73,7 @@ export enum ToastType {
OriginalMessageNotFound = 'OriginalMessageNotFound',
PinnedConversationsFull = 'PinnedConversationsFull',
PinnedMessageNotFound = 'PinnedMessageNotFound',
PinChangeCompleted = 'PinChangeCompleted',
PinReminderCompleted = 'PinReminderCompleted',
PollNotFound = 'PollNotFound',
ReactionFailed = 'ReactionFailed',
@@ -224,6 +225,7 @@ export type AnyToast =
maxPinnedConversations: number;
}
| { toastType: ToastType.PinnedMessageNotFound }
| { toastType: ToastType.PinChangeCompleted }
| { toastType: ToastType.PinReminderCompleted }
| { toastType: ToastType.PollNotFound }
| { toastType: ToastType.ReactionFailed }