Add WebLN support (#215)

* Add WebLN support

* Fix Variable Typo

* Invoice Generation

Signed-off-by: KoalaSat <111684255+KoalaSat@users.noreply.github.com>

* Code Review

* Second CR

* Catch cancelations

* Final Review

Signed-off-by: KoalaSat <111684255+KoalaSat@users.noreply.github.com>
This commit is contained in:
KoalaSat 2022-08-25 10:50:48 +02:00 committed by GitHub
parent 473c4de528
commit 7083423189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 192 additions and 6 deletions

View File

@ -2930,6 +2930,14 @@
"@babel/types": "^7.3.0"
}
},
"@types/chrome": {
"version": "0.0.74",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.74.tgz",
"integrity": "sha512-hzosS5CkQcIKCgxcsV2AzbJ36KNxG/Db2YEN/erEu7Boprg+KpMDLBQqKFmSo+JkQMGqRcicUyqCowJpuT+C6A==",
"requires": {
"@types/filesystem": "*"
}
},
"@types/eslint": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
@ -2956,6 +2964,19 @@
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"dev": true
},
"@types/filesystem": {
"version": "0.0.32",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz",
"integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==",
"requires": {
"@types/filewriter": "*"
}
},
"@types/filewriter": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz",
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ=="
},
"@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@ -9338,6 +9359,14 @@
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"webln": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/webln/-/webln-0.3.0.tgz",
"integrity": "sha512-QMLGIQtHzSVwYldhREjJsVGfVZ37q+hkBpi9KiruxI8FJwD0UocshrP9sbtd1H5N96uAAq53aywesy3/sw+YOA==",
"requires": {
"@types/chrome": "^0.0.74"
}
},
"webpack": {
"version": "5.72.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.0.tgz",

View File

@ -65,6 +65,7 @@
"react-world-flags": "^1.4.0",
"reconnecting-websocket": "^4.4.0",
"simple-plist": "^1.3.1",
"webln": "^0.3.0",
"websocket": "^1.0.34"
}
}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link as LinkRouter } from "react-router-dom";
@ -35,6 +35,7 @@ import { UserNinjaIcon, BitcoinIcon } from "../Icons";
import { getCookie } from "../../utils/cookies";
import { copyToClipboard } from "../../utils/clipboard";
import { getWebln } from "../../utils/webln";
type Props = {
isOpen: boolean;
@ -76,6 +77,14 @@ const ProfileDialog = ({
const [rewardInvoice, setRewardInvoice] = useState<string>("");
const [showRewards, setShowRewards] = useState<boolean>(false);
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false)
useEffect(() => {
getWebln()
.then((webln) => {
setWeblnEnabled(webln !== undefined)
})
}, [showRewards])
const copyTokenHandler = () => {
const robotToken = getCookie("robot_token");
@ -90,6 +99,18 @@ const ProfileDialog = ({
copyToClipboard(`http://${host}/ref/${referralCode}`);
};
const handleWeblnInvoiceClicked = async (e: any) =>{
e.preventDefault();
if (earnedRewards) {
const webln = await getWebln();
const invoice = webln.makeInvoice(earnedRewards).then(() => {
if (invoice) {
handleSubmitInvoiceClicked(e, invoice.paymentRequest)
}
})
}
}
return (
<Dialog
open={isOpen}
@ -324,7 +345,6 @@ const ProfileDialog = ({
}}
/>
</Grid>
<Grid item alignItems="stretch" style={{ display: "flex", maxWidth:80}}>
<Button
sx={{maxHeight:38}}
@ -338,6 +358,22 @@ const ProfileDialog = ({
</Button>
</Grid>
</Grid>
{weblnEnabled && (
<Grid container style={{ display: "flex", alignItems: "stretch"}}>
<Grid item alignItems="stretch" style={{ display: "flex", maxWidth:240}}>
<Button
sx={{maxHeight:38, minWidth: 230}}
onClick={(e) => handleWeblnInvoiceClicked(e)}
variant="contained"
color="primary"
size="small"
type="submit"
>
{t("Generate with Webln")}
</Button>
</Grid>
</Grid>
)}
</form>
)}
</ListItem>

View File

@ -19,11 +19,13 @@ import PriceChangeIcon from '@mui/icons-material/PriceChange';
import PaymentsIcon from '@mui/icons-material/Payments';
import ArticleIcon from '@mui/icons-material/Article';
import HourglassTopIcon from '@mui/icons-material/HourglassTop';
import CheckIcon from '@mui/icons-material/Check';
import { SendReceiveIcon } from "./Icons";
import { getCookie } from "../utils/cookies";
import { pn } from "../utils/prettyNumbers";
import { copyToClipboard } from "../utils/clipboard";
import { getWebln } from "../utils/webln";
class OrderPage extends Component {
constructor(props) {
@ -36,6 +38,8 @@ class OrderPage extends Component {
openCancel: false,
openCollaborativeCancel: false,
openInactiveMaker: false,
openWeblnDialog: false,
waitingWebln: false,
openStoreToken: false,
tabValue: 1,
orderId: this.props.match.params.orderId,
@ -92,7 +96,13 @@ class OrderPage extends Component {
this.setState({orderId:id})
fetch('/api/order' + '?order_id=' + id)
.then((response) => response.json())
.then((data) => (this.completeSetState(data) & this.setState({pauseLoading:false})));
.then(this.orderDetailsReceived);
}
orderDetailsReceived = (data) =>{
if (data.status !== this.state.status) { this.handleWebln(data) }
this.completeSetState(data)
this.setState({pauseLoading:false})
}
// These are used to refresh the data
@ -103,7 +113,7 @@ class OrderPage extends Component {
componentDidUpdate() {
clearInterval(this.interval);
this.interval = setInterval(this.tick, this.state.delay);
this.interval = setInterval(this.tick, this.state.delay);
}
componentWillUnmount() {
@ -113,6 +123,48 @@ class OrderPage extends Component {
this.getOrderDetails(this.state.orderId);
}
handleWebln = async (data) => {
const webln = await getWebln();
// If Webln implements locked payments compatibility, this logic might be simplier
if (data.is_maker & data.status == 0) {
webln.sendPayment(data.bond_invoice);
this.setState({ waitingWebln: true, openWeblnDialog: true});
} else if (data.is_taker & data.status == 3) {
webln.sendPayment(data.bond_invoice);
this.setState({ waitingWebln: true, openWeblnDialog: true});
} else if (data.is_seller & (data.status == 6 || data.status == 7 )) {
webln.sendPayment(data.escrow_invoice);
this.setState({ waitingWebln: true, openWeblnDialog: true});
} else if (data.is_buyer & (data.status == 6 || data.status == 8 )) {
this.setState({ waitingWebln: true, openWeblnDialog: true});
webln.makeInvoice(data.trade_satoshis)
.then((invoice) => {
if (invoice) {
this.sendWeblnInvoice(invoice.paymentRequest);
this.setState({ waitingWebln: false, openWeblnDialog: false });
}
}).catch(() => {
this.setState({ waitingWebln: false, openWeblnDialog: false });
});
} else {
this.setState({ waitingWebln: false });
}
}
sendWeblnInvoice = (invoice) => {
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'update_invoice',
'invoice': invoice,
}),
};
fetch('/api/order/' + '?order_id=' + this.state.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.completeSetState(data));
}
// Countdown Renderer callback with condition
countdownRenderer = ({ total, hours, minutes, seconds, completed }) => {
const { t } = this.props;
@ -263,7 +315,7 @@ class OrderPage extends Component {
};
fetch('/api/order/' + '?order_id=' + this.state.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.completeSetState(data));
.then((data) => this.handleWebln(data) & this.completeSetState(data));
}
// set delay to the one matching the order status. If null order status, delay goes to 9999999.
@ -744,6 +796,7 @@ class OrderPage extends Component {
:
(this.state.is_participant ?
<>
{this.weblnDialog()}
{/* Desktop View */}
<MediaQuery minWidth={920}>
{this.doubleOrderPageDesktop()}
@ -761,6 +814,44 @@ class OrderPage extends Component {
)
}
handleCloseWeblnDialog = () => {
this.setState({openWeblnDialog: false});
}
weblnDialog =() =>{
const { t } = this.props;
return(
<Dialog
open={this.state.openWeblnDialog}
onClose={this.handleCloseWeblnDialog}
aria-labelledby="webln-dialog-title"
aria-describedby="webln-dialog-description"
>
<DialogTitle id="webln-dialog-title">
{t("WebLN")}
</DialogTitle>
<DialogContent>
<DialogContentText id="webln-dialog-description">
{this.state.waitingWebln ?
<>
<CircularProgress size={16} thickness={5} style={{ marginRight: 10 }}/>
{this.state.is_buyer ? t("Invoice not received, please check your WebLN wallet.") : t("Payment not received, please check your WebLN wallet.")}
</>
: <>
<CheckIcon color="success"/>
{t("You can close now your WebLN wallet popup.")}
</>
}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleCloseWeblnDialog} autoFocus>{t("Done")}</Button>
</DialogActions>
</Dialog>
)
}
render (){
return (
// Only so nothing shows while requesting the first batch of data

View File

@ -0,0 +1,18 @@
import { requestProvider, WeblnProvider } from "webln";
export const getWebln = async (): Promise<WeblnProvider> => {
const resultPromise = new Promise<WeblnProvider>(async (resolve, reject) => {
try {
const webln = await requestProvider()
if (webln) {
webln.enable()
resolve(webln)
}
} catch (err) {
console.log("Coulnd't connect to Webln")
reject()
}
})
return resultPromise
}

View File

@ -302,6 +302,10 @@
"This order has been cancelled collaborativelly":"This order has been cancelled collaboratively",
"This order is not available":"This order is not available",
"The Robotic Satoshis working in the warehouse did not understand you. Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues":"The Robotic Satoshis working in the warehouse did not understand you. Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues",
"WebLN": "WebLN",
"Payment not received, please check your WebLN wallet.": "Payment not received, please check your WebLN wallet.",
"Invoice not received, please check your WebLN wallet.": "Invoice not received, please check your WebLN wallet.",
"Payment detected, you can close now your WebLN wallet popup.": "Payment detected, you can close now your WebLN wallet popup.",
"CHAT BOX - Chat.js":"Chat Box",
"You":"You",

View File

@ -302,7 +302,10 @@
"This order has been cancelled collaborativelly":"Esta orden se ha cancelado colaborativamente",
"This order is not available": "Esta orden no está disponible",
"The Robotic Satoshis working in the warehouse did not understand you. Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues": "Los Satoshis Robóticos del almacén no te entendieron. Por favor rellena un Bug Issue en Github https://github.com/reckless-satoshi/robosats/issues",
"WebLN": "WebLN",
"Payment not received, please check your WebLN wallet.": "No se ha recibido el pago, echa un vistazo a tu wallet WebLN.",
"Invoice not received, please check your WebLN wallet.": "No se ha recibido la factura, echa un vistazo a tu wallet WebLN.",
"You can close now your WebLN wallet popup.": "Ahora puedes cerrar el popup de tu wallet WebLN.",
"CHAT BOX - Chat.js": "Ventana del chat",
"You": "Tú",

View File

@ -300,6 +300,10 @@
"This order has been cancelled collaborativelly":"Этот ордер был отменён совместно",
"You are not allowed to see this order":"Вы не можете увидеть этот ордер",
"The Robotic Satoshis working in the warehouse did not understand you. Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues":"Роботизированные Сатоши, работающие на складе, не поняли Вас. Пожалуйста, заполните вопрос об ошибке в Github https://github.com/reckless-satoshi/robosats/issues",
"WebLN": "WebLN",
"Payment not received, please check your WebLN wallet.": "Платёж не получен. Пожалуйста, проверьте Ваш WebLN Кошелёк.",
"Invoice not received, please check your WebLN wallet.": "Платёж не получен. Пожалуйста, проверьте Ваш WebLN Кошелёк.",
"You can close now your WebLN wallet popup.": "Вы можете закрыть всплывающее окно WebLN Кошелька",
"CHAT BOX - Chat.js":"Chat Box",
"You":"Вы",