feat: Shared Wallets/Joint Accounts (Issue #3297) (#3376)

This commit is contained in:
Ben Weeks
2025-11-07 22:25:03 +02:00
committed by GitHub
parent bd07f7a5ef
commit b54eedee84
31 changed files with 2078 additions and 163 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -52,9 +52,17 @@ window.localisation.en = {
stored_paylinks: 'Stored LNURL pay links',
wallet: 'Wallet: ',
wallet_name: 'Wallet name',
wallet_type: 'Wallet type',
shared_wallet: 'Shared Wallet',
share_wallet: 'Share Wallet',
update_permissions: 'Update Permissions',
shared_wallet_id: 'Shared Wallet ID',
shared_wallet_desc:
"You have been invited to have access to someone else's wallet.",
wallets: 'Wallets',
exclude_wallets: 'Exclude Wallets',
add_wallet: 'Add wallet',
reject_wallet: 'Reject wallet',
add_new_wallet: 'Add a new wallet',
pin_wallet: 'Pin wallet',
delete_wallet: 'Delete wallet',
+4 -2
View File
@@ -123,9 +123,11 @@ window._lnbitsApi = {
getWallet(wallet) {
return this.request('get', '/api/v1/wallet', wallet.inkey)
},
createWallet(wallet, name) {
createWallet(wallet, name, walletType, ops = {}) {
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
name: name
name: name,
wallet_type: walletType,
...ops
}).then(res => {
window.location = '/wallet?wal=' + res.data.id
})
+12 -1
View File
@@ -52,22 +52,33 @@ window.LNbits = {
0,
data.wallets.length - data.extra.visible_wallet_count
)
obj.walletInvitesCount = data.extra.wallet_invite_requests?.length || 0
return obj
},
wallet(data) {
newWallet = {
id: data.id,
name: data.name,
walletType: data.wallet_type,
sharePermissions: data.share_permissions,
sharedWalletId: data.shared_wallet_id,
adminkey: data.adminkey,
inkey: data.inkey,
currency: data.currency,
extra: data.extra
extra: data.extra,
canReceivePayments: true,
canSendPayments: true
}
newWallet.msat = data.balance_msat
newWallet.sat = Math.floor(data.balance_msat / 1000)
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
newWallet.sat
)
if (newWallet.walletType === 'lightning-shared') {
const perms = newWallet.sharePermissions
newWallet.canReceivePayments = perms.includes('receive-payments')
newWallet.canSendPayments = perms.includes('send-payments')
}
newWallet.url = `/wallet?&wal=${data.id}`
return newWallet
},
+1 -2
View File
@@ -29,14 +29,13 @@ window.app.component('lnbits-wallet-list', {
return {
activeWallet: null,
balance: 0,
showForm: false,
walletName: '',
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
methods: {
createWallet() {
LNbits.api.createWallet(this.g.user.wallets[0], this.walletName)
this.$emit('wallet-action', {action: 'create-wallet'})
}
},
created() {
@@ -0,0 +1,74 @@
window.app.component('lnbits-new-user-wallet', {
props: ['form-data'],
template: '#lnbits-new-user-wallet',
mixins: [window.windowMixin],
data() {
return {
newWallet: {walletType: 'lightning', name: '', sharedWalletId: ''}
}
},
methods: {
async submitRejectWalletInvitation() {
try {
const inviteRequests = this.g.user.extra.wallet_invite_requests || []
const invite = inviteRequests.find(
invite => invite.to_wallet_id === this.newWallet.sharedWalletId
)
if (!invite) {
Quasar.Notify.create({
message: 'Cannot find invitation for the selected wallet.',
type: 'warning'
})
return
}
await LNbits.api.request(
'DELETE',
`/api/v1/wallet/share/invite/${invite.request_id}`,
this.g.wallet.adminkey
)
Quasar.Notify.create({
message: 'Invitation rejected.',
type: 'positive'
})
this.g.user.extra.wallet_invite_requests = inviteRequests.filter(
inv => inv.request_id !== invite.request_id
)
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
async submitAddWallet() {
console.log('### submitAddWallet', this.newWallet)
const data = this.newWallet
if (data.walletType === 'lightning' && !data.name) {
this.$q.notify({
message: 'Please enter a name for the wallet',
color: 'warning'
})
return
}
if (data.walletType === 'lightning-shared' && !data.sharedWalletId) {
this.$q.notify({
message: 'Missing a shared wallet ID',
color: 'warning'
})
return
}
try {
await LNbits.api.createWallet(
this.g.user.wallets[0],
data.name,
data.walletType,
{
shared_wallet_id: data.sharedWalletId
}
)
} catch (e) {
console.warn(e)
LNbits.utils.notifyApiError(e)
}
}
}
})
+5 -1
View File
@@ -6,7 +6,7 @@ window.PageWallets = {
user: null,
tab: 'wallets',
wallets: [],
showAddWalletDialog: {show: false},
addWalletDialog: {show: false},
walletsTable: {
columns: [
{
@@ -73,6 +73,10 @@ window.PageWallets = {
this.walletsTable.loading = false
}
},
showNewWalletDialog() {
this.addWalletDialog = {show: true, walletType: 'lightning'}
this.showAddNewWalletDialog() // from base.js
},
goToWallet(walletId) {
window.location = `/wallet?wal=${walletId}`
+89 -1
View File
@@ -113,6 +113,7 @@ window.WalletPageLogic = {
walletBalanceChart: null,
inkeyHidden: true,
adminkeyHidden: true,
walletIdHidden: true,
hasNfc: false,
nfcReaderAbortController: null,
isFiatPriority: false,
@@ -125,7 +126,16 @@ window.WalletPageLogic = {
showBalanceInOut: true,
showPaymentCountInOut: true
},
paymentsFilter: {}
paymentsFilter: {},
permissionOptions: [
{label: 'View', value: 'view-payments'},
{label: 'Receive', value: 'receive-payments'},
{label: 'Send', value: 'send-payments'}
],
walletShareInvite: {
unsername: '',
permissions: []
}
}
},
computed: {
@@ -168,6 +178,21 @@ window.WalletPageLogic = {
wallet() {
return this.g.wallet
},
walletApprovedShares() {
return this.g.wallet.extra.shared_with.filter(
s => s.status === 'approved'
)
},
walletPendingRequests() {
return this.g.wallet.extra.shared_with.filter(
s => s.status === 'request_access'
)
},
walletPendingInvites() {
return this.g.wallet.extra.shared_with.filter(
s => s.status === 'invite_sent'
)
},
hasChartActive() {
return (
this.chartConfig.showBalance ||
@@ -675,6 +700,69 @@ window.WalletPageLogic = {
LNbits.utils.notifyApiError(err)
})
},
async updateSharePermissions(permission) {
try {
const {data} = await LNbits.api.request(
'PUT',
'/api/v1/wallet/share',
this.g.wallet.adminkey,
permission
)
Object.assign(permission, data)
Quasar.Notify.create({
message: 'Wallet permission updated.',
type: 'positive'
})
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
async inviteUserToWallet() {
try {
const {data} = await LNbits.api.request(
'PUT',
'/api/v1/wallet/share/invite',
this.g.wallet.adminkey,
{
...this.walletShareInvite,
status: 'invite_sent',
wallet_id: this.g.wallet.id
}
)
this.g.wallet.extra.shared_with.push(data)
this.walletShareInvite = {username: '', permissions: []}
Quasar.Notify.create({
message: 'User invited to wallet.',
type: 'positive'
})
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
deleteSharePermission(permission) {
LNbits.utils
.confirmDialog('Are you sure you want to remove this share permission?')
.onOk(async () => {
try {
await LNbits.api.request(
'DELETE',
`/api/v1/wallet/share/${permission.request_id}`,
this.g.wallet.adminkey
)
this.g.wallet.extra.shared_with =
this.g.wallet.extra.shared_with.filter(
value => value.wallet_id !== permission.wallet_id
)
Quasar.Notify.create({
message: 'Wallet permission deleted.',
type: 'positive'
})
} catch (err) {
LNbits.utils.notifyApiError(err)
}
})
},
deleteWallet() {
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
+14 -16
View File
@@ -6,7 +6,8 @@ window.windowMixin = {
toggleSubs: true,
mobileSimple: true,
walletFlip: true,
showAddWalletDialog: {show: false},
addWalletDialog: {show: false, walletType: 'lightning'},
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
isUserAuthorized: false,
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
@@ -46,23 +47,14 @@ window.windowMixin = {
path: '/wallets'
})
},
submitAddWallet() {
if (
this.showAddWalletDialog.name &&
this.showAddWalletDialog.name.length > 0
) {
LNbits.api.createWallet(
this.g.user.wallets[0],
this.showAddWalletDialog.name
)
this.showAddWalletDialog = {show: false}
} else {
this.$q.notify({
message: 'Please enter a name for the wallet',
color: 'negative'
})
handleWalletAction(payload) {
if (payload.action === 'create-wallet') {
this.showAddNewWalletDialog()
}
},
showAddNewWalletDialog() {
this.addWalletDialog = {show: true, walletType: 'lightning'}
},
simpleMobile() {
this.$q.localStorage.set('lnbits.mobileSimple', !this.mobileSimple)
this.refreshRoute()
@@ -326,6 +318,12 @@ window.windowMixin = {
if (window.user) {
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
}
if (this.g.user?.extra?.wallet_invite_requests?.length) {
this.walletTypes.push({
label: `Lightning Wallet (Share Invite: ${this.g.user.extra.wallet_invite_requests.length})`,
value: 'lightning-shared'
})
}
if (window.wallet) {
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
}
+1
View File
@@ -63,6 +63,7 @@
"js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-library.js",
"js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-new-user-wallet.js",
"js/components/lnbits-qrcode.js",
"js/components/lnbits-qrcode-lnurl.js",
"js/components/extension-settings.js",