diff --git a/pkg/btc-wallet/config/webpack.dev.js b/pkg/btc-wallet/config/webpack.dev.js
index 368de77177..532f849740 100644
--- a/pkg/btc-wallet/config/webpack.dev.js
+++ b/pkg/btc-wallet/config/webpack.dev.js
@@ -1,6 +1,6 @@
const path = require('path');
const webpack = require('webpack');
-// const HtmlWebpackPlugin = require('html-webpack-plugin');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const urbitrc = require('./urbitrc');
const fs = require('fs-extra');
@@ -33,6 +33,7 @@ let devServer = {
host: '0.0.0.0',
disableHostCheck: true,
historyApiFallback: true,
+ publicPath: '/apps/bitcoin/',
};
const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`);
@@ -40,22 +41,19 @@ const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:
if(urbitrc.URL) {
devServer = {
...devServer,
- index: '',
- proxy: {
- '/~btc/js/bundle/index.*.js': {
- target: 'http://localhost:9000',
- pathRewrite: (req, path) => {
- return '/index.js'
+ index: 'index.html',
+ proxy: [{
+ target: 'http://localhost:9000',
+ changeOrigin: true,
+ target: urbitrc.URL,
+ router,
+ context: path => {
+ if(path === '/apps/bitcoin/desk.js') {
+ return true;
}
- },
- '**': {
- changeOrigin: true,
- target: urbitrc.URL,
- router,
- // ensure proxy doesn't timeout channels
- proxyTimeout: 0
+ return !path.startsWith('/apps/bitcoin')
}
- }
+ }]
};
}
@@ -107,7 +105,12 @@ module.exports = {
devtool: 'inline-source-map',
devServer: devServer,
plugins: [
- new UrbitShipPlugin(urbitrc)
+ new UrbitShipPlugin(urbitrc),
+ new HtmlWebpackPlugin({
+ title: 'Bitcoin Wallet',
+ template: './public/index.html'
+ })
+
],
watch: true,
watchOptions: {
@@ -118,7 +121,7 @@ module.exports = {
filename: 'index.js',
chunkFilename: 'index.js',
path: path.resolve(__dirname, '../dist'),
- publicPath: '/',
+ publicPath: '/apps/bitcoin/',
globalObject: 'this'
},
optimization: {
diff --git a/pkg/btc-wallet/config/webpack.prod.js b/pkg/btc-wallet/config/webpack.prod.js
index ed7403784e..7f184e5473 100644
--- a/pkg/btc-wallet/config/webpack.prod.js
+++ b/pkg/btc-wallet/config/webpack.prod.js
@@ -1,5 +1,6 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
// const urbitrc = require('./urbitrc');
module.exports = {
@@ -44,14 +45,18 @@ module.exports = {
},
devtool: 'source-map',
plugins: [
- new CleanWebpackPlugin()
+ new CleanWebpackPlugin(),
+ new HtmlWebpackPlugin({
+ title: 'Bitcoin Wallet',
+ template: './public/index.html'
+ })
],
output: {
filename: (pathData) => {
return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js';
},
- path: path.resolve(__dirname, `../../arvo/app/btc-wallet/js/bundle`),
- publicPath: '/',
+ path: path.resolve(__dirname, 'dist'),
+ publicPath: '/apps/bitcoin/',
},
optimization: {
minimize: true,
diff --git a/pkg/btc-wallet/public/index.html b/pkg/btc-wallet/public/index.html
new file mode 100644
index 0000000000..059cc631d4
--- /dev/null
+++ b/pkg/btc-wallet/public/index.html
@@ -0,0 +1,30 @@
+
+
+
+ Wallet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/btc-wallet/src/index.js b/pkg/btc-wallet/src/index.js
index d4852ef264..5aa431a923 100644
--- a/pkg/btc-wallet/src/index.js
+++ b/pkg/btc-wallet/src/index.js
@@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from './js/components/root.js';
import { api } from './js/api.js';
-import { subscription } from "./js/subscription.js";
+import Channel from './js/channel';
import './css/indigo-static.css';
import './css/fonts.css';
@@ -10,7 +10,7 @@ import './css/custom.css';
// rebuild x3
-const channel = new window.channel();
+const channel = new Channel();
api.setChannel(window.ship, channel);
diff --git a/pkg/btc-wallet/src/js/channel.js b/pkg/btc-wallet/src/js/channel.js
new file mode 100644
index 0000000000..48d7e62ef4
--- /dev/null
+++ b/pkg/btc-wallet/src/js/channel.js
@@ -0,0 +1,290 @@
+export default class Channel {
+ constructor() {
+ this.init();
+ this.deleteOnUnload();
+
+ // a way to handle channel errors
+ //
+ //
+ this.onChannelError = (err) => {
+ console.error('event source error: ', err);
+ };
+ this.onChannelOpen = (e) => {
+ console.log('open', e);
+ };
+ }
+
+ init() {
+ this.debounceInterval = 500;
+ // unique identifier: current time and random number
+ //
+ this.uid =
+ new Date().getTime().toString() +
+ "-" +
+ Math.random().toString(16).slice(-6);
+
+ this.requestId = 1;
+
+ // the currently connected EventSource
+ //
+ this.eventSource = null;
+
+ // the id of the last EventSource event we received
+ //
+ this.lastEventId = 0;
+
+ // this last event id acknowledgment sent to the server
+ //
+ this.lastAcknowledgedEventId = 0;
+
+ // a registry of requestId to successFunc/failureFunc
+ //
+ // These functions are registered during a +poke and are executed
+ // in the onServerEvent()/onServerError() callbacks. Only one of
+ // the functions will be called, and the outstanding poke will be
+ // removed after calling the success or failure function.
+ //
+
+ this.outstandingPokes = new Map();
+
+ // a registry of requestId to subscription functions.
+ //
+ // These functions are registered during a +subscribe and are
+ // executed in the onServerEvent()/onServerError() callbacks. The
+ // event function will be called whenever a new piece of data on this
+ // subscription is available, which may be 0, 1, or many times. The
+ // disconnect function may be called exactly once.
+ //
+ this.outstandingSubscriptions = new Map();
+
+ this.outstandingJSON = [];
+
+ this.debounceTimer = null;
+ }
+
+ resetDebounceTimer() {
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = null;
+ }
+ this.debounceTimer = setTimeout(() => {
+ this.sendJSONToChannel();
+ }, this.debounceInterval)
+ }
+
+ setOnChannelError(onError = (err) => {}) {
+ this.onChannelError = onError;
+ }
+
+ setOnChannelOpen(onOpen = (e) => {}) {
+ this.onChannelOpen = onOpen;
+ }
+
+ deleteOnUnload() {
+ window.addEventListener("beforeunload", (event) => {
+ this.delete();
+ });
+ }
+
+ clearQueue() {
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = null;
+ this.sendJSONToChannel();
+ }
+
+ // sends a poke to an app on an urbit ship
+ //
+ poke(ship, app, mark, json, successFunc, failureFunc) {
+ let id = this.nextId();
+ this.outstandingPokes.set(
+ id,
+ {
+ success: successFunc,
+ fail: failureFunc
+ }
+ );
+
+ const j = {
+ id,
+ action: "poke",
+ ship,
+ app,
+ mark,
+ json
+ };
+
+ this.sendJSONToChannel(j);
+ }
+
+ // subscribes to a path on an specific app and ship.
+ //
+ // Returns a subscription id, which is the same as the same internal id
+ // passed to your Urbit.
+ subscribe(
+ ship,
+ app,
+ path,
+ connectionErrFunc = () => {},
+ eventFunc = () => {},
+ quitFunc = () => {},
+ subAckFunc = () => {},
+ ) {
+ let id = this.nextId();
+ this.outstandingSubscriptions.set(
+ id,
+ {
+ err: connectionErrFunc,
+ event: eventFunc,
+ quit: quitFunc,
+ subAck: subAckFunc
+ }
+ );
+
+ const json = {
+ id,
+ action: "subscribe",
+ ship,
+ app,
+ path
+ }
+
+ this.resetDebounceTimer();
+
+ this.outstandingJSON.push(json);
+ return id;
+ }
+
+ // quit the channel
+ //
+ delete() {
+ let id = this.nextId();
+ clearInterval(this.ackTimer);
+ navigator.sendBeacon(this.channelURL(), JSON.stringify([{
+ id,
+ action: "delete"
+ }]));
+ if (this.eventSource) {
+ this.eventSource.close();
+ }
+ }
+
+ // unsubscribe to a specific subscription
+ //
+ unsubscribe(subscription) {
+ let id = this.nextId();
+ this.sendJSONToChannel({
+ id,
+ action: "unsubscribe",
+ subscription
+ });
+ }
+
+ // sends a JSON command command to the server.
+ //
+ sendJSONToChannel(j) {
+ let req = new XMLHttpRequest();
+ req.open("PUT", this.channelURL());
+ req.setRequestHeader("Content-Type", "application/json");
+
+ if (this.lastEventId == this.lastAcknowledgedEventId) {
+ if (j) {
+ this.outstandingJSON.push(j);
+ }
+
+ if (this.outstandingJSON.length > 0) {
+ let x = JSON.stringify(this.outstandingJSON);
+ req.send(x);
+ }
+ } else {
+ // we add an acknowledgment to clear the server side queue
+ //
+ // The server side puts messages it sends us in a queue until we
+ // acknowledge that we received it.
+ //
+ let payload = [
+ ...this.outstandingJSON,
+ {action: "ack", "event-id": this.lastEventId}
+ ];
+ if (j) {
+ payload.push(j)
+ }
+ let x = JSON.stringify(payload);
+ req.send(x);
+
+ this.lastAcknowledgedEventId = this.lastEventId;
+ }
+ this.outstandingJSON = [];
+
+ this.connectIfDisconnected();
+ }
+
+ // connects to the EventSource if we are not currently connected
+ //
+ connectIfDisconnected() {
+ if (this.eventSource) {
+ return;
+ }
+
+ this.eventSource = new EventSource(this.channelURL(), {withCredentials:true});
+ this.eventSource.onmessage = e => {
+ this.lastEventId = parseInt(e.lastEventId, 10);
+
+ let obj = JSON.parse(e.data);
+ let pokeFuncs = this.outstandingPokes.get(obj.id);
+ let subFuncs = this.outstandingSubscriptions.get(obj.id);
+
+ if (obj.response == "poke" && !!pokeFuncs) {
+ let funcs = pokeFuncs;
+ if (obj.hasOwnProperty("ok")) {
+ funcs["success"]();
+ } else if (obj.hasOwnProperty("err")) {
+ funcs["fail"](obj.err);
+ } else {
+ console.error("Invalid poke response: ", obj);
+ }
+ this.outstandingPokes.delete(obj.id);
+
+ } else if (obj.response == "subscribe" ||
+ (obj.response == "poke" && !!subFuncs)) {
+ let funcs = subFuncs;
+
+ if (obj.hasOwnProperty("err")) {
+ funcs["err"](obj.err);
+ this.outstandingSubscriptions.delete(obj.id);
+ } else if (obj.hasOwnProperty("ok")) {
+ funcs["subAck"](obj);
+ }
+ } else if (obj.response == "diff") {
+ // ensure we ack before channel clogs
+ if((this.lastEventId - this.lastAcknowledgedEventId) > 30) {
+ this.clearQueue();
+ }
+
+ let funcs = subFuncs;
+ funcs["event"](obj.json);
+ } else if (obj.response == "quit") {
+ let funcs = subFuncs;
+ funcs["quit"](obj);
+ this.outstandingSubscriptions.delete(obj.id);
+ } else {
+ console.log("Unrecognized response: ", e);
+ }
+ }
+
+ this.eventSource.onopen = this.onChannelOpen;
+
+ this.eventSource.onerror = e => {
+ this.delete();
+ this.init();
+ this.onChannelError(e);
+ }
+ }
+
+ channelURL() {
+ return "/~/channel/" + this.uid;
+ }
+
+ nextId() {
+ return this.requestId++;
+ }
+}
diff --git a/pkg/btc-wallet/src/js/components/lib/body.js b/pkg/btc-wallet/src/js/components/lib/body.js
index eff8c8d8ef..0fe1d798d6 100644
--- a/pkg/btc-wallet/src/js/components/lib/body.js
+++ b/pkg/btc-wallet/src/js/components/lib/body.js
@@ -40,7 +40,7 @@ export default class Body extends Component {
} else {
return (
-
+
-
+
+
{loaded ? (
diff --git a/pkg/btc-wallet/src/js/store.js b/pkg/btc-wallet/src/js/store.js
index 40cd3270fe..c10097bcc6 100644
--- a/pkg/btc-wallet/src/js/store.js
+++ b/pkg/btc-wallet/src/js/store.js
@@ -7,7 +7,7 @@ class Store {
constructor() {
this.state = {
loadedBtc: false,
- loadedSettings: false,
+ loadedSettings: true,
loaded: false,
providerPerms: {},
shipWallets: {},
@@ -23,7 +23,7 @@ class Store {
BTC: { last: 1, symbol: 'BTC' }
},
denomination: 'BTC',
- showWarning: true,
+ showWarning: false,
error: '',
broadcastSuccess: false,
};
diff --git a/pkg/btc-wallet/src/js/subscription.js b/pkg/btc-wallet/src/js/subscription.js
index d83f65d92b..10adfe1c68 100644
--- a/pkg/btc-wallet/src/js/subscription.js
+++ b/pkg/btc-wallet/src/js/subscription.js
@@ -19,6 +19,7 @@ export class Subscription {
}
initializeSettings() {
+ return;
let app = 'settings-store';
let path = '/bucket/btc-wallet';