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:
parent
f466559b51
commit
7aec14854c
6 changed files with 113 additions and 40 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 & 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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue