feat: multi-profile support and UI improvements

- Remove backend restriction on one merchant per user
- Add "Generate New Key" dialog with npub/nsec display
- Add "Import Existing Key" option with duplicate check
- Change "Save" to "Save & Publish" in edit profile dialog
- Remove standalone Publish button (now part of Save)
- Add trash icon to saved profile for removal
- Show display_name in saved profiles dropdown
- Hide nsec by default with eye toggle in generate dialog

🤖 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 14:05:18 +00:00
parent f466559b51
commit 7aec14854c
6 changed files with 113 additions and 40 deletions

View file

@ -50,10 +50,16 @@ window.app.component('edit-profile-dialog', {
this.adminkey, this.adminkey,
config config
) )
// Publish to Nostr
await LNbits.api.request(
'PUT',
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
this.adminkey
)
this.show = false this.show = false
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
message: 'Profile updated!' message: 'Profile saved and published to Nostr!'
}) })
this.$emit('profile-updated') this.$emit('profile-updated')
} catch (error) { } catch (error) {

View file

@ -18,6 +18,13 @@ window.app = Vue.createApp({
privateKey: null privateKey: null
} }
}, },
generateKeyDialog: {
show: false,
privateKey: null,
nsec: null,
npub: null,
showNsec: false
},
wsConnection: null, wsConnection: null,
nostrStatus: { nostrStatus: {
connected: false, connected: false,
@ -41,9 +48,18 @@ window.app = Vue.createApp({
} }
}, },
methods: { methods: {
generateKeys: async function () { generateKeys: function () {
const privateKey = nostr.generatePrivateKey() 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 () { importKeys: async function () {
this.importKeyDialog.show = false this.importKeyDialog.show = false
@ -55,11 +71,21 @@ window.app = Vue.createApp({
if (privateKey.toLowerCase().startsWith('nsec')) { if (privateKey.toLowerCase().startsWith('nsec')) {
privateKey = nostr.nip19.decode(privateKey).data 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) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'negative', type: 'negative',
message: `${error}` message: `${error}`
}) })
return
} }
await this.createMerchant(privateKey) await this.createMerchant(privateKey)
}, },

View file

@ -53,8 +53,13 @@
label="Lightning Address (lud16)" label="Lightning Address (lud16)"
></q-input> ></q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="saving" <q-btn
>Save</q-btn unelevated
color="primary"
type="submit"
:loading="saving"
icon="publish"
>Save &amp; Publish</q-btn
> >
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div> </div>

View file

@ -24,13 +24,6 @@
> >
<q-tooltip>Show Keys</q-tooltip> <q-tooltip>Show Keys</q-tooltip>
</q-btn> </q-btn>
<q-btn
outline
color="primary"
@click="publishProfile"
icon="publish"
>Publish</q-btn
>
<q-btn-dropdown <q-btn-dropdown
split split
outline outline
@ -40,14 +33,14 @@
> >
<q-list> <q-list>
<q-item-label header>Saved Profiles</q-item-label> <q-item-label header>Saved Profiles</q-item-label>
<q-item clickable v-close-popup> <q-item>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="check" color="primary"></q-icon> <q-icon name="check" color="primary"></q-icon>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label <q-item-label
><span ><span
v-text="merchantConfig?.name || 'Current Profile'" v-text="merchantConfig?.display_name || merchantConfig?.name || 'Current Profile'"
></span ></span
></q-item-label> ></q-item-label>
<q-item-label <q-item-label
@ -60,12 +53,18 @@
></span> ></span>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> <q-item-section side>
<q-item disable> <q-btn
<q-item-section> flat
<q-item-label caption class="text-grey-6"> dense
Multi-profile support coming soon round
</q-item-label> icon="delete"
color="negative"
size="sm"
@click.stop="removeMerchant"
>
<q-tooltip>Remove profile</q-tooltip>
</q-btn>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator></q-separator> <q-separator></q-separator>
@ -87,23 +86,6 @@
<q-item-label caption>Create a fresh nsec</q-item-label> <q-item-label caption>Create a fresh nsec</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="removeMerchant">
<q-item-section avatar>
<q-icon name="delete" color="negative"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label class="text-negative"
>Remove
<span
v-text="merchantConfig?.display_name || merchantConfig?.name || 'Profile'"
></span
></q-item-label>
<q-item-label caption
>Remove this nPub from the DB</q-item-label
>
</q-item-section>
</q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
<q-btn-dropdown <q-btn-dropdown

View file

@ -408,6 +408,63 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
</div> </div>
<!-- Generate Key Dialog -->
<q-dialog v-model="generateKeyDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<div class="text-h6 q-mb-md">Generate New Key</div>
<div class="q-mb-md">
<div class="text-subtitle2 q-mb-xs">Public Key (npub)</div>
<q-input :model-value="generateKeyDialog.npub" readonly dense outlined>
<template v-slot:append>
<q-btn
flat
dense
icon="content_copy"
@click="copyText(generateKeyDialog.npub, 'npub copied!')"
></q-btn>
</template>
</q-input>
</div>
<div class="q-mb-md">
<div class="text-subtitle2 q-mb-xs text-warning">
<q-icon name="warning" size="xs"></q-icon>
Private Key (nsec)
</div>
<q-input
:model-value="generateKeyDialog.showNsec ? generateKeyDialog.nsec : '••••••••••••••••••••••••••••••••••••••••••••••'"
readonly
dense
outlined
>
<template v-slot:append>
<q-btn
flat
dense
:icon="generateKeyDialog.showNsec ? 'visibility_off' : 'visibility'"
@click="generateKeyDialog.showNsec = !generateKeyDialog.showNsec"
></q-btn>
<q-btn
flat
dense
icon="content_copy"
@click="copyText(generateKeyDialog.nsec, 'nsec copied! Keep it safe!')"
></q-btn>
</template>
</q-input>
<div class="text-caption text-negative q-mt-xs">
<q-icon name="error" size="xs"></q-icon>
Never share your private key!
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" @click="confirmGenerateKey"
>Create Merchant</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-card>
</q-dialog>
</div> </div>
{% endblock%}{% block scripts %} {{ window_vars(user) }} {% endblock%}{% block scripts %} {{ window_vars(user) }}

View file

@ -98,9 +98,6 @@ async def api_create_merchant(
merchant = await get_merchant_by_pubkey(data.public_key) merchant = await get_merchant_by_pubkey(data.public_key)
assert merchant is None, "A merchant already uses this public key" assert merchant is None, "A merchant already uses this public key"
merchant = await get_merchant_for_user(wallet.wallet.user)
assert merchant is None, "A merchant already exists for this user"
merchant = await create_merchant(wallet.wallet.user, data) merchant = await create_merchant(wallet.wallet.user, data)
await create_zone( await create_zone(