diff --git a/models.py b/models.py
index f1af073..58842d5 100644
--- a/models.py
+++ b/models.py
@@ -33,8 +33,13 @@ class Nostrable:
class MerchantProfile(BaseModel):
name: str | None = None
+ display_name: str | None = None
about: str | None = None
picture: str | None = None
+ banner: str | None = None
+ website: str | None = None
+ nip05: str | None = None
+ lud16: str | None = None
class MerchantConfig(MerchantProfile):
@@ -86,11 +91,23 @@ class Merchant(PartialMerchant, Nostrable):
return merchant
def to_nostr_event(self, pubkey: str) -> NostrEvent:
- content = {
- "name": self.config.name,
- "about": self.config.about,
- "picture": self.config.picture,
- }
+ content: dict[str, str] = {}
+ if self.config.name:
+ content["name"] = self.config.name
+ if self.config.display_name:
+ content["display_name"] = self.config.display_name
+ if self.config.about:
+ content["about"] = self.config.about
+ if self.config.picture:
+ content["picture"] = self.config.picture
+ if self.config.banner:
+ content["banner"] = self.config.banner
+ if self.config.website:
+ content["website"] = self.config.website
+ if self.config.nip05:
+ content["nip05"] = self.config.nip05
+ if self.config.lud16:
+ content["lud16"] = self.config.lud16
event = NostrEvent(
pubkey=pubkey,
created_at=round(time.time()),
diff --git a/services.py b/services.py
index 4039dbb..b978e39 100644
--- a/services.py
+++ b/services.py
@@ -149,9 +149,8 @@ async def update_merchant_to_nostr(
stall.event_id = event.id
stall.event_created_at = event.created_at
await update_stall(merchant.id, stall)
- if delete_merchant:
- # merchant profile updates not supported yet
- event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
+ # Always publish merchant profile (kind 0)
+ event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
assert event
merchant.config.event_id = event.id
return merchant
diff --git a/static/components/edit-profile-dialog.js b/static/components/edit-profile-dialog.js
new file mode 100644
index 0000000..0736695
--- /dev/null
+++ b/static/components/edit-profile-dialog.js
@@ -0,0 +1,91 @@
+window.app.component('edit-profile-dialog', {
+ name: 'edit-profile-dialog',
+ template: '#edit-profile-dialog',
+ delimiters: ['${', '}'],
+ props: ['model-value', 'merchant-id', 'merchant-config', 'adminkey'],
+ emits: ['update:model-value', 'profile-updated'],
+ data: function () {
+ return {
+ saving: false,
+ formData: {
+ name: '',
+ display_name: '',
+ about: '',
+ picture: '',
+ banner: '',
+ website: '',
+ nip05: '',
+ lud16: ''
+ }
+ }
+ },
+ computed: {
+ show: {
+ get() {
+ return this.modelValue
+ },
+ set(value) {
+ this.$emit('update:model-value', value)
+ }
+ }
+ },
+ methods: {
+ saveProfile: async function () {
+ this.saving = true
+ try {
+ const config = {
+ ...this.merchantConfig,
+ name: this.formData.name || null,
+ display_name: this.formData.display_name || null,
+ about: this.formData.about || null,
+ picture: this.formData.picture || null,
+ banner: this.formData.banner || null,
+ website: this.formData.website || null,
+ nip05: this.formData.nip05 || null,
+ lud16: this.formData.lud16 || null
+ }
+ await LNbits.api.request(
+ 'PATCH',
+ `/nostrmarket/api/v1/merchant/${this.merchantId}`,
+ this.adminkey,
+ config
+ )
+ // Publish to Nostr
+ await LNbits.api.request(
+ 'PUT',
+ `/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
+ this.adminkey
+ )
+ this.show = false
+ this.$q.notify({
+ type: 'positive',
+ message: 'Profile saved and published to Nostr!'
+ })
+ this.$emit('profile-updated')
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ } finally {
+ this.saving = false
+ }
+ },
+ loadFormData: function () {
+ if (this.merchantConfig) {
+ this.formData.name = this.merchantConfig.name || ''
+ this.formData.display_name = this.merchantConfig.display_name || ''
+ this.formData.about = this.merchantConfig.about || ''
+ this.formData.picture = this.merchantConfig.picture || ''
+ this.formData.banner = this.merchantConfig.banner || ''
+ this.formData.website = this.merchantConfig.website || ''
+ this.formData.nip05 = this.merchantConfig.nip05 || ''
+ this.formData.lud16 = this.merchantConfig.lud16 || ''
+ }
+ }
+ },
+ watch: {
+ modelValue(newVal) {
+ if (newVal) {
+ this.loadFormData()
+ }
+ }
+ }
+})
diff --git a/static/components/key-pair.js b/static/components/key-pair.js
deleted file mode 100644
index 5bf9d23..0000000
--- a/static/components/key-pair.js
+++ /dev/null
@@ -1,22 +0,0 @@
-window.app.component('key-pair', {
- name: 'key-pair',
- template: '#key-pair',
- delimiters: ['${', '}'],
- props: ['public-key', 'private-key'],
- data: function () {
- return {
- showPrivateKey: false
- }
- },
- methods: {
- copyText: function (text, message, position) {
- var notify = this.$q.notify
- Quasar.copyToClipboard(text).then(function () {
- notify({
- message: message || 'Copied to clipboard!',
- position: position || 'bottom'
- })
- })
- }
- }
-})
diff --git a/static/components/merchant-tab.js b/static/components/merchant-tab.js
index c82d853..1194420 100644
--- a/static/components/merchant-tab.js
+++ b/static/components/merchant-tab.js
@@ -10,14 +10,46 @@ window.app.component('merchant-tab', {
'merchant-active',
'public-key',
'private-key',
- 'is-admin'
+ 'is-admin',
+ 'merchant-config'
],
+ emits: [
+ 'toggle-show-keys',
+ 'hide-keys',
+ 'merchant-deleted',
+ 'toggle-merchant-state',
+ 'restart-nostr-connection',
+ 'profile-updated',
+ 'import-key',
+ 'generate-key'
+ ],
+ data: function () {
+ return {
+ showEditProfileDialog: false,
+ showKeysDialog: false
+ }
+ },
computed: {
marketClientUrl: function () {
return '/nostrmarket/market'
}
},
methods: {
+ publishProfile: async function () {
+ try {
+ await LNbits.api.request(
+ 'PUT',
+ `/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
+ this.adminkey
+ )
+ this.$q.notify({
+ type: 'positive',
+ message: 'Profile published to Nostr!'
+ })
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
toggleShowKeys: function () {
this.$emit('toggle-show-keys')
},
@@ -27,11 +59,40 @@ window.app.component('merchant-tab', {
handleMerchantDeleted: function () {
this.$emit('merchant-deleted')
},
+ removeMerchant: function () {
+ const name =
+ this.merchantConfig?.display_name ||
+ this.merchantConfig?.name ||
+ 'this merchant'
+ LNbits.utils
+ .confirmDialog(
+ `Are you sure you want to remove "${name}"? This will delete all associated data (stalls, products, orders, messages).`
+ )
+ .onOk(async () => {
+ try {
+ await LNbits.api.request(
+ 'DELETE',
+ `/nostrmarket/api/v1/merchant/${this.merchantId}`,
+ this.adminkey
+ )
+ this.$emit('merchant-deleted')
+ this.$q.notify({
+ type: 'positive',
+ message: 'Merchant removed'
+ })
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ })
+ },
toggleMerchantState: function () {
this.$emit('toggle-merchant-state')
},
restartNostrConnection: function () {
this.$emit('restart-nostr-connection')
+ },
+ handleImageError: function (e) {
+ e.target.style.display = 'none'
}
}
})
diff --git a/static/components/nostr-keys-dialog.js b/static/components/nostr-keys-dialog.js
new file mode 100644
index 0000000..81c451f
--- /dev/null
+++ b/static/components/nostr-keys-dialog.js
@@ -0,0 +1,56 @@
+window.app.component('nostr-keys-dialog', {
+ name: 'nostr-keys-dialog',
+ template: '#nostr-keys-dialog',
+ delimiters: ['${', '}'],
+ props: ['public-key', 'private-key', 'model-value'],
+ emits: ['update:model-value'],
+ data: function () {
+ return {
+ showNsec: false
+ }
+ },
+ computed: {
+ show: {
+ get() {
+ return this.modelValue
+ },
+ set(value) {
+ this.$emit('update:model-value', value)
+ }
+ },
+ npub: function () {
+ if (!this.publicKey) return ''
+ try {
+ return window.NostrTools.nip19.npubEncode(this.publicKey)
+ } catch (e) {
+ return this.publicKey
+ }
+ },
+ nsec: function () {
+ if (!this.privateKey) return ''
+ try {
+ return window.NostrTools.nip19.nsecEncode(this.privateKey)
+ } catch (e) {
+ return this.privateKey
+ }
+ }
+ },
+ methods: {
+ copyText: function (text, message) {
+ var notify = this.$q.notify
+ Quasar.copyToClipboard(text).then(function () {
+ notify({
+ message: message || 'Copied to clipboard!',
+ position: 'bottom'
+ })
+ })
+ }
+ },
+ watch: {
+ modelValue(newVal) {
+ if (!newVal) {
+ this.showNsec = false
+ }
+ }
+ }
+})
diff --git a/static/js/index.js b/static/js/index.js
index 00560df..e2421c2 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -19,6 +19,13 @@ window.app = Vue.createApp({
privateKey: null
}
},
+ generateKeyDialog: {
+ show: false,
+ privateKey: null,
+ nsec: null,
+ npub: null,
+ showNsec: false
+ },
wsConnection: null,
nostrStatus: {
connected: false,
@@ -42,9 +49,18 @@ window.app = Vue.createApp({
}
},
methods: {
- generateKeys: async function () {
+ generateKeys: function () {
const privateKey = nostr.generatePrivateKey()
- await this.createMerchant(privateKey)
+ const publicKey = nostr.getPublicKey(privateKey)
+ this.generateKeyDialog.privateKey = privateKey
+ this.generateKeyDialog.nsec = nostr.nip19.nsecEncode(privateKey)
+ this.generateKeyDialog.npub = nostr.nip19.npubEncode(publicKey)
+ this.generateKeyDialog.showNsec = false
+ this.generateKeyDialog.show = true
+ },
+ confirmGenerateKey: async function () {
+ this.generateKeyDialog.show = false
+ await this.createMerchant(this.generateKeyDialog.privateKey)
},
importKeys: async function () {
this.importKeyDialog.show = false
@@ -56,11 +72,21 @@ window.app = Vue.createApp({
if (privateKey.toLowerCase().startsWith('nsec')) {
privateKey = nostr.nip19.decode(privateKey).data
}
+ // Check if this key is already in use
+ const publicKey = nostr.getPublicKey(privateKey)
+ if (this.merchant?.public_key === publicKey) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'This key is already your current profile'
+ })
+ return
+ }
} catch (error) {
this.$q.notify({
type: 'negative',
message: `${error}`
})
+ return
}
await this.createMerchant(privateKey)
},
diff --git a/templates/nostrmarket/components/edit-profile-dialog.html b/templates/nostrmarket/components/edit-profile-dialog.html
new file mode 100644
index 0000000..a447237
--- /dev/null
+++ b/templates/nostrmarket/components/edit-profile-dialog.html
@@ -0,0 +1,68 @@
+