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

250 lines
6.8 KiB
TypeScript

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement, ReactNode } from 'react';
import React, { useCallback } from 'react';
import classNames from 'classnames';
import type { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
import { I18n } from '../I18n';
import { Spinner } from '../Spinner';
import { QrCode } from '../QrCode';
import { TitlebarDragArea } from '../TitlebarDragArea';
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
import { InstallScreenUpdateDialog } from './InstallScreenUpdateDialog';
import { getClassNamesFor } from '../../util/getClassNamesFor';
import type { UpdatesStateType } from '../../state/ducks/updates';
import { Environment, getEnvironment } from '../../environment';
export enum LoadError {
Timeout = 'Timeout',
Unknown = 'Unknown',
NetworkIssue = 'NetworkIssue',
}
// We can't always use destructuring assignment because of the complexity of this props
// type.
export type PropsType = Readonly<{
i18n: LocalizerType;
provisioningUrl: Loadable<string, LoadError>;
hasExpired?: boolean;
updates: UpdatesStateType;
currentVersion: string;
OS: string;
retryGetQrCode: () => void;
startUpdate: () => void;
}>;
const getQrCodeClassName = getClassNamesFor(
'module-InstallScreenQrCodeNotScannedStep__qr-code'
);
const SUPPORT_PAGE =
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
export function InstallScreenQrCodeNotScannedStep({
currentVersion,
hasExpired,
i18n,
OS,
provisioningUrl,
retryGetQrCode,
startUpdate,
updates,
}: Readonly<PropsType>): ReactElement {
return (
<div className="module-InstallScreenQrCodeNotScannedStep">
<TitlebarDragArea />
<InstallScreenSignalLogo />
{hasExpired && (
<InstallScreenUpdateDialog
i18n={i18n}
{...updates}
startUpdate={startUpdate}
currentVersion={currentVersion}
OS={OS}
/>
)}
<div className="module-InstallScreenQrCodeNotScannedStep__contents">
<InstallScreenQrCode
i18n={i18n}
{...provisioningUrl}
retryGetQrCode={retryGetQrCode}
/>
<div className="module-InstallScreenQrCodeNotScannedStep__instructions">
<h1>{i18n('icu:Install__scan-this-code')}</h1>
<ol>
<li>{i18n('icu:Install__instructions__1')}</li>
<li>
<I18n
i18n={i18n}
id="icu:Install__instructions__2"
components={{
settings: (
<strong>
{i18n('icu:Install__instructions__2__settings')}
</strong>
),
linkedDevices: <strong>{i18n('icu:linkedDevices')}</strong>,
}}
/>
</li>
<li>
<I18n
i18n={i18n}
id="icu:Install__instructions__3"
components={{
linkNewDevice: <strong>{i18n('icu:linkNewDevice')}</strong>,
}}
/>
</li>
</ol>
{getEnvironment() !== Environment.Staging ? (
<a target="_blank" rel="noreferrer" href={SUPPORT_PAGE}>
{i18n('icu:Install__support-link')}
</a>
) : (
'THIS IS A STAGING DESKTOP'
)}
</div>
</div>
</div>
);
}
function InstallScreenQrCode(
props: Loadable<string, LoadError> & {
i18n: LocalizerType;
retryGetQrCode: () => void;
}
): ReactElement {
const { i18n } = props;
let contents: ReactNode;
switch (props.loadingState) {
case LoadingState.Loading:
contents = <Spinner size="24px" svgSize="small" />;
break;
case LoadingState.LoadFailed:
switch (props.error) {
case LoadError.Timeout:
contents = (
<>
<span
className={classNames(getQrCodeClassName('__error-message'))}
>
{i18n('icu:Install__qr-failed-load__error--timeout')}
</span>
<RetryButton i18n={i18n} onClick={props.retryGetQrCode} />
</>
);
break;
case LoadError.Unknown:
contents = (
<>
<span
className={classNames(getQrCodeClassName('__error-message'))}
>
<I18n
i18n={i18n}
id="icu:Install__qr-failed-load__error--unknown"
components={{ paragraph: Paragraph }}
/>
</span>
<RetryButton i18n={i18n} onClick={props.retryGetQrCode} />
</>
);
break;
case LoadError.NetworkIssue:
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;
default:
throw missingCaseError(props.error);
}
break;
case LoadingState.Loaded:
contents = (
<QrCode
alt={i18n('icu:Install__scan-this-code')}
className={getQrCodeClassName('__code')}
data={props.value}
/>
);
break;
default:
throw missingCaseError(props);
}
return (
<div
className={classNames(
getQrCodeClassName(''),
props.loadingState === LoadingState.Loaded &&
getQrCodeClassName('--loaded'),
props.loadingState === LoadingState.LoadFailed &&
getQrCodeClassName('--load-failed')
)}
>
{contents}
</div>
);
}
function RetryButton({
i18n,
onClick,
}: {
i18n: LocalizerType;
onClick: () => void;
}): 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"
>
{i18n('icu:Install__qr-failed-load__retry')}
</button>
);
}
function Paragraph(children: React.ReactNode): JSX.Element {
return <p>{children}</p>;
}