diff --git a/.env.example b/.env.example index 1c6a446fa..cc70644c1 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ PORT=5000 LNBITS_SITE_TITLE=LNbits LNBITS_ALLOWED_USERS="" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" -LNBITS_DATA_FOLDER="." +LNBITS_DATA_FOLDER="./data" LNBITS_DISABLED_EXTENSIONS="amilk" LNBITS_FORCE_HTTPS=true LNBITS_SERVICE_FEE="0.0" diff --git a/docs/devs/installation.md b/docs/devs/installation.md index 81ae2a4c6..55636181d 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -5,15 +5,11 @@ title: Installation nav_order: 1 --- - -Installation -============ +# Installation Download the latest stable release https://github.com/lnbits/lnbits/releases - -Application dependencies ------------------------- +## Application dependencies The application uses [Pipenv][pipenv] to manage Python packages. While in development, you will need to install all dependencies: @@ -24,7 +20,7 @@ $ pipenv install --dev ``` If any of the modules fails to install, try checking and upgrading your setupTool module. -`pip install -U setuptools` +`pip install -U setuptools` If you wish to use a version of Python higher than 3.7: @@ -41,9 +37,7 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and ` Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment. - -Running the server ------------------- +## Running the server LNbits uses [Quart][quart] as an application server. @@ -51,12 +45,18 @@ LNbits uses [Quart][quart] as an application server. $ pipenv run python -m lnbits ``` -Frontend --------- +**Note**: You'll need to use _https_ for some endpoints and/or extensions. You can use [ngrok](https://ngrok.com/) for that. Follow the installation instructions on the website and when it's all set you can run: + +```sh +$ ./nrok http 5000 +``` + +this will give you an _https_ tunnel for the _localhost_, use that URL for navigating to LNBits. + +## Frontend The frontend uses [Vue.js and Quasar][quasar]. - [quart]: https://pgjones.gitlab.io/ [pipenv]: https://pipenv.pypa.io/ [polar]: https://lightningpolar.com/ diff --git a/docs/guide/installation.md b/docs/guide/installation.md index eabef16e9..2f580d8ee 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -17,6 +17,7 @@ cd lnbits/ python3 -m venv venv ./venv/bin/pip install -r requirements.txt cp .env.example .env +mkdir data ./venv/bin/quart assets ./venv/bin/quart migrate ./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()' @@ -29,3 +30,31 @@ Now modify the `.env` file with any settings you prefer and add a proper [fundin Then you can run restart it and it will be using the new settings. You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source. + +Docker installation +=================== + +To install using docker you first need to build the docker image as: +``` +git clone https://github.com/lnbits/lnbits.git +cd lnbits/ # ${PWD} refered as +docker build -t lnbits . +``` + +You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there +``` +cp /.env.example .env +``` +and change the configuration in `.env` as required. + +Then create the data directory for the user ID 1000, which is the user that runs the lnbits within the docker container. +``` +mkdir data +sudo chown 1000:1000 ./data/ +``` + +Then the image can be run as: +``` +docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits +`` +Finally you can access the lnbits on your machine port 5000. diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index 64b61bae9..888fc8d6c 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -54,7 +54,7 @@ Using this wallet requires the installation of the `lndgrpc` and `purerpc` Pytho ### LNPay -For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `/wallet/webhook` with the "Wallet Receive" event and no secret. +For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment. - `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet** - `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/ diff --git a/lnbits/app.py b/lnbits/app.py index 6137fdaf3..35852cd9b 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -108,6 +108,7 @@ def register_assets(app: QuartTrio): def register_filters(app: QuartTrio): """Jinja filters.""" app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] + app.jinja_env.globals["LNBITS_VERSION"] = app.config["LNBITS_COMMIT"] app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index 13a5e85a3..d4f36e47b 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -104,4 +104,4 @@ async def api_lnurl_callback(link_id): if success_action: resp["success_action"] = success_action - return jsonify(), HTTPStatus.OK + return jsonify(resp), HTTPStatus.OK diff --git a/lnbits/extensions/lnurlp/static/js/index.js b/lnbits/extensions/lnurlp/static/js/index.js index dbc0df1e3..fbf6a60fc 100644 --- a/lnbits/extensions/lnurlp/static/js/index.js +++ b/lnbits/extensions/lnurlp/static/js/index.js @@ -62,7 +62,9 @@ new Vue({ LNbits.utils.notifyApiError(err) }) }, - closeFormDialog() {}, + closeFormDialog() { + this.resetFormData() + }, openQrCodeDialog(linkId) { var link = _.findWhere(this.payLinks, {id: linkId}) if (link.currency) this.updateFiatRate(link.currency) @@ -116,6 +118,13 @@ new Vue({ this.createPayLink(wallet, data) } }, + resetFormData() { + this.formDialog = { + show: false, + fixedAmount: true, + data: {} + } + }, updatePayLink(wallet, data) { let values = _.omit( _.pick( @@ -147,6 +156,7 @@ new Vue({ this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id) this.payLinks.push(mapPayLink(response.data)) this.formDialog.show = false + this.resetFormData() }) .catch(err => { LNbits.utils.notifyApiError(err) @@ -158,6 +168,7 @@ new Vue({ .then(response => { this.payLinks.push(mapPayLink(response.data)) this.formDialog.show = false + this.resetFormData() }) .catch(err => { LNbits.utils.notifyApiError(err) diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html index 6dfbc9933..1a388e92a 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/index.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/index.html @@ -291,7 +291,8 @@ dense v-model.number="itemDialog.data.price" type="number" - min="1" + step="0.001" + min="0.001" :label="`Item price (${itemDialog.data.unit})`" > Charges: diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html index d23814b4a..b3386074e 100644 --- a/lnbits/extensions/satspay/templates/satspay/display.html +++ b/lnbits/extensions/satspay/templates/satspay/display.html @@ -169,7 +169,7 @@ > List[Target]: + rows = await db.fetchall("SELECT * FROM targets WHERE source = ?", (source_wallet,)) + return [Target(**dict(row)) for row in rows] + + +async def set_targets(source_wallet: str, targets: List[Target]): + async with db.connect() as conn: + await conn.execute("DELETE FROM targets WHERE source = ?", (source_wallet,)) + for target in targets: + await conn.execute( + """ + INSERT INTO targets + (source, wallet, percent, alias) + VALUES (?, ?, ?, ?) + """, + (source_wallet, target.wallet, target.percent, target.alias), + ) diff --git a/lnbits/extensions/splitpayments/migrations.py b/lnbits/extensions/splitpayments/migrations.py new file mode 100644 index 000000000..cb7cf34dc --- /dev/null +++ b/lnbits/extensions/splitpayments/migrations.py @@ -0,0 +1,16 @@ +async def m001_initial(db): + """ + Initial split payment table. + """ + await db.execute( + """ + CREATE TABLE targets ( + wallet TEXT NOT NULL, + source TEXT NOT NULL, + percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100), + alias TEXT, + + UNIQUE (source, wallet) + ); + """ + ) diff --git a/lnbits/extensions/splitpayments/models.py b/lnbits/extensions/splitpayments/models.py new file mode 100644 index 000000000..17578f871 --- /dev/null +++ b/lnbits/extensions/splitpayments/models.py @@ -0,0 +1,8 @@ +from typing import NamedTuple + + +class Target(NamedTuple): + wallet: str + source: str + percent: int + alias: str diff --git a/lnbits/extensions/splitpayments/static/js/index.js b/lnbits/extensions/splitpayments/static/js/index.js new file mode 100644 index 000000000..d9750bef1 --- /dev/null +++ b/lnbits/extensions/splitpayments/static/js/index.js @@ -0,0 +1,143 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +function hashTargets(targets) { + return targets + .filter(isTargetComplete) + .map(({wallet, percent, alias}) => `${wallet}${percent}${alias}`) + .join('') +} + +function isTargetComplete(target) { + return target.wallet && target.wallet.trim() !== '' && target.percent > 0 +} + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data() { + return { + selectedWallet: null, + currentHash: '', // a string that must match if the edit data is unchanged + targets: [] + } + }, + computed: { + isDirty() { + return hashTargets(this.targets) !== this.currentHash + } + }, + methods: { + clearTargets() { + this.targets = [{}] + this.$q.notify({ + message: + 'Cleared the form, but not saved. You must click to save manually.', + timeout: 500 + }) + }, + getTargets() { + LNbits.api + .request( + 'GET', + '/splitpayments/api/v1/targets', + this.selectedWallet.adminkey + ) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + .then(response => { + this.currentHash = hashTargets(response.data) + this.targets = response.data.concat({}) + }) + }, + changedWallet(wallet) { + this.selectedWallet = wallet + this.getTargets() + }, + targetChanged(isPercent, index) { + // fix percent min and max range + if (isPercent) { + if (this.targets[index].percent > 100) this.targets[index].percent = 100 + if (this.targets[index].percent < 0) this.targets[index].percent = 0 + } + + // remove empty lines (except last) + if (this.targets.length >= 2) { + for (let i = this.targets.length - 2; i >= 0; i--) { + let target = this.targets[i] + if ( + (!target.wallet || target.wallet.trim() === '') && + (!target.alias || target.alias.trim() === '') && + !target.percent + ) { + this.targets.splice(i, 1) + } + } + } + + // add a line at the end if the last one is filled + let last = this.targets[this.targets.length - 1] + if (last.wallet && last.wallet.trim() !== '' && last.percent > 0) { + this.targets.push({}) + } + + // sum of all percents + let currentTotal = this.targets.reduce( + (acc, target) => acc + (target.percent || 0), + 0 + ) + + // remove last (unfilled) line if the percent is already 100 + if (currentTotal >= 100) { + let last = this.targets[this.targets.length - 1] + if ( + (!last.wallet || last.wallet.trim() === '') && + (!last.alias || last.alias.trim() === '') && + !last.percent + ) { + this.targets = this.targets.slice(0, -1) + } + } + + // adjust percents of other lines (not this one) + if (currentTotal > 100 && isPercent) { + let diff = (currentTotal - 100) / (100 - this.targets[index].percent) + this.targets.forEach((target, t) => { + if (t !== index) target.percent -= Math.round(diff * target.percent) + }) + } + + // overwrite so changes appear + this.targets = this.targets + }, + saveTargets() { + LNbits.api + .request( + 'PUT', + '/splitpayments/api/v1/targets', + this.selectedWallet.adminkey, + { + targets: this.targets + .filter(isTargetComplete) + .map(({wallet, percent, alias}) => ({wallet, percent, alias})) + } + ) + .then(response => { + this.$q.notify({ + message: 'Split payments targets set.', + timeout: 700 + }) + this.getTargets() + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + } + }, + created() { + this.selectedWallet = this.g.user.wallets[0] + this.getTargets() + } +}) diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py new file mode 100644 index 000000000..7e57a9e9f --- /dev/null +++ b/lnbits/extensions/splitpayments/tasks.py @@ -0,0 +1,77 @@ +import json +import trio # type: ignore + +from lnbits.core.models import Payment +from lnbits.core.crud import create_payment +from lnbits.core import db as core_db +from lnbits.tasks import register_invoice_listener, internal_invoice_paid +from lnbits.helpers import urlsafe_short_hash + +from .crud import get_targets + + +async def register_listeners(): + invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) + register_invoice_listener(invoice_paid_chan_send) + await wait_for_paid_invoices(invoice_paid_chan_recv) + + +async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): + async for payment in invoice_paid_chan: + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if "splitpayments" == payment.extra.get("tag") or payment.extra.get("splitted"): + # already splitted, ignore + return + + # now we make some special internal transfers (from no one to the receiver) + targets = await get_targets(payment.wallet_id) + transfers = [ + (target.wallet, int(target.percent * payment.amount / 100)) + for target in targets + ] + transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0] + amount_left = payment.amount - sum([amount for _, amount in transfers]) + + if amount_left < 0: + print("splitpayments failure: amount_left is negative.", payment.payment_hash) + return + + if not targets: + return + + # mark the original payment with one extra key, "splitted" + # (this prevents us from doing this process again and it's informative) + # and reduce it by the amount we're going to send to the producer + await core_db.execute( + """ + UPDATE apipayments + SET extra = ?, amount = ? + WHERE hash = ? + AND checking_id NOT LIKE 'internal_%' + """, + ( + json.dumps(dict(**payment.extra, splitted=True)), + amount_left, + payment.payment_hash, + ), + ) + + # perform the internal transfer using the same payment_hash + for wallet, amount in transfers: + internal_checking_id = f"internal_{urlsafe_short_hash()}" + await create_payment( + wallet_id=wallet, + checking_id=internal_checking_id, + payment_request="", + payment_hash=payment.payment_hash, + amount=amount, + memo=payment.memo, + pending=False, + extra={"tag": "splitpayments"}, + ) + + # manually send this for now + await internal_invoice_paid.send(internal_checking_id) diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html new file mode 100644 index 000000000..e92fac96f --- /dev/null +++ b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html @@ -0,0 +1,90 @@ + + + +

+ Add some wallets to the list of "Target Wallets", each with an + associated percent. After saving, every time any payment + arrives at the "Source Wallet" that payment will be split with the + target wallets according to their percent. +

+

This is valid for every payment, doesn't matter how it was created.

+

Target wallets can be any wallet from this same LNbits instance.

+

+ To remove a wallet from the targets list, just erase its fields and + save. To remove all, click "Clear" then save. +

+
+
+
+ + + + + + GET + /splitpayments/api/v1/targets +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [{"wallet": <wallet id>, "alias": <chosen name for this + wallet>, "percent": <number between 1 and 100>}, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + PUT + /splitpayments/api/v1/targets +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+
Curl example
+ curl -X PUT {{ request.url_root }}api/v1/splitpayments/targets -H + "X-Api-Key: {{ g.user.wallets[0].adminkey }}" -H 'Content-Type: + application/json' -d '{"targets": [{"wallet": <wallet id or invoice + key>, "alias": <name to identify this>, "percent": <number + between 1 and 100>}, ...]}' + +
+
+
+
diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/index.html b/lnbits/extensions/splitpayments/templates/splitpayments/index.html new file mode 100644 index 000000000..d592bcd1c --- /dev/null +++ b/lnbits/extensions/splitpayments/templates/splitpayments/index.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + + + + + + + + + +
+
Target Wallets
+
+ + +
+ + + +
+ + + + + Clear + + + + + + Save Targets + + + +
+
+
+
+ +
+ + +
LNbits SplitPayments extension
+
+ + + {% include "splitpayments/_api_docs.html" %} + +
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/splitpayments/views.py b/lnbits/extensions/splitpayments/views.py new file mode 100644 index 000000000..acded737e --- /dev/null +++ b/lnbits/extensions/splitpayments/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import splitpayments_ext + + +@splitpayments_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("splitpayments/index.html", user=g.user) diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py new file mode 100644 index 000000000..e0fe475ed --- /dev/null +++ b/lnbits/extensions/splitpayments/views_api.py @@ -0,0 +1,70 @@ +from quart import g, jsonify +from http import HTTPStatus + +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.core.crud import get_wallet, get_wallet_for_key + +from . import splitpayments_ext +from .crud import get_targets, set_targets +from .models import Target + + +@splitpayments_ext.route("/api/v1/targets", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_targets_get(): + targets = await get_targets(g.wallet.id) + return jsonify([target._asdict() for target in targets] or []) + + +@splitpayments_ext.route("/api/v1/targets", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "targets": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "wallet": {"type": "string"}, + "alias": {"type": "string"}, + "percent": {"type": "integer"}, + }, + }, + } + } +) +async def api_targets_set(): + targets = [] + + for entry in g.data["targets"]: + wallet = await get_wallet(entry["wallet"]) + if not wallet: + wallet = await get_wallet_for_key(entry["wallet"], "invoice") + if not wallet: + return ( + jsonify({"message": f"Invalid wallet '{entry['wallet']}'."}), + HTTPStatus.BAD_REQUEST, + ) + + if wallet.id == g.wallet.id: + return ( + jsonify({"message": "Can't split to itself."}), + HTTPStatus.BAD_REQUEST, + ) + + if entry["percent"] < 0: + return ( + jsonify({"message": f"Invalid percent '{entry['percent']}'."}), + HTTPStatus.BAD_REQUEST, + ) + + targets.append( + Target(wallet.id, g.wallet.id, entry["percent"], entry["alias"] or "") + ) + + percent_sum = sum([target.percent for target in targets]) + if percent_sum > 100: + return jsonify({"message": "Splitting over 100%."}), HTTPStatus.BAD_REQUEST + + await set_targets(g.wallet.id, targets) + return "", HTTPStatus.OK diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md index 49dfc223d..729f40f41 100644 --- a/lnbits/extensions/subdomains/README.md +++ b/lnbits/extensions/subdomains/README.md @@ -1,27 +1,29 @@

Subdomains Extension

-So the goal of the extension is to allow the owner of a domain to sell their subdomain to the anyone who is willing to pay some money for it. +So the goal of the extension is to allow the owner of a domain to sell subdomains to anyone who is willing to pay some money for it. + +[![video tutorial livestream](http://img.youtube.com/vi/O1X0fy3uNpw/0.jpg)](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains') ## Requirements -- Free cloudflare account -- Cloudflare as a dns server provider -- Cloudflare TOKEN and Cloudflare zone-id where the domain is parked +- Free Cloudflare account +- Cloudflare as a DNS server provider +- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked ## Usage -1. Register at cloudflare and setup your domain with them. (Just follow instructions they provide...) -2. Change DNS server at your domain registrar to point to cloudflare's -3. Get Cloudflare zoneID for your domain +1. Register at Cloudflare and setup your domain with them. (Just follow instructions they provide...) +2. Change DNS server at your domain registrar to point to Cloudflare's +3. Get Cloudflare zone-ID for your domain -4. get Cloudflare API TOKEN +4. Get Cloudflare API TOKEN -5. Open the lnbits subdomains extension and register your domain with lnbits +5. Open the LNBits subdomains extension and register your domain 6. Click on the button in the table to open the public form that was generated for your domain -- Extension also supports webhooks so you can get notified when someone buys a new domain - + - Extension also supports webhooks so you can get notified when someone buys a new subdomain\ + ## API Endpoints @@ -36,8 +38,6 @@ So the goal of the extension is to allow the owner of a domain to sell their sub - GET /api/v1/subdomains/ - DELETE /api/v1/subdomains/ -## Useful - ### Cloudflare - Cloudflare offers programmatic subdomain registration... (create new A record) diff --git a/lnbits/extensions/usermanager/README.md b/lnbits/extensions/usermanager/README.md index 6cb9deca9..b6f306275 100644 --- a/lnbits/extensions/usermanager/README.md +++ b/lnbits/extensions/usermanager/README.md @@ -1,3 +1,26 @@ -

User Manager

-

Make and manager users/wallets

-To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the develpoers stack as the user and wallet manager. +# User Manager + +## Make and manage users/wallets + +To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. + +For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the developers stack as the user and wallet manager. Or someone wanting to manage their family's wallets (wife, children, parents, etc...) or you want to host a community Lightning Network node and want to manage wallets for the users. + +## Usage + +1. Click the button "NEW USER" to create a new user\ + ![new user](https://i.imgur.com/4yZyfJE.png) +2. Fill the user information\ + - username + - the generated wallet name, user can create other wallets later on + - email + - set a password + ![user information](https://i.imgur.com/40du7W5.png) +3. After creating your user, it will appear in the **Users** section, and a user's wallet in the **Wallets** section. +4. Next you can share the wallet with the corresponding user\ + ![user wallet](https://i.imgur.com/gAyajbx.png) +5. If you need to create more wallets for some user, click "NEW WALLET" at the top\ + ![multiple wallets](https://i.imgur.com/wovVnim.png) + - select the existing user you wish to add the wallet + - set a wallet name\ + ![new wallet](https://i.imgur.com/sGwG8dC.png) diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html index a75140894..06e5423ab 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -214,7 +214,7 @@ {% endblock %} {% block scripts %} {{ window_vars(user) }}