Mobile signer support (via Qr) (#1583)
* Add external accounts via Qr * WIP on signing * And we are off to the races * Add qr state * Name checks * Bump deps
This commit is contained in:
parent
6726280b8f
commit
86b8954b1c
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@polkadot/react-components": "^0.35.0-beta.35",
|
||||
"@polkadot/react-qr": "^0.43.0-beta.11",
|
||||
"@types/file-saver": "^2.0.0",
|
||||
"@types/yargs": "^13.0.2",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
|
||||
@ -24,6 +24,7 @@ interface State {
|
||||
genesisHash: string | null;
|
||||
isBackupOpen: boolean;
|
||||
isEditable: boolean;
|
||||
isExternal: boolean;
|
||||
isForgetOpen: boolean;
|
||||
isPasswordOpen: boolean;
|
||||
isTransferOpen: boolean;
|
||||
@ -43,6 +44,7 @@ class Account extends React.PureComponent<Props, State> {
|
||||
isEditable: account
|
||||
? !(account.meta.isInjected)
|
||||
: false,
|
||||
isExternal: (account && account.meta.isExternal) || false,
|
||||
isForgetOpen: false,
|
||||
isPasswordOpen: false,
|
||||
isTransferOpen: false
|
||||
@ -188,20 +190,22 @@ class Account extends React.PureComponent<Props, State> {
|
||||
|
||||
private renderButtons (): React.ReactNode {
|
||||
const { t } = this.props;
|
||||
const { genesisHash, isEditable } = this.state;
|
||||
const { genesisHash, isEditable, isExternal } = this.state;
|
||||
|
||||
return (
|
||||
<div className='accounts--Account-buttons buttons'>
|
||||
<div className='actions'>
|
||||
{isEditable && (
|
||||
<Button
|
||||
isNegative
|
||||
onClick={this.toggleForget}
|
||||
icon='trash'
|
||||
size='small'
|
||||
tooltip={t('Forget this account')}
|
||||
/>
|
||||
)}
|
||||
{isEditable && !isExternal && (
|
||||
<>
|
||||
<Button
|
||||
isNegative
|
||||
onClick={this.toggleForget}
|
||||
icon='trash'
|
||||
size='small'
|
||||
tooltip={t('Forget this account')}
|
||||
/>
|
||||
<Button
|
||||
icon='cloud download'
|
||||
isPrimary
|
||||
@ -226,7 +230,7 @@ class Account extends React.PureComponent<Props, State> {
|
||||
tooltip={t('Send funds from this account')}
|
||||
/>
|
||||
</div>
|
||||
{isEditable && (
|
||||
{isEditable && !isExternal && (
|
||||
<div className='others'>
|
||||
<ChainLock
|
||||
genesisHash={genesisHash}
|
||||
|
||||
@ -13,6 +13,7 @@ import { Button, CardGrid } from '@polkadot/react-components';
|
||||
|
||||
import CreateModal from './modals/Create';
|
||||
import ImportModal from './modals/Import';
|
||||
import QrModal from './modals/Qr';
|
||||
import Account from './Account';
|
||||
import Banner from './Banner';
|
||||
import translate from './translate';
|
||||
@ -24,24 +25,24 @@ interface Props extends ComponentProps, I18nProps {
|
||||
interface State {
|
||||
isCreateOpen: boolean;
|
||||
isImportOpen: boolean;
|
||||
isQrOpen: boolean;
|
||||
}
|
||||
|
||||
class Overview extends React.PureComponent<Props, State> {
|
||||
public constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
const { state: { isCreateOpen = false } = {} } = this.props.location;
|
||||
|
||||
this.state = {
|
||||
isCreateOpen,
|
||||
isImportOpen: false
|
||||
isCreateOpen: false,
|
||||
isImportOpen: false,
|
||||
isQrOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
public render (): React.ReactNode {
|
||||
const { accounts, t } = this.props;
|
||||
const { isCreateOpen, isImportOpen } = this.state;
|
||||
const emptyScreen = !isCreateOpen && !isImportOpen && (!accounts || Object.keys(accounts).length === 0);
|
||||
const { accounts = [], onStatusChange, t } = this.props;
|
||||
const { isCreateOpen, isImportOpen, isQrOpen } = this.state;
|
||||
const emptyScreen = !(isCreateOpen || isImportOpen || isQrOpen) && (Object.keys(accounts).length === 0);
|
||||
|
||||
return (
|
||||
<CardGrid
|
||||
@ -59,14 +60,36 @@ class Overview extends React.PureComponent<Props, State> {
|
||||
label={t('Restore JSON')}
|
||||
onClick={this.toggleImport}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Add via Qr')}
|
||||
onClick={this.toggleQr}
|
||||
/>
|
||||
</Button.Group>
|
||||
}
|
||||
isEmpty={emptyScreen}
|
||||
emptyText={t('No account yet?')}
|
||||
>
|
||||
{this.renderCreate()}
|
||||
{this.renderImport()}
|
||||
{accounts && Object.keys(accounts).map((address): React.ReactNode => (
|
||||
{isCreateOpen && (
|
||||
<CreateModal
|
||||
onClose={this.toggleCreate}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
)}
|
||||
{isImportOpen && (
|
||||
<ImportModal
|
||||
onClose={this.toggleImport}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
)}
|
||||
{isQrOpen && (
|
||||
<QrModal
|
||||
onClose={this.toggleQr}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
)}
|
||||
{Object.keys(accounts).map((address): React.ReactNode => (
|
||||
<Account
|
||||
address={address}
|
||||
key={address}
|
||||
@ -76,38 +99,6 @@ class Overview extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private renderCreate (): React.ReactNode {
|
||||
const { isCreateOpen } = this.state;
|
||||
const { onStatusChange } = this.props;
|
||||
|
||||
if (!isCreateOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateModal
|
||||
onClose={this.toggleCreate}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderImport (): React.ReactNode {
|
||||
const { isImportOpen } = this.state;
|
||||
const { onStatusChange } = this.props;
|
||||
|
||||
if (!isImportOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImportModal
|
||||
onClose={this.toggleImport}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private toggleCreate = (): void => {
|
||||
this.setState(({ isCreateOpen }): Pick<State, never> => ({
|
||||
isCreateOpen: !isCreateOpen
|
||||
@ -119,6 +110,12 @@ class Overview extends React.PureComponent<Props, State> {
|
||||
isImportOpen: !isImportOpen
|
||||
}));
|
||||
}
|
||||
|
||||
private toggleQr = (): void => {
|
||||
this.setState(({ isQrOpen }): Pick<State, never> => ({
|
||||
isQrOpen: !isQrOpen
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
|
||||
@ -367,7 +367,7 @@ class Create extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
private onChangeName = (name: string): void => {
|
||||
this.nextState({ name });
|
||||
this.nextState({ name: name.trim() });
|
||||
}
|
||||
|
||||
private onChangePairType = (pairType: KeypairType): void => {
|
||||
|
||||
112
packages/app-accounts/src/modals/Qr.tsx
Normal file
112
packages/app-accounts/src/modals/Qr.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { I18nProps } from '@polkadot/react-components/types';
|
||||
import { ModalProps } from '../types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { AddressRow, Button, Input, Modal } from '@polkadot/react-components';
|
||||
import { InputAddress } from '@polkadot/react-components/InputAddress';
|
||||
import { QrScanAddress } from '@polkadot/react-qr';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
|
||||
import translate from '../translate';
|
||||
|
||||
interface Scanned {
|
||||
address: string;
|
||||
genesisHash: string;
|
||||
}
|
||||
|
||||
interface Props extends I18nProps, ModalProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function QrModal ({ className, onClose, onStatusChange, t }: Props): React.ReactElement<Props> {
|
||||
const [name, setName] = useState('');
|
||||
const [scanned, setScanned] = useState<Scanned | null>(null);
|
||||
const isNameValid = !!name;
|
||||
|
||||
const _onNameChange = (name: string): void => setName(name.trim());
|
||||
const _onSave = (): void => {
|
||||
if (!scanned || !isNameValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { address, genesisHash } = scanned;
|
||||
|
||||
keyring.addExternal(address, { genesisHash, name });
|
||||
InputAddress.setLastValue('account', address);
|
||||
|
||||
onStatusChange({
|
||||
account: address,
|
||||
action: 'create',
|
||||
message: t('created account'),
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
dimmer='inverted'
|
||||
open
|
||||
>
|
||||
<Modal.Header>{t('Add account via Qr')}</Modal.Header>
|
||||
<Modal.Content>
|
||||
{
|
||||
scanned
|
||||
? (
|
||||
<>
|
||||
<AddressRow
|
||||
defaultName={name}
|
||||
value={scanned.address}
|
||||
/>
|
||||
<Input
|
||||
autoFocus
|
||||
className='full'
|
||||
help={t('Name given to this account. You can change it at any point in the future.')}
|
||||
isError={!isNameValid}
|
||||
label={t('name')}
|
||||
onChange={_onNameChange}
|
||||
onEnter={_onSave}
|
||||
value={name}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<div className='qr-wrapper'>
|
||||
<QrScanAddress onScan={setScanned} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button.Group>
|
||||
<Button
|
||||
label={t('Cancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={!scanned || !isNameValid}
|
||||
isPrimary
|
||||
onClick={_onSave}
|
||||
label={t('Create')}
|
||||
/>
|
||||
</Button.Group>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(
|
||||
styled(QrModal)`
|
||||
.qr-wrapper {
|
||||
margin: 0 auto;
|
||||
max-width: 30rem;
|
||||
}
|
||||
`
|
||||
);
|
||||
@ -32,7 +32,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@polkadot/api": "^0.91.0-beta.22",
|
||||
"@polkadot/extension-dapp": "^0.10.0-beta.3",
|
||||
"@polkadot/extension-dapp": "^0.10.0-beta.7",
|
||||
"edgeware-node-types": "^1.0.9",
|
||||
"rxjs-compat": "^6.5.3"
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ function renderExtended ({ balances_all, t, address, withExtended }: Props): Rea
|
||||
)}
|
||||
{extendedDisplay.crypto && (
|
||||
<>
|
||||
<Label label={t('crypto type')} />
|
||||
<Label label={t('type')} />
|
||||
<CryptoType
|
||||
accountId={address}
|
||||
className='result'
|
||||
|
||||
@ -26,7 +26,9 @@ export default function CryptoType ({ accountId, className, label = '' }: Props)
|
||||
if (current) {
|
||||
type = current.meta.isInjected
|
||||
? 'injected'
|
||||
: current.type;
|
||||
: current.meta.isExternal
|
||||
? 'external'
|
||||
: current.type;
|
||||
}
|
||||
} catch (error) {
|
||||
// cannot determine, keep unknown
|
||||
|
||||
@ -19,76 +19,6 @@ interface Props extends I18nProps {
|
||||
txqueue?: QueueTx[];
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
right: 0.25rem;
|
||||
top: 0.25rem;
|
||||
width: 20rem;
|
||||
z-index: 1001;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-align: right;
|
||||
|
||||
> .wrapper > .container {
|
||||
align-items: center;
|
||||
background: #00688b;
|
||||
border-radius: $small-corner;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
text-align: center;
|
||||
opacity: 0.95;
|
||||
vertical-align: middle;
|
||||
|
||||
.desc {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.status {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.short {
|
||||
font-size: 2.5rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
i.icon {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.cancelled > .wrapper > .container {
|
||||
background: #cd9b1d
|
||||
}
|
||||
|
||||
&.event > .wrapper > .container {
|
||||
background: teal;
|
||||
}
|
||||
|
||||
&.completed > .wrapper > .container,
|
||||
&.finalized > .wrapper > .container,
|
||||
&.sent > .wrapper > .container,
|
||||
&.success > .wrapper > .container {
|
||||
background: green;
|
||||
}
|
||||
|
||||
&.dropped > .wrapper > .container,
|
||||
&.error > .wrapper > .container,
|
||||
&.invalid > .wrapper > .container,
|
||||
&.usurped > .wrapper > .container {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function iconName (status: string): any {
|
||||
switch (status) {
|
||||
case 'error':
|
||||
@ -204,7 +134,7 @@ function renderItem ({ id, extrinsic, error, removeItem, rpc, status }: QueueTx)
|
||||
);
|
||||
}
|
||||
|
||||
function Status ({ stqueue = [], txqueue = [] }: Props): React.ReactElement<Props> | null {
|
||||
function Status ({ className, stqueue = [], txqueue = [] }: Props): React.ReactElement<Props> | null {
|
||||
const allst: QueueStatus[] = stqueue.filter(({ isCompleted }): boolean => !isCompleted);
|
||||
const alltx: QueueTx[] = txqueue.filter(({ status }): boolean =>
|
||||
!['completed', 'incomplete'].includes(status)
|
||||
@ -215,11 +145,81 @@ function Status ({ stqueue = [], txqueue = [] }: Props): React.ReactElement<Prop
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper className='ui--Status'>
|
||||
<div className={`ui--Status ${className}`}>
|
||||
{alltx.map(renderItem)}
|
||||
{allst.map(renderStatus)}
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(Status);
|
||||
export default translate(
|
||||
styled(Status)`
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
right: 0.25rem;
|
||||
top: 0.25rem;
|
||||
width: 20rem;
|
||||
z-index: 1001;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-align: right;
|
||||
|
||||
> .wrapper > .container {
|
||||
align-items: center;
|
||||
background: #00688b;
|
||||
border-radius: $small-corner;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
text-align: center;
|
||||
opacity: 0.95;
|
||||
vertical-align: middle;
|
||||
|
||||
.desc {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.status {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.short {
|
||||
font-size: 2.5rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
i.icon {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.cancelled > .wrapper > .container {
|
||||
background: #cd9b1d
|
||||
}
|
||||
|
||||
&.event > .wrapper > .container {
|
||||
background: teal;
|
||||
}
|
||||
|
||||
&.completed > .wrapper > .container,
|
||||
&.finalized > .wrapper > .container,
|
||||
&.sent > .wrapper > .container,
|
||||
&.success > .wrapper > .container {
|
||||
background: green;
|
||||
}
|
||||
|
||||
&.dropped > .wrapper > .container,
|
||||
&.error > .wrapper > .container,
|
||||
&.invalid > .wrapper > .container,
|
||||
&.usurped > .wrapper > .container {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
@ -22,7 +22,7 @@ export interface AccountInfo {
|
||||
accountId?: string | null;
|
||||
}
|
||||
|
||||
export type QueueTxStatus = 'future' | 'ready' | 'finalized' | 'usurped' | 'dropped' | 'invalid' | 'broadcast' | 'cancelled' | 'completed' | 'error' | 'incomplete' | 'queued' | 'sending' | 'sent' | 'blocked';
|
||||
export type QueueTxStatus = 'future' | 'ready' | 'finalized' | 'usurped' | 'dropped' | 'invalid' | 'broadcast' | 'cancelled' | 'completed' | 'error' | 'incomplete' | 'queued' | 'qr' | 'sending' | 'signing' | 'sent' | 'blocked';
|
||||
|
||||
export type SignerCallback = (id: number, result: SignerResult | null) => void;
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@polkadot/react-components": "^0.35.0-beta.35"
|
||||
"@polkadot/react-components": "^0.35.0-beta.35",
|
||||
"@polkadot/react-qr": "^0.43.0-beta.11"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { SignerOptions } from '@polkadot/api/types';
|
||||
import { SignerOptions, SignerResult } from '@polkadot/api/types';
|
||||
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
|
||||
import { ApiProps } from '@polkadot/react-api/types';
|
||||
import { I18nProps, BareProps } from '@polkadot/react-components/types';
|
||||
@ -25,8 +25,9 @@ import { format } from '@polkadot/util/logger';
|
||||
|
||||
import PasswordCheck from './PasswordCheck';
|
||||
import Transaction from './Transaction';
|
||||
import translate from './translate';
|
||||
import Qr from './Qr';
|
||||
import Unlock from './Unlock';
|
||||
import translate from './translate';
|
||||
|
||||
interface BaseProps extends BareProps {
|
||||
queue: QueueTx[];
|
||||
@ -39,21 +40,33 @@ interface Props extends I18nProps, ApiProps, BaseProps {
|
||||
|
||||
interface State {
|
||||
currentItem?: QueueTx;
|
||||
isQrScanning: boolean;
|
||||
isQrVisible: boolean;
|
||||
isSendable: boolean;
|
||||
isV2?: boolean;
|
||||
password: string;
|
||||
qrAddress: string;
|
||||
qrPayload: Uint8Array;
|
||||
qrResolve?: (result: SignerResult) => void;
|
||||
qrReject?: (error: Error) => void;
|
||||
tip?: BN;
|
||||
unlockError?: string | null;
|
||||
}
|
||||
|
||||
let qrId = 0;
|
||||
|
||||
class Signer extends React.PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
isSendable: false,
|
||||
isQrScanning: false,
|
||||
isQrVisible: false,
|
||||
password: '',
|
||||
qrAddress: '',
|
||||
qrPayload: new Uint8Array(),
|
||||
unlockError: null
|
||||
};
|
||||
|
||||
public static getDerivedStateFromProps ({ allAccounts, api, queue }: Props, { currentItem, password, unlockError }: State): State {
|
||||
public static getDerivedStateFromProps ({ allAccounts, api, queue }: Props, { currentItem, password, unlockError }: State): Partial<State> {
|
||||
let isV2: boolean;
|
||||
try {
|
||||
isV2 = !!api.tx.session.setKeys;
|
||||
@ -61,7 +74,7 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
isV2 = false;
|
||||
}
|
||||
|
||||
const nextItem = queue.find(({ status }): boolean => status === 'queued');
|
||||
const nextItem = queue.find(({ status }): boolean => ['queued', 'qr'].includes(status));
|
||||
const isSame =
|
||||
!!nextItem &&
|
||||
!!currentItem &&
|
||||
@ -122,41 +135,59 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
|
||||
private renderButtons (): React.ReactNode {
|
||||
const { t } = this.props;
|
||||
const { currentItem, isSendable } = this.state;
|
||||
const { currentItem, isQrScanning, isQrVisible, isSendable } = this.state;
|
||||
|
||||
if (!currentItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isExternal = this.isExternal(currentItem.accountId);
|
||||
|
||||
return (
|
||||
<Modal.Actions>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isNegative
|
||||
onClick={this.onCancel}
|
||||
onClick={
|
||||
isQrVisible
|
||||
? this.onCancelQr
|
||||
: this.onCancel
|
||||
}
|
||||
tabIndex={3}
|
||||
label={t('Cancel')}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
className='ui--signer-Signer-Submit'
|
||||
isDisabled={!isSendable}
|
||||
isPrimary
|
||||
onClick={this.onSend}
|
||||
tabIndex={2}
|
||||
label={
|
||||
currentItem.isUnsigned
|
||||
? t('Submit (no signature)')
|
||||
: t('Sign and Submit')
|
||||
}
|
||||
/>
|
||||
{(!isQrVisible || !isQrScanning) && (
|
||||
<>
|
||||
<Button.Or />
|
||||
<Button
|
||||
className='ui--signer-Signer-Submit'
|
||||
isDisabled={!isSendable}
|
||||
isPrimary
|
||||
onClick={
|
||||
isQrVisible
|
||||
? this.activateQrScanning
|
||||
: this.onSend
|
||||
}
|
||||
tabIndex={2}
|
||||
label={
|
||||
isQrVisible
|
||||
? t('Scan Signature Qr')
|
||||
: currentItem.isUnsigned
|
||||
? t('Submit (no signature)')
|
||||
: isExternal
|
||||
? t('Sign via Qr')
|
||||
: t('Sign and Submit')
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Button.Group>
|
||||
</Modal.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContent (): React.ReactNode {
|
||||
const { currentItem, isSendable } = this.state;
|
||||
const { currentItem, isQrScanning, isQrVisible, isSendable, qrAddress, qrPayload } = this.state;
|
||||
|
||||
if (!currentItem) {
|
||||
return null;
|
||||
@ -164,11 +195,27 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<Transaction
|
||||
hideDetails={isQrVisible}
|
||||
isSendable={isSendable}
|
||||
value={currentItem}
|
||||
>
|
||||
{this.renderTip()}
|
||||
{this.renderUnlock()}
|
||||
{
|
||||
isQrVisible
|
||||
? (
|
||||
<Qr
|
||||
address={qrAddress}
|
||||
isScanning={isQrScanning}
|
||||
onSignature={this.addQrSignature}
|
||||
payload={qrPayload}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{this.renderTip()}
|
||||
{this.renderUnlock()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Transaction>
|
||||
);
|
||||
}
|
||||
@ -194,7 +241,7 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
private renderUnlock (): React.ReactNode {
|
||||
const { currentItem, isSendable, password, unlockError } = this.state;
|
||||
|
||||
if (!isSendable || !currentItem || currentItem.isUnsigned) {
|
||||
if (!isSendable || !currentItem || currentItem.isUnsigned || this.isExternal(currentItem.accountId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -215,6 +262,26 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private isExternal (accountId?: string | null): boolean {
|
||||
if (!accountId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let publicKey;
|
||||
|
||||
try {
|
||||
publicKey = keyring.decodeAddress(accountId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const pair = keyring.getPair(publicKey);
|
||||
|
||||
return !!pair.meta.isExternal;
|
||||
}
|
||||
|
||||
private unlockAccount (accountId: string, password?: string): string | null {
|
||||
let publicKey;
|
||||
|
||||
@ -228,7 +295,7 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
|
||||
const pair = keyring.getPair(publicKey);
|
||||
|
||||
if (!pair.isLocked || pair.meta.isInjected) {
|
||||
if (!pair.isLocked || pair.meta.isInjected || pair.meta.isExternal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -254,6 +321,19 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
this.setState({ tip });
|
||||
}
|
||||
|
||||
private onCancelQr = (): void => {
|
||||
this.setState({
|
||||
isQrScanning: false,
|
||||
isQrVisible: false
|
||||
}, (): void => {
|
||||
const { qrReject } = this.state;
|
||||
|
||||
qrReject && qrReject(new Error('cancelled'));
|
||||
|
||||
this.onCancel();
|
||||
});
|
||||
};
|
||||
|
||||
private onCancel = (): void => {
|
||||
const { queueSetTxStatus } = this.props;
|
||||
const { currentItem } = this.state;
|
||||
@ -287,6 +367,38 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
return this.sendExtrinsic(currentItem, password);
|
||||
}
|
||||
|
||||
private signQrPayload = (payload: SignerPayloadJSON): Promise<SignerResult> => {
|
||||
console.error('signQrPayload', payload);
|
||||
|
||||
return new Promise((resolve, reject): void => {
|
||||
this.setState({
|
||||
isQrVisible: true,
|
||||
qrAddress: payload.address,
|
||||
qrPayload: createType('ExtrinsicPayload', payload, { version: payload.version }).toU8a(),
|
||||
qrResolve: resolve,
|
||||
qrReject: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private addQrSignature = ({ signature }: { signature: string }): void => {
|
||||
this.setState(({ qrResolve }: State): Pick<State, never> => {
|
||||
qrResolve && qrResolve({
|
||||
id: ++qrId,
|
||||
signature
|
||||
});
|
||||
|
||||
return {
|
||||
isQrScanning: false,
|
||||
isQrVisible: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private activateQrScanning = (): void => {
|
||||
this.setState({ isQrScanning: true });
|
||||
}
|
||||
|
||||
private sendRpc = async ({ id, rpc, values = [] }: QueueTx): Promise<void> => {
|
||||
if (!rpc) {
|
||||
return;
|
||||
@ -304,7 +416,7 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
private async sendExtrinsic (queueTx: QueueTx, password?: string): Promise<void> {
|
||||
const { isV2, tip } = this.state;
|
||||
|
||||
const { accountId, extrinsic, id, payload, isUnsigned } = queueTx;
|
||||
const { accountId, extrinsic, payload, isUnsigned } = queueTx;
|
||||
|
||||
if (!isUnsigned) {
|
||||
assert(accountId, 'Expected an accountId with signed transactions');
|
||||
@ -317,8 +429,6 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const { queueSetTxStatus } = this.props;
|
||||
|
||||
if (payload) {
|
||||
return this.makeExtrinsicSignature(
|
||||
{
|
||||
@ -333,7 +443,6 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
const submittable = extrinsic as SubmittableExtrinsic;
|
||||
|
||||
assert(submittable, 'Expected an extrinsic to be supplied to sendExtrinsic');
|
||||
queueSetTxStatus(id, 'sending');
|
||||
|
||||
return isUnsigned
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
@ -373,15 +482,21 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
const params = [];
|
||||
|
||||
if (pair) {
|
||||
const { address, meta: { isExternal, isInjected, source } } = pair;
|
||||
|
||||
queueSetTxStatus(id, 'signing');
|
||||
|
||||
// set the signer
|
||||
if (pair.meta.isInjected) {
|
||||
const { address, meta: { source } } = pair;
|
||||
if (isExternal) {
|
||||
queueSetTxStatus(id, 'qr');
|
||||
api.setSigner({ signPayload: this.signQrPayload });
|
||||
params.push(address);
|
||||
} else if (isInjected) {
|
||||
const injected = await web3FromSource(source);
|
||||
|
||||
assert(injected, `Unable to find a signer for ${address}`);
|
||||
|
||||
api.setSigner(injected.signer);
|
||||
|
||||
params.push(address);
|
||||
} else {
|
||||
params.push(pair);
|
||||
@ -389,8 +504,7 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
if (isV2 && tip) {
|
||||
const signerOptions: Partial<SignerOptions> = { tip };
|
||||
params.push(signerOptions);
|
||||
params.push({ tip } as Partial<SignerOptions>);
|
||||
}
|
||||
|
||||
if (isFunction(txStartCb)) {
|
||||
@ -429,6 +543,8 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
txFailedCb(result);
|
||||
}
|
||||
}]);
|
||||
|
||||
queueSetTxStatus(id, 'sending');
|
||||
} catch (error) {
|
||||
console.error('makeExtrinsicCall: error:', error);
|
||||
queueSetTxStatus(id, 'error', {}, error);
|
||||
|
||||
43
packages/react-signer/src/Qr.tsx
Normal file
43
packages/react-signer/src/Qr.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2017-2019 @polkadot/react-signer authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { I18nProps, BareProps } from '@polkadot/react-components/types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { QrDisplayPayload, QrScanSignature } from '@polkadot/react-qr';
|
||||
|
||||
import translate from './translate';
|
||||
|
||||
interface Props extends I18nProps, BareProps {
|
||||
address: string;
|
||||
isScanning: boolean;
|
||||
onSignature: (signature: { signature: string }) => void;
|
||||
payload: Uint8Array;
|
||||
}
|
||||
|
||||
const CMD_MORTAL = 2;
|
||||
|
||||
function Qr ({ address, className, isScanning, onSignature, payload }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
{
|
||||
isScanning
|
||||
? <QrScanSignature onScan={onSignature} />
|
||||
: <QrDisplayPayload
|
||||
address={address}
|
||||
cmd={CMD_MORTAL}
|
||||
payload={payload}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(
|
||||
styled(Qr)`
|
||||
margin: 0 auto;
|
||||
max-width: 30rem;
|
||||
`
|
||||
);
|
||||
@ -14,44 +14,12 @@ import translate from './translate';
|
||||
|
||||
interface Props extends I18nProps {
|
||||
children?: React.ReactNode;
|
||||
hideDetails?: boolean;
|
||||
isSendable: boolean;
|
||||
value: QueueTx;
|
||||
}
|
||||
|
||||
function renderAccount ({ t, value: { accountId, isUnsigned } }: Props): React.ReactNode {
|
||||
if (isUnsigned || !accountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputAddress
|
||||
className='full'
|
||||
defaultValue={accountId}
|
||||
isDisabled
|
||||
isInput
|
||||
label={t('sending from my account')}
|
||||
withLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderChecks ({ isSendable, value: { accountId, extrinsic, isUnsigned } }: Props): React.ReactNode {
|
||||
if (isUnsigned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Checks
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
isSendable={isSendable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Transaction (props: Props): React.ReactElement<Props> | null {
|
||||
const { children, value: { extrinsic } } = props;
|
||||
|
||||
function Transaction ({ children, hideDetails, isSendable, value: { accountId, extrinsic, isUnsigned }, t }: Props): React.ReactElement<Props> | null {
|
||||
if (!extrinsic) {
|
||||
return null;
|
||||
}
|
||||
@ -69,9 +37,28 @@ function Transaction (props: Props): React.ReactElement<Props> | null {
|
||||
}</summary></details></label>
|
||||
</Modal.Header>
|
||||
<Modal.Content className='ui--signer-Signer-Content'>
|
||||
{renderAccount(props)}
|
||||
<Call value={extrinsic} />
|
||||
{renderChecks(props)}
|
||||
{!hideDetails && (
|
||||
<>
|
||||
{!isUnsigned && accountId && (
|
||||
<InputAddress
|
||||
className='full'
|
||||
defaultValue={accountId}
|
||||
isDisabled
|
||||
isInput
|
||||
label={t('sending from my account')}
|
||||
withLabel
|
||||
/>
|
||||
)}
|
||||
<Call value={extrinsic} />
|
||||
{!isUnsigned && (
|
||||
<Checks
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
isSendable={isSendable}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</Modal.Content>
|
||||
</>
|
||||
|
||||
74
yarn.lock
74
yarn.lock
@ -2048,18 +2048,18 @@
|
||||
typescript "^3.6.2"
|
||||
vuepress "^1.0.3"
|
||||
|
||||
"@polkadot/extension-dapp@^0.10.0-beta.3":
|
||||
version "0.10.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/@polkadot/extension-dapp/-/extension-dapp-0.10.0-beta.3.tgz#ffdfdf4600e6762d02f5b8d86969e47b67825c3d"
|
||||
integrity sha512-4Mb5ZRCzeV1AWh9MJB0NKTJHGM69/DUmslm1ej+tdV3MvMzlcQw0mx8WHfxOoYycDcTDWpwGEPJ8cUbwaWZI1w==
|
||||
"@polkadot/extension-dapp@^0.10.0-beta.7":
|
||||
version "0.10.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/@polkadot/extension-dapp/-/extension-dapp-0.10.0-beta.7.tgz#8f118f75ea5071cb1f4cd16cf13990158efa509a"
|
||||
integrity sha512-cLEs1ywhK4EvgiY9fK5Kzek4ZnhlTsXFUhBwKXQ+bJnEJKVOnEBvZa7kElT4t7fse+DZegF7x4fNy8ZIr5O6fQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
"@polkadot/extension-inject" "^0.10.0-beta.3"
|
||||
"@polkadot/extension-inject" "^0.10.0-beta.7"
|
||||
|
||||
"@polkadot/extension-inject@^0.10.0-beta.3":
|
||||
version "0.10.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/@polkadot/extension-inject/-/extension-inject-0.10.0-beta.3.tgz#1f53734dbfd388177a67f12495ba622ee4761d60"
|
||||
integrity sha512-m1xI0bRhPhmdVN1BXYKIJcl5U9lgWUmunxmGfkJzpJOiJOPqCAZKBbGMb/EFcj9YNiqoRL8k0H1gTQramp07Gg==
|
||||
"@polkadot/extension-inject@^0.10.0-beta.7":
|
||||
version "0.10.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/@polkadot/extension-inject/-/extension-inject-0.10.0-beta.7.tgz#64112a91d1d6575f2e289a89b901e49296d5d9c2"
|
||||
integrity sha512-TWHQTvDFi2/CBqh9Cdz4vvw7Tba9JTWHpUPFMFau2aI32nkr+OddrY4sd7LomY3foSEa+z1+Tp/FggM9HbO6yg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
|
||||
@ -2094,6 +2094,16 @@
|
||||
react-copy-to-clipboard "^5.0.1"
|
||||
styled-components "^4.3.1"
|
||||
|
||||
"@polkadot/react-qr@^0.43.0-beta.11":
|
||||
version "0.43.0-beta.11"
|
||||
resolved "https://registry.yarnpkg.com/@polkadot/react-qr/-/react-qr-0.43.0-beta.11.tgz#38b0d9c5d763560dd09a7595521a51e362b63c3a"
|
||||
integrity sha512-VYBSopaTedYWUhl1Fhyefe0vKjYIsMIV2jzr6XCYL9hc1RptGOIf0lpm6peQaSPLxNQEAxSAvrItKKXtAhwV5A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
"@types/react-qr-reader" "^2.1.1"
|
||||
qrcode-generator "^1.4.3"
|
||||
react-qr-reader "^2.2.1"
|
||||
|
||||
"@polkadot/rpc-core@^0.91.0-beta.22":
|
||||
version "0.91.0-beta.22"
|
||||
resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.91.0-beta.22.tgz#886855d016eea3143f993d81ee3ed6b8b7a59899"
|
||||
@ -2478,6 +2488,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-qr-reader@^2.1.1":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-qr-reader/-/react-qr-reader-2.1.2.tgz#da7a674cf9e3ceb1c026e89d5aa41fb1587fc66b"
|
||||
integrity sha512-tDLqqiQMZlTKBKtWedcf7QZ3L9fyz8nPHNq/j+1mcJoEvrdM3Y89i3/FKwdmZ4H7G6uUk4JqShpWXLCuYpZK+w==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-dom@^4.3.5":
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.5.tgz#72f229967690c890d00f96e6b85e9ee5780db31f"
|
||||
@ -9214,6 +9231,11 @@ jsprim@^1.2.2:
|
||||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
jsqr@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.2.0.tgz#f93fc65fa7d1ded78b1bcb020fa044352b04261a"
|
||||
integrity sha512-wKcQS9QC2VHGk7aphWCp1RrFyC0CM6fMgC5prZZ2KV/Lk6OKNoCod9IR6bao+yx3KPY0gZFC5dc+h+KFzCI0Wg==
|
||||
|
||||
jstransform@^11.0.3:
|
||||
version "11.0.3"
|
||||
resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223"
|
||||
@ -12500,6 +12522,11 @@ q@^1.1.2, q@^1.5.1:
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
|
||||
qrcode-generator@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.3.tgz#4876e8f280e65b6c94615f4c19c484f6b964b199"
|
||||
integrity sha512-++rVRvMRq5BlHfmAafl8a4ppUntzUxCCUTT2t0siUgqKwdnqRzY8IH6f6WSX5dZUhD2Ul5/MIKuTJddflwrGzw==
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
@ -12722,6 +12749,15 @@ react-popper@^1.3.3:
|
||||
typed-styles "^0.0.7"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-qr-reader@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.2.1.tgz#dc89046d1c1a1da837a683dd970de5926817d55b"
|
||||
integrity sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA==
|
||||
dependencies:
|
||||
jsqr "^1.2.0"
|
||||
prop-types "^15.7.2"
|
||||
webrtc-adapter "^7.2.1"
|
||||
|
||||
react-router-dom@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be"
|
||||
@ -13380,6 +13416,13 @@ rsvp@^4.8.4:
|
||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
||||
|
||||
rtcpeerconnection-shim@^1.2.15:
|
||||
version "1.2.15"
|
||||
resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz#e7cc189a81b435324c4949aa3dfb51888684b243"
|
||||
integrity sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==
|
||||
dependencies:
|
||||
sdp "^2.6.0"
|
||||
|
||||
run-async@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
@ -13486,6 +13529,11 @@ schema-utils@^2.0.0, schema-utils@^2.0.1:
|
||||
ajv "^6.10.2"
|
||||
ajv-keywords "^3.4.1"
|
||||
|
||||
sdp@^2.10.0, sdp@^2.6.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.10.0.tgz#643fff1a43cdba54a739c7b202f56bd854474af2"
|
||||
integrity sha512-H+VjfyQpRz9GezhshJmkXTtCAT9/2g9az3GFDPYfGOz0eAOQU1fCrL3S9Dq/eUT9FtOyLi/czdR9PzK3fKUYOQ==
|
||||
|
||||
secp256k1@^3.7.0:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.7.1.tgz#12e473e0e9a7c2f2d4d4818e722ad0e14cc1e2f1"
|
||||
@ -15687,6 +15735,14 @@ webpackbar@3.2.0:
|
||||
text-table "^0.2.0"
|
||||
wrap-ansi "^5.1.0"
|
||||
|
||||
webrtc-adapter@^7.2.1:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.3.0.tgz#a65e18aad42759bab6ced7f8cfff11b051321e2a"
|
||||
integrity sha512-pKcwt6IR6RLCD6jlcdOOi88iVwdzppHlkOhtgTSuZHtYTxdD09t5fA1Di7GJU7je8oHcCBlNfb7zwBsetERnmQ==
|
||||
dependencies:
|
||||
rtcpeerconnection-shim "^1.2.15"
|
||||
sdp "^2.10.0"
|
||||
|
||||
websocket-driver@>=0.5.1:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user