From 17d13dbe6b7c6809502c1565bf60f5ac09b298e0 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Nov 2025 12:10:40 +0200 Subject: [PATCH 01/57] fix: point multiplication (#115) --- config.json | 3 ++- helpers.py | 6 +++++- pyproject.toml | 2 +- uv.lock | 8 +++++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index fe7456f..a02b0b0 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,9 @@ { "name": "Nostr Market", + "version": "1.1.0", "short_description": "Nostr Webshop/market on LNbits", "tile": "/nostrmarket/static/images/bitcoin-shop.png", - "min_lnbits_version": "1.0.0", + "min_lnbits_version": "1.4.0", "contributors": [ { "name": "motorina0", diff --git a/helpers.py b/helpers.py index 478c0e1..dcc0f06 100644 --- a/helpers.py +++ b/helpers.py @@ -10,7 +10,11 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def get_shared_secret(privkey: str, pubkey: str): pk = coincurve.PublicKey(bytes.fromhex("02" + pubkey)) sk = coincurve.PrivateKey(bytes.fromhex(privkey)) - return sk.ecdh(pk.format()) + shared_point = pk.multiply(sk.secret) + + shared_point_bytes = shared_point.format(compressed=False) + x_coord = shared_point_bytes[1:33] + return x_coord def decrypt_message(encoded_message: str, encryption_key) -> str: diff --git a/pyproject.toml b/pyproject.toml index 7a0c533..46dba55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nostrmarket" -version = "0.0.0" +version = "1.1.0" requires-python = ">=3.10,<3.13" description = "LNbits, free and open-source Lightning wallet and accounts system." authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] diff --git a/uv.lock b/uv.lock index 60799b1..ef945f8 100644 --- a/uv.lock +++ b/uv.lock @@ -847,6 +847,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, @@ -856,6 +858,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, @@ -865,6 +869,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, ] @@ -1322,7 +1328,7 @@ wheels = [ [[package]] name = "nostrmarket" -version = "0.0.0" +version = "1.1.0" source = { virtual = "." } dependencies = [ { name = "lnbits" }, From 697fc1260d15433cd49fbe669f4944e646784519 Mon Sep 17 00:00:00 2001 From: Ben Weeks Date: Sun, 21 Dec 2025 17:47:48 +0000 Subject: [PATCH 02/57] fix: respect admin-configured allowed currencies in dropdowns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use window.g.allowedCurrencies instead of fetching all currencies from the API, so currency dropdowns only show currencies configured by the admin in LNbits settings. Closes #116 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 4.5 --- static/components/shipping-zones.js | 17 ++++------------- static/components/stall-list.js | 20 +++++--------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/static/components/shipping-zones.js b/static/components/shipping-zones.js index 742021a..17a4a1e 100644 --- a/static/components/shipping-zones.js +++ b/static/components/shipping-zones.js @@ -162,22 +162,13 @@ window.app.component('shipping-zones', { LNbits.utils.notifyApiError(error) } }, - async getCurrencies() { - try { - const {data} = await LNbits.api.request( - 'GET', - '/nostrmarket/api/v1/currencies', - this.inkey - ) - - this.currencies = ['sat', ...data] - } catch (error) { - LNbits.utils.notifyApiError(error) - } + getCurrencies() { + const currencies = window.g.allowedCurrencies || [] + this.currencies = ['sat', ...currencies] } }, created: async function () { await this.getZones() - await this.getCurrencies() + this.getCurrencies() } }) diff --git a/static/components/stall-list.js b/static/components/stall-list.js index 1ef4d70..c424950 100644 --- a/static/components/stall-list.js +++ b/static/components/stall-list.js @@ -149,19 +149,9 @@ window.app.component('stall-list', { } }) }, - getCurrencies: async function () { - try { - const {data} = await LNbits.api.request( - 'GET', - '/nostrmarket/api/v1/currencies', - this.inkey - ) - - return ['sat', ...data] - } catch (error) { - LNbits.utils.notifyApiError(error) - } - return [] + getCurrencies: function () { + const currencies = window.g.allowedCurrencies || [] + return ['sat', ...currencies] }, getStalls: async function (pending = false) { try { @@ -207,7 +197,7 @@ window.app.component('stall-list', { } }, openCreateStallDialog: async function (stallData) { - this.currencies = await this.getCurrencies() + this.currencies = this.getCurrencies() this.zoneOptions = await this.getZones() if (!this.zoneOptions || !this.zoneOptions.length) { this.$q.notify({ @@ -256,7 +246,7 @@ window.app.component('stall-list', { }, created: async function () { this.stalls = await this.getStalls() - this.currencies = await this.getCurrencies() + this.currencies = this.getCurrencies() this.zoneOptions = await this.getZones() } }) From 71f458b9b96f295bfc2be6bb4a4c990dbe93b5c6 Mon Sep 17 00:00:00 2001 From: Ben Weeks Date: Sun, 21 Dec 2025 19:06:28 +0000 Subject: [PATCH 03/57] feat: restructure UI with tab-based navigation (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganizes the merchant dashboard into a tab-based layout: - Tabs: Merchant | Shipping | Stalls | Products | Messages | Orders - Publish dropdown with NIP-15 options (NIP-99 disabled/coming soon) - Consistent UI patterns across all tabs: - Search/filter/buttons aligned right - Actions column on right side of tables - Equal-width tabs - Stalls tab: popup edit dialog, navigation to Products/Orders - Products tab: filter icon dropdown for stall filtering - Modular component structure for each tab section - Fixed product API calls to use correct endpoints Closes #119 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- static/components/merchant-tab.js | 37 ++ static/components/product-list.js | 256 +++++++++++ static/components/shipping-zones-list.js | 203 +++++++++ static/components/stall-list.js | 184 ++++---- static/js/index.js | 82 ++++ .../nostrmarket/components/merchant-tab.html | 96 ++++ .../nostrmarket/components/product-list.html | 289 ++++++++++++ .../components/shipping-zones-list.html | 122 +++++ .../nostrmarket/components/stall-list.html | 421 ++++++++++-------- templates/nostrmarket/index.html | 299 +++++++------ 10 files changed, 1605 insertions(+), 384 deletions(-) create mode 100644 static/components/merchant-tab.js create mode 100644 static/components/product-list.js create mode 100644 static/components/shipping-zones-list.js create mode 100644 templates/nostrmarket/components/merchant-tab.html create mode 100644 templates/nostrmarket/components/product-list.html create mode 100644 templates/nostrmarket/components/shipping-zones-list.html diff --git a/static/components/merchant-tab.js b/static/components/merchant-tab.js new file mode 100644 index 0000000..c82d853 --- /dev/null +++ b/static/components/merchant-tab.js @@ -0,0 +1,37 @@ +window.app.component('merchant-tab', { + name: 'merchant-tab', + template: '#merchant-tab', + delimiters: ['${', '}'], + props: [ + 'merchant-id', + 'inkey', + 'adminkey', + 'show-keys', + 'merchant-active', + 'public-key', + 'private-key', + 'is-admin' + ], + computed: { + marketClientUrl: function () { + return '/nostrmarket/market' + } + }, + methods: { + toggleShowKeys: function () { + this.$emit('toggle-show-keys') + }, + hideKeys: function () { + this.$emit('hide-keys') + }, + handleMerchantDeleted: function () { + this.$emit('merchant-deleted') + }, + toggleMerchantState: function () { + this.$emit('toggle-merchant-state') + }, + restartNostrConnection: function () { + this.$emit('restart-nostr-connection') + } + } +}) diff --git a/static/components/product-list.js b/static/components/product-list.js new file mode 100644 index 0000000..eb42281 --- /dev/null +++ b/static/components/product-list.js @@ -0,0 +1,256 @@ +window.app.component('product-list', { + name: 'product-list', + template: '#product-list', + delimiters: ['${', '}'], + props: ['adminkey', 'inkey', 'stall-filter'], + data: function () { + return { + filter: '', + stalls: [], + products: [], + pendingProducts: [], + selectedStall: null, + productDialog: { + showDialog: false, + showRestore: false, + data: null + }, + productsTable: { + columns: [ + {name: 'name', align: 'left', label: 'Name', field: 'name'}, + {name: 'stall', align: 'left', label: 'Stall', field: 'stall_id'}, + {name: 'price', align: 'left', label: 'Price', field: 'price'}, + {name: 'quantity', align: 'left', label: 'Quantity', field: 'quantity'}, + {name: 'actions', align: 'right', label: 'Actions', field: ''} + ], + pagination: { + rowsPerPage: 10 + } + } + } + }, + computed: { + stallOptions: function () { + return this.stalls.map(s => ({ + label: s.name, + value: s.id + })) + }, + filteredProducts: function () { + if (!this.selectedStall) { + return this.products + } + return this.products.filter(p => p.stall_id === this.selectedStall) + } + }, + watch: { + stallFilter: { + immediate: true, + handler(newVal) { + if (newVal) { + this.selectedStall = newVal + } + } + } + }, + methods: { + getStalls: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/stall?pending=false', + this.inkey + ) + this.stalls = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getProducts: async function () { + try { + // Fetch products from all stalls + const allProducts = [] + for (const stall of this.stalls) { + const {data} = await LNbits.api.request( + 'GET', + `/nostrmarket/api/v1/stall/product/${stall.id}?pending=false`, + this.inkey + ) + allProducts.push(...data) + } + this.products = allProducts + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getPendingProducts: async function () { + try { + // Fetch pending products from all stalls + const allPending = [] + for (const stall of this.stalls) { + const {data} = await LNbits.api.request( + 'GET', + `/nostrmarket/api/v1/stall/product/${stall.id}?pending=true`, + this.inkey + ) + allPending.push(...data) + } + this.pendingProducts = allPending + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getStallName: function (stallId) { + const stall = this.stalls.find(s => s.id === stallId) + return stall ? stall.name : 'Unknown' + }, + getStallCurrency: function (stallId) { + const stall = this.stalls.find(s => s.id === stallId) + return stall ? stall.currency : 'sat' + }, + getStall: function (stallId) { + return this.stalls.find(s => s.id === stallId) + }, + newEmptyProductData: function () { + return { + id: null, + stall_id: this.stalls.length ? this.stalls[0].id : null, + name: '', + categories: [], + images: [], + image: null, + price: 0, + quantity: 0, + config: { + description: '', + use_autoreply: false, + autoreply_message: '' + } + } + }, + showNewProductDialog: function () { + this.productDialog.data = this.newEmptyProductData() + this.productDialog.showDialog = true + }, + editProduct: function (product) { + this.productDialog.data = {...product, image: null} + if (!this.productDialog.data.config) { + this.productDialog.data.config = {description: ''} + } + this.productDialog.showDialog = true + }, + sendProductFormData: async function () { + const data = { + stall_id: this.productDialog.data.stall_id, + id: this.productDialog.data.id, + name: this.productDialog.data.name, + images: this.productDialog.data.images || [], + price: this.productDialog.data.price, + quantity: this.productDialog.data.quantity, + categories: this.productDialog.data.categories || [], + config: this.productDialog.data.config + } + this.productDialog.showDialog = false + + if (this.productDialog.data.id) { + data.pending = false + await this.updateProduct(data) + } else { + await this.createProduct(data) + } + }, + createProduct: async function (payload) { + try { + const {data} = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/product', + this.adminkey, + payload + ) + this.products.unshift(data) + this.$q.notify({ + type: 'positive', + message: 'Product Created' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + updateProduct: async function (product) { + try { + const {data} = await LNbits.api.request( + 'PATCH', + '/nostrmarket/api/v1/product/' + product.id, + this.adminkey, + product + ) + const index = this.products.findIndex(p => p.id === product.id) + if (index !== -1) { + this.products.splice(index, 1, data) + } else { + this.products.unshift(data) + } + this.$q.notify({ + type: 'positive', + message: 'Product Updated' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + deleteProduct: function (product) { + LNbits.utils + .confirmDialog(`Are you sure you want to delete "${product.name}"?`) + .onOk(async () => { + try { + await LNbits.api.request( + 'DELETE', + '/nostrmarket/api/v1/product/' + product.id, + this.adminkey + ) + this.products = this.products.filter(p => p.id !== product.id) + this.$q.notify({ + type: 'positive', + message: 'Product Deleted' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }) + }, + toggleProductActive: async function (product) { + await this.updateProduct({...product, active: !product.active}) + }, + addProductImage: function () { + if (!this.productDialog.data.image) return + if (!this.productDialog.data.images) { + this.productDialog.data.images = [] + } + this.productDialog.data.images.push(this.productDialog.data.image) + this.productDialog.data.image = null + }, + removeProductImage: function (imageUrl) { + const index = this.productDialog.data.images.indexOf(imageUrl) + if (index !== -1) { + this.productDialog.data.images.splice(index, 1) + } + }, + openSelectPendingProductDialog: async function () { + await this.getPendingProducts() + this.productDialog.showRestore = true + }, + openRestoreProductDialog: function (pendingProduct) { + pendingProduct.pending = true + this.productDialog.data = {...pendingProduct, image: null} + this.productDialog.showDialog = true + }, + shortLabel: function (value = '') { + if (value.length <= 44) return value + return value.substring(0, 40) + '...' + } + }, + created: async function () { + await this.getStalls() + await this.getProducts() + } +}) diff --git a/static/components/shipping-zones-list.js b/static/components/shipping-zones-list.js new file mode 100644 index 0000000..a8cf58f --- /dev/null +++ b/static/components/shipping-zones-list.js @@ -0,0 +1,203 @@ +window.app.component('shipping-zones-list', { + name: 'shipping-zones-list', + props: ['adminkey', 'inkey'], + template: '#shipping-zones-list', + delimiters: ['${', '}'], + data: function () { + return { + zones: [], + zoneDialog: { + showDialog: false, + data: { + id: null, + name: '', + countries: [], + cost: 0, + currency: 'sat' + } + }, + currencies: [], + shippingZoneOptions: [ + 'Free (digital)', + 'Flat rate', + 'Worldwide', + 'Europe', + 'Australia', + 'Austria', + 'Belgium', + 'Brazil', + 'Canada', + 'Denmark', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Hong Kong', + 'Hungary', + 'Ireland', + 'Indonesia', + 'Israel', + 'Italy', + 'Japan', + 'Kazakhstan', + 'Korea', + 'Luxembourg', + 'Malaysia', + 'Mexico', + 'Netherlands', + 'New Zealand', + 'Norway', + 'Poland', + 'Portugal', + 'Romania', + 'Russia', + 'Saudi Arabia', + 'Singapore', + 'Spain', + 'Sweden', + 'Switzerland', + 'Thailand', + 'Turkey', + 'Ukraine', + 'United Kingdom**', + 'United States***', + 'Vietnam', + 'China' + ], + zonesTable: { + columns: [ + { + name: 'name', + align: 'left', + label: 'Name', + field: 'name' + }, + { + name: 'countries', + align: 'left', + label: 'Countries', + field: 'countries' + }, + { + name: 'currency', + align: 'left', + label: 'Currency', + field: 'currency' + }, + { + name: 'cost', + align: 'left', + label: 'Cost', + field: 'cost' + }, + { + name: 'actions', + align: 'right', + label: 'Actions', + field: '' + } + ], + pagination: { + rowsPerPage: 10 + } + } + } + }, + methods: { + openZoneDialog: function (data) { + data = data || { + id: null, + name: '', + countries: [], + cost: 0, + currency: 'sat' + } + this.zoneDialog.data = {...data} + this.zoneDialog.showDialog = true + }, + getZones: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/zone', + this.inkey + ) + this.zones = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + sendZoneFormData: async function () { + this.zoneDialog.showDialog = false + if (this.zoneDialog.data.id) { + await this.updateShippingZone(this.zoneDialog.data) + } else { + await this.createShippingZone(this.zoneDialog.data) + } + await this.getZones() + }, + createShippingZone: async function (newZone) { + try { + await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/zone', + this.adminkey, + newZone + ) + this.$q.notify({ + type: 'positive', + message: 'Zone created!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + updateShippingZone: async function (updatedZone) { + try { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/zone/${updatedZone.id}`, + this.adminkey, + updatedZone + ) + this.$q.notify({ + type: 'positive', + message: 'Zone updated!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + confirmDeleteZone: function (zone) { + LNbits.utils + .confirmDialog(`Are you sure you want to delete zone "${zone.name}"?`) + .onOk(async () => { + await this.deleteShippingZone(zone.id) + }) + }, + deleteShippingZone: async function (zoneId) { + try { + await LNbits.api.request( + 'DELETE', + `/nostrmarket/api/v1/zone/${zoneId}`, + this.adminkey + ) + this.$q.notify({ + type: 'positive', + message: 'Zone deleted!' + }) + await this.getZones() + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getCurrencies() { + const currencies = window.g.allowedCurrencies || [] + this.currencies = ['sat', ...currencies] + } + }, + created: async function () { + await this.getZones() + this.getCurrencies() + } +}) diff --git a/static/components/stall-list.js b/static/components/stall-list.js index 1ef4d70..4108a56 100644 --- a/static/components/stall-list.js +++ b/static/components/stall-list.js @@ -2,7 +2,7 @@ window.app.component('stall-list', { name: 'stall-list', template: '#stall-list', delimiters: ['${', '}'], - props: [`adminkey`, 'inkey', 'wallet-options'], + props: ['adminkey', 'inkey', 'wallet-options'], data: function () { return { filter: '', @@ -20,39 +20,25 @@ window.app.component('stall-list', { shippingZones: [] } }, + editDialog: { + show: false, + data: { + id: '', + name: '', + description: '', + wallet: null, + currency: 'sat', + shippingZones: [] + } + }, zoneOptions: [], stallsTable: { columns: [ - { - name: '', - align: 'left', - label: '', - field: '' - }, - { - name: 'id', - align: 'left', - label: 'Name', - field: 'id' - }, - { - name: 'currency', - align: 'left', - label: 'Currency', - field: 'currency' - }, - { - name: 'description', - align: 'left', - label: 'Description', - field: 'description' - }, - { - name: 'shippingZones', - align: 'left', - label: 'Shipping Zones', - field: 'shippingZones' - } + {name: 'name', align: 'left', label: 'Name', field: 'name'}, + {name: 'currency', align: 'left', label: 'Currency', field: 'currency'}, + {name: 'description', align: 'left', label: 'Description', field: row => row.config?.description || ''}, + {name: 'shippingZones', align: 'left', label: 'Shipping Zones', field: row => row.shipping_zones?.map(z => z.name).join(', ') || ''}, + {name: 'actions', align: 'right', label: 'Actions', field: ''} ], pagination: { rowsPerPage: 10 @@ -65,6 +51,11 @@ window.app.component('stall-list', { return this.zoneOptions.filter( z => z.currency === this.stallDialog.data.currency ) + }, + editFilteredZoneOptions: function () { + return this.zoneOptions.filter( + z => z.currency === this.editDialog.data.currency + ) } }, methods: { @@ -94,7 +85,6 @@ window.app.component('stall-list', { stall ) this.stallDialog.show = false - data.expanded = false this.stalls.unshift(data) this.$q.notify({ type: 'positive', @@ -114,7 +104,6 @@ window.app.component('stall-list', { stallData ) this.stallDialog.show = false - data.expanded = false this.stalls.unshift(data) this.$q.notify({ type: 'positive', @@ -124,44 +113,66 @@ window.app.component('stall-list', { LNbits.utils.notifyApiError(error) } }, - deleteStall: async function (pendingStall) { - LNbits.utils - .confirmDialog( - ` - Are you sure you want to delete this pending stall '${pendingStall.name}'? - ` - ) - .onOk(async () => { - try { - await LNbits.api.request( - 'DELETE', - '/nostrmarket/api/v1/stall/' + pendingStall.id, - this.adminkey - ) - this.$q.notify({ - type: 'positive', - message: 'Pending Stall Deleted', - timeout: 5000 - }) - } catch (error) { - console.warn(error) - LNbits.utils.notifyApiError(error) - } - }) - }, - getCurrencies: async function () { + updateStall: async function () { try { + const stallData = { + id: this.editDialog.data.id, + name: this.editDialog.data.name, + wallet: this.editDialog.data.wallet, + currency: this.editDialog.data.currency, + shipping_zones: this.editDialog.data.shippingZones, + config: { + description: this.editDialog.data.description + } + } const {data} = await LNbits.api.request( - 'GET', - '/nostrmarket/api/v1/currencies', - this.inkey + 'PUT', + `/nostrmarket/api/v1/stall/${stallData.id}`, + this.adminkey, + stallData ) - - return ['sat', ...data] + this.editDialog.show = false + const index = this.stalls.findIndex(s => s.id === data.id) + if (index !== -1) { + this.stalls.splice(index, 1, data) + } + this.$q.notify({ + type: 'positive', + message: 'Stall updated!' + }) } catch (error) { LNbits.utils.notifyApiError(error) } - return [] + }, + deleteStall: async function (stall) { + try { + await LNbits.api.request( + 'DELETE', + '/nostrmarket/api/v1/stall/' + stall.id, + this.adminkey + ) + this.stalls = this.stalls.filter(s => s.id !== stall.id) + this.pendingStalls = this.pendingStalls.filter(s => s.id !== stall.id) + this.$q.notify({ + type: 'positive', + message: 'Stall deleted' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + confirmDeleteStall: function (stall) { + LNbits.utils + .confirmDialog( + `Products and orders will be deleted also! Are you sure you want to delete stall "${stall.name}"?` + ) + .onOk(async () => { + await this.deleteStall(stall) + }) + }, + getCurrencies: function () { + const currencies = window.g.allowedCurrencies || [] + return ['sat', ...currencies] }, getStalls: async function (pending = false) { try { @@ -170,7 +181,7 @@ window.app.component('stall-list', { `/nostrmarket/api/v1/stall?pending=${pending}`, this.inkey ) - return data.map(s => ({...s, expanded: false})) + return data } catch (error) { LNbits.utils.notifyApiError(error) } @@ -194,20 +205,8 @@ window.app.component('stall-list', { } return [] }, - handleStallDeleted: function (stallId) { - this.stalls = _.reject(this.stalls, function (obj) { - return obj.id === stallId - }) - }, - handleStallUpdated: function (stall) { - const index = this.stalls.findIndex(r => r.id === stall.id) - if (index !== -1) { - stall.expanded = true - this.stalls.splice(index, 1, stall) - } - }, openCreateStallDialog: async function (stallData) { - this.currencies = await this.getCurrencies() + this.currencies = this.getCurrencies() this.zoneOptions = await this.getZones() if (!this.zoneOptions || !this.zoneOptions.length) { this.$q.notify({ @@ -225,6 +224,24 @@ window.app.component('stall-list', { } this.stallDialog.show = true }, + openEditStallDialog: async function (stall) { + this.currencies = this.getCurrencies() + this.zoneOptions = await this.getZones() + this.editDialog.data = { + id: stall.id, + name: stall.name, + description: stall.config?.description || '', + wallet: stall.wallet, + currency: stall.currency, + shippingZones: (stall.shipping_zones || []).map(z => ({ + ...z, + label: z.name + ? `${z.name} (${z.countries.join(', ')})` + : z.countries.join(', ') + })) + } + this.editDialog.show = true + }, openSelectPendingStallDialog: async function () { this.stallDialog.showRestore = true this.pendingStalls = await this.getStalls(true) @@ -246,8 +263,11 @@ window.app.component('stall-list', { })) }) }, - customerSelectedForOrder: function (customerPubkey) { - this.$emit('customer-selected-for-order', customerPubkey) + goToProducts: function (stall) { + this.$emit('go-to-products', stall.id) + }, + goToOrders: function (stall) { + this.$emit('go-to-orders', stall.id) }, shortLabel(value = '') { if (value.length <= 64) return value @@ -256,7 +276,7 @@ window.app.component('stall-list', { }, created: async function () { this.stalls = await this.getStalls() - this.currencies = await this.getCurrencies() + this.currencies = this.getCurrencies() this.zoneOptions = await this.getZones() } }) diff --git a/static/js/index.js b/static/js/index.js index 3d89779..b9861ef 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,6 +5,8 @@ window.app = Vue.createApp({ mixins: [window.windowMixin], data: function () { return { + activeTab: 'merchant', + selectedStallFilter: null, merchant: {}, shippingZones: [], activeChatCustomer: '', @@ -212,6 +214,86 @@ window.app = Vue.createApp({ LNbits.utils.notifyApiError(error) } }) + }, + publishNip15: async function () { + try { + const {data: stalls} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/stall?pending=false', + this.g.user.wallets[0].inkey + ) + for (const stall of stalls) { + await LNbits.api.request( + 'PUT', + `/nostrmarket/api/v1/stall/${stall.id}`, + this.g.user.wallets[0].adminkey, + stall + ) + } + // Fetch products from all stalls + let productCount = 0 + for (const stall of stalls) { + const {data: products} = await LNbits.api.request( + 'GET', + `/nostrmarket/api/v1/stall/product/${stall.id}?pending=false`, + this.g.user.wallets[0].inkey + ) + for (const product of products) { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/product/${product.id}`, + this.g.user.wallets[0].adminkey, + product + ) + productCount++ + } + } + this.$q.notify({ + type: 'positive', + message: `Published ${stalls.length} stall(s) and ${productCount} product(s) to Nostr (NIP-15)` + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + refreshNip15: async function () { + LNbits.utils + .confirmDialog('This will sync your stalls and products from Nostr relays. Continue?') + .onOk(async () => { + try { + await LNbits.api.request( + 'PUT', + '/nostrmarket/api/v1/restart', + this.g.user.wallets[0].adminkey + ) + this.$q.notify({ + type: 'positive', + message: 'Refreshing NIP-15 data from Nostr...' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }) + }, + deleteNip15: async function () { + LNbits.utils + .confirmDialog( + 'WARNING: This will delete all your stalls and products from Nostr relays. This cannot be undone! Are you sure?' + ) + .onOk(async () => { + this.$q.notify({ + type: 'info', + message: 'Delete NIP-15 from Nostr not yet implemented' + }) + }) + }, + goToProducts: function (stallId) { + this.selectedStallFilter = stallId + this.activeTab = 'products' + }, + goToOrders: function (stallId) { + this.selectedStallFilter = stallId + this.activeTab = 'orders' } }, created: async function () { diff --git a/templates/nostrmarket/components/merchant-tab.html b/templates/nostrmarket/components/merchant-tab.html new file mode 100644 index 0000000..ef1be39 --- /dev/null +++ b/templates/nostrmarket/components/merchant-tab.html @@ -0,0 +1,96 @@ +
+
+
+
+
+ +
+
+
+ + +
+
+
+ + + Restart the connection to the nostrclient extension + + +
+
+
+
+
+ Hide Keys +
+
+
+
+ +
+
+
+
+
+ + +
Nostr Market Extension
+
+ + + + + + + A decentralized marketplace powered by Nostr and Lightning Network. + Create stalls, add products, and start selling with Bitcoin. + + + + + + + Open Market Client + + + + + +
+
+
+
diff --git a/templates/nostrmarket/components/product-list.html b/templates/nostrmarket/components/product-list.html new file mode 100644 index 0000000..292db52 --- /dev/null +++ b/templates/nostrmarket/components/product-list.html @@ -0,0 +1,289 @@ +
+
+
+ + + +
+
+ + + + + All Stalls + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+ +
No stalls found. Please create a stall first in the Stalls tab.
+
+ + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ + +
+ +
+
+ + +
+ + + + + + +
+
+ +
+ + Create Product + Cancel +
+
+
+
+ + + + +
+ + + + + + + Restore + + +
+
There are no products to be restored.
+
+ Close +
+
+
+
diff --git a/templates/nostrmarket/components/shipping-zones-list.html b/templates/nostrmarket/components/shipping-zones-list.html new file mode 100644 index 0000000..2ff9f28 --- /dev/null +++ b/templates/nostrmarket/components/shipping-zones-list.html @@ -0,0 +1,122 @@ +
+
+
+ +
+
+ + + + + + + + + + + + +
+
+ Update +
+
+ Create Shipping Zone +
+ + Cancel +
+
+
+
+
diff --git a/templates/nostrmarket/components/stall-list.html b/templates/nostrmarket/components/stall-list.html index 673e8a7..36b1db9 100644 --- a/templates/nostrmarket/components/stall-list.html +++ b/templates/nostrmarket/components/stall-list.html @@ -1,43 +1,38 @@
-
-
- - - - New Stall - Create a new stall - - - - - Restore Stall - Restore existing stall from Nostr - - - +
+
-