Web view with web3.site support (#4313)
* Web-domain based routing * Support base32-encoded urls * Proper support for non-domain based routing * Handling long domain names * Switching to web3.site * Encoding for *.web3.site urls * Add DappUrlInput component * Update Web views with store * Update spec description * Update spec description * edited url does not allow in-place store edits * Fixing dapps access on 127.0.0.1:8180 * Use /web/<hash> urls for iframe * Redirecting to parity.web3.site * Disabling the redirection
This commit is contained in:
61
js/src/ui/Form/DappUrlInput/dappUrlInput.js
Normal file
61
js/src/ui/Form/DappUrlInput/dappUrlInput.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import keycode from 'keycode';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
export default class DappUrlInput extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onGoto: PropTypes.func.isRequired,
|
||||
onRestore: PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, url } = this.props;
|
||||
|
||||
return (
|
||||
<input
|
||||
className={ className }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this.onKeyDown }
|
||||
type='text'
|
||||
value={ url }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onChange = (event) => {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
onKeyDown = (event) => {
|
||||
switch (keycode(event)) {
|
||||
case 'esc':
|
||||
this.props.onRestore();
|
||||
break;
|
||||
|
||||
case 'enter':
|
||||
this.props.onGoto();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
70
js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js
Normal file
70
js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DappUrlInput from './';
|
||||
|
||||
let component;
|
||||
let onChange;
|
||||
let onGoto;
|
||||
let onRestore;
|
||||
|
||||
function render (props = { url: 'http://some.url' }) {
|
||||
onChange = sinon.stub();
|
||||
onGoto = sinon.stub();
|
||||
onRestore = sinon.stub();
|
||||
|
||||
component = shallow(
|
||||
<DappUrlInput
|
||||
onChange={ onChange }
|
||||
onGoto={ onGoto }
|
||||
onRestore={ onRestore }
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('ui/Form/DappUrlInput', () => {
|
||||
it('renders defaults', () => {
|
||||
expect(render()).to.be.ok;
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls the onChange callback as provided', () => {
|
||||
component.simulate('change', { target: { value: 'testing' } });
|
||||
expect(onChange).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onKeyDown', () => {
|
||||
it('calls the onGoto callback on enter', () => {
|
||||
component.simulate('keyDown', { keyCode: 13 });
|
||||
expect(onGoto).to.have.been.called;
|
||||
});
|
||||
|
||||
it('calls the onRestor callback on esc', () => {
|
||||
component.simulate('keyDown', { keyCode: 27 });
|
||||
expect(onRestore).to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
17
js/src/ui/Form/DappUrlInput/index.js
Normal file
17
js/src/ui/Form/DappUrlInput/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default from './dappUrlInput';
|
||||
@@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import AddressSelect from './AddressSelect';
|
||||
import DappUrlInput from './DappUrlInput';
|
||||
import FormWrap from './FormWrap';
|
||||
import Input from './Input';
|
||||
import InputAddress from './InputAddress';
|
||||
@@ -31,6 +32,7 @@ import TypedInput from './TypedInput';
|
||||
export default from './form';
|
||||
export {
|
||||
AddressSelect,
|
||||
DappUrlInput,
|
||||
FormWrap,
|
||||
Input,
|
||||
InputAddress,
|
||||
|
||||
@@ -35,7 +35,7 @@ import DappIcon from './DappIcon';
|
||||
import Editor from './Editor';
|
||||
import Errors from './Errors';
|
||||
import Features, { FEATURES, FeaturesStore } from './Features';
|
||||
import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
||||
import Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
||||
import GasPriceEditor from './GasPriceEditor';
|
||||
import GasPriceSelector from './GasPriceSelector';
|
||||
import Icons from './Icons';
|
||||
@@ -80,8 +80,9 @@ export {
|
||||
ContextProvider,
|
||||
CopyToClipboard,
|
||||
CurrencySymbol,
|
||||
DappIcon,
|
||||
DappCard,
|
||||
DappIcon,
|
||||
DappUrlInput,
|
||||
Editor,
|
||||
Errors,
|
||||
FEATURES,
|
||||
|
||||
60
js/src/util/dapplink.js
Normal file
60
js/src/util/dapplink.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base32 from 'base32.js';
|
||||
|
||||
const BASE_URL = '.web.web3.site';
|
||||
const ENCODER_OPTS = { type: 'crockford' };
|
||||
|
||||
export function encodePath (token, url) {
|
||||
const encoder = new base32.Encoder(ENCODER_OPTS);
|
||||
const chars = `${token}+${url}`
|
||||
.split('')
|
||||
.map((char) => char.charCodeAt(0));
|
||||
|
||||
return encoder
|
||||
.write(chars) // add the characters to encode
|
||||
.finalize(); // create the encoded string
|
||||
}
|
||||
|
||||
export function encodeUrl (token, url) {
|
||||
const encoded = encodePath(token, url)
|
||||
.match(/.{1,63}/g) // split into 63-character chunks, max length is 64 for URLs parts
|
||||
.join('.'); // add '.' between URL parts
|
||||
|
||||
return `${encoded}${BASE_URL}`;
|
||||
}
|
||||
|
||||
// TODO: This export is really more a helper along the way of verifying the actual
|
||||
// encoding (being able to decode test values from the node layer), than meant to
|
||||
// be used as-is. Should the need arrise to decode URLs as well (instead of just
|
||||
// producing), it would make sense to further split the output into the token/URL
|
||||
export function decode (encoded) {
|
||||
const decoder = new base32.Decoder(ENCODER_OPTS);
|
||||
const sanitized = encoded
|
||||
.replace(BASE_URL, '') // remove the BASE URL
|
||||
.split('.') // split the string on the '.' (63-char boundaries)
|
||||
.join(''); // combine without the '.'
|
||||
|
||||
return decoder
|
||||
.write(sanitized) // add the string to decode
|
||||
.finalize() // create the decoded buffer
|
||||
.toString(); // create string from buffer
|
||||
}
|
||||
|
||||
export {
|
||||
BASE_URL
|
||||
};
|
||||
83
js/src/util/dapplink.spec.js
Normal file
83
js/src/util/dapplink.spec.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { BASE_URL, decode, encodePath, encodeUrl } from './dapplink';
|
||||
|
||||
const TEST_TOKEN = 'token';
|
||||
const TEST_URL = 'https://parity.io';
|
||||
const TEST_URL_LONG = 'http://some.very.very.very.long.long.long.domain.example.com';
|
||||
const TEST_PREFIX = 'EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY';
|
||||
const TEST_PREFIX_LONG = [
|
||||
'EHQPPSBE5DM78X3G78QJYWVFDNJJWXK5E9WJWXK5E9WJWXK5E9WJWV3FDSKJWV3', 'FDSKJWV3FDSKJWS3FDNGPJVHECNW62VBGDHJJWRVFDM'
|
||||
].join('.');
|
||||
const TEST_RESULT = `${TEST_PREFIX}${BASE_URL}`;
|
||||
const TEST_ENCODED = `${TEST_TOKEN}+${TEST_URL}`;
|
||||
|
||||
describe('util/ethlink', () => {
|
||||
describe('decode', () => {
|
||||
it('decodes into encoded url', () => {
|
||||
expect(decode(TEST_PREFIX)).to.equal(TEST_ENCODED);
|
||||
});
|
||||
|
||||
it('decodes full into encoded url', () => {
|
||||
expect(decode(TEST_RESULT)).to.equal(TEST_ENCODED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodePath', () => {
|
||||
it('encodes a url/token combination', () => {
|
||||
expect(encodePath(TEST_TOKEN, TEST_URL)).to.equal(TEST_PREFIX);
|
||||
});
|
||||
|
||||
it('changes when token changes', () => {
|
||||
expect(encodePath('test-token-2', TEST_URL)).not.to.equal(TEST_PREFIX);
|
||||
});
|
||||
|
||||
it('changes when url changes', () => {
|
||||
expect(encodePath(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_PREFIX);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeUrl', () => {
|
||||
it('encodes a url/token combination', () => {
|
||||
expect(encodeUrl(TEST_TOKEN, TEST_URL)).to.equal(TEST_RESULT);
|
||||
});
|
||||
|
||||
it('changes when token changes', () => {
|
||||
expect(encodeUrl('test-token-2', TEST_URL)).not.to.equal(TEST_RESULT);
|
||||
});
|
||||
|
||||
it('changes when url changes', () => {
|
||||
expect(encodeUrl(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_RESULT);
|
||||
});
|
||||
|
||||
describe('splitting', () => {
|
||||
let encoded;
|
||||
|
||||
beforeEach(() => {
|
||||
encoded = encodeUrl(TEST_TOKEN, TEST_URL_LONG);
|
||||
});
|
||||
|
||||
it('splits long values into boundary parts', () => {
|
||||
expect(encoded).to.equal(`${TEST_PREFIX_LONG}${BASE_URL}`);
|
||||
});
|
||||
|
||||
it('first part 63 characters', () => {
|
||||
expect(encoded.split('.')[0].length).to.equal(63);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,101 +14,62 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import Refresh from 'material-ui/svg-icons/navigation/refresh';
|
||||
import Close from 'material-ui/svg-icons/navigation/close';
|
||||
import Subdirectory from 'material-ui/svg-icons/navigation/subdirectory-arrow-left';
|
||||
|
||||
import { Button } from '~/ui';
|
||||
|
||||
const KEY_ESC = 27;
|
||||
const KEY_ENTER = 13;
|
||||
import { Button, DappUrlInput } from '~/ui';
|
||||
import { CloseIcon, RefreshIcon } from '~/ui/Icons';
|
||||
|
||||
@observer
|
||||
export default class AddressBar extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRefresh: PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired
|
||||
store: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
currentUrl: this.props.url
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.url === nextProps.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
currentUrl: nextProps.url
|
||||
});
|
||||
}
|
||||
|
||||
isPristine () {
|
||||
return this.state.currentUrl === this.props.url;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isLoading } = this.props;
|
||||
const { currentUrl } = this.state;
|
||||
const isPristine = this.isPristine();
|
||||
const { isLoading, isPristine, nextUrl } = this.props.store;
|
||||
|
||||
return (
|
||||
<div className={ this.props.className }>
|
||||
<Button
|
||||
disabled={ isLoading }
|
||||
onClick={ this.onRefreshUrl }
|
||||
icon={
|
||||
isLoading
|
||||
? <Close />
|
||||
: <Refresh />
|
||||
? <CloseIcon />
|
||||
: <RefreshIcon />
|
||||
}
|
||||
onClick={ this.onGo }
|
||||
/>
|
||||
<input
|
||||
onChange={ this.onUpdateUrl }
|
||||
onKeyDown={ this.onKey }
|
||||
type='text'
|
||||
value={ currentUrl }
|
||||
<DappUrlInput
|
||||
onChange={ this.onChangeUrl }
|
||||
onGoto={ this.onGotoUrl }
|
||||
onRestore={ this.onRestoreUrl }
|
||||
url={ nextUrl }
|
||||
/>
|
||||
<Button
|
||||
disabled={ isPristine }
|
||||
onClick={ this.onGotoUrl }
|
||||
icon={ <Subdirectory /> }
|
||||
onClick={ this.onGo }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onUpdateUrl = (ev) => {
|
||||
this.setState({
|
||||
currentUrl: ev.target.value
|
||||
});
|
||||
};
|
||||
onRefreshUrl = () => {
|
||||
this.props.store.reload();
|
||||
}
|
||||
|
||||
onKey = (ev) => {
|
||||
const key = ev.which;
|
||||
onChangeUrl = (url) => {
|
||||
this.props.store.setNextUrl(url);
|
||||
}
|
||||
|
||||
if (key === KEY_ESC) {
|
||||
this.setState({
|
||||
currentUrl: this.props.url
|
||||
});
|
||||
return;
|
||||
}
|
||||
onGotoUrl = () => {
|
||||
this.props.store.gotoUrl();
|
||||
}
|
||||
|
||||
if (key === KEY_ENTER) {
|
||||
this.onGo();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onGo = () => {
|
||||
if (this.isPristine()) {
|
||||
this.props.onRefresh();
|
||||
} else {
|
||||
this.props.onChange(this.state.currentUrl);
|
||||
}
|
||||
};
|
||||
onRestoreUrl = () => {
|
||||
this.props.store.restoreUrl();
|
||||
}
|
||||
}
|
||||
|
||||
48
js/src/views/Web/AddressBar/addressBar.spec.js
Normal file
48
js/src/views/Web/AddressBar/addressBar.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import AddressBar from './';
|
||||
|
||||
let component;
|
||||
let store;
|
||||
|
||||
function createStore () {
|
||||
store = {
|
||||
nextUrl: 'https://parity.io'
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function render (props = {}) {
|
||||
component = shallow(
|
||||
<AddressBar
|
||||
className='testClass'
|
||||
store={ createStore() }
|
||||
/>
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('views/Web/AddressBar', () => {
|
||||
it('renders defaults', () => {
|
||||
expect(render()).to.be.ok;
|
||||
});
|
||||
});
|
||||
158
js/src/views/Web/store.js
Normal file
158
js/src/views/Web/store.js
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { action, computed, observable, transaction } from 'mobx';
|
||||
import localStore from 'store';
|
||||
import { parse as parseUrl } from 'url';
|
||||
|
||||
import { encodePath, encodeUrl } from '~/util/dapplink';
|
||||
|
||||
const DEFAULT_URL = 'https://mkr.market';
|
||||
const LS_LAST_ADDRESS = '_parity::webLastAddress';
|
||||
|
||||
const hasProtocol = /^https?:\/\//;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class Store {
|
||||
@observable counter = Date.now();
|
||||
@observable currentUrl = null;
|
||||
@observable history = [];
|
||||
@observable isLoading = false;
|
||||
@observable parsedUrl = null;
|
||||
@observable nextUrl = null;
|
||||
@observable token = null;
|
||||
|
||||
constructor (api) {
|
||||
this._api = api;
|
||||
|
||||
this.nextUrl = this.currentUrl = this.loadLastUrl();
|
||||
}
|
||||
|
||||
@computed get encodedPath () {
|
||||
return `${this._api.dappsUrl}/web/${encodePath(this.token, this.currentUrl)}?t=${this.counter}`;
|
||||
}
|
||||
|
||||
@computed get encodedUrl () {
|
||||
return `http://${encodeUrl(this.token, this.currentUrl)}:${this._api.dappsPort}?t=${this.counter}`;
|
||||
}
|
||||
|
||||
@computed get frameId () {
|
||||
return `_web_iframe_${this.counter}`;
|
||||
}
|
||||
|
||||
@computed get isPristine () {
|
||||
return this.currentUrl === this.nextUrl;
|
||||
}
|
||||
|
||||
@action gotoUrl = (_url) => {
|
||||
transaction(() => {
|
||||
let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
|
||||
|
||||
if (!hasProtocol.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
this.setNextUrl(url);
|
||||
this.setCurrentUrl(this.nextUrl);
|
||||
});
|
||||
}
|
||||
|
||||
@action reload = () => {
|
||||
transaction(() => {
|
||||
this.setLoading(true);
|
||||
this.counter = Date.now();
|
||||
});
|
||||
}
|
||||
|
||||
@action restoreUrl = () => {
|
||||
this.setNextUrl(this.currentUrl);
|
||||
}
|
||||
|
||||
@action setHistory = (history) => {
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
@action setLoading = (isLoading) => {
|
||||
this.isLoading = isLoading;
|
||||
}
|
||||
|
||||
@action setToken = (token) => {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@action setCurrentUrl = (_url) => {
|
||||
const url = _url || this.currentUrl;
|
||||
|
||||
transaction(() => {
|
||||
this.currentUrl = url;
|
||||
this.parsedUrl = parseUrl(url);
|
||||
|
||||
this.saveLastUrl();
|
||||
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
@action setNextUrl = (url) => {
|
||||
this.nextUrl = url;
|
||||
}
|
||||
|
||||
generateToken = () => {
|
||||
this.setToken(null);
|
||||
|
||||
return this._api.signer
|
||||
.generateWebProxyAccessToken()
|
||||
.then((token) => {
|
||||
this.setToken(token);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('generateToken', error);
|
||||
});
|
||||
}
|
||||
|
||||
loadHistory = () => {
|
||||
return this._api.parity
|
||||
.listRecentDapps()
|
||||
.then((apps) => {
|
||||
this.setHistory(apps);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('loadHistory', error);
|
||||
});
|
||||
}
|
||||
|
||||
loadLastUrl = () => {
|
||||
return localStore.get(LS_LAST_ADDRESS) || DEFAULT_URL;
|
||||
}
|
||||
|
||||
saveLastUrl = () => {
|
||||
return localStore.set(LS_LAST_ADDRESS, this.currentUrl);
|
||||
}
|
||||
|
||||
static get (api) {
|
||||
if (!instance) {
|
||||
instance = new Store(api);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DEFAULT_URL,
|
||||
LS_LAST_ADDRESS
|
||||
};
|
||||
202
js/src/views/Web/store.spec.js
Normal file
202
js/src/views/Web/store.spec.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Store from './store';
|
||||
|
||||
const TEST_HISTORY = ['somethingA', 'somethingB'];
|
||||
const TEST_TOKEN = 'testing-123';
|
||||
const TEST_URL1 = 'http://some.test.domain.com';
|
||||
const TEST_URL2 = 'http://something.different.com';
|
||||
|
||||
let api;
|
||||
let store;
|
||||
|
||||
function createApi () {
|
||||
api = {
|
||||
dappsPort: 8080,
|
||||
dappsUrl: 'http://home.web3.site:8080',
|
||||
parity: {
|
||||
listRecentDapps: sinon.stub().resolves(TEST_HISTORY)
|
||||
},
|
||||
signer: {
|
||||
generateWebProxyAccessToken: sinon.stub().resolves(TEST_TOKEN)
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function create () {
|
||||
store = new Store(createApi());
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
describe('views/Web/Store', () => {
|
||||
beforeEach(() => {
|
||||
create();
|
||||
});
|
||||
|
||||
describe('@action', () => {
|
||||
describe('gotoUrl', () => {
|
||||
it('uses the nextUrl when none specified', () => {
|
||||
store.setNextUrl('https://parity.io');
|
||||
store.gotoUrl();
|
||||
|
||||
expect(store.currentUrl).to.equal('https://parity.io');
|
||||
});
|
||||
|
||||
it('adds https when no protocol', () => {
|
||||
store.gotoUrl('google.com');
|
||||
|
||||
expect(store.currentUrl).to.equal('https://google.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreUrl', () => {
|
||||
it('sets the nextUrl to the currentUrl', () => {
|
||||
store.setCurrentUrl(TEST_URL1);
|
||||
store.setNextUrl(TEST_URL2);
|
||||
store.restoreUrl();
|
||||
|
||||
expect(store.nextUrl).to.equal(TEST_URL1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCurrentUrl', () => {
|
||||
beforeEach(() => {
|
||||
store.setCurrentUrl(TEST_URL1);
|
||||
});
|
||||
|
||||
it('sets the url', () => {
|
||||
expect(store.currentUrl).to.equal(TEST_URL1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setHistory', () => {
|
||||
it('sets the history', () => {
|
||||
store.setHistory(TEST_HISTORY);
|
||||
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLoading', () => {
|
||||
beforeEach(() => {
|
||||
store.setLoading(true);
|
||||
});
|
||||
|
||||
it('sets the loading state (true)', () => {
|
||||
expect(store.isLoading).to.be.true;
|
||||
});
|
||||
|
||||
it('sets the loading state (false)', () => {
|
||||
store.setLoading(false);
|
||||
|
||||
expect(store.isLoading).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('setNextUrl', () => {
|
||||
it('sets the url', () => {
|
||||
store.setNextUrl(TEST_URL1);
|
||||
|
||||
expect(store.nextUrl).to.equal(TEST_URL1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setToken', () => {
|
||||
it('sets the token', () => {
|
||||
store.setToken(TEST_TOKEN);
|
||||
|
||||
expect(store.token).to.equal(TEST_TOKEN);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@computed', () => {
|
||||
describe('encodedUrl', () => {
|
||||
describe('encodedPath', () => {
|
||||
it('encodes current', () => {
|
||||
store.setCurrentUrl(TEST_URL1);
|
||||
expect(store.encodedPath).to.match(
|
||||
/http:\/\/home\.web3\.site:8080\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('encodes current', () => {
|
||||
store.setCurrentUrl(TEST_URL1);
|
||||
expect(store.encodedUrl).to.match(
|
||||
/^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8080\?t=[0-9]*$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frameId', () => {
|
||||
it('creates an id', () => {
|
||||
expect(store.frameId).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPristine', () => {
|
||||
it('is true when current === next', () => {
|
||||
store.setCurrentUrl(TEST_URL1);
|
||||
store.setNextUrl(TEST_URL1);
|
||||
|
||||
expect(store.isPristine).to.be.true;
|
||||
});
|
||||
|
||||
it('is false when current !== next', () => {
|
||||
store.setCurrentUrl(TEST_URL1);
|
||||
store.setNextUrl(TEST_URL2);
|
||||
|
||||
expect(store.isPristine).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('operations', () => {
|
||||
describe('generateToken', () => {
|
||||
beforeEach(() => {
|
||||
return store.generateToken();
|
||||
});
|
||||
|
||||
it('calls signer_generateWebProxyAccessToken', () => {
|
||||
expect(api.signer.generateWebProxyAccessToken).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('sets the token as retrieved', () => {
|
||||
expect(store.token).to.equal(TEST_TOKEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadHistory', () => {
|
||||
beforeEach(() => {
|
||||
return store.loadHistory();
|
||||
});
|
||||
|
||||
it('calls parity_listRecentDapps', () => {
|
||||
expect(api.parity.listRecentDapps).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('sets the history as retrieved', () => {
|
||||
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,19 +14,16 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import store from 'store';
|
||||
import { parse as parseUrl, format as formatUrl } from 'url';
|
||||
import { parse as parseQuery } from 'querystring';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import AddressBar from './AddressBar';
|
||||
import Store from './store';
|
||||
|
||||
import styles from './web.css';
|
||||
|
||||
const LS_LAST_ADDRESS = '_parity::webLastAddress';
|
||||
|
||||
const hasProtocol = /^https?:\/\//;
|
||||
|
||||
@observer
|
||||
export default class Web extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
@@ -36,120 +33,62 @@ export default class Web extends Component {
|
||||
params: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
displayedUrl: null,
|
||||
isLoading: true,
|
||||
token: null,
|
||||
url: null
|
||||
};
|
||||
store = Store.get(this.context.api);
|
||||
|
||||
componentDidMount () {
|
||||
const { api } = this.context;
|
||||
const { params } = this.props;
|
||||
|
||||
api
|
||||
.signer
|
||||
.generateWebProxyAccessToken()
|
||||
.then((token) => {
|
||||
this.setState({ token });
|
||||
});
|
||||
|
||||
this.setUrl(params.url);
|
||||
this.store.gotoUrl(this.props.params.url);
|
||||
return this.store.generateToken();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this.setUrl(props.params.url);
|
||||
this.store.gotoUrl(props.params.url);
|
||||
}
|
||||
|
||||
setUrl = (url) => {
|
||||
url = url || store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
|
||||
if (!hasProtocol.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
this.setState({ url, displayedUrl: url });
|
||||
};
|
||||
|
||||
render () {
|
||||
const { displayedUrl, isLoading, token } = this.state;
|
||||
const { currentUrl, token } = this.store;
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className={ styles.wrapper }>
|
||||
<h1 className={ styles.loading }>
|
||||
Requesting access token...
|
||||
<FormattedMessage
|
||||
id='web.requestToken'
|
||||
defaultMessage='Requesting access token...'
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { dappsUrl } = this.context.api;
|
||||
const { url } = this.state;
|
||||
return currentUrl
|
||||
? this.renderFrame()
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!url || !token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = parseUrl(url);
|
||||
const { protocol, host, path } = parsed;
|
||||
const address = `${dappsUrl}/web/${token}/${protocol.slice(0, -1)}/${host}${path}`;
|
||||
renderFrame () {
|
||||
const { encodedPath, frameId } = this.store;
|
||||
|
||||
return (
|
||||
<div className={ styles.wrapper }>
|
||||
<AddressBar
|
||||
className={ styles.url }
|
||||
isLoading={ isLoading }
|
||||
onChange={ this.onUrlChange }
|
||||
onRefresh={ this.onRefresh }
|
||||
url={ displayedUrl }
|
||||
store={ this.store }
|
||||
/>
|
||||
<iframe
|
||||
className={ styles.frame }
|
||||
frameBorder={ 0 }
|
||||
name={ name }
|
||||
id={ frameId }
|
||||
name={ frameId }
|
||||
onLoad={ this.iframeOnLoad }
|
||||
sandbox='allow-forms allow-same-origin allow-scripts'
|
||||
scrolling='auto'
|
||||
src={ address }
|
||||
src={ encodedPath }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onUrlChange = (url) => {
|
||||
if (!hasProtocol.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
store.set(LS_LAST_ADDRESS, url);
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
displayedUrl: url,
|
||||
url: url
|
||||
});
|
||||
};
|
||||
|
||||
onRefresh = () => {
|
||||
const { displayedUrl } = this.state;
|
||||
|
||||
// Insert timestamp
|
||||
// This is a hack to prevent caching.
|
||||
const parsed = parseUrl(displayedUrl);
|
||||
|
||||
parsed.query = parseQuery(parsed.query);
|
||||
parsed.query.t = Date.now().toString();
|
||||
delete parsed.search;
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
url: formatUrl(parsed)
|
||||
});
|
||||
};
|
||||
|
||||
iframeOnLoad = () => {
|
||||
this.setState({
|
||||
isLoading: false
|
||||
});
|
||||
this.store.setLoading(false);
|
||||
};
|
||||
}
|
||||
|
||||
56
js/src/views/Web/web.spec.js
Normal file
56
js/src/views/Web/web.spec.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Web from './';
|
||||
|
||||
const TEST_URL = 'https://mkr.market';
|
||||
|
||||
let api;
|
||||
let component;
|
||||
|
||||
function createApi () {
|
||||
api = {};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function render (url = TEST_URL) {
|
||||
component = shallow(
|
||||
<Web params={ { url } } />,
|
||||
{
|
||||
context: { api: createApi() }
|
||||
}
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('views/Web', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
it('renders loading with no token', () => {
|
||||
expect(component.find('FormattedMessage').props().id).to.equal('web.requestToken');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user