From ecaaa6ee65131802ea7564e21b8c67802a07f2f3 Mon Sep 17 00:00:00 2001 From: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:30:44 -0700 Subject: [PATCH] Change PIN for standalone Co-authored-by: Scott Nonnenberg --- _locales/en/messages.json | 52 +++++ ts/components/GlobalModalContainer.dom.tsx | 12 ++ ts/components/PinChangeModal.dom.stories.tsx | 24 +++ ts/components/PinChangeModal.dom.tsx | 184 ++++++++++++++++++ .../PinReminderModal.dom.stories.tsx | 2 + ts/components/PinReminderModal.dom.tsx | 8 + ts/components/Preferences.dom.stories.tsx | 1 + ts/components/Preferences.dom.tsx | 13 ++ ts/components/ToastManager.dom.stories.tsx | 2 + ts/components/ToastManager.dom.tsx | 6 + ts/services/pinReminder.preload.ts | 6 + ts/state/ducks/globalModals.preload.ts | 79 ++++++++ .../smart/GlobalModalContainer.preload.tsx | 8 + ts/state/smart/PinChangeModal.preload.tsx | 21 ++ ts/state/smart/PinReminderModal.preload.tsx | 3 +- ts/state/smart/Preferences.preload.tsx | 3 + ts/types/Toast.dom.tsx | 2 + 17 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 ts/components/PinChangeModal.dom.stories.tsx create mode 100644 ts/components/PinChangeModal.dom.tsx create mode 100644 ts/state/smart/PinChangeModal.preload.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6459b80bb7..e49ec59a6a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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. Learn more", + "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}. Learn more", "description": "Body of a dialog displayed on unsupported operating systems" diff --git a/ts/components/GlobalModalContainer.dom.tsx b/ts/components/GlobalModalContainer.dom.tsx index 82360cf1c9..6c2378d29e 100644 --- a/ts/components/GlobalModalContainer.dom.tsx +++ b/ts/components/GlobalModalContainer.dom.tsx @@ -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(); } diff --git a/ts/components/PinChangeModal.dom.stories.tsx b/ts/components/PinChangeModal.dom.stories.tsx new file mode 100644 index 0000000000..fefec2ff71 --- /dev/null +++ b/ts/components/PinChangeModal.dom.stories.tsx @@ -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 ( + + ); +} diff --git a/ts/components/PinChangeModal.dom.tsx b/ts/components/PinChangeModal.dom.tsx new file mode 100644 index 0000000000..d06ef5e4d1 --- /dev/null +++ b/ts/components/PinChangeModal.dom.tsx @@ -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) => ( + + {parts} + +); + +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 ( + { + if (!isOpen) { + onCancel(); + } + }} + > + +
{ + event.preventDefault(); + handleSubmit(); + }} + > + + {step === 'confirm' && } + + {step === 'create' + ? i18n('icu:PinChangeModal__title--create') + : i18n('icu:PinChangeModal__title--confirm')} + + + + +
+
+ {step === 'create' ? ( + + ) : ( + + )} +
+ }> + + +
+
+ + + + {i18n('icu:PinChangeModal__cancel')} + + + {step === 'create' + ? i18n('icu:PinChangeModal__continue') + : i18n('icu:PinChangeModal__confirm')} + + {/* This is used so the Enter key submits the form. */} + {/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */} +