├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── components │ ├── Widget.ce.vue │ └── utils │ │ └── helpers.js ├── main.js └── server │ └── main.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: '@babel/eslint-parser' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Alby contributors (https://getalby.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡ Lightning Widget 2 | 3 | Start accepting Bitcoin payments on your website in minutes. Beautiful widgets, no fees, instant payouts. 4 | 5 | ## Demo 6 | ➡️ [Website](https://widgets.twentyuno.net/) 7 | 8 | ## Configuration options 9 | - `name`: The name being displayed on the widget 10 | - `image`: Path to the image that is displayed on the widget 11 | - `to`: Where the payments should be sent to ([Lightning Address](https://lightningaddress.com/) or [LNURL](https://github.com/fiatjaf/lnurl-rfc)) 12 | - `accent`: An accent color in RGB format to make your widget more colorful (i.e. `#20C997`) 13 | - `button-text`: The text to be displayed on the initial state of the widget (default: `Donate sats`) 14 | - `debug`: Debug mode, validates settings during initialization and displays configuration errors 15 | 16 | **Example** 17 | ``` 18 | 23 | 24 | ``` 25 | 26 | ## 🧑‍💻 Development 27 | 28 | Checkout this repository and run the following commands to spin up a development server: 29 | 30 | ``` 31 | npm install 32 | npm run serve 33 | ``` 34 | 35 | ## ⁉️ Questions, Feedback & Support 36 | 37 | Please feel free to reach out to me if you have any questions & feedback or you just need a little help to integrate the widget into your website. 38 | 39 | ➡️ [Telegram](https://t.me/reneaaron) 40 | 41 | ## Donations 42 | 43 | If this widget is useful for you or you just want to support development please consider a donation. [#value4value](https://twitter.com/search?q=%23value4value&src=typed_query) 44 | 45 | ⚡ reneaaron@getalby.com 46 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightning-widget", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "run": "node src/server/main.js", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "engines": { 12 | "node": ">=16" 13 | }, 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "invoices": "^2.0.4", 17 | "js-confetti": "^0.10.2", 18 | "lnurl-pay": "^0.2.3", 19 | "qrcode": "^1.5.0", 20 | "stream": "^0.0.2", 21 | "vue": "^3.2.32", 22 | "vue-custom-element": "^3.3.0", 23 | "vue-template-compiler": "^2.6.14" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.17.10", 27 | "eslint": "^8.14.0", 28 | "babel-eslint": "^10.1.0", 29 | "@babel/eslint-parser": "^7.17.0", 30 | "@vue/cli-plugin-babel": "~5.0.0", 31 | "@vue/cli-plugin-eslint": "~5.0.0", 32 | "@vue/cli-service": "~5.0.0", 33 | "@vue/compiler-sfc": "^3.2.31", 34 | "eslint-plugin-vue": "^8.7.1", 35 | "webpack-bundle-analyzer": "^4.5.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reneaaron/lightning-widget/4b620b8aad4ddafa895330487c8ed2443a24d29b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ⚡ Widgets 9 | 29 | 30 | 31 | 32 | 35 |

Widget states (default)


36 |
37 | 42 | 48 | 54 | 60 | 66 | 72 |
73 | 74 |

Widget states (light)

75 |
76 | 81 | 87 | 93 | 99 | 105 | 111 |
112 | 113 |

Wide layout

114 | 119 | 120 |

Advanced configuration options

121 | 129 | 130 |

LNURL

131 | 136 | 137 |

Keysend

138 | 143 | 144 |

Configurable amounts

145 |
146 | 152 | 157 | 162 | 168 | 173 |
174 |

LNURL errors

175 |
176 | 182 | 188 | 194 | 200 |
201 |

Screenshot time

202 |
203 |
204 | 210 |
211 |
212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /src/components/Widget.ce.vue: -------------------------------------------------------------------------------- 1 | 141 | 362 | 363 | -------------------------------------------------------------------------------- /src/components/utils/helpers.js: -------------------------------------------------------------------------------- 1 | const host = "https://embed.twentyuno.net"; 2 | 3 | function contrastingColor(color) 4 | { 5 | return (luma(color) >= 200) ? '#000' : '#fff'; 6 | } 7 | 8 | function luma(color) 9 | { 10 | var rgb = hexToRGB(color); 11 | return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]); 12 | } 13 | 14 | function hexToRGB(color) 15 | { 16 | var rgb = []; 17 | for (var i = 0; i <= 2; i++) 18 | rgb[i] = parseInt(color.substr(i * 2, 2), 16); 19 | return rgb; 20 | } 21 | 22 | async function fetchInvoice(to, amount, comment) { 23 | const response = await fetch(host + `/invoice?to=${to}&amount=${amount}&comment=${comment}`, { 24 | headers: { 25 | "Accept": "application/json", 26 | "Content-Type": "application/json", 27 | }, 28 | }); 29 | 30 | if(!response.ok) { 31 | throw new Error(response.error); 32 | } 33 | 34 | return response.json(); 35 | } 36 | 37 | async function fetchParams(to) { 38 | const response = await fetch(host + `/params?to=${to}`, { 39 | headers: { 40 | "Accept": "application/json", 41 | "Content-Type": "application/json", 42 | }, 43 | }); 44 | 45 | if(!response.ok) { 46 | throw new Error(response.error); 47 | } 48 | 49 | return response.json(); 50 | } 51 | 52 | const formatAmount = (amount, decimals = 1) => { 53 | let i = 0; 54 | for (i; amount >= 1000; i++) { 55 | amount /= 1000; 56 | } 57 | return Number.parseFloat(amount).toFixed(i > 0 ? decimals : 0) + ["", "k", "M", "G"][i]; 58 | } 59 | 60 | module.exports = { 61 | fetchInvoice, 62 | luma, 63 | contrastingColor, 64 | fetchParams, 65 | formatAmount 66 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { defineCustomElement } from 'vue' 2 | 3 | import Widget from './components/Widget.ce.vue' 4 | 5 | const LightningWidgetElement = defineCustomElement(Widget) 6 | 7 | customElements.define('lightning-widget', LightningWidgetElement) 8 | -------------------------------------------------------------------------------- /src/server/main.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const path = require("path"); 3 | const lnurlPay = require('lnurl-pay'); 4 | const bodyParser = require("body-parser"); 5 | const { parsePaymentRequest } = require('invoices'); 6 | var QRCode = require('qrcode') 7 | const { PassThrough } = require('stream'); 8 | var cors = require('cors') 9 | 10 | const app = express(); 11 | 12 | app.use(cors()) 13 | 14 | app.use(express.static(path.join(__dirname, '../../dist'), { maxAge: 5 * 60 * 1000 })) 15 | app.use('/assets', express.static(path.join(__dirname, '../../assets'))) 16 | 17 | app.use(bodyParser.urlencoded({ extended: false })); 18 | app.use(bodyParser.json()); 19 | 20 | app.get("/invoice", async function (req, res) { 21 | try { 22 | const { invoice } = await lnurlPay.requestInvoice({ 23 | lnUrlOrAddress: req.query.to, 24 | tokens: req.query.amount, 25 | comment: req.query.comment 26 | }); 27 | 28 | const requestDetails = parsePaymentRequest({ request: invoice }); 29 | res.json({ payment_request: invoice, payment_hash: requestDetails.id }); 30 | } 31 | catch(e) { 32 | res.status(500).json({ error: e.message }); 33 | } 34 | }); 35 | 36 | app.get("/params", async function (req, res) { 37 | try { 38 | const params = await lnurlPay.requestPayServiceParams({ 39 | lnUrlOrAddress: req.query.to 40 | }); 41 | res.json(params); 42 | } 43 | catch(e) { 44 | res.status(500).json({ error: e.message }); 45 | } 46 | }); 47 | 48 | app.get("/qr/:data", async function (req, res) { 49 | try { 50 | 51 | const content = req.params.data; 52 | const qrStream = new PassThrough(); 53 | await QRCode.toFileStream(qrStream, content.toUpperCase(), 54 | { 55 | type: 'png', 56 | width: 300, 57 | margin: 2, 58 | errorCorrectionLevel: 'L' 59 | } 60 | ); 61 | 62 | qrStream.pipe(res); 63 | } catch (ex) { 64 | res.status(500); 65 | res.send(); 66 | } 67 | }); 68 | 69 | const port = process.env.PORT || 3002; 70 | console.log(`Running on ${port}`); 71 | app.listen(port); 72 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | const webpack = require('webpack') 3 | 4 | module.exports = defineConfig({ 5 | filenameHashing: false, 6 | transpileDependencies: true, 7 | configureWebpack: { 8 | devServer: { 9 | headers: { "Access-Control-Allow-Origin": "*" } 10 | }, 11 | plugins: [ 12 | new webpack.optimize.LimitChunkCountPlugin({ 13 | maxChunks: 1 14 | }) 15 | ] 16 | }, 17 | chainWebpack: 18 | config => { 19 | config.optimization.delete('splitChunks') 20 | } 21 | }) 22 | --------------------------------------------------------------------------------