BlueWallet/screen/lnd/browser.js

502 lines
16 KiB
JavaScript
Raw Normal View History

2019-01-12 01:23:32 +01:00
import React, { Component } from 'react';
2019-01-19 16:10:23 +01:00
import { TouchableOpacity, ActivityIndicator, View, Platform, Alert, Dimensions } from 'react-native';
2019-01-12 01:23:32 +01:00
import { WebView } from 'react-native-webview';
2019-01-19 16:10:23 +01:00
import WKWebView from 'react-native-wkwebview-reborn';
2019-01-16 05:06:16 +01:00
import { BlueNavigationStyle, SafeBlueArea } from '../../BlueComponents';
2019-01-12 01:23:32 +01:00
import { FormInput } from 'react-native-elements';
import Ionicons from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
2019-01-12 03:08:03 +01:00
const { width } = Dimensions.get('window');
2019-01-12 01:23:32 +01:00
2019-01-12 23:33:02 +01:00
let processedInvoices = {};
let lastTimeTriedToPay = 0;
2019-01-16 17:54:17 +01:00
/// ///////////////////////////////////////////////////////////////////////
// this code has no use in RN, it gets copypasted in webview injected code
//
let bluewalletResponses = {};
// eslint-disable-next-line
2019-01-17 21:34:29 +01:00
var webln = {
2019-01-16 17:54:17 +01:00
enable: function() {
window.postMessage(JSON.stringify({ enable: true }));
2019-01-16 17:54:17 +01:00
return new Promise(function(resolve, reject) {
resolve(true);
});
},
getInfo: function() {
window.postMessage('getInfo');
return new Promise(function(resolve, reject) {
reject(new Error('not implemented'));
});
},
sendPayment: function(paymentRequest) {
window.postMessage(JSON.stringify({ sendPayment: paymentRequest }));
return new Promise(function(resolve, reject) {
2019-01-17 21:34:29 +01:00
/* nop. intentionally, forever hang promise.
lapp page usually asynchroniously checks payment itself, via ajax,
so atm there's no need to pass payment preimage from RN to webview and fullfill promise.
might change in future */
2019-01-16 17:54:17 +01:00
});
},
makeInvoice: function(RequestInvoiceArgs) {
2019-01-17 21:34:29 +01:00
var id = Math.random();
2019-01-16 17:54:17 +01:00
window.postMessage(JSON.stringify({ makeInvoice: RequestInvoiceArgs, id: id }));
return new Promise(function(resolve, reject) {
2019-01-17 21:34:29 +01:00
var interval = setInterval(function() {
2019-01-16 17:54:17 +01:00
if (bluewalletResponses[id]) {
clearInterval(interval);
resolve(bluewalletResponses[id]);
}
}, 1000);
});
},
signMessage: function() {
window.postMessage('signMessage');
return new Promise(function(resolve, reject) {
reject(new Error('not implemented'));
});
},
verifyMessage: function() {
window.postMessage('verifyMessage');
return new Promise(function(resolve, reject) {
reject(new Error('not implemented'));
});
},
};
// end injected code
/// /////////////////
/// /////////////////
let alreadyInjected = false;
const injectedParadise = `
/* rules:
no 'let', only 'var'
no arrow functions
globals without 'var'
should work if compressed to single line
*/
/* this is a storage of responses from OUTER code (react native)
it gets written by message bus handler callback:
webview makes a call through bus to RN, each request with a unique ID.
RN processes the request from the bus, and posts response to the bus, with the same ID.
webview callback handler writes it in this hashmap. Then, some other code that patiently
waits for a response will see that the answer with such ID is present, and will fulfill a promise */
bluewalletResponses = {};
/* this is injected WEBLN provider */
webln = {
enable : function () {
window.postMessage(JSON.stringify({'enable': true}));
return new Promise(function(resolve, reject){
resolve(true);
})
},
getInfo : function () {
window.postMessage('getInfo');
return new Promise(function(resolve, reject){
reject('not implemented');
})
},
sendPayment: function(paymentRequest) {
window.postMessage(JSON.stringify({ sendPayment: paymentRequest }));
return new Promise(function(resolve, reject) {
/* nop. intentionally, forever hang promise.
lapp page usually asynchroniously checks payment itself, via ajax,
so atm there's no need to pass payment preimage from RN to webview and fullfill promise.
might change in future */
});
},
makeInvoice: function (RequestInvoiceArgs) {
var id = Math.random();
window.postMessage(JSON.stringify({makeInvoice: RequestInvoiceArgs, id: id}));
return new Promise(function(resolve, reject) {
var interval = setInterval(function () {
if (bluewalletResponses[id]) {
clearInterval(interval);
resolve(bluewalletResponses[id]);
}
}, 1000);
});
},
signMessage: function () {
window.postMessage('signMessage');
return new Promise(function(resolve, reject){
reject('not implemented');
})
},
verifyMessage: function () {
window.postMessage('verifyMessage');
return new Promise(function(resolve, reject){
reject('not implemented');
})
},
};
/* end WEBLN */
/* listening to events that might come from RN: */
document.addEventListener("message", function(event) {
window.postMessage("inside webview, received post message: " + event.data);
var json;
try {
json = JSON.parse(event.data);
} catch (_) {}
if (json && json.bluewalletResponse) {
/* this is an answer to one of our inside-webview calls.
we store it in answers hashmap for someone who cares about it */
bluewalletResponses[json.id] = json.bluewalletResponse
}
}, false);
function tryToPay(invoice) {
window.postMessage(JSON.stringify({sendPayment:invoice}));
}
/* for non-webln compatible pages we do it oldschool,
searching for all bolt11 manually */
setInterval(function() {
window.postMessage('interval');
var searchText = "lnbc";
var aTags = document.getElementsByTagName("span");
var i;
for (i = 0; i < aTags.length; i++) {
if (aTags[i].textContent.indexOf(searchText) === 0) {
tryToPay(aTags[i].textContent);
}
}
/* ------------------------- */
aTags = document.getElementsByTagName("input");
for (i = 0; i < aTags.length; i++) {
if (aTags[i].value.indexOf(searchText) === 0) {
tryToPay(aTags[i].value);
}
}
/* ------------------------- */
aTags = document.getElementsByTagName("a");
searchText = "lightning:lnbc";
for (i = 0; i < aTags.length; i++) {
var href = aTags[i].getAttribute('href') + '';
if (href.indexOf(searchText) === 0) {
tryToPay(href.replace('lightning:', ''));
}
}
}, 1000);
`;
2019-01-12 01:23:32 +01:00
export default class Browser extends Component {
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: 'Lapp Browser',
headerLeft: null,
});
constructor(props) {
super(props);
2019-01-12 03:08:03 +01:00
if (!props.navigation.getParam('fromSecret')) throw new Error('Invalid param');
2019-01-16 17:54:17 +01:00
if (!props.navigation.getParam('fromWallet')) throw new Error('Invalid param');
2019-01-12 03:08:03 +01:00
2019-01-16 17:54:17 +01:00
this.state = {
url: 'https://bluewallet.io/marketplace/',
pageIsLoading: false,
fromSecret: props.navigation.getParam('fromSecret'),
fromWallet: props.navigation.getParam('fromWallet'),
};
2019-01-12 01:23:32 +01:00
}
2019-01-19 16:10:23 +01:00
renderWebView = () => {
if (Platform.OS === 'android') {
2019-01-19 17:46:11 +01:00
return (
2019-01-19 16:10:23 +01:00
<WebView
2019-01-19 17:46:11 +01:00
ref={ref => (this.webview = ref)}
source={{ uri: this.state.url }}
onMessage={e => {
// this is a handler which receives messages sent from within the browser
console.log('---- message from the bus:', e.nativeEvent.data);
let json = false;
try {
json = JSON.parse(e.nativeEvent.data);
} catch (_) {}
// message from browser has ln invoice
if (json && json.sendPayment) {
// checking that already asked about this invoice:
if (processedInvoices[json.sendPayment]) {
return;
} else {
2019-01-19 21:39:09 +01:00
// checking that we do not trigger alert too often:
if (+new Date() - lastTimeTriedToPay < 3000) {
return;
}
lastTimeTriedToPay = +new Date();
//
2019-01-19 17:46:11 +01:00
processedInvoices[json.sendPayment] = 1;
}
Alert.alert(
'Page',
'This page asks for permission to pay an invoice',
[
{ text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Pay',
onPress: () => {
console.log('OK Pressed');
this.props.navigation.navigate({
routeName: 'ScanLndInvoice',
params: {
uri: json.sendPayment,
fromSecret: this.state.fromSecret,
},
});
},
},
],
{ cancelable: false },
);
2019-01-19 16:10:23 +01:00
}
2019-01-19 17:46:11 +01:00
if (json && json.makeInvoice) {
let amount = Math.max(+json.makeInvoice.minimumAmount, +json.makeInvoice.maximumAmount, +json.makeInvoice.defaultAmount);
Alert.alert(
'Page',
'This page wants to pay you ' + amount + ' sats (' + json.makeInvoice.defaultMemo + ')',
[
{ text: 'No thanks', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Accept',
onPress: async () => {
/** @type {LightningCustodianWallet} */
const fromWallet = this.state.fromWallet;
const payreq = await fromWallet.addInvoice(amount, json.makeInvoice.defaultMemo || ' ');
this.webview.postMessage(JSON.stringify({ bluewalletResponse: { paymentRequest: payreq }, id: json.id }));
},
},
],
{ cancelable: false },
);
2019-01-19 16:10:23 +01:00
}
2019-01-19 17:46:11 +01:00
if (json && json.enable) {
console.log('webln enabled');
this.setState({ weblnEnabled: true });
}
}}
onLoadStart={e => {
alreadyInjected = false;
console.log('load start');
this.setState({ pageIsLoading: true, weblnEnabled: false });
}}
onLoadEnd={e => {
console.log('load end');
this.setState({ url: e.nativeEvent.url, pageIsLoading: false });
}}
onLoadProgress={e => {
console.log('progress:', e.nativeEvent.progress);
if (!alreadyInjected && e.nativeEvent.progress > 0.5) {
this.webview.injectJavaScript(injectedParadise);
alreadyInjected = true;
console.log('injected');
}
}}
/>
);
2019-01-19 16:10:23 +01:00
} else if (Platform.OS === 'ios') {
2019-01-19 17:46:11 +01:00
return (
2019-01-19 16:10:23 +01:00
<WKWebView
2019-01-19 17:46:11 +01:00
ref={ref => (this.webview = ref)}
source={{ uri: this.state.url }}
injectJavaScript={injectedParadise}
onMessage={e => {
// this is a handler which receives messages sent from within the browser
console.log('---- message from the bus:', e.nativeEvent.data);
let json = false;
try {
json = JSON.parse(e.nativeEvent.data);
} catch (_) {}
// message from browser has ln invoice
if (json && json.sendPayment) {
// checking that we do not trigger alert too often:
if (+new Date() - lastTimeTriedToPay < 3000) {
return;
}
lastTimeTriedToPay = +new Date();
// checking that already asked about this invoice:
if (processedInvoices[json.sendPayment]) {
return;
} else {
processedInvoices[json.sendPayment] = 1;
}
Alert.alert(
'Page',
'This page asks for permission to pay an invoice',
[
{ text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Pay',
onPress: () => {
console.log('OK Pressed');
this.props.navigation.navigate({
routeName: 'ScanLndInvoice',
params: {
uri: json.sendPayment,
fromSecret: this.state.fromSecret,
},
});
},
},
],
{ cancelable: false },
);
2019-01-19 16:10:23 +01:00
}
2019-01-19 17:46:11 +01:00
if (json && json.makeInvoice) {
let amount = Math.max(+json.makeInvoice.minimumAmount, +json.makeInvoice.maximumAmount, +json.makeInvoice.defaultAmount);
Alert.alert(
'Page',
'This page wants to pay you ' + amount + ' sats (' + json.makeInvoice.defaultMemo + ')',
[
{ text: 'No thanks', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Accept',
onPress: async () => {
/** @type {LightningCustodianWallet} */
const fromWallet = this.state.fromWallet;
const payreq = await fromWallet.addInvoice(amount, json.makeInvoice.defaultMemo || ' ');
this.webview.postMessage(JSON.stringify({ bluewalletResponse: { paymentRequest: payreq }, id: json.id }));
},
},
],
{ cancelable: false },
);
2019-01-19 16:10:23 +01:00
}
2019-01-19 17:46:11 +01:00
if (json && json.enable) {
console.log('webln enabled');
this.setState({ weblnEnabled: true });
}
}}
onLoadStart={e => {
alreadyInjected = false;
console.log('load start');
this.setState({ pageIsLoading: true, weblnEnabled: false });
}}
onLoadEnd={e => {
console.log('load end');
this.setState({ url: e.nativeEvent.url, pageIsLoading: false });
}}
/>
);
2019-01-19 16:10:23 +01:00
}
2019-01-19 17:46:11 +01:00
};
2019-01-12 01:23:32 +01:00
render() {
return (
2019-01-16 05:06:16 +01:00
<SafeBlueArea>
2019-01-12 01:23:32 +01:00
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
2019-01-12 01:23:32 +01:00
onPress={() => {
this.webview.goBack();
}}
>
<Ionicons
name={'ios-arrow-round-back'}
size={36}
style={{
color: 'red',
backgroundColor: 'transparent',
paddingLeft: 10,
}}
/>
</TouchableOpacity>
2019-01-12 01:23:32 +01:00
<FormInput
inputStyle={{ color: '#0c2550', maxWidth: width - 150, fontSize: 16 }}
2019-01-12 01:23:32 +01:00
containerStyle={{
maxWidth: width - 150,
2019-01-12 01:23:32 +01:00
borderColor: '#d2d2d2',
borderWidth: 0.5,
backgroundColor: '#f5f5f5',
}}
value={this.state.url}
/>
<TouchableOpacity
2019-01-12 01:23:32 +01:00
onPress={() => {
processedInvoices = {};
this.setState({ url: 'https://bluewallet.io/marketplace/' });
2019-01-12 01:23:32 +01:00
}}
>
<Ionicons
name={'ios-home'}
size={36}
style={{
color: this.state.weblnEnabled ? 'green' : 'red',
backgroundColor: 'transparent',
}}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
let reloadUrl = this.state.url;
this.setState({ url: 'about:blank' });
processedInvoices = {};
setTimeout(() => this.setState({ url: reloadUrl }), 500);
// this.webview.reload();
2019-01-12 01:23:32 +01:00
}}
>
{(!this.state.pageIsLoading && (
<Ionicons
name={'ios-sync'}
size={36}
style={{
color: 'red',
backgroundColor: 'transparent',
paddingLeft: 15,
}}
/>
)) || (
<View style={{ paddingLeft: 20 }}>
<ActivityIndicator />
</View>
)}
</TouchableOpacity>
2019-01-12 01:23:32 +01:00
</View>
2019-01-19 17:46:11 +01:00
{this.renderWebView()}
2019-01-16 05:06:16 +01:00
</SafeBlueArea>
2019-01-12 01:23:32 +01:00
);
}
}
Browser.propTypes = {
navigation: PropTypes.shape({
2019-01-12 03:08:03 +01:00
getParam: PropTypes.function,
2019-01-12 01:23:32 +01:00
navigate: PropTypes.func,
}),
};