From e15c3adddc4f1432188ad9a316dfbdc8a088cdc2 Mon Sep 17 00:00:00 2001 From: trevor-signal <131492920+trevor-signal@users.noreply.github.com> Date: Mon, 11 May 2026 10:30:56 -0400 Subject: [PATCH] Improved relink prompt Co-authored-by: Scott Nonnenberg --- _locales/en/messages.json | 44 +++++ ts/components/DialogRelink.dom.stories.tsx | 1 + ts/components/DialogRelink.dom.tsx | 50 ++++- ts/components/LeftPane.dom.stories.tsx | 1 + .../MaybeTransferModal.dom.stories.tsx | 40 ++++ ts/components/MaybeTransferModal.dom.tsx | 181 ++++++++++++++++++ ts/state/smart/LeftPane.preload.tsx | 2 +- ...ialog.dom.tsx => RelinkDialog.preload.tsx} | 2 + ts/test-mock/bootstrap.node.ts | 1 + 9 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 ts/components/MaybeTransferModal.dom.stories.tsx create mode 100644 ts/components/MaybeTransferModal.dom.tsx rename ts/state/smart/{RelinkDialog.dom.tsx => RelinkDialog.preload.tsx} (87%) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1e4346d473..ca2b901102 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -11022,6 +11022,50 @@ "messageformat": "Slide", "description": "aria-label for a slide within a carousel UI layout" }, + "icu:MaybeTransferModal__title": { + "messageformat": "Transfer your phone’s message history to this desktop?", + "description": "Title for dialog shown when user is unlinked and clicks to relink" + }, + "icu:MaybeTransferModal__description": { + "messageformat": "When you re-link this desktop, you can transfer your phone’s message history to this device.", + "description": "Description for dialog shown when user is unlinked and clicks to relink" + }, + "icu:MaybeTransferModal__learnMore": { + "messageformat": "Learn more", + "description": "Text for link to a support article about relinking a linked device" + }, + "icu:MaybeTransferModal__replaceHistory": { + "messageformat": "This will replace the message history on this desktop.", + "description": "Bullet point in relink dialog for what will happen if you link & sync" + }, + "icu:MaybeTransferModal__scanQrCode": { + "messageformat": "You’ll need to relink by scanning a QR code.", + "description": "Bullet point in relink dialog for what will happen if you link & sync" + }, + "icu:MaybeTransferModal__media": { + "messageformat": "If you have paid Signal Secure Backups, all of your media will be transferred. Otherwise, your last 45 days of media will be included.", + "description": "Bullet point in relink dialog for what will happen if you link & sync" + }, + "icu:MaybeTransferModal__transfer": { + "messageformat": "Transfer", + "description": "Button text when user wants to transfer data from their phone" + }, + "icu:MaybeTransferModal__dontTransfer": { + "messageformat": "Don't transfer", + "description": "Button text when user does not want to transfer data from their phone" + }, + "icu:DeleteDataAndRelinkConfirmationDialog__title": { + "messageformat": "Delete all data?", + "description": "Title in confirmation dialog shown when user is unlinked and chose to delete data and relink" + }, + "icu:DeleteDataAndRelinkConfirmationDialog__pending": { + "messageformat": "Deleting all data", + "description": "Accessiblity label for button when data is in progress of being deleted" + }, + "icu:DeleteDataAndRelinkConfirmationDialog__body": { + "messageformat": "All current data in Signal Desktop will be deleted. You can then scan a QR code and transfer your message history from your phone.", + "description": "Description in confirmation dialog shown when user is unlinked and chose to delete data and relink" + }, "icu:WhatsNew__bugfixes": { "messageformat": "This version contains a number of small tweaks and bug fixes to keep Signal running smoothly.", "description": "Release notes for releases that only include bug fixes", diff --git a/ts/components/DialogRelink.dom.stories.tsx b/ts/components/DialogRelink.dom.stories.tsx index c5812ad54c..9fe54906aa 100644 --- a/ts/components/DialogRelink.dom.stories.tsx +++ b/ts/components/DialogRelink.dom.stories.tsx @@ -16,6 +16,7 @@ const defaultProps = { containerWidthBreakpoint: WidthBreakpoint.Wide, i18n, relinkDevice: action('relink-device'), + renderClearingDataView: action('render-clearing-data-view'), reregister: action('reregister'), weArePrimaryDevice: false, }; diff --git a/ts/components/DialogRelink.dom.tsx b/ts/components/DialogRelink.dom.tsx index 945b82fd45..aaedae97ef 100644 --- a/ts/components/DialogRelink.dom.tsx +++ b/ts/components/DialogRelink.dom.tsx @@ -1,17 +1,22 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { JSX } from 'react'; +import { useState, type JSX } from 'react'; import type { LocalizerType } from '../types/Util.std.ts'; import type { WidthBreakpoint } from './_util.std.ts'; import { LeftPaneDialog } from './LeftPaneDialog.dom.tsx'; +import { + DeleteDataAndRelinkConfirmationDialog, + MaybeTransferModal, +} from './MaybeTransferModal.dom.tsx'; export type PropsType = { containerWidthBreakpoint: WidthBreakpoint; i18n: LocalizerType; relinkDevice: () => void; + renderClearingDataView: () => void; reregister: () => void; weArePrimaryDevice: boolean; }; @@ -20,9 +25,14 @@ export function DialogRelink({ containerWidthBreakpoint, i18n, relinkDevice, + renderClearingDataView, reregister, weArePrimaryDevice, }: PropsType): JSX.Element | null { + const [relinkDialogStep, setRelinkDialogStep] = useState< + 'maybe-transfer' | 'confirm-deletion' | null + >(null); + if (weArePrimaryDevice) { return ( + <> + setRelinkDialogStep('maybe-transfer')} + title={i18n('icu:unlinked')} + hasAction + /> + setRelinkDialogStep('confirm-deletion')} + onDontTransfer={relinkDevice} + onCancel={() => setRelinkDialogStep(null)} + /> + {relinkDialogStep === 'confirm-deletion' ? ( + setRelinkDialogStep('maybe-transfer')} + onConfirm={() => { + renderClearingDataView(); + setRelinkDialogStep(null); + }} + /> + ) : null} + ); } diff --git a/ts/components/LeftPane.dom.stories.tsx b/ts/components/LeftPane.dom.stories.tsx index 19efa14a20..04b6065b1f 100644 --- a/ts/components/LeftPane.dom.stories.tsx +++ b/ts/components/LeftPane.dom.stories.tsx @@ -260,6 +260,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { + ); +} + +export function DeleteDataConfirmationDialog(): JSX.Element { + return ( + action('onConfirm')()} + i18n={i18n} + /> + ); +} diff --git a/ts/components/MaybeTransferModal.dom.tsx b/ts/components/MaybeTransferModal.dom.tsx new file mode 100644 index 0000000000..badd8e08a9 --- /dev/null +++ b/ts/components/MaybeTransferModal.dom.tsx @@ -0,0 +1,181 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { type JSX, type ReactNode } 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 { type AxoSymbolIconName } from '../axo/_internal/AxoSymbolDefs.generated.std.ts'; +import { AxoSymbol } from '../axo/AxoSymbol.dom.tsx'; +import { I18n } from './I18n.dom.tsx'; +import { AxoAlertDialog } from '../axo/AxoAlertDialog.dom.tsx'; +import { AxoButton } from '../axo/AxoButton.dom.tsx'; + +const LEARN_MORE_LINK = + 'https://support.signal.org/hc/articles/360007320551-Linked-Devices'; + +export function MaybeTransferModal({ + i18n, + onCancel, + onDontTransfer, + onTransfer, + open, +}: { + i18n: LocalizerType; + onCancel: () => void; + onDontTransfer: () => void; + onTransfer: () => void; + open: boolean; +}): JSX.Element { + return ( + { + if (!isOpen) { + onCancel(); + } + }} + > + + +
+ + + {i18n('icu:MaybeTransferModal__title')} + +
+ {i18n('icu:MaybeTransferModal__title')} +
+
+
+
+
{i18n('icu:MaybeTransferModal__description')}
+ + {i18n('icu:MaybeTransferModal__learnMore')} + +
+
    + + } + /> + + +
+
+
+ + + + {i18n('icu:MaybeTransferModal__dontTransfer')} + + + {i18n('icu:MaybeTransferModal__transfer')} + + + +
+
+ ); +} + +function ListItemWithIcon({ + iconName, + content, +}: { + iconName: AxoSymbolIconName; + content: ReactNode; +}): ReactNode { + return ( +
  • +
    + +
    +
    {content}
    +
  • + ); +} + +function Bold(parts: Array) { + return {parts}; +} + +export function DeleteDataAndRelinkConfirmationDialog({ + i18n, + onCancel, + onConfirm, + open, +}: { + i18n: LocalizerType; + onCancel: () => void; + onConfirm: () => void; + open: boolean; +}): JSX.Element { + return ( + { + if (!isOpen) { + onCancel(); + } + }} + > + + + + {i18n('icu:DeleteDataAndRelinkConfirmationDialog__title')} + + + {i18n('icu:DeleteDataAndRelinkConfirmationDialog__body')} + + + + + {i18n('icu:cancel')} + + + {i18n('icu:delete')} + + + + + ); +} diff --git a/ts/state/smart/LeftPane.preload.tsx b/ts/state/smart/LeftPane.preload.tsx index b405ef5b3c..7ddcdf3ea5 100644 --- a/ts/state/smart/LeftPane.preload.tsx +++ b/ts/state/smart/LeftPane.preload.tsx @@ -104,7 +104,7 @@ import { SmartCaptchaDialog } from './CaptchaDialog.preload.tsx'; import { SmartCrashReportDialog } from './CrashReportDialog.preload.tsx'; import { SmartMessageSearchResult } from './MessageSearchResult.preload.tsx'; import { SmartNetworkStatus } from './NetworkStatus.preload.tsx'; -import { SmartRelinkDialog } from './RelinkDialog.dom.tsx'; +import { SmartRelinkDialog } from './RelinkDialog.preload.tsx'; import { renderToastManagerWithoutMegaphone, SmartToastManager, diff --git a/ts/state/smart/RelinkDialog.dom.tsx b/ts/state/smart/RelinkDialog.preload.tsx similarity index 87% rename from ts/state/smart/RelinkDialog.dom.tsx rename to ts/state/smart/RelinkDialog.preload.tsx index 6c3d98de97..aa59b87651 100644 --- a/ts/state/smart/RelinkDialog.dom.tsx +++ b/ts/state/smart/RelinkDialog.preload.tsx @@ -8,6 +8,7 @@ import { areWePrimaryDevice, getIntl } from '../selectors/user.std.ts'; import { useNetworkActions } from '../ducks/network.dom.ts'; import type { WidthBreakpoint } from '../../components/_util.std.ts'; +import { renderClearingDataView } from '../../shims/renderClearingDataView.preload.tsx'; type SmartRelinkDialogProps = Readonly<{ containerWidthBreakpoint: WidthBreakpoint; @@ -25,6 +26,7 @@ export const SmartRelinkDialog = memo(function SmartRelinkDialog({ i18n={i18n} containerWidthBreakpoint={containerWidthBreakpoint} relinkDevice={relinkDevice} + renderClearingDataView={renderClearingDataView} reregister={reregister} weArePrimaryDevice={weArePrimaryDevice} /> diff --git a/ts/test-mock/bootstrap.node.ts b/ts/test-mock/bootstrap.node.ts index a50039f325..29b232d850 100644 --- a/ts/test-mock/bootstrap.node.ts +++ b/ts/test-mock/bootstrap.node.ts @@ -425,6 +425,7 @@ export class Bootstrap { return; } await relinkButton.click(); + await window.getByRole('button', { name: "Don't transfer" }).click(); } catch { // Ignore, provision will fail if QR code was never generated }