Cashu: UI upgrades and fixes (#1453)

* Cashu: UI updates and automatic mint activation

* welcome dialog

* welcome dialog

* UI updates
This commit is contained in:
calle
2023-02-05 12:44:56 +01:00
committed by GitHub
parent 9d2008d45c
commit 970a0ce1d0

View File

@@ -3,7 +3,7 @@ endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
page_container %}
<q-page-container>
<q-page>
<div class="row q-col-gutter-md justify-center q-pt-sm q-pb-md">
<div class="row q-col-gutter-y-md justify-center q-pt-sm q-pb-md">
<div class="col-12 col-sm-11 col-md-8 text-center q-gutter-y-md">
<q-card class="q-my-md q-py-sm">
<q-card-section class="q-mt-sm q-py-xs">
@@ -18,11 +18,18 @@ page_container %}
</h3>
</div>
</div>
<div class="row q-mt-sm q-mb-none">
<div class="row q-mt-xs q-mb-none" v-if="mints.length > 0">
<div class="col-12">
<a>
All mints: {% raw %}{{ getTotalBalance }} {{tickerShort}} {%
endraw %}</a
<a class="text-weight-light">
Balance on all mints: {% raw %}
<b>{{ getTotalBalance }} {{tickerShort}} </b>{% endraw %}</a
>
</div>
</div>
<div class="row q-mt-none q-mb-none" v-if="activeMintURL">
<div class="col-12">
<a class="text-weight-light">
Mint: {% raw %}{{ getActiveMintUrlShort()}} {% endraw %}</a
>
</div>
</div>
@@ -35,7 +42,7 @@ page_container %}
<div class="row items-center no-wrap q-mb-sm">
<div class="col-6 col-sm-5 col-md-4 q-px-xs">
<q-btn
size="12px"
size="0.75rem"
rectangle
unelevated
color="primary"
@@ -50,7 +57,7 @@ page_container %}
<div class="col-0 col-sm-2 col-md-4"></div>
<div class="col-6 col-sm-5 col-md-4 q-px-xs">
<q-btn
size="12px"
size="0.75rem"
rectangle
unelevated
align="between"
@@ -85,10 +92,10 @@ page_container %}
<q-list padding>
<q-item>
<q-item-section>
<q-item-label overline>Cashu mints</q-item-label>
<q-item-label overline>Mints</q-item-label>
<q-item-label caption
>You can use use multiple Cashu mints in this wallet.
Add the mint URL and select the mint you want to
>You can connect your wallet to multiple Cashu mints.
Enter a mint URL and select the mint your want to
use.</q-item-label
>
</q-item-section>
@@ -213,14 +220,15 @@ page_container %}
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-badge
size="lg"
color="secondary"
class="q-mr-md cursor-pointer"
<q-icon
name="sync"
size="xs"
color="grey"
class="q-mr-xs cursor-pointer"
@click="checkInvoice(props.row.hash)"
>
Check
</q-badge>
<q-tooltip>Check status</q-tooltip>
</q-icon>
</div>
<div v-if="props.row.status === 'paid'">
<q-icon
@@ -258,6 +266,9 @@ page_container %}
<q-td key="hash" :props="props">
<div>{{props.row.hash}}</div>
</q-td>
<q-td key="mint" :props="props">
<div>{{props.row.mint}}</div>
</q-td>
</q-tr>
</template>
{% endraw %}
@@ -288,14 +299,15 @@ page_container %}
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-badge
size="lg"
color="secondary"
class="q-mr-md cursor-pointer"
<q-icon
name="sync"
size="xs"
color="grey"
class="q-mr-xs cursor-pointer"
@click="checkTokenSpendable(props.row.token)"
>
Check
</q-badge>
<q-tooltip>Check status</q-tooltip>
</q-icon>
</div>
<div v-if="props.row.status === 'paid'">
<q-icon
@@ -344,7 +356,7 @@ page_container %}
<div class="col-4 q-pt-none">
<q-btn
class="full-width gt-sm"
size="14px"
size="1.0rem"
icon-right="bolt"
icon="file_download"
align="between"
@@ -359,16 +371,17 @@ page_container %}
<div align="center">
<q-btn
class="q-mx-xs q-px-none"
size="10px"
size="0.5rem"
rectangle
color="warning"
icon="warning"
outline
@click="showDisclaimerDialog"
></q-btn>
><q-tooltip>Warning</q-tooltip></q-btn
>
<q-btn
class="q-mx-xs q-px-none"
size="10px"
size="0.5rem"
outline
rectangle
color="warning"
@@ -382,7 +395,7 @@ page_container %}
<q-btn
class="full-width gt-sm"
@click="showParseDialog"
size="14px"
size="1.0rem"
icon-right="bolt"
icon="file_upload"
align="between"
@@ -396,6 +409,7 @@ page_container %}
</div>
</div>
<!-- // BOTTOM BAR -->
<div class="q-col-gutter">
<q-tabs
class="lt-md fixed-bottom q-px-none q-py-md left-0 right-0 bg-primary text-white shadow-2 z-top q-px-0"
@@ -410,7 +424,7 @@ page_container %}
>
</q-tab>
<q-tab
class="col-2"
class="col-2 q-pb-md"
icon="photo_camera"
v-if="hasCamera"
@click="showCamera"
@@ -429,14 +443,15 @@ page_container %}
<q-dialog v-model="payInvoiceData.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="payInvoiceData.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
<!-- <h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{
parseFloat(String(payInvoiceData.invoice.fsat).replaceAll(",",
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
"")) / 100 }} {% endraw %} asdasdasd {{LNBITS_DENOMINATION}} {%
raw %}
</h6> -->
<h6 class="q-my-none">
{{ payInvoiceData.invoice.fsat }}{% endraw %}
{{LNBITS_DENOMINATION}} {% raw %}
{{LNBITS_DENOMINATION}} {% raw %} asdasdsd
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
@@ -649,6 +664,66 @@ page_container %}
</q-card>
</q-dialog>
<q-dialog
class="z-top"
persistent
v-model="welcomeDialog.show"
position="top"
>
<q-card class="q-pa-lg z-top">
<q-toolbar>
<q-avatar>
<img
src="https://raw.githubusercontent.com/cashubtc/cashu-ui/main/ui/icons/circle/128x128.png"
/>
</q-avatar>
<q-toolbar-title
><span class="text-weight-bold">Cashu</span>
wallet</q-toolbar-title
>
</q-toolbar>
<q-card-section>
<p>Please take a moment to read the following information.</p>
<p>
<strong>Open this wallet on your device's native browser</strong>
Cashu stores your ecash on your device locally. For the best
experience, use this wallet with your device's native web browser
(for example Safari for iOS, Chrome for Android).
</p>
<p>
<strong>Add to home screen.</strong>
Add Cashu to your home screen as a progressive web app (PWA). On
Android Chrome, click the hamburger menu at the upper right. On
iOS Safari, click the share button. Now press the Add to Home
screen button.
</p>
<p>
<strong>This software is in BETA!</strong> We hold no
responsibility for people losing access to funds. Use at your own
risk! Ecash is a bearer asset, meaning losing access to this
wallet will mean you will lose the funds. This wallet stores ecash
tokens in its database. If you lose the link or delete your your
data without backing up, you will lose your tokens. Press the
Backup button to download a copy of your tokens.
</p>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(baseURL)"
>Copy wallet URL</q-btn
>
<q-btn
v-close-popup
flat
color="primary"
class="q-ml-auto"
@click="setWelcomeDialogSeen()"
>Continue</q-btn
>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Warning</h6>
@@ -668,7 +743,7 @@ page_container %}
Home screen button.
</p>
<p>
<strong>This service is in BETA!</strong> We hold no responsibility
<strong>This software is in BETA!</strong> We hold no responsibility
for people losing access to funds. Use at your own risk!
</p>
<div class="row q-mt-lg">
@@ -706,12 +781,12 @@ page_container %}
class="q-mb-lg"
@keyup.enter="requestMintButton"
></q-input>
<q-input
<!-- <q-input
filled
dense
v-model.trim="invoiceData.memo"
label="Memo"
></q-input>
></q-input> -->
</div>
<div v-else class="text-center q-mb-lg">
<a class="text-secondary" :href="'lightning:' + invoiceData.bolt11">
@@ -770,19 +845,19 @@ page_container %}
class="q-mb-lg"
@keyup.enter="sendTokens"
></q-input>
<q-input
<!-- <q-input
filled
dense
v-model.trim="sendData.memo"
label="Memo"
></q-input>
></q-input> -->
</div>
<div v-else class="text-center q-mb-lg">
<div class="text-center q-mb-lg">
<div class="text-center q-mb-lg" v-if="sendData.tokens.length < 2">
<!-- <a class="text-secondary" :href="'cashu:' + sendData.tokensBase64"> -->
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="walletURL + '&recv_token=' + sendData.tokensBase64"
:value="baseURL + '&recv_token=' + sendData.tokensBase64"
:options="{width: 340}"
class="rounded-borders"
>
@@ -818,7 +893,8 @@ page_container %}
>
<q-btn
color="primary"
@click="copyText(walletURL + '&recv_token=' + sendData.tokensBase64)"
outline
@click="copyText(baseURL + '&recv_token=' + sendData.tokensBase64)"
>Copy link</q-btn
>
</div>
@@ -851,12 +927,6 @@ page_container %}
<div class="row q-mt-lg">
<q-btn @click="redeem" color="primary">Receive</q-btn>
<q-btn
unelevated
icon="content_copy"
class="q-mx-0"
@click="copyText(receiveData.tokensBase64)"
></q-btn>
<q-btn
unelevated
icon="photo_camera"
@@ -878,13 +948,15 @@ page_container %}
controls your funds. Make sure that you trust the operator of this
mint.
</p>
<q-field outlined dense>
<template v-slot:control>
<div class="self-center full-width no-outline" tabindex="0">
{% raw %}{{ mintToAdd }} {% endraw %}
</div>
</template>
</q-field>
<q-input
outlined
readonly
v-model="mintToAdd"
label="Mint URL"
type="textarea"
autogrow
class="q-mb-xs"
></q-input>
<div class="row q-mt-lg">
<q-btn
outline
@@ -1112,6 +1184,13 @@ page_container %}
label: 'Hash',
field: 'hash',
sortable: false
},
{
name: 'mint',
align: 'left',
label: 'Mint',
field: 'mint',
sortable: true
}
],
pagination: {
@@ -1171,6 +1250,9 @@ page_container %}
paymentsChart: {
show: false
},
welcomeDialog: {
show: false
},
disclaimerDialog: {
show: false,
location: window.location,
@@ -1230,6 +1312,7 @@ page_container %}
removeMint: function (url) {
this.mints = this.mints.filter(m => m.url != url)
localStorage.setItem('cashu.mints', JSON.stringify(this.mints))
// todo: we always reset to the first mint, improve this
this.activateMint(this.mints[0].url)
this.notifySuccess('Mint removed.')
},
@@ -1244,7 +1327,26 @@ page_container %}
}
return balance
},
activateMint: async function (url, verbose = false) {
getActiveMintUrlShort: function () {
url = this.activeMintURL.replace('https://', '')
const cut_param = 40
if (url.length > cut_param && url.indexOf('/') != -1) {
url =
url.substring(0, url.indexOf('/') + 1) +
'...' +
url.substring(url.length - cut_param / 2, url.length)
}
return url
},
activateMint: async function (url, verbose = false, stop_workers = true) {
if (url == this.activeMintURL) {
return
}
if (stop_workers) {
// we need to stop workers because they will reset the activeMint again
this.clearAllWorkers()
}
let presiouvURL = this.activeMintURL
try {
this.activeMintURL = url
@@ -1253,13 +1355,13 @@ page_container %}
this.activeProofs = this.proofs.filter(p =>
this.keysets.includes(p.id)
)
if (verbose) {
this.notifySuccess('Mint added.')
}
console.log('### activateMint: Mint activated: ', this.activeMintURL)
} catch (error) {
this.activeMintURL = presiouvURL
this.notifyError('Could not add mint.')
this.notifyError('Could not connect to mint.')
throw error
}
},
@@ -1324,6 +1426,18 @@ page_container %}
this.camera.show = false
this.focusInput('pasteInput')
},
showWelcomeDialog: function () {
if (localStorage.getItem('cashu.welcomeDialogSeen') != 'seen') {
this.welcomeDialog.show = true
}
},
setWelcomeDialogSeen: function () {
localStorage.setItem('cashu.welcomeDialogSeen', 'seen')
// kick of notification that no mints are present
// we have to do this because we disabled this notification since it
// covers the dialog
this.showNoMintsWarning()
},
showDisclaimerDialog: function () {
this.disclaimerDialog.show = true
},
@@ -1391,9 +1505,9 @@ page_container %}
// } else if (req.indexOf('cashu:') !== 1) {
// this.receiveData.tokensBase64 = req.slice(req.indexOf('cashu:'))
// reqtype = 'cashu'
} else if (req.indexOf('W3siaWQ') !== 1) {
} else if (req.indexOf('eyJwcm') !== 1) {
// very dirty way of parsing cashu tokens
this.receiveData.tokensBase64 = req.slice(req.indexOf('W3siaWQ'))
this.receiveData.tokensBase64 = req.slice(req.indexOf('eyJwcm'))
reqtype = 'cashu'
}
@@ -1532,11 +1646,10 @@ page_container %}
this.invoiceCheckWorker()
},
showTokenDialog: function (token) {
showTokenDialog: function (tokensBase64) {
console.log('##### showTokenDialog')
// TODO: this must be decoded and desiarlized!
this.sendData.tokens = _.clone(token)
this.sendData.tokensBase64 = _.clone(token)
this.sendData.tokens = JSON.parse(atob(tokensBase64)).proofs
this.sendData.tokensBase64 = _.clone(tokensBase64)
this.showSendTokens = true
// kick off token check worker
this.checkTokenSpendableWorker()
@@ -1672,7 +1785,8 @@ page_container %}
this.invoicesCashu.push({
..._.clone(this.invoiceData),
date: currentDateStr(),
status: 'pending'
status: 'pending',
mint: this.activeMintURL
})
this.storeinvoicesCashu()
this.tab = 'invoices'
@@ -2030,7 +2144,8 @@ page_container %}
hash: this.payInvoiceData.data.hash,
memo: this.payInvoiceData.data.memo,
date: currentDateStr(),
status: 'paid'
status: 'paid',
mint: this.activeMintURL
})
this.storeinvoicesCashu()
this.tab = 'invoices'
@@ -2170,10 +2285,13 @@ page_container %}
console.log('### checkInvoice.hash', payment_hash)
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
try {
if (invoice.mint != null) {
this.activateMint(invoice.mint, false, false)
}
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
return proofs
} catch (error) {
console.log('Invoice still pending')
console.log('Invoice still pending', invoice.hash)
throw error
}
},
@@ -2208,8 +2326,14 @@ page_container %}
checks whether a base64-encoded token (from the history table) has been spent already.
if it is spent, the appropraite entry in the history table is set to paid.
*/
const tokenJson = atob(token)
const proofs = JSON.parse(tokenJson).proofs
const tokenJson = JSON.parse(atob(token))
const proofs = tokenJson.proofs
if (tokenJson.mints != null && tokenJson.mints[0].url != null) {
// todo: we activate only the first mint in the token
this.activateMint(tokenJson.mints[0].url)
}
const spendable = await this.checkProofsSpendable(proofs)
let paid = false
if (spendable.includes(false)) {
@@ -2224,7 +2348,7 @@ page_container %}
if (verbose) {
this.notify('Token still pending', (color = 'grey'))
}
this.sendData.tokens = token
// this.sendData.tokens = token
}
return paid
},
@@ -2328,12 +2452,30 @@ page_container %}
throw new Error(`Mint error: ${response.error}`)
}
},
showNoMintsWarning: function () {
if (!this.activeMintURL) {
this.walletURL = this.baseURL
// we have to check whether the welcome dialog was pressed away because
// otherwise quasar falsely covers the welcome dialog with this notify
if (localStorage.getItem('cashu.welcomeDialogSeen') == 'seen') {
this.notifyWarning(
'You are not connected to any mints yet.',
'Add a new mint URL in the settings.',
30000
)
}
this.tab = 'settings'
}
},
notifySuccess: async function (message, position = 'top') {
this.$q.notify({
timeout: 5000,
type: 'positive',
message: message,
position: position,
progress: true,
actions: [
{
icon: 'close',
@@ -2349,6 +2491,7 @@ page_container %}
message: message,
caption: caption,
position: 'top',
progress: true,
actions: [
{
icon: 'close',
@@ -2365,10 +2508,11 @@ page_container %}
message: message,
caption: caption,
position: 'top',
progress: true,
actions: [
{
icon: 'close',
color: 'white',
color: 'black',
handler: () => {}
}
]
@@ -2515,15 +2659,7 @@ page_container %}
activeMintURL = localStorage.getItem('cashu.activeMintURL')
this.addMint(activeMintURL)
}
if (!this.activeMintURL) {
this.walletURL = this.baseURL
this.notifyWarning(
'You are not connected to any mints yet.',
'Add a new mint URL in the settings.',
20000
)
this.tab = 'settings'
}
this.showNoMintsWarning()
// todo: remove:
if (!this.mintId.length) {
@@ -2533,6 +2669,8 @@ page_container %}
console.log('Mint URL ' + this.activeMintURL)
console.log('Wallet URL ' + this.walletURL)
const startupMintUrl = this.activeMintURL
// get name
if (params.get('mint_name')) {
this.mintName = params.get('mint_name')
@@ -2608,6 +2746,13 @@ page_container %}
this.checkPendingTokens().catch(err => {
return
})
// reset to the mint from settings after workers have run
if (startupMintUrl != null) {
this.activateMint(startupMintUrl)
}
this.showWelcomeDialog()
}
})
</script>