Redesign device link screens

This commit is contained in:
Evan Hahn 2021-12-16 09:02:22 -06:00 committed by GitHub
parent a023fc1bb0
commit 364f00f37a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1358 additions and 803 deletions

View File

@ -3,6 +3,7 @@
<!-- prettier-ignore -->
<link rel="stylesheet" href="../stylesheets/manifest.css" />
<script src="../components/qrcode/qrcode.js"></script>
<script>
window.SignalWindow = window.SignalWindow || {};
window.SignalWindow.log = {

View File

@ -1306,29 +1306,75 @@
"message": "Privacy is possible. Signal makes it easy.",
"description": "Tagline displayed under 'installWelcome' string on the install page"
},
"linkYourPhone": {
"message": "Link your phone to Signal Desktop",
"description": "Shown on the front page when the application first starts, above the QR code"
},
"signalSettings": {
"message": "Signal Settings",
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
},
"linkedDevices": {
"message": "Linked Devices",
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
},
"plusButton": {
"message": "'+' Button",
"description": "The button used in Signal Android to add a new linked device"
},
"linkNewDevice": {
"message": "Link New Device",
"description": "The menu option shown in Signal iOS to add a new linked device"
},
"LinkScreen__scan-this-code": {
"Install__scan-this-code": {
"message": "Scan this code in the Signal app on your phone",
"description": "Alt text for the QR code on the device link screen"
"description": "Title of the device link screen. Also used as alt text for the QR code on the device link screen"
},
"Install__instructions__1": {
"message": "Open Signal on your phone",
"description": "Instructions on the device link screen"
},
"Install__instructions__2": {
"message": "Tap into $settings$, then tap $linkedDevices$",
"description": "Instructions on the device link screen",
"placeholders": {
"settings": {
"content": "$1",
"example": "<strong>Settings</strong>"
},
"linkedDevices": {
"content": "$2",
"example": "<strong>Linked Devices</strong>"
}
}
},
"Install__instructions__2__settings": {
"message": "Settings",
"description": "Part of the 2nd instruction on the device link screen"
},
"Install__instructions__3": {
"message": "Tap $plusButton$ (Android) or $linkNewDevice$ (iPhone)",
"description": "Instructions on the device link screen",
"placeholders": {
"plusButton": {
"content": "$1",
"example": "<PlusButton />"
},
"linkNewDevice": {
"content": "$2",
"example": "<strong>Link New Device</strong>"
}
}
},
"Install__qr-failed": {
"message": "The QR code couldn't load. Check your internet and try again. $learnMore$",
"description": "Shown on the install screen if the QR code fails to load",
"placeholders": {
"learnMore": {
"content": "$1",
"example": "<a>Learn more</a>"
}
}
},
"Install__qr-failed__learn-more": {
"message": "Learn more",
"description": "Shown on the install screen if the QR code fails to load"
},
"Install__choose-device-name__description": {
"message": "You'll see this name under \"Linked Devices\" on your phone",
"description": "The subheader shown on the 'choose device name' screen in the device linking process"
},
"Install__choose-device-name__placeholder": {
"message": "My Computer",
"description": "The placeholder for the 'choose device name' input"
},
"Preferences--device-name": {
"message": "Device name",
@ -1346,6 +1392,10 @@
"message": "Syncing contacts and groups",
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
},
"initialSync__subtitle": {
"message": "Note: Your chat history will not be synced to this device",
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
},
"installConnectionFailed": {
"message": "Failed to connect to server.",
"description": "Displayed when we can't connect to the server."
@ -1359,6 +1409,9 @@
"installErrorHeader": {
"message": "Something went wrong!"
},
"installUnknownError": {
"message": "An unexpected error occurred. Please try again."
},
"installTryAgain": {
"message": "Try again"
},

View File

@ -91,105 +91,6 @@
</div>
</script>
<script type="text/x-tmpl-mustache" id="link-flow-template">
<div class='module-title-bar-drag-area'></div>
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<div class='header'>{{ linkYourPhone }}</div>
<div id="qr">
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</div>
<div class='nav'>
<div class='instructions'>
<div class='android'>
<div class='label'>
<span class='os-icon android'></span>
</div>
<div class='body'>
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ androidFinalStep }}</div>
</div>
</div>
<div class='apple'>
<div class='label'>
<span class='os-icon apple'></span>
</div>
<div class='body'>
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ appleFinalStep }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{/isStep3}}
{{#isStep4}}
<form id='link-phone'>
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon lead-pencil'></span>
<div class='header'>{{ chooseName }}</div>
<div>
<input type='text' class='device-name' spellcheck='false' maxlength='50' tabIndex="0" />
</div>
</div>
<div class='nav'>
<div>
<button class="button finish" type="submit" tabIndex="0">{{ finishLinkingPhoneButton }}</button>
</div>
</div>
</div>
</div>
</form>
{{/isStep4}}
{{#isStep5}}
<div id='step5' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon sync'></span>
<div class='header'>{{ syncing }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
</div>
</div>
</div>
</div>
{{/isStep5}}
{{#isError}}
<div id='error' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon alert-outline'></span>
<div class='header'>{{ errorHeader }}</div>
<div class='body'>{{ errorMessage }}</div>
</div>
<div class='nav'>
<a class='button try-again'>{{ errorButton }}</a>
{{#errorSecondButton}}
<a class='button second'>{{ errorSecondButton }}</a>
{{/errorSecondButton}}
</div>
</div>
</div>
{{/isError}}
</script>
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="ts/set_os_class.js"></script>
<script

View File

@ -1 +0,0 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m15 5h-1v-1h1m-5 1h-1v-1h1m5.53-1.84 1.31-1.31c.19-.19.19-.51 0-.71-.2-.19-.52-.19-.71 0l-1.48 1.48c-.8-.39-1.7-.62-2.65-.62-.96 0-1.86.23-2.66.63l-1.49-1.49c-.19-.19-.51-.19-.7 0-.2.2-.2.52 0 .71l1.31 1.31c-1.49 1.1-2.46 2.84-2.46 4.84h12c0-2-1-3.75-2.47-4.84m4.97 5.84a1.5 1.5 0 0 0 -1.5 1.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7a1.5 1.5 0 0 0 -1.5-1.5m-17 0a1.5 1.5 0 0 0 -1.5 1.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7a1.5 1.5 0 0 0 -1.5-1.5m2.5 10a1 1 0 0 0 1 1h1v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-3.5h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-3.5h1a1 1 0 0 0 1-1v-10h-12z"/></svg>

Before

Width:  |  Height:  |  Size: 723 B

View File

@ -1 +0,0 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53-1.71-2.47-3.02-7.02-1.26-10.08.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83m-5.71-16c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>

Before

Width:  |  Height:  |  Size: 546 B

View File

@ -458,7 +458,6 @@ try {
require('./ts/backbone/views/whisper_view');
require('./ts/views/conversation_view');
require('./ts/views/inbox_view');
require('./ts/views/install_view');
require('./ts/SignalProtocolStore');
require('./ts/background');

View File

@ -323,64 +323,6 @@ $loading-height: 16px;
}
}
#qr {
display: inline-block;
&.ready {
border: 5px solid $color-ultramarine;
box-shadow: 2px 2px 4px $color-black-alpha-40;
}
img {
height: 20em;
border: 5px solid $color-white;
}
@media (max-height: 475px) {
img {
width: 8em;
height: 8em;
}
}
.dot {
width: 14px;
height: 14px;
border: 3px solid $color-ultramarine;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms;
&:nth-child(2) {
animation: loading 1500ms ease infinite 333ms;
}
&:nth-child(3) {
animation: loading 1500ms ease infinite 666ms;
}
}
canvas {
display: none;
}
}
.os-icon {
height: 3em;
width: 3em;
vertical-align: text-bottom;
display: inline-block;
margin: 0.5em;
&.apple {
@include color-svg('../images/full-screen-flow/apple.svg', black);
}
&.android {
@include color-svg('../images/full-screen-flow/android.svg', black);
}
}
.header {
font-weight: normal;
line-height: 1em;

View File

@ -613,3 +613,76 @@
visibility: visible;
}
}
@mixin normal-input {
@include font-body-1;
padding: 8px 12px;
border-radius: 6px;
border-width: 2px;
border-style: solid;
width: 100%;
@include light-theme {
background: $color-white;
color: $color-black;
border-color: $color-gray-15;
&:disabled {
background: $color-gray-02;
border-color: $color-gray-05;
color: $color-gray-90;
}
}
@include dark-theme {
background: $color-gray-80;
color: $color-gray-05;
border-color: $color-gray-45;
&:disabled {
background: $color-gray-95;
border-color: $color-gray-60;
color: $color-gray-20;
}
}
&:focus {
outline: none;
@include light-theme {
border-color: $color-ultramarine;
}
@include dark-theme {
border-color: $color-ultramarine-light;
}
}
}
@mixin install-screen {
align-items: center;
display: flex;
height: 100vh;
justify-content: center;
line-height: 30px;
user-select: none;
width: 100vw;
@include light-theme {
background: $color-gray-02;
color: $color-black;
}
@include dark-theme {
background: $color-gray-95;
color: $color-white;
}
h1 {
@include font-title-2;
}
h2 {
@include font-body-1;
font-weight: normal;
}
}

View File

@ -2,47 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
.module-GroupInput {
@include font-body-1;
padding: 8px 12px;
border-radius: 6px;
border-width: 2px;
border-style: solid;
width: 100%;
@include light-theme {
background: $color-white;
color: $color-black;
border-color: $color-gray-15;
&:disabled {
background: $color-gray-02;
border-color: $color-gray-05;
color: $color-gray-90;
}
}
@include dark-theme {
background: $color-gray-80;
color: $color-gray-05;
border-color: $color-gray-45;
&:disabled {
background: $color-gray-95;
border-color: $color-gray-60;
color: $color-gray-20;
}
}
&:focus {
outline: none;
@include light-theme {
border-color: $color-ultramarine;
}
@include dark-theme {
border-color: $color-ultramarine-light;
}
}
@include normal-input;
&__description {
resize: none;

View File

@ -0,0 +1,22 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-InstallScreenChoosingDeviceNameStep {
@include install-screen;
text-align: center;
&__inputs {
display: flex;
flex-direction: column;
padding: 32px 0 16px 0;
align-items: center;
}
&__input {
@include normal-input;
width: 90%;
max-width: 300px;
margin-bottom: 18px;
}
}

View File

@ -0,0 +1,21 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-InstallScreenErrorStep {
@include install-screen;
flex-direction: column;
padding-left: 2rem;
padding-right: 2rem;
text-align: center;
&__buttons {
margin-top: 1rem;
.module-Button {
margin-left: 1rem;
&:first-child {
margin-left: 0;
}
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-InstallScreenLinkInProgressStep {
@include install-screen;
flex-direction: column;
text-align: center;
h1 {
@include font-body-1-bold;
margin-top: 18px;
}
h2 {
@include font-subtitle;
font-weight: normal;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
}

View File

@ -0,0 +1,136 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-InstallScreenQrCodeNotScannedStep {
@include install-screen;
&__contents {
align-items: center;
border-radius: 8px;
display: flex;
flex-direction: row;
max-width: 760px;
padding: 22px;
margin: 20px;
animation: 500ms module-InstallScreenQrCodeNotScannedStep__slide-in;
position: relative;
@include light-theme {
background: $color-white;
}
@include dark-theme {
background: $color-gray-80;
}
}
&__qr-code {
// This should match the size defined in the JavaScript.
$size: 256px;
align-items: center;
border: 2px solid transparent;
border-radius: 4px;
box-sizing: content-box;
display: flex;
padding: 8px;
flex-direction: column;
justify-content: center;
margin-right: 38px;
min-height: $size;
min-width: $size;
width: $size;
&--loaded {
background: $color-white;
}
&--load-failed {
@include font-subtitle;
@include light-theme {
color: $color-gray-60;
border-color: $color-gray-05;
}
@include dark-theme {
color: $color-gray-25;
border-color: $color-gray-60;
}
}
&__code {
display: flex;
flex-direction: column;
height: $size;
width: $size;
animation: 1s module-InstallScreenQrCodeNotScannedStep__slide-in;
position: relative;
}
&__error-message {
text-align: center;
&::before {
@include color-svg(
'../images/icons/v2/error-outline-24.svg',
$color-accent-red
);
content: '';
display: block;
height: 22px;
margin: 8px auto 0 auto;
width: 22px;
}
a {
color: $color-ultramarine;
text-decoration: none;
}
}
}
ol {
@include font-body-1;
line-height: 26px;
list-style-position: inside;
padding-inline-start: 0;
}
&__android-plus {
background: $color-gray-25;
border-radius: 100%;
display: inline-block;
padding: 5px;
vertical-align: middle;
&::before {
content: '';
display: block;
height: 12px;
width: 12px;
}
@include light-theme {
&::before {
@include color-svg('../images/icons/v2/plus-24.svg', $color-white);
}
}
@include dark-theme {
&::before {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-80);
}
}
}
}
@keyframes module-InstallScreenQrCodeNotScannedStep__slide-in {
from {
top: -8px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}

View File

@ -0,0 +1,21 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.InstallScreenSignalLogo {
@include font-title-1;
align-items: center;
display: flex;
font-weight: bold;
position: absolute;
top: calc(35px + var(--title-bar-drag-area-height));
left: 32px;
&::before {
@include color-svg('../images/signal-logo.svg', $color-ultramarine);
content: '';
display: block;
height: 32px;
margin-right: 6px;
width: 32px;
}
}

View File

@ -70,6 +70,11 @@
@import './components/Inbox.scss';
@import './components/IncomingCallBar.scss';
@import './components/Input.scss';
@import './components/InstallScreenChoosingDeviceNameStep.scss';
@import './components/InstallScreenErrorStep.scss';
@import './components/InstallScreenLinkInProgressStep.scss';
@import './components/InstallScreenQrCodeNotScannedStep.scss';
@import './components/InstallScreenSignalLogo.scss';
@import './components/LeftPaneDialog.scss';
@import './components/LeftPaneSearchInput.scss';
@import './components/Lightbox.scss';

View File

@ -65,102 +65,6 @@
</div>
</script>
<script type="text/x-tmpl-mustache" id="link-flow-template">
<div class='module-title-bar-drag-area'></div>
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<div class='header'>{{ linkYourPhone }}</div>
<div id="qr">
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
</div>
</div>
</div>
<div class='nav'>
<div class='instructions'>
<div class='android'>
<div class='label'>
<span class='os-icon android'></span>
</div>
<div class='body'>
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ androidFinalStep }}</div>
</div>
</div>
<div class='apple'>
<div class='label'>
<span class='os-icon apple'></span>
</div>
<div class='body'>
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ appleFinalStep }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{/isStep3}}
{{#isStep4}}
<form id='link-phone'>
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon lead-pencil'></span>
<div class='header'>{{ chooseName }}</div>
<div>
<input type='text' class='device-name' spellcheck='false' maxlength='50' tabIndex="0" />
</div>
</div>
<div class='nav'>
<div>
<button class="button finish" type="submit" tabIndex="0">{{ finishLinkingPhoneButton }}</button>
</div>
</div>
</div>
</div>
</form>
{{/isStep4}}
{{#isStep5}}
<div id='step5' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon sync'></span>
<div class='header'>{{ syncing }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
</div>
</div>
</div>
</div>
{{/isStep5}}
{{#isError}}
<div id='error' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon alert-outline'></span>
<div class='header'>{{ errorHeader }}</div>
<div class='body'>{{ errorMessage }}</div>
</div>
<div class='nav'>
<a class='button try-again'>{{ errorButton }}</a>
</div>
</div>
</div>
{{/isError}}
</script>
<script type="text/javascript" src="../js/components.js"></script>
<script type="text/javascript" src="../ts/backboneJquery.js"></script>
<script

View File

@ -8,7 +8,7 @@ import classNames from 'classnames';
import { AppViewType } from '../state/ducks/app';
import { Inbox } from './Inbox';
import { Install } from './Install';
import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util';
import { usePageVisibility } from '../hooks/usePageVisibility';
@ -50,7 +50,7 @@ export const App = ({
let contents;
if (appView === AppViewType.Installer) {
contents = <Install />;
contents = <SmartInstallScreen />;
} else if (appView === AppViewType.Standalone) {
const onComplete = () => {
window.removeSetupMenuItems();

View File

@ -1,14 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { BackboneHost } from './BackboneHost';
export const Install = (): JSX.Element => {
return (
<BackboneHost
className="full-screen-flow"
View={window.Whisper.InstallView}
/>
);
};

View File

@ -0,0 +1,64 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentProps, ReactElement } from 'react';
import React from 'react';
import { missingCaseError } from '../util/missingCaseError';
import { InstallScreenErrorStep } from './installScreen/InstallScreenErrorStep';
import { InstallScreenChoosingDeviceNameStep } from './installScreen/InstallScreenChoosingDeviceNameStep';
import { InstallScreenLinkInProgressStep } from './installScreen/InstallScreenLinkInProgressStep';
import { InstallScreenQrCodeNotScannedStep } from './installScreen/InstallScreenQrCodeNotScannedStep';
export enum InstallScreenStep {
Error,
QrCodeNotScanned,
ChoosingDeviceName,
LinkInProgress,
}
// We can't always use destructuring assignment because of the complexity of this props
// type.
/* eslint-disable react/destructuring-assignment */
type PropsType =
| {
step: InstallScreenStep.Error;
screenSpecificProps: ComponentProps<typeof InstallScreenErrorStep>;
}
| {
step: InstallScreenStep.QrCodeNotScanned;
screenSpecificProps: ComponentProps<
typeof InstallScreenQrCodeNotScannedStep
>;
}
| {
step: InstallScreenStep.ChoosingDeviceName;
screenSpecificProps: ComponentProps<
typeof InstallScreenChoosingDeviceNameStep
>;
}
| {
step: InstallScreenStep.LinkInProgress;
screenSpecificProps: ComponentProps<
typeof InstallScreenLinkInProgressStep
>;
};
export function InstallScreen(props: Readonly<PropsType>): ReactElement {
switch (props.step) {
case InstallScreenStep.Error:
return <InstallScreenErrorStep {...props.screenSpecificProps} />;
case InstallScreenStep.QrCodeNotScanned:
return (
<InstallScreenQrCodeNotScannedStep {...props.screenSpecificProps} />
);
case InstallScreenStep.ChoosingDeviceName:
return (
<InstallScreenChoosingDeviceNameStep {...props.screenSpecificProps} />
);
case InstallScreenStep.LinkInProgress:
return <InstallScreenLinkInProgressStep {...props.screenSpecificProps} />;
default:
throw missingCaseError(props);
}
}

View File

@ -0,0 +1,9 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
export const TitlebarDragArea = (): ReactElement => (
<div className="module-title-bar-drag-area" />
);

View File

@ -0,0 +1,36 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { InstallScreenChoosingDeviceNameStep } from './InstallScreenChoosingDeviceNameStep';
const i18n = setupI18n('en', enMessages);
const story = storiesOf(
'Components/InstallScreen/InstallScreenChoosingDeviceNameStep',
module
);
story.add('Default', () => {
const Wrapper = () => {
const [deviceName, setDeviceName] = useState<string>('Default value');
return (
<InstallScreenChoosingDeviceNameStep
i18n={i18n}
deviceName={deviceName}
setDeviceName={setDeviceName}
onSubmit={action('onSubmit')}
/>
);
};
return <Wrapper />;
});

View File

@ -0,0 +1,85 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React, { useRef } from 'react';
import type { LocalizerType } from '../../types/Util';
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
import { Button, ButtonVariant } from '../Button';
import { TitlebarDragArea } from '../TitlebarDragArea';
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
// This is the string's `.length`, which is the number of UTF-16 code points. Instead, we
// want this to be either 50 graphemes or 256 encrypted bytes, whichever is smaller. See
// DESKTOP-2844.
export const MAX_DEVICE_NAME_LENGTH = 50;
type PropsType = {
deviceName: string;
i18n: LocalizerType;
onSubmit: () => void;
setDeviceName: (value: string) => void;
};
export function InstallScreenChoosingDeviceNameStep({
deviceName,
i18n,
onSubmit,
setDeviceName,
}: Readonly<PropsType>): ReactElement {
const hasFocusedRef = useRef<boolean>(false);
const focusRef = (el: null | HTMLElement) => {
if (el) {
el.focus();
hasFocusedRef.current = true;
}
};
const normalizedName = normalizeDeviceName(deviceName);
const canSubmit =
normalizedName.length > 0 &&
normalizedName.length <= MAX_DEVICE_NAME_LENGTH;
return (
<form
className="module-InstallScreenChoosingDeviceNameStep"
onSubmit={event => {
event.preventDefault();
onSubmit();
}}
>
<TitlebarDragArea />
<InstallScreenSignalLogo />
<div className="module-InstallScreenChoosingDeviceNameStep__contents">
<div className="module-InstallScreenChoosingDeviceNameStep__header">
<h1>{i18n('chooseDeviceName')}</h1>
<h2>{i18n('Install__choose-device-name__description')}</h2>
</div>
<div className="module-InstallScreenChoosingDeviceNameStep__inputs">
<input
className="module-InstallScreenChoosingDeviceNameStep__input"
maxLength={MAX_DEVICE_NAME_LENGTH}
onChange={event => {
setDeviceName(event.target.value);
}}
placeholder={i18n('Install__choose-device-name__placeholder')}
ref={focusRef}
spellCheck={false}
value={deviceName}
/>
<Button
disabled={!canSubmit}
variant={ButtonVariant.Primary}
type="submit"
>
{i18n('finishLinkingPhone')}
</Button>
</div>
</div>
</form>
);
}

View File

@ -0,0 +1,51 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { InstallScreenErrorStep, InstallError } from './InstallScreenErrorStep';
const i18n = setupI18n('en', enMessages);
const story = storiesOf(
'Components/InstallScreen/InstallScreenErrorStep',
module
);
const defaultProps = {
i18n,
quit: action('quit'),
tryAgain: action('tryAgain'),
};
story.add('Too many devices', () => (
<InstallScreenErrorStep
{...defaultProps}
error={InstallError.TooManyDevices}
/>
));
story.add('Too old', () => (
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
));
story.add('Too old', () => (
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
));
story.add('Connection failed', () => (
<InstallScreenErrorStep
{...defaultProps}
error={InstallError.ConnectionFailed}
/>
));
story.add('Unknown error', () => (
<InstallScreenErrorStep {...defaultProps} error={InstallError.UnknownError} />
));

View File

@ -0,0 +1,77 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
import type { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
import { Button, ButtonVariant } from '../Button';
import { TitlebarDragArea } from '../TitlebarDragArea';
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
export enum InstallError {
TooManyDevices,
TooOld,
ConnectionFailed,
UnknownError,
}
export function InstallScreenErrorStep({
error,
i18n,
quit,
tryAgain,
}: Readonly<{
error: InstallError;
i18n: LocalizerType;
quit: () => unknown;
tryAgain: () => unknown;
}>): ReactElement {
let errorMessage: string;
let buttonText = i18n('installTryAgain');
let onClickButton = () => tryAgain();
let shouldShowQuitButton = false;
switch (error) {
case InstallError.TooManyDevices:
errorMessage = i18n('installTooManyDevices');
break;
case InstallError.TooOld:
errorMessage = i18n('installTooOld');
buttonText = i18n('upgrade');
onClickButton = () => {
openLinkInWebBrowser('https://signal.org/download');
};
shouldShowQuitButton = true;
break;
case InstallError.ConnectionFailed:
errorMessage = i18n('installConnectionFailed');
break;
case InstallError.UnknownError:
errorMessage = i18n('installUnknownError');
break;
default:
throw missingCaseError(error);
}
return (
<div className="module-InstallScreenErrorStep">
<TitlebarDragArea />
<InstallScreenSignalLogo />
<h1>{i18n('installErrorHeader')}</h1>
<h2>{errorMessage}</h2>
<div className="module-InstallScreenErrorStep__buttons">
<Button onClick={onClickButton}>{buttonText}</Button>
{shouldShowQuitButton && (
<Button onClick={() => quit()} variant={ButtonVariant.Secondary}>
{i18n('quit')}
</Button>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,20 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { InstallScreenLinkInProgressStep } from './InstallScreenLinkInProgressStep';
const i18n = setupI18n('en', enMessages);
const story = storiesOf(
'Components/InstallScreen/InstallScreenLinkInProgressStep',
module
);
story.add('Default', () => <InstallScreenLinkInProgressStep i18n={i18n} />);

View File

@ -0,0 +1,25 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
import type { LocalizerType } from '../../types/Util';
import { Spinner } from '../Spinner';
import { TitlebarDragArea } from '../TitlebarDragArea';
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
export const InstallScreenLinkInProgressStep = ({
i18n,
}: Readonly<{ i18n: LocalizerType }>): ReactElement => (
<div className="module-InstallScreenLinkInProgressStep">
<TitlebarDragArea />
<InstallScreenSignalLogo />
<Spinner size="50px" svgSize="normal" />
<h1>{i18n('initialSync')}</h1>
<h2>{i18n('initialSync__subtitle')}</h2>
</div>
);

View File

@ -0,0 +1,91 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useState } from 'react';
import { storiesOf } from '@storybook/react';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
import { InstallScreenQrCodeNotScannedStep } from './InstallScreenQrCodeNotScannedStep';
const i18n = setupI18n('en', enMessages);
const story = storiesOf(
'Components/InstallScreen/InstallScreenQrCodeNotScannedStep',
module
);
const Simulation = ({ finalResult }: { finalResult: Loadable<string> }) => {
const [provisioningUrl, setProvisioningUrl] = useState<Loadable<string>>({
loadingState: LoadingState.Loading,
});
useEffect(() => {
const timeout = setTimeout(() => {
setProvisioningUrl(finalResult);
}, 2000);
return () => {
clearTimeout(timeout);
};
}, [finalResult]);
return (
<InstallScreenQrCodeNotScannedStep
i18n={i18n}
provisioningUrl={provisioningUrl}
/>
);
};
story.add('QR code loading', () => (
<InstallScreenQrCodeNotScannedStep
i18n={i18n}
provisioningUrl={{
loadingState: LoadingState.Loading,
}}
/>
));
story.add('QR code failed to load', () => (
<InstallScreenQrCodeNotScannedStep
i18n={i18n}
provisioningUrl={{
loadingState: LoadingState.LoadFailed,
error: new Error('uh oh'),
}}
/>
));
story.add('QR code loaded', () => (
<InstallScreenQrCodeNotScannedStep
i18n={i18n}
provisioningUrl={{
loadingState: LoadingState.Loaded,
value:
'https://example.com/fake-signal-link?uuid=56cdd548-e595-4962-9a27-3f1e8210a959&pub_key=SW4gdGhlIHZhc3QsIGRlZXAgZm9yZXN0IG9mIEh5cnVsZS4uLg%3D%3D',
}}
/>
));
story.add('Simulated loading', () => (
<Simulation
finalResult={{
loadingState: LoadingState.Loaded,
value:
'https://example.com/fake-signal-link?uuid=56cdd548-e595-4962-9a27-3f1e8210a959&pub_key=SW4gdGhlIHZhc3QsIGRlZXAgZm9yZXN0IG9mIEh5cnVsZS4uLg%3D%3D',
}}
/>
));
story.add('Simulated failure', () => (
<Simulation
finalResult={{
loadingState: LoadingState.LoadFailed,
error: new Error('uh oh'),
}}
/>
));

View File

@ -0,0 +1,152 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement, ReactNode } from 'react';
import React, { useEffect, useRef } from 'react';
import classNames from 'classnames';
import { noop } from 'lodash';
import type { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
import { Intl } from '../Intl';
import { Spinner } from '../Spinner';
import { TitlebarDragArea } from '../TitlebarDragArea';
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
import { getClassNamesFor } from '../../util/getClassNamesFor';
// We can't always use destructuring assignment because of the complexity of this props
// type.
/* eslint-disable react/destructuring-assignment */
type PropsType = {
i18n: LocalizerType;
provisioningUrl: Loadable<string>;
};
// This should match the size in the CSS.
const QR_CODE_SIZE = 256;
const QR_CODE_FAILED_LINK =
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
const getQrCodeClassName = getClassNamesFor(
'module-InstallScreenQrCodeNotScannedStep__qr-code'
);
export const InstallScreenQrCodeNotScannedStep = ({
i18n,
provisioningUrl,
}: Readonly<PropsType>): ReactElement => (
<div className="module-InstallScreenQrCodeNotScannedStep">
<TitlebarDragArea />
<InstallScreenSignalLogo />
<div className="module-InstallScreenQrCodeNotScannedStep__contents">
<QrCode i18n={i18n} {...provisioningUrl} />
<div className="module-InstallScreenQrCodeNotScannedStep__instructions">
<h1>{i18n('Install__scan-this-code')}</h1>
<ol>
<li>{i18n('Install__instructions__1')}</li>
<li>
<Intl
i18n={i18n}
id="Install__instructions__2"
components={{
settings: (
<strong>{i18n('Install__instructions__2__settings')}</strong>
),
linkedDevices: <strong>{i18n('linkedDevices')}</strong>,
}}
/>
</li>
<li>
<Intl
i18n={i18n}
id="Install__instructions__3"
components={{
plusButton: (
<div
className="module-InstallScreenQrCodeNotScannedStep__android-plus"
aria-label="+"
/>
),
linkNewDevice: <strong>{i18n('linkNewDevice')}</strong>,
}}
/>
</li>
</ol>
</div>
</div>
</div>
);
function QrCode(
props: Loadable<string> & { i18n: LocalizerType }
): ReactElement {
const { i18n } = props;
const qrCodeElRef = useRef<null | HTMLDivElement>(null);
const valueToRender =
props.loadingState === LoadingState.Loaded ? props.value : undefined;
useEffect(() => {
const qrCodeEl = qrCodeElRef.current;
if (!qrCodeEl || !valueToRender) {
return noop;
}
const qrCode = new window.QRCode(qrCodeEl, {
text: valueToRender,
width: QR_CODE_SIZE * window.devicePixelRatio,
height: QR_CODE_SIZE * window.devicePixelRatio,
});
return qrCode.clear.bind(qrCode);
}, [valueToRender]);
let contents: ReactNode;
switch (props.loadingState) {
case LoadingState.Loading:
contents = <Spinner size="24px" svgSize="small" />;
break;
case LoadingState.LoadFailed:
contents = (
<span className={classNames(getQrCodeClassName('__error-message'))}>
<Intl
i18n={i18n}
id="Install__qr-failed"
components={[
<a href={QR_CODE_FAILED_LINK}>
{i18n('Install__qr-failed__learn-more')}
</a>,
]}
/>
</span>
);
break;
case LoadingState.Loaded:
contents = (
<div className={getQrCodeClassName('__code')} ref={qrCodeElRef} />
);
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>
);
}

View File

@ -0,0 +1,10 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
export const InstallScreenSignalLogo = (): ReactElement => (
// Because "Signal" should be the same in every language, this is not localized.
<div className="InstallScreenSignalLogo">Signal</div>
);

View File

@ -0,0 +1,267 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentProps, ReactElement } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { getIntl } from '../selectors/user';
import * as log from '../../logging/log';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
import { assert } from '../../util/assert';
import { explodePromise } from '../../util/explodePromise';
import { missingCaseError } from '../../util/missingCaseError';
import {
InstallScreen,
InstallScreenStep,
} from '../../components/InstallScreen';
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
import { HTTPError } from '../../textsecure/Errors';
import { isRecord } from '../../util/isRecord';
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
type PropsType = ComponentProps<typeof InstallScreen>;
type StateType =
| {
step: InstallScreenStep.Error;
error: InstallError;
}
| {
step: InstallScreenStep.QrCodeNotScanned;
provisioningUrl: Loadable<string>;
}
| {
step: InstallScreenStep.ChoosingDeviceName;
deviceName: string;
}
| {
step: InstallScreenStep.LinkInProgress;
};
const INITIAL_STATE: StateType = {
step: InstallScreenStep.QrCodeNotScanned,
provisioningUrl: { loadingState: LoadingState.Loading },
};
function getInstallError(err: unknown): InstallError {
if (err instanceof HTTPError) {
switch (err.code) {
case -1:
return InstallError.ConnectionFailed;
case 409:
return InstallError.TooOld;
case 411:
return InstallError.TooManyDevices;
default:
return InstallError.UnknownError;
}
}
// AccountManager.registerSecondDevice uses this specific "websocket closed" error
// message.
if (isRecord(err) && err.message === 'websocket closed') {
return InstallError.ConnectionFailed;
}
return InstallError.UnknownError;
}
export function SmartInstallScreen(): ReactElement {
const i18n = useSelector(getIntl);
const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());
const [state, setState] = useState<StateType>(INITIAL_STATE);
const setProvisioningUrl = useCallback(
(value: string) => {
setState(currentState => {
if (currentState.step !== InstallScreenStep.QrCodeNotScanned) {
return currentState;
}
return {
...currentState,
provisioningUrl: {
loadingState: LoadingState.Loaded,
value,
},
};
});
},
[setState]
);
const onQrCodeScanned = useCallback(() => {
setState(currentState => {
if (currentState.step !== InstallScreenStep.QrCodeNotScanned) {
return currentState;
}
return {
step: InstallScreenStep.ChoosingDeviceName,
deviceName: normalizeDeviceName(
window.textsecure.storage.user.getDeviceName() ||
window.getHostName() ||
''
).slice(0, MAX_DEVICE_NAME_LENGTH),
};
});
}, [setState]);
const setDeviceName = useCallback(
(deviceName: string) => {
setState(currentState => {
if (currentState.step !== InstallScreenStep.ChoosingDeviceName) {
return currentState;
}
return {
...currentState,
deviceName,
};
});
},
[setState]
);
const onSubmitDeviceName = useCallback(() => {
if (state.step !== InstallScreenStep.ChoosingDeviceName) {
return;
}
let deviceName: string = normalizeDeviceName(state.deviceName);
if (!deviceName.length) {
// This should be impossible, but we have it here just in case.
assert(
false,
'Unexpected empty device name. Falling back to placeholder value'
);
deviceName = i18n('Install__choose-device-name__placeholder');
}
chooseDeviceNamePromiseWrapperRef.current.resolve(deviceName);
setState({ step: InstallScreenStep.LinkInProgress });
}, [state, i18n]);
useEffect(() => {
let hasCleanedUp = false;
const accountManager = window.getAccountManager();
assert(accountManager, 'Expected an account manager');
const updateProvisioningUrl = (value: string): void => {
if (hasCleanedUp) {
return;
}
setProvisioningUrl(value);
};
const confirmNumber = async (): Promise<string> => {
if (hasCleanedUp) {
throw new Error('Cannot confirm number; the component was unmounted');
}
onQrCodeScanned();
if (window.CI) {
chooseDeviceNamePromiseWrapperRef.current.resolve(window.CI.deviceName);
}
const result = await chooseDeviceNamePromiseWrapperRef.current.promise;
if (hasCleanedUp) {
throw new Error('Cannot confirm number; the component was unmounted');
}
// Delete all data from the database unless we're in the middle of a re-link.
// Without this, the app restarts at certain times and can cause weird things to
// happen, like data from a previous light import showing up after a new install.
const shouldRetainData = window.Signal.Util.Registration.everDone();
if (!shouldRetainData) {
try {
await window.textsecure.storage.protocol.removeAllData();
} catch (error) {
log.error(
'confirmNumber: error clearing database',
error && error.stack ? error.stack : error
);
}
}
if (hasCleanedUp) {
throw new Error('Cannot confirm number; the component was unmounted');
}
window.Signal.Util.postLinkExperience.start();
return result;
};
(async () => {
try {
await accountManager.registerSecondDevice(
updateProvisioningUrl,
confirmNumber
);
} catch (err: unknown) {
if (hasCleanedUp) {
return;
}
setState({
step: InstallScreenStep.Error,
error: getInstallError(err),
});
}
})();
return () => {
hasCleanedUp = true;
};
}, [setProvisioningUrl, onQrCodeScanned]);
let props: PropsType;
switch (state.step) {
case InstallScreenStep.Error:
props = {
step: InstallScreenStep.Error,
screenSpecificProps: {
i18n,
error: state.error,
quit: () => window.shutdown(),
tryAgain: () => setState(INITIAL_STATE),
},
};
break;
case InstallScreenStep.QrCodeNotScanned:
props = {
step: InstallScreenStep.QrCodeNotScanned,
screenSpecificProps: {
i18n,
provisioningUrl: state.provisioningUrl,
},
};
break;
case InstallScreenStep.ChoosingDeviceName:
props = {
step: InstallScreenStep.ChoosingDeviceName,
screenSpecificProps: {
i18n,
deviceName: state.deviceName,
setDeviceName,
onSubmit: onSubmitDeviceName,
},
};
break;
case InstallScreenStep.LinkInProgress:
props = {
step: InstallScreenStep.LinkInProgress,
screenSpecificProps: { i18n },
};
break;
default:
throw missingCaseError(state);
}
return <InstallScreen {...props} />;
}

View File

@ -0,0 +1,22 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
describe('normalizeDeviceName', () => {
it('leaves normal device names untouched', () => {
for (const name of ['foo', 'bar Baz', '💅💅💅']) {
assert.strictEqual(normalizeDeviceName(name), name);
}
});
it('trims device names', () => {
assert.strictEqual(normalizeDeviceName(' foo\t'), 'foo');
});
it('removes null characters', () => {
assert.strictEqual(normalizeDeviceName('\0foo\0bar'), 'foobar');
});
});

View File

@ -7756,6 +7756,22 @@
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
},
{
"rule": "React-useRef",
"path": "ts/components/installScreen/InstallScreenChoosingDeviceNameStep.tsx",
"line": " const hasFocusedRef = useRef<boolean>(false);",
"reasonCategory": "usageTrusted",
"updated": "2021-12-06T23:07:28.947Z",
"reasonDetail": "Doesn't touch the DOM."
},
{
"rule": "React-useRef",
"path": "ts/components/installScreen/InstallScreenQrCodeNotScannedStep.tsx",
"line": " const qrCodeElRef = useRef<null | HTMLDivElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-12-06T23:07:28.947Z",
"reasonDetail": "Uses our QR code library."
},
{
"rule": "React-createRef",
"path": "ts/components/stickers/StickerManager.js",
@ -8020,6 +8036,14 @@
"reasonCategory": "falseMatch",
"updated": "2021-11-04T16:14:03.477Z"
},
{
"rule": "React-useRef",
"path": "ts/state/smart/InstallScreen.tsx",
"line": " const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());",
"reasonCategory": "usageTrusted",
"updated": "2021-12-06T23:07:28.947Z",
"reasonDetail": "Doesn't touch the DOM."
},
{
"rule": "jQuery-load(",
"path": "ts/types/Stickers.js",
@ -8310,244 +8334,6 @@
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " template: () => $('#link-flow-template').html(),",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr img').remove();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr canvas').remove();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr .container').show();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr').removeClass('ready');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " if ($('#qr').length === 0) {",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr .container').hide();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.qr = new window.QRCode(this.$('#qr')[0]).makeCode(url);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr').removeAttr('title');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr').addClass('ready');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#link-phone').submit((e) => {",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " let name = this.$(DEVICE_NAME_SELECTOR).val();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.js",
"line": " this.$('#qr img').attr('alt', window.i18n('LinkScreen__scan-this-code'));",
"reasonCategory": "usageTrusted",
"updated": "2021-09-28T00:13:42.086Z"
},
{
"rule": "jQuery-html(",
"path": "ts/views/install_view.js",
"line": " template: () => $('#link-flow-template').html(),",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " template: () => $('#link-flow-template').html(),",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr img').remove();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr canvas').remove();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr .container').show();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr').removeClass('ready');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " if ($('#qr').length === 0) {",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr .container').hide();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.qr = new window.QRCode(this.$('#qr')[0]).makeCode(url);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr').removeAttr('title');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr').addClass('ready');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#link-phone').submit((e: SubmitEvent) => {",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " let name = this.$(DEVICE_NAME_SELECTOR).val();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/install_view.ts",
"line": " this.$('#qr img').attr('alt', window.i18n('LinkScreen__scan-this-code'));",
"reasonCategory": "usageTrusted",
"updated": "2021-09-28T00:13:42.086Z"
},
{
"rule": "jQuery-html(",
"path": "ts/views/install_view.ts",
"line": " template: () => $('#link-flow-template').html(),",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "DOM-innerHTML",
"path": "ts/windows/loading/start.js",

15
ts/util/loadable.ts Normal file
View File

@ -0,0 +1,15 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export enum LoadingState {
Loading,
Loaded,
LoadFailed,
}
export type Loadable<ValueT, ErrorT = unknown> =
| {
loadingState: LoadingState.Loading;
}
| { loadingState: LoadingState.Loaded; value: ValueT }
| { loadingState: LoadingState.LoadFailed; error: ErrorT };

View File

@ -0,0 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function normalizeDeviceName(rawDeviceName: string): string {
// We want to do additional normalization here. See DESKTOP-2845.
return rawDeviceName.trim().replace(/\0/g, '');
}

View File

@ -1,236 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { HTTPError } from '../textsecure/Errors';
window.Whisper = window.Whisper || {};
const { Whisper } = window;
enum Steps {
INSTALL_SIGNAL = 2,
SCAN_QR_CODE = 3,
ENTER_NAME = 4,
PROGRESS_BAR = 5,
TOO_MANY_DEVICES = 'TooManyDevices',
NETWORK_ERROR = 'NetworkError',
}
const DEVICE_NAME_SELECTOR = 'input.device-name';
const CONNECTION_ERROR = -1;
const TOO_MANY_DEVICES = 411;
const TOO_OLD = 409;
Whisper.InstallView = Whisper.View.extend({
template: () => $('#link-flow-template').html(),
className: 'main full-screen-flow',
events: {
'click .try-again': 'connect',
'click .second': 'shutdown',
// the actual next step happens in confirmNumber() on submit form #link-phone
},
initialize(options: { hasExistingData?: boolean } = {}) {
window.readyForUpdates();
this.selectStep(Steps.SCAN_QR_CODE);
this.connect();
this.on('disconnected', this.reconnect);
// Keep data around if it's a re-link, or the middle of a light import
this.shouldRetainData =
window.Signal.Util.Registration.everDone() || options.hasExistingData;
},
render_attributes() {
let errorMessage;
let errorButton = window.i18n('installTryAgain');
let errorSecondButton = null;
if (this.error) {
if (
this.error instanceof HTTPError &&
this.error.code === TOO_MANY_DEVICES
) {
errorMessage = window.i18n('installTooManyDevices');
} else if (
this.error instanceof HTTPError &&
this.error.code === TOO_OLD
) {
errorMessage = window.i18n('installTooOld');
errorButton = window.i18n('upgrade');
errorSecondButton = window.i18n('quit');
} else if (
this.error instanceof HTTPError &&
this.error.code === CONNECTION_ERROR
) {
errorMessage = window.i18n('installConnectionFailed');
} else if (this.error.message === 'websocket closed') {
// AccountManager.registerSecondDevice uses this specific
// 'websocket closed' error message
errorMessage = window.i18n('installConnectionFailed');
}
return {
isError: true,
errorHeader: window.i18n('installErrorHeader'),
errorMessage,
errorButton,
errorSecondButton,
};
}
return {
isStep3: this.step === Steps.SCAN_QR_CODE,
linkYourPhone: window.i18n('linkYourPhone'),
signalSettings: window.i18n('signalSettings'),
linkedDevices: window.i18n('linkedDevices'),
androidFinalStep: window.i18n('plusButton'),
appleFinalStep: window.i18n('linkNewDevice'),
isStep4: this.step === Steps.ENTER_NAME,
chooseName: window.i18n('chooseDeviceName'),
finishLinkingPhoneButton: window.i18n('finishLinkingPhone'),
isStep5: this.step === Steps.PROGRESS_BAR,
syncing: window.i18n('initialSync'),
};
},
selectStep(step: Steps) {
this.step = step;
this.render();
},
shutdown() {
window.shutdown();
},
async connect() {
if (this.error instanceof HTTPError && this.error.code === TOO_OLD) {
openLinkInWebBrowser('https://signal.org/download');
return;
}
this.error = null;
this.selectStep(Steps.SCAN_QR_CODE);
this.clearQR();
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
const accountManager = window.getAccountManager();
try {
await accountManager.registerSecondDevice(
this.setProvisioningUrl.bind(this),
this.confirmNumber.bind(this)
);
} catch (err) {
this.handleDisconnect(err);
}
},
handleDisconnect(error: Error) {
log.error(
'provisioning failed',
error && error.stack ? error.stack : error
);
this.error = error;
this.render();
if (error.message === 'websocket closed') {
this.trigger('disconnected');
} else if (
!(error instanceof HTTPError) ||
(error.code !== CONNECTION_ERROR && error.code !== TOO_MANY_DEVICES)
) {
throw error;
}
},
reconnect() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.timeout = setTimeout(this.connect.bind(this), 10000);
},
clearQR() {
this.$('#qr img').remove();
this.$('#qr canvas').remove();
this.$('#qr .container').show();
this.$('#qr').removeClass('ready');
},
setProvisioningUrl(url: string) {
if ($('#qr').length === 0) {
log.error('Did not find #qr element in the DOM!');
return;
}
this.clearQR();
this.$('#qr .container').hide();
this.qr = new window.QRCode(this.$('#qr')[0]).makeCode(url);
this.$('#qr').removeAttr('title');
this.$('#qr').addClass('ready');
this.$('#qr img').attr('alt', window.i18n('LinkScreen__scan-this-code'));
},
setDeviceNameDefault() {
const deviceName = window.textsecure.storage.user.getDeviceName();
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());
this.$(DEVICE_NAME_SELECTOR).focus();
},
confirmNumber() {
window.removeSetupMenuItems();
this.selectStep(Steps.ENTER_NAME);
this.setDeviceNameDefault();
return new Promise(resolve => {
const onDeviceName = async (name: string) => {
this.selectStep(Steps.PROGRESS_BAR);
const finish = () => {
window.Signal.Util.postLinkExperience.start();
return resolve(name);
};
// Delete all data from database unless we're in the middle
// of a re-link, or we are finishing a light import. Without this,
// app restarts at certain times can cause weird things to happen,
// like data from a previous incomplete light import showing up
// after a new install.
if (this.shouldRetainData) {
return finish();
}
try {
await window.textsecure.storage.protocol.removeAllData();
} catch (error) {
log.error(
'confirmNumber: error clearing database',
error && error.stack ? error.stack : error
);
} finally {
finish();
}
};
if (window.CI) {
onDeviceName(window.CI.deviceName);
return;
}
// eslint-disable-next-line consistent-return
this.$('#link-phone').submit((e: SubmitEvent) => {
e.stopPropagation();
e.preventDefault();
let name = this.$(DEVICE_NAME_SELECTOR).val();
name = name.replace(/\0/g, ''); // strip unicode null
if (name.trim().length === 0) {
this.$(DEVICE_NAME_SELECTOR).focus();
return;
}
onDeviceName(name);
});
});
},
});

30
ts/window.d.ts vendored
View File

@ -129,6 +129,32 @@ type ConfirmationDialogViewProps = {
resolve: () => void;
};
declare enum QRCodeCorrectLevel {
H = 2,
L = 1,
M = 0,
Q = 3,
}
declare class QRCode {
static CorrectLevel: typeof QRCodeCorrectLevel;
constructor(
el: HTMLElement | string,
vOption?:
| string
| {
colorDark?: string;
colorLight?: string;
correctLevel?: QRCodeCorrectLevel;
height?: number;
text?: string;
width?: number;
}
);
makeCode(sText: string): void;
clear(): void;
}
export declare class WebAudioRecorderClass {
constructor(
node: GainNode,
@ -158,7 +184,6 @@ declare global {
interface Window {
startApp: () => void;
QRCode: any;
removeSetupMenuItems: () => unknown;
showPermissionsPopup: () => Promise<void>;
@ -266,6 +291,8 @@ declare global {
updateTrayIcon: (count: number) => void;
Backbone: typeof Backbone;
CI?: CI;
QRCode: typeof QRCode;
Accessibility: {
reducedMotionSetting: boolean;
};
@ -580,7 +607,6 @@ export type WhisperType = {
ConversationLoadingScreen: typeof AnyViewClass;
GroupMemberList: typeof AnyViewClass;
InboxView: typeof AnyViewClass;
InstallView: typeof AnyViewClass;
KeyVerificationPanelView: typeof AnyViewClass;
ReactWrapperView: typeof BasicReactWrapperViewClass;
SafetyNumberChangeDialogView: typeof AnyViewClass;