signal-desktop/ts/components/installScreen/InstallScreenQrCodeNotScannedStep.tsx

343 lines
9.3 KiB
TypeScript
Raw Normal View History

2023-01-03 11:55:46 -08:00
// Copyright 2021 Signal Messenger, LLC
2021-12-16 09:02:22 -06:00
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement, ReactNode } from 'react';
2025-01-17 13:04:30 -08:00
import React, { useCallback, useState, useEffect } from 'react';
2021-12-16 09:02:22 -06:00
import classNames from 'classnames';
2025-01-17 13:04:30 -08:00
import { noop } from 'lodash';
2021-12-16 09:02:22 -06:00
import type { LocalizerType } from '../../types/Util';
import {
InstallScreenStep,
InstallScreenQRCodeError,
} from '../../types/InstallScreen';
2021-12-16 09:02:22 -06:00
import { missingCaseError } from '../../util/missingCaseError';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
2025-01-17 13:04:30 -08:00
import { drop } from '../../util/drop';
import { getEnvironment, Environment } from '../../environment';
2021-12-16 09:02:22 -06:00
2024-05-15 14:48:02 -07:00
import { I18n } from '../I18n';
2021-12-16 09:02:22 -06:00
import { Spinner } from '../Spinner';
2025-01-17 13:04:30 -08:00
import { BrandedQRCode } from '../BrandedQRCode';
2021-12-16 09:02:22 -06:00
import { TitlebarDragArea } from '../TitlebarDragArea';
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
2023-03-20 13:42:00 -07:00
import { InstallScreenUpdateDialog } from './InstallScreenUpdateDialog';
2021-12-16 09:02:22 -06:00
import { getClassNamesFor } from '../../util/getClassNamesFor';
2023-03-20 13:42:00 -07:00
import type { UpdatesStateType } from '../../state/ducks/updates';
2021-12-16 09:02:22 -06:00
// We can't always use destructuring assignment because of the complexity of this props
// type.
2022-11-17 16:45:19 -08:00
export type PropsType = Readonly<{
2021-12-16 09:02:22 -06:00
i18n: LocalizerType;
2024-09-03 19:56:13 -07:00
provisioningUrl: Loadable<string, InstallScreenQRCodeError>;
2023-03-20 13:42:00 -07:00
hasExpired?: boolean;
updates: UpdatesStateType;
currentVersion: string;
OS: string;
2024-09-04 11:12:45 -07:00
isStaging: boolean;
retryGetQrCode: () => void;
2023-03-20 13:42:00 -07:00
startUpdate: () => void;
forceUpdate: () => void;
2023-03-20 13:42:00 -07:00
}>;
2021-12-16 09:02:22 -06:00
const getQrCodeClassName = getClassNamesFor(
'module-InstallScreenQrCodeNotScannedStep__qr-code'
);
2024-07-01 14:51:49 -07:00
const SUPPORT_PAGE =
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
2022-11-17 16:45:19 -08:00
export function InstallScreenQrCodeNotScannedStep({
currentVersion,
hasExpired,
2021-12-16 09:02:22 -06:00
i18n,
2024-09-04 11:12:45 -07:00
isStaging,
OS,
2021-12-16 09:02:22 -06:00
provisioningUrl,
retryGetQrCode,
2023-03-20 13:42:00 -07:00
startUpdate,
forceUpdate,
updates,
2022-11-17 16:45:19 -08:00
}: Readonly<PropsType>): ReactElement {
return (
<div className="module-InstallScreenQrCodeNotScannedStep">
<TitlebarDragArea />
2021-12-16 09:02:22 -06:00
2022-11-17 16:45:19 -08:00
<InstallScreenSignalLogo />
2021-12-16 09:02:22 -06:00
2023-03-20 13:42:00 -07:00
{hasExpired && (
<InstallScreenUpdateDialog
i18n={i18n}
{...updates}
step={InstallScreenStep.QrCodeNotScanned}
2023-03-20 13:42:00 -07:00
startUpdate={startUpdate}
forceUpdate={forceUpdate}
2023-03-20 13:42:00 -07:00
currentVersion={currentVersion}
OS={OS}
/>
)}
2022-11-17 16:45:19 -08:00
<div className="module-InstallScreenQrCodeNotScannedStep__contents">
<InstallScreenQrCode
i18n={i18n}
{...provisioningUrl}
retryGetQrCode={retryGetQrCode}
/>
2022-11-17 16:45:19 -08:00
<div className="module-InstallScreenQrCodeNotScannedStep__instructions">
2023-03-29 17:03:25 -07:00
<h1>{i18n('icu:Install__scan-this-code')}</h1>
2022-11-17 16:45:19 -08:00
<ol>
2023-03-29 17:03:25 -07:00
<li>{i18n('icu:Install__instructions__1')}</li>
2022-11-17 16:45:19 -08:00
<li>
2024-05-15 14:48:02 -07:00
<I18n
2022-11-17 16:45:19 -08:00
i18n={i18n}
2023-03-29 17:03:25 -07:00
id="icu:Install__instructions__2"
2022-11-17 16:45:19 -08:00
components={{
settings: (
<strong>
2023-03-29 17:03:25 -07:00
{i18n('icu:Install__instructions__2__settings')}
2022-11-17 16:45:19 -08:00
</strong>
),
2023-03-29 17:03:25 -07:00
linkedDevices: <strong>{i18n('icu:linkedDevices')}</strong>,
2022-11-17 16:45:19 -08:00
}}
/>
</li>
<li>
2024-05-15 14:48:02 -07:00
<I18n
2022-11-17 16:45:19 -08:00
i18n={i18n}
2023-03-29 17:03:25 -07:00
id="icu:Install__instructions__3"
2022-11-17 16:45:19 -08:00
components={{
2023-03-29 17:03:25 -07:00
linkNewDevice: <strong>{i18n('icu:linkNewDevice')}</strong>,
2022-11-17 16:45:19 -08:00
}}
/>
</li>
</ol>
2024-09-04 11:12:45 -07:00
{isStaging ? (
'THIS IS A STAGING DESKTOP'
) : (
2024-07-01 14:51:49 -07:00
<a target="_blank" rel="noreferrer" href={SUPPORT_PAGE}>
{i18n('icu:Install__support-link')}
</a>
)}
2022-11-17 16:45:19 -08:00
</div>
2021-12-16 09:02:22 -06:00
</div>
</div>
2022-11-17 16:45:19 -08:00
);
}
2021-12-16 09:02:22 -06:00
2022-01-05 11:59:59 -06:00
function InstallScreenQrCode(
2024-09-03 19:56:13 -07:00
props: Loadable<string, InstallScreenQRCodeError> & {
2024-07-01 14:51:49 -07:00
i18n: LocalizerType;
retryGetQrCode: () => void;
}
2021-12-16 09:02:22 -06:00
): ReactElement {
2025-01-28 09:11:22 -08:00
const { i18n, retryGetQrCode } = props;
2021-12-16 09:02:22 -06:00
let contents: ReactNode;
2025-01-14 12:14:32 -08:00
2025-01-28 09:11:22 -08:00
const loadError =
props.loadingState === LoadingState.LoadFailed ? props.error : undefined;
useEffect(() => {
if (loadError !== InstallScreenQRCodeError.MaxRotations) {
return noop;
}
const cleanup = () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
};
const onVisibilityChange = () => {
if (document.hidden) {
return;
}
cleanup();
retryGetQrCode();
};
document.addEventListener('visibilitychange', onVisibilityChange);
return cleanup;
}, [retryGetQrCode, loadError]);
2025-01-14 12:14:32 -08:00
let isJustButton = false;
2021-12-16 09:02:22 -06:00
switch (props.loadingState) {
case LoadingState.Loading:
contents = <Spinner size="24px" svgSize="small" />;
break;
case LoadingState.LoadFailed:
2024-07-01 14:51:49 -07:00
switch (props.error) {
2024-09-03 19:56:13 -07:00
case InstallScreenQRCodeError.Timeout:
2024-07-01 14:51:49 -07:00
contents = (
<>
<span
className={classNames(getQrCodeClassName('__error-message'))}
>
{i18n('icu:Install__qr-failed-load__error--timeout')}
</span>
2025-01-28 09:11:22 -08:00
<RetryButton onClick={retryGetQrCode}>
2025-01-14 12:14:32 -08:00
{i18n('icu:Install__qr-failed-load__retry')}
</RetryButton>
2024-07-01 14:51:49 -07:00
</>
);
break;
2024-09-03 19:56:13 -07:00
case InstallScreenQRCodeError.Unknown:
2024-07-01 14:51:49 -07:00
contents = (
<>
<span
className={classNames(getQrCodeClassName('__error-message'))}
>
<I18n
i18n={i18n}
id="icu:Install__qr-failed-load__error--unknown"
components={{ paragraph: Paragraph }}
/>
</span>
2025-01-28 09:11:22 -08:00
<RetryButton onClick={retryGetQrCode}>
2025-01-14 12:14:32 -08:00
{i18n('icu:Install__qr-failed-load__retry')}
</RetryButton>
2024-07-01 14:51:49 -07:00
</>
);
break;
2024-09-03 19:56:13 -07:00
case InstallScreenQRCodeError.NetworkIssue:
2024-07-01 14:51:49 -07:00
contents = (
<>
<span
className={classNames(getQrCodeClassName('__error-message'))}
>
{i18n('icu:Install__qr-failed-load__error--network')}
</span>
<a
className={classNames(getQrCodeClassName('__get-help'))}
target="_blank"
rel="noreferrer"
href={SUPPORT_PAGE}
>
{i18n('icu:Install__qr-failed-load__get-help')}
</a>
</>
);
break;
2025-01-14 12:14:32 -08:00
case InstallScreenQRCodeError.MaxRotations:
isJustButton = true;
contents = (
2025-01-28 09:11:22 -08:00
<RetryButton onClick={retryGetQrCode}>
2025-01-14 12:14:32 -08:00
{i18n('icu:Install__qr-max-rotations__retry')}
</RetryButton>
);
break;
2024-07-01 14:51:49 -07:00
default:
throw missingCaseError(props.error);
}
2021-12-16 09:02:22 -06:00
break;
case LoadingState.Loaded:
2025-01-17 13:04:30 -08:00
contents = <QRCodeImage i18n={i18n} link={props.value} />;
2021-12-16 09:02:22 -06:00
break;
default:
throw missingCaseError(props);
}
return (
<div
className={classNames(
getQrCodeClassName(''),
props.loadingState === LoadingState.Loaded &&
getQrCodeClassName('--loaded'),
props.loadingState === LoadingState.LoadFailed &&
2025-01-14 12:14:32 -08:00
getQrCodeClassName('--load-failed'),
isJustButton && getQrCodeClassName('--just-button')
2021-12-16 09:02:22 -06:00
)}
>
{contents}
</div>
);
}
2024-07-01 14:51:49 -07:00
2025-01-17 13:04:30 -08:00
function QRCodeImage({
i18n,
link,
}: {
i18n: LocalizerType;
link: string;
}): JSX.Element {
const [isCopying, setIsCopying] = useState(false);
// Add a development-only feature to copy a QR code to the clipboard by double-clicking.
// This can be used to quickly inspect the code, or to link this Desktop with an iOS
// simulator primary, which has a debug-only option to paste the linking URL instead of
// scanning it. (By the time you read this comment Android may have a similar feature.)
const onDoubleClick = useCallback(() => {
if (getEnvironment() === Environment.PackagedApp) {
return;
}
drop(navigator.clipboard.writeText(link));
setIsCopying(true);
}, [link]);
useEffect(() => {
if (!isCopying) {
return noop;
}
const timer = setTimeout(() => {
setIsCopying(false);
}, 250);
return () => clearTimeout(timer);
}, [isCopying]);
return (
<svg
role="img"
aria-label={i18n('icu:Install__scan-this-code')}
className={classNames(
getQrCodeClassName('__code'),
isCopying && getQrCodeClassName('__code--copying')
)}
onDoubleClick={onDoubleClick}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<BrandedQRCode size={16} link={link} color="black" />
</svg>
);
}
2024-07-01 14:51:49 -07:00
function RetryButton({
onClick,
2025-01-14 12:14:32 -08:00
children,
2024-07-01 14:51:49 -07:00
}: {
onClick: () => void;
2025-01-14 12:14:32 -08:00
children: ReactNode;
2024-07-01 14:51:49 -07:00
}): JSX.Element {
const onKeyDown = useCallback(
(ev: React.KeyboardEvent<HTMLButtonElement>) => {
if (ev.key === 'Enter') {
ev.preventDefault();
ev.stopPropagation();
onClick();
}
},
[onClick]
);
return (
<button
className={getQrCodeClassName('__link')}
onClick={onClick}
onKeyDown={onKeyDown}
type="button"
>
2025-01-14 12:14:32 -08:00
{children}
2024-07-01 14:51:49 -07:00
</button>
);
}
function Paragraph(children: React.ReactNode): JSX.Element {
return <p>{children}</p>;
}