Fix error handling in QR code screen
This commit is contained in:
parent
06789623d5
commit
c046d36851
9 changed files with 253 additions and 65 deletions
|
@ -1488,7 +1488,27 @@
|
||||||
},
|
},
|
||||||
"icu:Install__qr-failed-load": {
|
"icu:Install__qr-failed-load": {
|
||||||
"messageformat": "The QR code couldn't load. Check your internet and try again. <retry>Retry</retry>",
|
"messageformat": "The QR code couldn't load. Check your internet and try again. <retry>Retry</retry>",
|
||||||
"description": "Shown on the install screen if the QR code fails to load"
|
"description": "(Deleted 2024/07/01) Shown on the install screen if the QR code fails to load"
|
||||||
|
},
|
||||||
|
"icu:Install__qr-failed-load__error--timeout": {
|
||||||
|
"messageformat": "The QR code couldn't load. Check your connection and try again.",
|
||||||
|
"description": "Shown on the install screen if the QR code fails to load due to connection timeout"
|
||||||
|
},
|
||||||
|
"icu:Install__qr-failed-load__error--unknown": {
|
||||||
|
"messageformat": "<paragraph>An unexpected error occurred.</paragraph><paragraph>Please try again.</paragraph>",
|
||||||
|
"description": "Shown on the install screen if the QR code fails to load due to an unknown error"
|
||||||
|
},
|
||||||
|
"icu:Install__qr-failed-load__error--network": {
|
||||||
|
"messageformat": "Signal cannot link this device using your current network.",
|
||||||
|
"description": "Shown on the install screen if the QR code fails to load due to a network error"
|
||||||
|
},
|
||||||
|
"icu:Install__qr-failed-load__retry": {
|
||||||
|
"messageformat": "Retry",
|
||||||
|
"description": "Text of the button shown on the install screen if the QR code fails to load"
|
||||||
|
},
|
||||||
|
"icu:Install__qr-failed-load__get-help": {
|
||||||
|
"messageformat": "Get help",
|
||||||
|
"description": "Text of the link to support page shown on the install screen if the QR code fails to load"
|
||||||
},
|
},
|
||||||
"icu:Install__support-link": {
|
"icu:Install__support-link": {
|
||||||
"messageformat": "Need help?",
|
"messageformat": "Need help?",
|
||||||
|
|
1
images/icons/v3/open/open-compact-bold.svg
Normal file
1
images/icons/v3/open/open-compact-bold.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.768 1.25c-.813 0-1.469 0-2 .043-.546.045-1.026.14-1.47.366a3.75 3.75 0 0 0-1.64 1.639c-.226.444-.32.924-.365 1.47-.043.531-.043 1.187-.043 2v2.464c0 .813 0 1.469.043 2 .045.546.14 1.026.366 1.47a3.75 3.75 0 0 0 1.639 1.64c.444.226.924.32 1.47.365.531.043 1.187.043 2 .043h2.464c.813 0 1.469 0 2-.043.546-.045 1.026-.14 1.47-.366a3.75 3.75 0 0 0 1.64-1.638c.226-.445.32-.925.365-1.471.043-.531.043-1.187.043-2V8.75a.75.75 0 0 0-1.5 0v.45c0 .853 0 1.447-.038 1.91-.037.453-.107.714-.207.912-.216.423-.56.767-.984.983-.197.1-.458.17-.912.207-.462.037-1.056.038-1.909.038H6.8c-.852 0-1.447 0-1.91-.038-.453-.037-.714-.107-.911-.207a2.25 2.25 0 0 1-.984-.984c-.1-.197-.17-.458-.207-.912-.037-.462-.038-1.056-.038-1.909V6.8c0-.852 0-1.447.038-1.91.037-.453.107-.714.207-.911a2.25 2.25 0 0 1 .984-.984c.197-.1.458-.17.912-.207.462-.037 1.057-.038 1.909-.038h.45a.75.75 0 0 0 0-1.5h-.482Z" fill="#000"/><path d="M13.25 6V3.599L12.03 5.03 7.78 9.28a.75.75 0 0 1-1.06-1.06l4.25-4.25 1.43-1.22H10a.75.75 0 0 1 0-1.5h4a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
images/icons/v3/refresh/refresh-bold.svg
Normal file
1
images/icons/v3/refresh/refresh-bold.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.58.572a.73.73 0 0 0-1.1.627v.483a8.334 8.334 0 1 0 6.848 2.896.833.833 0 0 0-1.265 1.085 6.667 6.667 0 1 1-5.584-2.31v.448a.73.73 0 0 0 1.101.627l2.196-1.3a.73.73 0 0 0 0-1.255L10.58.572Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 296 B |
|
@ -33,7 +33,7 @@
|
||||||
$size: 256px;
|
$size: 256px;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 2px solid transparent;
|
border: 1.5px solid transparent;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -51,13 +51,31 @@
|
||||||
&--load-failed {
|
&--load-failed {
|
||||||
@include font-subtitle;
|
@include font-subtitle;
|
||||||
border-color: $color-gray-05;
|
border-color: $color-gray-05;
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
|
@include font-body-2-bold;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
color: $color-ultramarine;
|
color: $color-ultramarine;
|
||||||
|
margin-block-start: 16px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/refresh/refresh-bold.svg',
|
||||||
|
$color-ultramarine
|
||||||
|
);
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__code {
|
&__code {
|
||||||
|
@ -69,6 +87,7 @@
|
||||||
|
|
||||||
&__error-message {
|
&__error-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@include font-body-2;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
|
@ -78,14 +97,39 @@
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
margin-block: 8px 0;
|
margin-block: 0 8px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
margin-inline: 24px;
|
||||||
color: $color-ultramarine;
|
}
|
||||||
text-decoration: none;
|
|
||||||
|
&__error-message p {
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__get-help {
|
||||||
|
@include font-body-2-bold;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
margin-block-start: 16px;
|
||||||
|
|
||||||
|
color: $color-ultramarine;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/open/open-compact-bold.svg',
|
||||||
|
$color-ultramarine
|
||||||
|
);
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,3 @@ export const _ConnectionFailed = (): JSX.Element => (
|
||||||
error={InstallError.ConnectionFailed}
|
error={InstallError.ConnectionFailed}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const _UnknownError = (): JSX.Element => (
|
|
||||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.UnknownError} />
|
|
||||||
);
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ export enum InstallError {
|
||||||
TooManyDevices,
|
TooManyDevices,
|
||||||
TooOld,
|
TooOld,
|
||||||
ConnectionFailed,
|
ConnectionFailed,
|
||||||
UnknownError,
|
|
||||||
QRCodeFailed,
|
QRCodeFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +51,6 @@ export function InstallScreenErrorStep({
|
||||||
case InstallError.ConnectionFailed:
|
case InstallError.ConnectionFailed:
|
||||||
errorMessage = i18n('icu:installConnectionFailed');
|
errorMessage = i18n('icu:installConnectionFailed');
|
||||||
break;
|
break;
|
||||||
case InstallError.UnknownError:
|
|
||||||
errorMessage = i18n('icu:installUnknownError');
|
|
||||||
break;
|
|
||||||
case InstallError.QRCodeFailed:
|
case InstallError.QRCodeFailed:
|
||||||
buttonText = i18n('icu:Install__learn-more');
|
buttonText = i18n('icu:Install__learn-more');
|
||||||
errorMessage = i18n('icu:installUnknownError');
|
errorMessage = i18n('icu:installUnknownError');
|
||||||
|
|
|
@ -10,7 +10,10 @@ import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { Loadable } from '../../util/loadable';
|
import type { Loadable } from '../../util/loadable';
|
||||||
import { LoadingState } from '../../util/loadable';
|
import { LoadingState } from '../../util/loadable';
|
||||||
import type { PropsType } from './InstallScreenQrCodeNotScannedStep';
|
import type { PropsType } from './InstallScreenQrCodeNotScannedStep';
|
||||||
import { InstallScreenQrCodeNotScannedStep } from './InstallScreenQrCodeNotScannedStep';
|
import {
|
||||||
|
InstallScreenQrCodeNotScannedStep,
|
||||||
|
LoadError,
|
||||||
|
} from './InstallScreenQrCodeNotScannedStep';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -34,8 +37,14 @@ export default {
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
} satisfies Meta<PropsType>;
|
} satisfies Meta<PropsType>;
|
||||||
|
|
||||||
function Simulation({ finalResult }: { finalResult: Loadable<string> }) {
|
function Simulation({
|
||||||
const [provisioningUrl, setProvisioningUrl] = useState<Loadable<string>>({
|
finalResult,
|
||||||
|
}: {
|
||||||
|
finalResult: Loadable<string, LoadError>;
|
||||||
|
}) {
|
||||||
|
const [provisioningUrl, setProvisioningUrl] = useState<
|
||||||
|
Loadable<string, LoadError>
|
||||||
|
>({
|
||||||
loadingState: LoadingState.Loading,
|
loadingState: LoadingState.Loading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,7 +92,7 @@ export function QrCodeFailedToLoad(): JSX.Element {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
provisioningUrl={{
|
provisioningUrl={{
|
||||||
loadingState: LoadingState.LoadFailed,
|
loadingState: LoadingState.LoadFailed,
|
||||||
error: new Error('uh oh'),
|
error: LoadError.Unknown,
|
||||||
}}
|
}}
|
||||||
updates={DEFAULT_UPDATES}
|
updates={DEFAULT_UPDATES}
|
||||||
OS="macOS"
|
OS="macOS"
|
||||||
|
@ -112,12 +121,34 @@ export function SimulatedLoading(): JSX.Element {
|
||||||
return <Simulation finalResult={LOADED_URL} />;
|
return <Simulation finalResult={LOADED_URL} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SimulatedFailure(): JSX.Element {
|
export function SimulatedUnknownError(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Simulation
|
<Simulation
|
||||||
finalResult={{
|
finalResult={{
|
||||||
loadingState: LoadingState.LoadFailed,
|
loadingState: LoadingState.LoadFailed,
|
||||||
error: new Error('uh oh'),
|
error: LoadError.Unknown,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SimulatedNetworkIssue(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Simulation
|
||||||
|
finalResult={{
|
||||||
|
loadingState: LoadingState.LoadFailed,
|
||||||
|
error: LoadError.NetworkIssue,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SimulatedTimeout(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Simulation
|
||||||
|
finalResult={{
|
||||||
|
loadingState: LoadingState.LoadFailed,
|
||||||
|
error: LoadError.Timeout,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
@ -20,12 +20,18 @@ import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||||
import type { UpdatesStateType } from '../../state/ducks/updates';
|
import type { UpdatesStateType } from '../../state/ducks/updates';
|
||||||
import { Environment, getEnvironment } from '../../environment';
|
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
|
// We can't always use destructuring assignment because of the complexity of this props
|
||||||
// type.
|
// type.
|
||||||
|
|
||||||
export type PropsType = Readonly<{
|
export type PropsType = Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
provisioningUrl: Loadable<string>;
|
provisioningUrl: Loadable<string, LoadError>;
|
||||||
hasExpired?: boolean;
|
hasExpired?: boolean;
|
||||||
updates: UpdatesStateType;
|
updates: UpdatesStateType;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
|
@ -38,6 +44,9 @@ const getQrCodeClassName = getClassNamesFor(
|
||||||
'module-InstallScreenQrCodeNotScannedStep__qr-code'
|
'module-InstallScreenQrCodeNotScannedStep__qr-code'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SUPPORT_PAGE =
|
||||||
|
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
|
||||||
|
|
||||||
export function InstallScreenQrCodeNotScannedStep({
|
export function InstallScreenQrCodeNotScannedStep({
|
||||||
currentVersion,
|
currentVersion,
|
||||||
hasExpired,
|
hasExpired,
|
||||||
|
@ -105,7 +114,7 @@ export function InstallScreenQrCodeNotScannedStep({
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
{getEnvironment() !== Environment.Staging ? (
|
{getEnvironment() !== Environment.Staging ? (
|
||||||
<a href="https://support.signal.org/hc/articles/360007320451#desktop_multiple_device">
|
<a target="_blank" rel="noreferrer" href={SUPPORT_PAGE}>
|
||||||
{i18n('icu:Install__support-link')}
|
{i18n('icu:Install__support-link')}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
@ -118,7 +127,10 @@ export function InstallScreenQrCodeNotScannedStep({
|
||||||
}
|
}
|
||||||
|
|
||||||
function InstallScreenQrCode(
|
function InstallScreenQrCode(
|
||||||
props: Loadable<string> & { i18n: LocalizerType; retryGetQrCode: () => void }
|
props: Loadable<string, LoadError> & {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
retryGetQrCode: () => void;
|
||||||
|
}
|
||||||
): ReactElement {
|
): ReactElement {
|
||||||
const { i18n } = props;
|
const { i18n } = props;
|
||||||
|
|
||||||
|
@ -128,33 +140,58 @@ function InstallScreenQrCode(
|
||||||
contents = <Spinner size="24px" svgSize="small" />;
|
contents = <Spinner size="24px" svgSize="small" />;
|
||||||
break;
|
break;
|
||||||
case LoadingState.LoadFailed:
|
case LoadingState.LoadFailed:
|
||||||
contents = (
|
switch (props.error) {
|
||||||
<span className={classNames(getQrCodeClassName('__error-message'))}>
|
case LoadError.Timeout:
|
||||||
<I18n
|
contents = (
|
||||||
i18n={i18n}
|
<>
|
||||||
id="icu:Install__qr-failed-load"
|
<span
|
||||||
components={{
|
className={classNames(getQrCodeClassName('__error-message'))}
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
>
|
||||||
retry: (parts: Array<string | JSX.Element>) => (
|
{i18n('icu:Install__qr-failed-load__error--timeout')}
|
||||||
<button
|
</span>
|
||||||
className={getQrCodeClassName('__link')}
|
<RetryButton i18n={i18n} onClick={props.retryGetQrCode} />
|
||||||
onClick={props.retryGetQrCode}
|
</>
|
||||||
onKeyDown={ev => {
|
);
|
||||||
if (ev.key === 'Enter') {
|
break;
|
||||||
props.retryGetQrCode();
|
case LoadError.Unknown:
|
||||||
ev.preventDefault();
|
contents = (
|
||||||
ev.stopPropagation();
|
<>
|
||||||
}
|
<span
|
||||||
}}
|
className={classNames(getQrCodeClassName('__error-message'))}
|
||||||
type="button"
|
>
|
||||||
>
|
<I18n
|
||||||
{parts}
|
i18n={i18n}
|
||||||
</button>
|
id="icu:Install__qr-failed-load__error--unknown"
|
||||||
),
|
components={{ paragraph: Paragraph }}
|
||||||
}}
|
/>
|
||||||
/>
|
</span>
|
||||||
</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;
|
break;
|
||||||
case LoadingState.Loaded:
|
case LoadingState.Loaded:
|
||||||
contents = (
|
contents = (
|
||||||
|
@ -183,3 +220,37 @@ function InstallScreenQrCode(
|
||||||
</div>
|
</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>;
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
InstallScreenStep,
|
InstallScreenStep,
|
||||||
} from '../../components/InstallScreen';
|
} from '../../components/InstallScreen';
|
||||||
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
|
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
|
||||||
|
import { LoadError } from '../../components/installScreen/InstallScreenQrCodeNotScannedStep';
|
||||||
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
|
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
|
||||||
import { WidthBreakpoint } from '../../components/_util';
|
import { WidthBreakpoint } from '../../components/_util';
|
||||||
import { HTTPError } from '../../textsecure/Errors';
|
import { HTTPError } from '../../textsecure/Errors';
|
||||||
|
@ -44,7 +45,7 @@ type StateType =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
step: InstallScreenStep.QrCodeNotScanned;
|
step: InstallScreenStep.QrCodeNotScanned;
|
||||||
provisioningUrl: Loadable<string>;
|
provisioningUrl: Loadable<string, LoadError>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
step: InstallScreenStep.ChoosingDeviceName;
|
step: InstallScreenStep.ChoosingDeviceName;
|
||||||
|
@ -67,25 +68,33 @@ const qrCodeBackOff = new BackOff([
|
||||||
60 * SECOND,
|
60 * SECOND,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getInstallError(err: unknown): InstallError {
|
function classifyError(
|
||||||
|
err: unknown
|
||||||
|
): { installError: InstallError } | { loadError: LoadError } {
|
||||||
if (err instanceof HTTPError) {
|
if (err instanceof HTTPError) {
|
||||||
switch (err.code) {
|
switch (err.code) {
|
||||||
case -1:
|
case -1:
|
||||||
return InstallError.ConnectionFailed;
|
if (
|
||||||
|
isRecord(err.cause) &&
|
||||||
|
err.cause.code === 'SELF_SIGNED_CERT_IN_CHAIN'
|
||||||
|
) {
|
||||||
|
return { loadError: LoadError.NetworkIssue };
|
||||||
|
}
|
||||||
|
return { installError: InstallError.ConnectionFailed };
|
||||||
case 409:
|
case 409:
|
||||||
return InstallError.TooOld;
|
return { installError: InstallError.TooOld };
|
||||||
case 411:
|
case 411:
|
||||||
return InstallError.TooManyDevices;
|
return { installError: InstallError.TooManyDevices };
|
||||||
default:
|
default:
|
||||||
return InstallError.UnknownError;
|
return { loadError: LoadError.Unknown };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// AccountManager.registerSecondDevice uses this specific "websocket closed" error
|
// AccountManager.registerSecondDevice uses this specific "websocket closed" error
|
||||||
// message.
|
// message.
|
||||||
if (isRecord(err) && err.message === 'websocket closed') {
|
if (isRecord(err) && err.message === 'websocket closed') {
|
||||||
return InstallError.ConnectionFailed;
|
return { installError: InstallError.ConnectionFailed };
|
||||||
}
|
}
|
||||||
return InstallError.UnknownError;
|
return { loadError: LoadError.Unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
|
@ -255,8 +264,13 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
);
|
);
|
||||||
const sleepMs = qrCodeBackOff.getAndIncrement();
|
const sleepMs = qrCodeBackOff.getAndIncrement();
|
||||||
log.info(`InstallScreen/getQRCode: race to ${sleepMs}ms`);
|
log.info(`InstallScreen/getQRCode: race to ${sleepMs}ms`);
|
||||||
await pTimeout(qrCodeResolution.promise, sleepMs, sleepError);
|
await Promise.all([
|
||||||
await qrCodePromise;
|
pTimeout(qrCodeResolution.promise, sleepMs, sleepError),
|
||||||
|
|
||||||
|
// Note that `registerSecondDevice` resolves once the registration
|
||||||
|
// is fully complete and thus should not be subjected to a timeout.
|
||||||
|
qrCodePromise,
|
||||||
|
]);
|
||||||
|
|
||||||
window.IPC.removeSetupMenuItems();
|
window.IPC.removeSetupMenuItems();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -280,12 +294,26 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
if (error === sleepError) {
|
if (error === sleepError) {
|
||||||
setState({
|
setState({
|
||||||
step: InstallScreenStep.QrCodeNotScanned,
|
step: InstallScreenStep.QrCodeNotScanned,
|
||||||
provisioningUrl: { loadingState: LoadingState.LoadFailed, error },
|
provisioningUrl: {
|
||||||
|
loadingState: LoadingState.LoadFailed,
|
||||||
|
error: LoadError.Timeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const classifiedError = classifyError(error);
|
||||||
|
if ('installError' in classifiedError) {
|
||||||
|
setState({
|
||||||
|
step: InstallScreenStep.Error,
|
||||||
|
error: classifiedError.installError,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState({
|
setState({
|
||||||
step: InstallScreenStep.Error,
|
step: InstallScreenStep.QrCodeNotScanned,
|
||||||
error: getInstallError(error),
|
provisioningUrl: {
|
||||||
|
loadingState: LoadingState.LoadFailed,
|
||||||
|
error: classifiedError.loadError,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue