mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 20:21:35 +00:00
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:
parent
473c4de528
commit
7083423189
29
frontend/package-lock.json
generated
29
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
18
frontend/src/utils/webln.ts
Normal file
18
frontend/src/utils/webln.ts
Normal 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
|
||||
}
|
@ -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",
|
||||
|
@ -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ú",
|
||||
|
@ -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":"Вы",
|
||||
|
Loading…
Reference in New Issue
Block a user