session-desktop/ts/components/DevicePairingDialog.tsx

391 lines
11 KiB
TypeScript

import React, { ChangeEvent } from 'react';
import { QRCode } from 'react-qr-svg';
import { SessionModal } from './session/SessionModal';
import { SessionButton, SessionButtonColor } from './session/SessionButton';
import { SessionSpinner } from './session/SessionSpinner';
import classNames from 'classnames';
import { ToastUtils } from '../session/utils';
interface Props {
onClose: any;
pubKeyToUnpair: string | undefined;
}
interface State {
currentPubKey: string | undefined;
accepted: boolean;
pubKeyRequests: Array<any>;
currentView: 'filterRequestView' | 'qrcodeView' | 'unpairDeviceView';
errors: any;
loading: boolean;
deviceAlias: string | undefined;
}
export class DevicePairingDialog extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.closeDialog = this.closeDialog.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.stopReceivingRequests = this.stopReceivingRequests.bind(this);
this.startReceivingRequests = this.startReceivingRequests.bind(this);
this.skipDevice = this.skipDevice.bind(this);
this.allowDevice = this.allowDevice.bind(this);
this.validateSecondaryDevice = this.validateSecondaryDevice.bind(this);
this.handleUpdateDeviceAlias = this.handleUpdateDeviceAlias.bind(this);
this.triggerUnpairDevice = this.triggerUnpairDevice.bind(this);
this.state = {
currentPubKey: undefined,
accepted: false,
pubKeyRequests: Array(),
currentView: props.pubKeyToUnpair ? 'unpairDeviceView' : 'qrcodeView',
loading: false,
errors: undefined,
deviceAlias: 'Unnamed Device',
};
}
public componentWillMount() {
if (this.state.currentView === 'qrcodeView') {
this.startReceivingRequests();
}
}
public componentWillUnmount() {
this.closeDialog();
}
public renderErrors() {
const { errors } = this.state;
return (
<>
{errors && (
<>
<div className="spacer-xs" />
<div className="session-label danger">{errors}</div>
</>
)}
</>
);
}
public renderFilterRequestsView() {
const { currentPubKey, accepted, deviceAlias } = this.state;
let secretWords: string | undefined;
if (currentPubKey) {
secretWords = window.mnemonic.pubkey_to_secret_words(currentPubKey);
}
if (accepted) {
return (
<SessionModal
title={window.i18n('provideDeviceAlias')}
onOk={() => null}
onClose={this.closeDialog}
>
<div className="session-modal__centered">
<div className="spacer-lg" />
{this.renderErrors()}
<input
type="text"
onChange={this.handleUpdateDeviceAlias}
value={deviceAlias}
id={currentPubKey}
/>
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('ok')}
onClick={this.validateSecondaryDevice}
disabled={!deviceAlias}
buttonColor={SessionButtonColor.Green}
/>
</div>
<SessionSpinner loading={this.state.loading} />
</div>
</SessionModal>
);
}
return (
<SessionModal
title={window.i18n('allowPairing')}
onOk={() => null}
onClose={this.closeDialog}
>
<div className="session-modal__centered">
<h4 className="device-pairing-dialog__desc">
{window.i18n('allowPairingWithDevice')}
</h4>
{this.renderErrors()}
<div className="subtle">{window.i18n('secretWords')}</div>
<div className="device-pairing-dialog__secret-words">
<div className="subtle">{secretWords}</div>
</div>
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('cancel')}
onClick={this.skipDevice}
/>
<SessionButton
text={window.i18n('allowPairing')}
onClick={this.allowDevice}
buttonColor={SessionButtonColor.Green}
/>
</div>
</div>
</SessionModal>
);
}
public renderQrCodeView() {
const requestReceived = this.hasReceivedRequests();
const title = window.i18n('pairingDevice');
return (
<SessionModal title={title} onOk={() => null} onClose={this.closeDialog}>
<div className="session-modal__centered">
{this.renderErrors()}
<h4>{window.i18n('waitingForDeviceToRegister')}</h4>
<small className="subtle">{window.i18n('pairNewDevicePrompt')}</small>
<div className="spacer-lg" />
<div className="qr-image">
<QRCode
value={window.textsecure.storage.user.getNumber()}
level="L"
/>
</div>
<div className="spacer-lg" />
<div className="session-modal__button-group__center">
{!requestReceived ? (
<SessionButton
text={window.i18n('cancel')}
onClick={this.closeDialog}
/>
) : null}
</div>
</div>
</SessionModal>
);
}
public renderUnpairDeviceView() {
const { pubKeyToUnpair } = this.props;
if (!pubKeyToUnpair) {
throw new Error('pubKeyToUnpair must be set in renderUnpairDeviceView()');
}
const secretWords = window.mnemonic.pubkey_to_secret_words(pubKeyToUnpair);
const conv = window.ConversationController.get(pubKeyToUnpair);
let description;
if (conv && conv.getNickname()) {
description = `${conv.getNickname()}: ${window.shortenPubkey(
pubKeyToUnpair
)} ${secretWords}`;
} else {
description = `${window.shortenPubkey(pubKeyToUnpair)} ${secretWords}`;
}
return (
<SessionModal
title={window.i18n('unpairDevice')}
onOk={() => null}
onClose={this.closeDialog}
>
<div className="session-modal__centered">
{this.renderErrors()}
<p className="session-modal__description">
{window.i18n('confirmUnpairingTitle')}
<br />
<span className="subtle">{description}</span>
</p>
<div className="spacer-xs" />
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('cancel')}
onClick={this.closeDialog}
/>
<SessionButton
text={window.i18n('unpairDevice')}
onClick={this.triggerUnpairDevice}
buttonColor={SessionButtonColor.Danger}
/>
</div>
<SessionSpinner loading={this.state.loading} />
</div>
</SessionModal>
);
}
public render() {
const { currentView } = this.state;
const renderQrCodeView = currentView === 'qrcodeView';
const renderFilterRequestView = currentView === 'filterRequestView';
const renderUnpairDeviceView = currentView === 'unpairDeviceView';
return (
<>
{renderQrCodeView && this.renderQrCodeView()}
{renderFilterRequestView && this.renderFilterRequestsView()}
{renderUnpairDeviceView && this.renderUnpairDeviceView()}
</>
);
}
private reset() {
this.setState({
currentPubKey: undefined,
accepted: false,
pubKeyRequests: Array(),
currentView: 'filterRequestView',
deviceAlias: 'Unnamed Device',
});
}
private startReceivingRequests() {
this.reset();
window.Whisper.events.on(
'devicePairingRequestReceived',
(pubKey: string) => {
this.requestReceived(pubKey);
}
);
this.setState({ currentView: 'qrcodeView' });
}
private stopReceivingRequests() {
this.setState({ currentView: 'filterRequestView' });
window.Whisper.events.off('devicePairingRequestReceived');
}
private requestReceived(secondaryDevicePubKey: string | EventHandlerNonNull) {
// FIFO: push at the front of the array with unshift()
this.state.pubKeyRequests.unshift(secondaryDevicePubKey);
if (!this.state.currentPubKey) {
this.nextPubKey();
this.stopReceivingRequests();
}
}
private allowDevice() {
this.setState({
accepted: true,
});
}
private transmissionCB(errors: any) {
if (!errors) {
this.setState({
errors: null,
});
this.closeDialog();
ToastUtils.push({
title: window.i18n('devicePairedSuccessfully'),
type: 'success',
});
const { currentPubKey } = this.state;
if (currentPubKey) {
const conv = window.ConversationController.get(currentPubKey);
if (conv) {
void conv.setNickname(this.state.deviceAlias);
}
}
return;
}
this.setState({
errors: errors,
});
}
private skipDevice() {
window.Whisper.events.trigger(
'devicePairingRequestRejected',
this.state.currentPubKey
);
this.closeDialog();
}
private nextPubKey() {
// FIFO: pop at the back of the array using pop()
this.setState({
currentPubKey: this.state.pubKeyRequests.pop(),
});
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private validateSecondaryDevice() {
this.setState({ loading: true });
window.Whisper.events.trigger(
'devicePairingRequestAccepted',
this.state.currentPubKey,
(errors: any) => {
this.transmissionCB(errors);
window.Whisper.events.trigger('refreshLinkedDeviceList');
return true;
}
);
}
private hasReceivedRequests() {
return this.state.currentPubKey || this.state.pubKeyRequests.length > 0;
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.stopReceivingRequests();
window.Whisper.events.off('devicePairingRequestReceived');
if (this.state.currentPubKey && !this.state.accepted) {
window.Whisper.events.trigger(
'devicePairingRequestRejected',
this.state.currentPubKey
);
}
this.props.onClose();
}
private handleUpdateDeviceAlias(value: ChangeEvent<HTMLInputElement>) {
const trimmed = value.target.value.trim();
if (!!trimmed) {
this.setState({ deviceAlias: trimmed });
} else {
this.setState({ deviceAlias: undefined });
}
}
private triggerUnpairDevice() {
const deviceUnpaired = () => {
ToastUtils.push({
title: window.i18n('deviceUnpaired'),
});
this.closeDialog();
this.setState({ loading: false });
};
this.setState({ loading: true });
window.Whisper.events.trigger(
'deviceUnpairingRequested',
this.props.pubKeyToUnpair,
deviceUnpaired
);
}
}