feat: add merchant profile edit, keys dialog, and publish to Nostr

- Add PATCH endpoint for updating merchant profile config
- Add website field to MerchantProfile model
- Fix to_nostr_event to include all profile fields (display_name, banner, website, nip05, lud16)
- Always publish merchant profile (kind 0) when publishing to Nostr
- Extract edit-profile-dialog and nostr-keys-dialog into separate components
- Fix profile avatar alignment to match original design
- Simplify keys dialog to show only npub QR code (no nsec QR)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ben Weeks 2025-12-24 12:42:03 +00:00
parent c3dea9f01d
commit a21b5289c1
12 changed files with 635 additions and 109 deletions

View file

@ -0,0 +1,85 @@
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
)
this.show = false
this.$q.notify({
type: 'positive',
message: 'Profile updated!'
})
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()
}
}
}
})

View file

@ -1,11 +0,0 @@
window.app.component('key-pair', {
name: 'key-pair',
template: '#key-pair',
delimiters: ['${', '}'],
props: ['public-key', 'private-key', 'merchant-config'],
methods: {
handleImageError: function (event) {
event.target.style.display = 'none'
}
}
})

View file

@ -13,12 +13,41 @@ window.app.component('merchant-tab', {
'is-admin',
'merchant-config'
],
emits: [
'toggle-show-keys',
'hide-keys',
'merchant-deleted',
'toggle-merchant-state',
'restart-nostr-connection',
'profile-updated'
],
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')
},
@ -33,6 +62,9 @@ window.app.component('merchant-tab', {
},
restartNostrConnection: function () {
this.$emit('restart-nostr-connection')
},
handleImageError: function (e) {
e.target.style.display = 'none'
}
}
})

View file

@ -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
}
}
}
})