fix: fiat tracking / updating wallet without reloads (#2891)

This commit is contained in:
Arc 2025-01-22 08:27:05 +00:00 committed by GitHub
parent 8a759c8fc1
commit 273ab9781f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 278 additions and 88 deletions

View file

@ -23,6 +23,7 @@ async def create_wallet(
user=user_id, user=user_id,
adminkey=uuid4().hex, adminkey=uuid4().hex,
inkey=uuid4().hex, inkey=uuid4().hex,
currency=settings.lnbits_default_accounting_currency or "USD",
) )
await (conn or db).insert("wallets", wallet) await (conn or db).insert("wallets", wallet)
return wallet return wallet

View file

@ -23,36 +23,119 @@
<q-card <q-card
:style="$q.screen.lt.md ? { :style="$q.screen.lt.md ? {
background: $q.screen.lt.md ? 'none !important': '' background: $q.screen.lt.md ? 'none !important': ''
, boxShadow: $q.screen.lt.md ? 'none !important': '' , boxShadow: $q.screen.lt.md ? 'none !important': ''
, margin: $q.screen.lt.md && mobileSimple ? 'auto !important': '' , width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
, width: $q.screen.lt.md && mobileSimple ? '90% !important': '' } : ''"
} : ''"
> >
<q-card-section> <q-card-section style="height: 130px">
<h3 class="q-my-none text-no-wrap">
<strong v-text="formattedBalance"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
<lnbits-update-balance
:wallet_id="this.g.wallet.id"
class="q-ml-md"
></lnbits-update-balance>
</h3>
<div class="row"> <div class="row">
<div class="col"> <div class="col-1" style="max-width: 30px">
<div v-if="g.wallet.currency"> <q-btn
v-if="g.fiatTracking"
@click="swapBalancePriority"
style="height: 50px"
class="q-mt-lg"
flat
dense
icon="swap_vert"
></q-btn>
</div>
<div class="col-11">
<div
class="column"
:class="{
'q-pt-sm': g.fiatTracking,
'q-pt-lg': !g.fiatTracking
}"
v-if="!isPrioritySwapped || !g.fiatTracking"
style="height: 100px"
>
<div class="col-7">
<div class="row">
<div class="col-auto">
<div class="text-h3 q-my-none text-no-wrap">
<strong v-text="formattedBalance"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
</div>
</div>
<div class="col-auto">
<lnbits-update-balance
v-if="$q.screen.lt.lg"
:wallet_id="this.g.wallet.id"
:callback="updateBalanceCallback"
:small_btn="true"
></lnbits-update-balance>
</div>
</div>
</div>
<div class="col-2">
<div v-if="g.fiatTracking">
<span
class="text-h5 text-italic"
v-text="formattedFiatAmount"
style="opacity: 0.75"
></span>
</div>
</div>
</div>
<div
class="column"
v-if="isPrioritySwapped && g.fiatTracking"
:class="{
'q-pt-sm': g.fiatTracking,
'q-pt-lg': !g.fiatTracking
}"
style="height: 100px"
>
<div class="col-7">
<div class="row">
<div class="col-auto">
<div
v-if="g.fiatTracking"
class="text-h3 q-my-none text-no-wrap"
>
<strong v-text="formattedFiatAmount"></strong>
</div>
</div>
<div class="col-auto">
<lnbits-update-balance
v-if="$q.screen.lt.lg"
:wallet_id="this.g.wallet.id"
:callback="updateBalanceCallback"
:small_btn="true"
></lnbits-update-balance>
</div>
</div>
</div>
<div class="col-2">
<span
class="text-h5 text-italic"
style="opacity: 0.75"
v-text="formattedBalance + ' {{LNBITS_DENOMINATION}}'"
>
</span>
</div>
</div>
<div
class="absolute-right q-pa-md"
v-if="$q.screen.gt.md && g.fiatTracking"
>
<div class="text-bold text-italic">BTC Price</div>
<span <span
class="text-h5 text-italic" class="text-bold text-italic"
v-text="formattedFiatBalance" v-text="formattedExchange"
style="opacity: 0.75"
></span> ></span>
</div> </div>
</div>
<div class="col">
<q-btn <q-btn
v-if="$q.screen.lt.md"
@click="simpleMobile()" @click="simpleMobile()"
color="primary" color="primary"
class="float-right lt-md" class="q-ml-xl absolute-right"
dense
size="sm" size="sm"
style="height: 20px; margin-top: 75px"
flat flat
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'" :icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
:label="mobileSimple ? $t('more') : $t('less')" :label="mobileSimple ? $t('more') : $t('less')"
@ -65,35 +148,32 @@
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
class="full-width" class="q-mr-md"
@click="showParseDialog" @click="showParseDialog"
:label="$t('paste_request')" :label="$t('paste_request')"
></q-btn> ></q-btn>
</div>
<div class="col">
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
class="full-width" class="q-mr-md"
@click="showReceiveDialog" @click="showReceiveDialog"
:label="$t('create_invoice')" :label="$t('create_invoice')"
></q-btn> ></q-btn>
</div> <q-btn unelevated color="secondary" icon="qr_code_scanner">
<div class="col">
<q-btn
unelevated
color="secondary"
icon="photo_camera"
@click="showCamera"
:label="$t('scan')"
>
<q-tooltip <q-tooltip
><span v-text="$t('camera_tooltip')"></span ><span v-text="$t('camera_tooltip')"></span
></q-tooltip> ></q-tooltip>
</q-btn> </q-btn>
<lnbits-update-balance
v-if="$q.screen.gt.md"
:wallet_id="this.g.wallet.id"
:callback="updateBalanceCallback"
:small_btn="false"
></lnbits-update-balance>
</div> </div>
</div> </div>
</q-card> </q-card>
<q-card <q-card
:style=" :style="
$q.screen.lt.md $q.screen.lt.md
@ -109,7 +189,8 @@
<payment-list <payment-list
:update="updatePayments" :update="updatePayments"
:mobile-simple="mobileSimple" :mobile-simple="mobileSimple"
/> :expand-details="expandDetails"
></payment-list>
</q-card-section> </q-card-section>
</q-card> </q-card>
<div id="hiddenQrCodeContainer" style="display: none"> <div id="hiddenQrCodeContainer" style="display: none">
@ -121,10 +202,7 @@
{% if HIDE_API %} {% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md"> <div class="col-12 col-md-4 q-gutter-y-md">
{% else %} {% else %}
<div <div v-if="!mobileSimple" class="col-12 col-md-5 q-gutter-y-md">
v-if="!mobileSimple || $q.screen.gt.sm"
class="col-12 col-md-5 q-gutter-y-md"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm"> <h6 class="text-subtitle1 q-mt-none q-mb-sm">
@ -215,24 +293,27 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<div style="max-width: 360px"> <div style="max-width: 360px">
<q-select <div class="row">
filled <div class="col">
dense <q-select
clearable filled
v-model="update.currency" dense
type="text" v-model="update.currency"
:label="$t('currency')" type="text"
:options="receive.units.filter((u) => u !== 'sat')" :disable="g.fiatTracking"
></q-select> :options="receive.units.filter((u) => u !== 'sat')"
></q-select>
</div>
<div class="col-auto">
<q-btn
color="primary"
@click="handleFiatTracking()"
:disable="update.currency == ''"
:label="g.fiatTracking ? 'Remove' : 'Add'"
></q-btn>
</div>
</div>
</div> </div>
<q-btn
:disable="!update.name.length"
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_currency')"
@click="updateWallet({ currency: update.currency || '' })"
></q-btn>
<q-btn <q-btn
v-if="g.user.admin" v-if="g.user.admin"
class="absolute-top-right" class="absolute-top-right"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -444,6 +444,9 @@ if (!window.g) {
extensions: [], extensions: [],
user: null, user: null,
wallet: {}, wallet: {},
fiatBalance: 0,
exchangeRate: 0,
fiatTracking: false,
wallets: [], wallets: [],
payments: [], payments: [],
allowedThemes: null, allowedThemes: null,
@ -822,7 +825,12 @@ window.windowMixin = {
await this.checkUsrInUrl() await this.checkUsrInUrl()
this.themeParams() this.themeParams()
this.walletFlip = this.$q.localStorage.getItem('lnbits.walletFlip') this.walletFlip = this.$q.localStorage.getItem('lnbits.walletFlip')
this.mobileSimple = this.$q.localStorage.getItem('lnbits.mobileSimple') if (
this.$q.screen.gt.sm ||
this.$q.localStorage.getItem('lnbits.mobileSimple') == false
) {
this.mobileSimple = false
}
}, },
mounted() { mounted() {
if (this.g.user) { if (this.g.user) {

View file

@ -453,7 +453,7 @@ window.app.component('lnbits-dynamic-chips', {
window.app.component('lnbits-update-balance', { window.app.component('lnbits-update-balance', {
template: '#lnbits-update-balance', template: '#lnbits-update-balance',
mixins: [window.windowMixin], mixins: [window.windowMixin],
props: ['wallet_id'], props: ['wallet_id', 'small_btn'],
computed: { computed: {
denomination() { denomination() {
return LNBITS_DENOMINATION return LNBITS_DENOMINATION

View file

@ -44,7 +44,6 @@ window.WalletPageLogic = {
show: false, show: false,
location: window.location location: window.location
}, },
fiatBalance: 0,
update: { update: {
name: null, name: null,
currency: null currency: null
@ -53,6 +52,9 @@ window.WalletPageLogic = {
adminkeyHidden: true, adminkeyHidden: true,
hasNfc: false, hasNfc: false,
nfcReaderAbortController: null, nfcReaderAbortController: null,
isPrioritySwapped: false,
formattedFiatAmount: 0,
formattedExchange: null,
primaryColor: this.$q.localStorage.getItem('lnbits.primaryColor') primaryColor: this.$q.localStorage.getItem('lnbits.primaryColor')
} }
}, },
@ -64,14 +66,6 @@ window.WalletPageLogic = {
return LNbits.utils.formatSat(this.g.wallet.sat) return LNbits.utils.formatSat(this.g.wallet.sat)
} }
}, },
formattedFiatBalance() {
if (this.fiatBalance) {
return LNbits.utils.formatCurrency(
this.fiatBalance.toFixed(2),
this.g.wallet.currency
)
}
},
canPay() { canPay() {
if (!this.parse.invoice) return false if (!this.parse.invoice) return false
return this.parse.invoice.sat <= this.g.wallet.sat return this.parse.invoice.sat <= this.g.wallet.sat
@ -94,6 +88,17 @@ window.WalletPageLogic = {
} }
}, },
methods: { methods: {
formatFiatAmount(amount, currency) {
this.update.currency = currency
this.formattedFiatAmount = LNbits.utils.formatCurrency(
amount.toFixed(2),
currency
)
this.formattedExchange = LNbits.utils.formatCurrency(
this.g.exchangeRate,
currency
)
},
msatoshiFormat(value) { msatoshiFormat(value) {
return LNbits.utils.formatSat(value / 1000) return LNbits.utils.formatSat(value / 1000)
}, },
@ -145,7 +150,6 @@ window.WalletPageLogic = {
this.g.wallet.sat = this.g.wallet.sat + value this.g.wallet.sat = this.g.wallet.sat + value
}, },
createInvoice() { createInvoice() {
console.log('Creating invoice...')
this.receive.status = 'loading' this.receive.status = 'loading'
if (LNBITS_DENOMINATION != 'sats') { if (LNBITS_DENOMINATION != 'sats') {
this.receive.data.amount = this.receive.data.amount * 100 this.receive.data.amount = this.receive.data.amount * 100
@ -498,7 +502,16 @@ window.WalletPageLogic = {
LNbits.api LNbits.api
.request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data) .request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data)
.then(response => { .then(response => {
this.refreshRoute() this.g.wallet = {...this.g.wallet, ...response.data}
const walletIndex = this.g.user.wallets.findIndex(
wallet => wallet.id === response.data.id
)
if (walletIndex !== -1) {
this.g.user.wallets[walletIndex] = {
...this.g.user.wallets[walletIndex],
...response.data
}
}
Quasar.Notify.create({ Quasar.Notify.create({
message: 'Wallet and user updated.', message: 'Wallet and user updated.',
type: 'positive', type: 'positive',
@ -527,20 +540,35 @@ window.WalletPageLogic = {
}) })
}) })
}, },
updateFiatBalance() { updateFiatBalance(currency) {
if (!this.g.wallet.currency) return 0 // set rate from local storage to avoid clunky api calls
if (this.$q.localStorage.getItem('lnbits.exchangeRate.' + currency)) {
this.g.exchangeRate = this.$q.localStorage.getItem(
'lnbits.exchangeRate.' + currency
)
this.g.fiatBalance =
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
this.formatFiatAmount(this.g.fiatBalance, currency)
}
LNbits.api LNbits.api
.request('POST', `/api/v1/conversion`, null, { .request('GET', `/api/v1/rate/` + currency, null)
amount: this.g.wallet.sat,
to: this.g.wallet.currency
})
.then(response => { .then(response => {
this.fiatBalance = response.data[this.g.wallet.currency] if (this.g.wallet.currency == currency) {
this.g.fiatBalance =
(response.data.price / 100000000) * this.g.wallet.sat
this.g.exchangeRate = response.data.price.toFixed(2)
this.g.fiatTracking = true
this.formatFiatAmount(this.g.fiatBalance, this.g.wallet.currency)
this.$q.localStorage.set(
'lnbits.exchangeRate.' + currency,
this.g.exchangeRate
)
}
}) })
.catch(e => console.error(e)) .catch(e => console.error(e))
}, },
pasteToTextArea() { pasteToTextArea() {
this.$refs.textArea.focus() // Set cursor to textarea this.$refs.textArea.focus()
navigator.clipboard.readText().then(text => { navigator.clipboard.readText().then(text => {
this.parse.data.request = text.trim() this.parse.data.request = text.trim()
}) })
@ -641,11 +669,37 @@ window.WalletPageLogic = {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
}, },
swapBalancePriority() {
this.isPrioritySwapped = !this.isPrioritySwapped
this.$q.localStorage.setItem(
'lnbits.isPrioritySwapped',
this.isPrioritySwapped
)
},
handleFiatTracking() {
this.g.fiatTracking = !this.g.fiatTracking
if (!this.g.fiatTracking) {
this.$q.localStorage.setItem('lnbits.isPrioritySwapped', false)
this.isPrioritySwapped = false
this.update.currency = ''
this.g.wallet.currency = ''
this.updateWallet({currency: ''})
} else {
this.g.wallet.currency = this.update.currency
this.updateWallet({currency: this.update.currency})
this.updateFiatBalance(this.update.currency)
}
},
createdTasks() { createdTasks() {
this.update.name = this.g.wallet.name this.update.name = this.g.wallet.name
this.update.currency = this.g.wallet.currency
this.receive.units = ['sat', ...window.currencies] this.receive.units = ['sat', ...window.currencies]
this.updateFiatBalance() if (this.g.wallet.currency != '') {
this.g.fiatTracking = true
this.updateFiatBalance(this.g.wallet.currency)
} else {
this.update.currency = ''
this.g.fiatTracking = false
}
} }
}, },
created() { created() {
@ -656,9 +710,6 @@ window.WalletPageLogic = {
this.decodeRequest() this.decodeRequest()
this.parse.show = true this.parse.show = true
} }
if (this.$q.screen.lt.md) {
this.mobileSimple = true
}
this.createdTasks() this.createdTasks()
}, },
watch: { watch: {
@ -668,7 +719,19 @@ window.WalletPageLogic = {
this.receive.show = false this.receive.show = false
this.receive.paymentHash = null this.receive.paymentHash = null
} }
this.updateFiatBalance() if (
this.g.wallet.currency &&
this.$q.localStorage.getItem(
'lnbits.exchangeRate.' + this.g.wallet.currency
)
) {
this.g.exchangeRate = this.$q.localStorage.getItem(
'lnbits.exchangeRate.' + this.g.wallet.currency
)
this.g.fiatBalance =
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
this.formatFiatAmount(this.g.fiatBalance, this.g.wallet.currency)
}
}, },
'$q.screen.gt.sm'(value) { '$q.screen.gt.sm'(value) {
if (value == true) { if (value == true) {
@ -683,13 +746,19 @@ window.WalletPageLogic = {
} }
}, },
mounted() { mounted() {
// show disclaimer
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) { if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true) this.$q.localStorage.set('lnbits.disclaimerShown', true)
// Turn on payment reactions by default
this.$q.localStorage.set('lnbits.reactions', 'confettiTop') this.$q.localStorage.set('lnbits.reactions', 'confettiTop')
} }
if (this.$q.localStorage.getItem('lnbits.isPrioritySwapped')) {
this.isPrioritySwapped = this.$q.localStorage.getItem(
'lnbits.isPrioritySwapped'
)
} else {
this.isPrioritySwapped = false
this.$q.localStorage.setItem('lnbits.isPrioritySwapped', false)
}
} }
} }

View file

@ -518,7 +518,38 @@
</template> </template>
<template id="lnbits-update-balance"> <template id="lnbits-update-balance">
<q-btn v-if="admin" :label="$t('credit_debit')" color="secondary" size="sm"> <q-btn
v-if="admin && small_btn"
flat
round
color="primary"
size="sm"
icon="add"
>
<q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit">
<q-input
filled
:label="$t('credit_label', {denomination: denomination})"
v-model="scope.value"
dense
autofocus
@keyup.enter="updateBalance(scope)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
</q-popup-edit>
<q-tooltip v-text="$t('credit_hint')"></q-tooltip>
</q-btn>
<q-btn
v-if="admin && !small_btn"
color="primary"
:label="$t('credit_debit')"
class="float-right q-mt-sm"
size="sm"
>
<q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit"> <q-popup-edit class="bg-accent text-white" v-slot="scope" v-model="credit">
<q-input <q-input
filled filled