From 7083423189063995e15a904e9738dce1292c3773 Mon Sep 17 00:00:00 2001 From: KoalaSat <111684255+KoalaSat@users.noreply.github.com> Date: Thu, 25 Aug 2022 10:50:48 +0200 Subject: [PATCH] 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> --- frontend/package-lock.json | 29 ++++++ frontend/package.json | 1 + frontend/src/components/Dialogs/Profile.tsx | 40 ++++++++- frontend/src/components/OrderPage.js | 97 ++++++++++++++++++++- frontend/src/utils/webln.ts | 18 ++++ frontend/static/locales/en.json | 4 + frontend/static/locales/es.json | 5 +- frontend/static/locales/ru.json | 4 + 8 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 frontend/src/utils/webln.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8ba7bdee..28336b5b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 8b92c3d1..23d832f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } } diff --git a/frontend/src/components/Dialogs/Profile.tsx b/frontend/src/components/Dialogs/Profile.tsx index 3ba3f51f..a2d4eec8 100644 --- a/frontend/src/components/Dialogs/Profile.tsx +++ b/frontend/src/components/Dialogs/Profile.tsx @@ -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(""); const [showRewards, setShowRewards] = useState(false); const [openClaimRewards, setOpenClaimRewards] = useState(false); + const [weblnEnabled, setWeblnEnabled] = useState(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 ( - + + + )} )} diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 6edb01b1..b844473d 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -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 */} {this.doubleOrderPageDesktop()} @@ -761,6 +814,44 @@ class OrderPage extends Component { ) } + handleCloseWeblnDialog = () => { + this.setState({openWeblnDialog: false}); + } + + weblnDialog =() =>{ + const { t } = this.props; + + return( + + + {t("WebLN")} + + + + {this.state.waitingWebln ? + <> + + {this.state.is_buyer ? t("Invoice not received, please check your WebLN wallet.") : t("Payment not received, please check your WebLN wallet.")} + + : <> + + {t("You can close now your WebLN wallet popup.")} + + } + + + + + + + ) + } + render (){ return ( // Only so nothing shows while requesting the first batch of data diff --git a/frontend/src/utils/webln.ts b/frontend/src/utils/webln.ts new file mode 100644 index 00000000..4d3e3eb7 --- /dev/null +++ b/frontend/src/utils/webln.ts @@ -0,0 +1,18 @@ +import { requestProvider, WeblnProvider } from "webln"; + +export const getWebln = async (): Promise => { + const resultPromise = new Promise(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 +} diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index 13f8595f..b038f568 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -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", diff --git a/frontend/static/locales/es.json b/frontend/static/locales/es.json index 938a20c5..dde31970 100644 --- a/frontend/static/locales/es.json +++ b/frontend/static/locales/es.json @@ -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ú", diff --git a/frontend/static/locales/ru.json b/frontend/static/locales/ru.json index 78aaddbd..cd287755 100644 --- a/frontend/static/locales/ru.json +++ b/frontend/static/locales/ru.json @@ -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":"Вы",