mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-09-20 00:35:17 +01:00
Change PIN for standalone
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
co-authored by
Scott Nonnenberg
parent
01411b3a3d
commit
ecaaa6ee65
@@ -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. We’ll 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 computer’s version of {OS} soon. To keep using Signal, update your computer’s 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user