Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding NDEF capability #239

Merged
merged 28 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a7e8742
NFC tags read/flash
lollerfirst Oct 4, 2024
fa69c6a
better specify record type + fix error handdler
lollerfirst Oct 8, 2024
19d3219
mime type text/plain
lollerfirst Oct 8, 2024
a868ef6
fix error handler
lollerfirst Oct 8, 2024
49953cd
extract and add mint from token
lollerfirst Oct 12, 2024
6872f59
disable button if NDEF unsupported.
lollerfirst Oct 12, 2024
78ee630
ndefSupported as a var instead of func
lollerfirst Oct 15, 2024
ab2a743
tokenURL
lollerfirst Oct 18, 2024
ef715f8
fix broken index search
lollerfirst Oct 18, 2024
ab4fd42
move knowThisMintOfTokenJson to mintsStore
lollerfirst Oct 18, 2024
f39eb7f
remove pointless loop in knowThisMintOfTokenJson
lollerfirst Oct 21, 2024
6dc1518
Merge remote-tracking branch 'upstream/main' into nfc
lollerfirst Nov 23, 2024
93b84ea
post-merge fixups
lollerfirst Nov 23, 2024
8e613be
use `#` params for token url
lollerfirst Nov 30, 2024
4a336eb
Merge remote-tracking branch 'upstream/main' into nfc
lollerfirst Nov 30, 2024
47fa120
Update src/pages/WalletPage.vue
lollerfirst Dec 12, 2024
ce43f4b
move button to send dialog
lollerfirst Dec 12, 2024
7bb2116
encoding settings
lollerfirst Dec 13, 2024
29217be
remove serial print
lollerfirst Dec 13, 2024
521cef6
better UI for NFC errors
prusnak Dec 13, 2024
32eaf0e
Merge pull request #1 from prusnak/nfc
lollerfirst Dec 13, 2024
d838db9
nfc: implement reading/writing of text/weburl tokens
prusnak Dec 13, 2024
d5564fd
Merge pull request #2 from prusnak/nfc2
lollerfirst Dec 13, 2024
ba82e00
remove automatic redeem() on receive
lollerfirst Dec 13, 2024
8649b5c
edits
callebtc Dec 15, 2024
1bc506a
NFC ux
callebtc Dec 15, 2024
01ff47a
clean settings
callebtc Dec 15, 2024
3e546ad
clean
callebtc Dec 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 131 additions & 20 deletions src/components/SendTokenDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,7 @@
<template v-slot:loading>
<q-spinner-hourglass />
</template>
</q-btn
>
</q-btn>
<div
v-if="sendData.p2pkPubkey && isValidPubkey(sendData.p2pkPubkey)"
class="row"
Expand Down Expand Up @@ -236,12 +235,12 @@
Size: {{ fragmentLengthLabel }}
</q-btn>
<q-badge
:color="!isV4Token ? 'primary' : 'grey'"
:label="isV4Token ? 'V4' : 'V3'"
class="q-my-sm q-mx-md cursor-pointer"
@click="toggleTokenEncoding"
:outline="isV4Token"
/>
:color="!isV4Token ? 'primary' : 'grey'"
:label="isV4Token ? 'V4' : 'V3'"
class="q-my-sm q-mx-md cursor-pointer"
@click="toggleTokenEncoding"
:outline="isV4Token"
/>
</div>
<q-card-section class="q-pa-sm">
<div class="row justify-center">
Expand Down Expand Up @@ -289,12 +288,37 @@
color="grey"
icon="delete"
size="md"
@click="showDeleteDialog = true"
@click="
showDeleteDialog = true;
closeCardScanner();
"
flat
>
<q-tooltip>Delete from history</q-tooltip>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
<q-btn
:disabled="!ndefSupported"
:loading="scanningCard"
class="q-mx-none"
color="grey"
icon="nfc"
size="md"
@click="writeTokensToCard"
flat
>
<q-tooltip>{{
ndefSupported ? "Flash to NFC card" : "NDEF unsupported"
}}</q-tooltip>
<template v-slot:loading>
<q-spinner />
</template>
</q-btn>
<q-btn
v-close-popup
@click="closeCardScanner"
flat
color="grey"
class="q-ml-auto"
>Close</q-btn
>
</div>
Expand Down Expand Up @@ -356,12 +380,21 @@ import { Buffer } from "buffer";
import { useCameraStore } from "src/stores/camera";
import { useP2PKStore } from "src/stores/p2pk";
import TokenInformation from "components/TokenInformation.vue";
import { getDecodedToken, getEncodedTokenV4, getEncodedToken } from "@cashu/cashu-ts";
import {
getDecodedToken,
getEncodedTokenV4,
getEncodedToken,
} from "@cashu/cashu-ts";

import { mapActions, mapState, mapWritableState } from "pinia";
import ChooseMint from "components/ChooseMint.vue";
import { UR, UREncoder } from "@gandlaf21/bc-ur";

import {
notifyError,
notifySuccess,
notify,
notifyWarning,
} from "src/js/notify.ts";
export default defineComponent({
name: "SendTokenDialog",
mixins: [windowMixin],
Expand Down Expand Up @@ -394,6 +427,8 @@ export default defineComponent({
framentInervalSlow: 500,
fragmentSpeedLabel: "F",
isV4Token: false,
scanningCard: false,
ndefSupported: "NDEFReader" in globalThis,
};
},
computed: {
Expand All @@ -403,7 +438,11 @@ export default defineComponent({
]),
...mapWritableState(useSendTokensStore, ["sendData"]),
...mapWritableState(useCameraStore, ["camera", "hasCamera"]),
...mapState(useUiStore, ["tickerShort", "canPasteFromClipboard", "globalMutexLock"]),
...mapState(useUiStore, [
"tickerShort",
"canPasteFromClipboard",
"globalMutexLock",
]),
...mapState(useMintsStore, [
"activeProofs",
"activeUnit",
Expand Down Expand Up @@ -596,17 +635,13 @@ export default defineComponent({
// if it starts with 'cashuB', it is a v4 token
if (this.sendData.tokensBase64.startsWith("cashuA")) {
try {
this.sendData.tokensBase64 = getEncodedTokenV4(decodedToken)
this.sendData.tokensBase64 = getEncodedTokenV4(decodedToken);
} catch {
console.log("### Could not encode token to V4");
this.sendData.tokensBase64 = getEncodedToken(
decodedToken
);
this.sendData.tokensBase64 = getEncodedToken(decodedToken);
}
} else {
this.sendData.tokensBase64 = getEncodedToken(
decodedToken
);
this.sendData.tokensBase64 = getEncodedToken(decodedToken);
}
},
deleteThisToken: function () {
Expand All @@ -615,6 +650,82 @@ export default defineComponent({
this.showDeleteDialog = false;
this.clearAllWorkers();
},
writeTokensToCard: function () {
if (!this.scanningCard) {
try {
this.ndef = new NDEFReader();
this.controller = new AbortController();
const signal = this.controller.signal;
this.ndef
.scan({ signal })
.then(() => {
console.log("> Scan started");

this.ndef.onreadingerror = (error) => {
console.error(`Cannot read NDEF data! ${error}`);
notifyError("Cannot read data from the NFC tag");
this.controller.abort();
this.scanningCard = false;
};

this.ndef.onreading = ({ message, serialNumber }) => {
console.log(`Read card ${serialNumber}`);
notify(`Serial: ${serialNumber}`);
this.controller.abort();
this.scanningCard = false;
try {
const tokenURL =
window.location.toString() +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether we should use this or just hardcode wallet.cashu.me here

Both approaches have their advantages and disadvantages. What do you think @callebtc?

"?token=" +
this.sendData.tokensBase64;
this.ndef
.write(
{
records: [
{
recordType: "url",
data: tokenURL,
},
],
},
{
overwrite: true,
}
)
.then(() => {
console.log("Successfully flashed tokens to card!");
notifySuccess("Successfully flashed tokens to card!");
this.showSendTokens = false;
})
.catch((err) => {
console.error(`Argh! ${err}`);
notifyError(`Argh! ${err}`);
});
} catch (err) {
console.error(`Argh! ${err}`);
notifyError(`Argh! ${err}`);
}
};
this.scanningCard = true;
})
.catch((error) => {
console.error(`Argh! ${error}`);
notifyError(`Argh! ${error}`);
this.scanningCard = false;
});
notifyWarning("THIS WILL OVERWRITE YOUR CARD!");
} catch (error) {
console.error(`Argh! ${error}`);
notifyError(`Argh! ${error}`);
this.scanningCard = false;
}
}
},
closeCardScanner: function () {
console.log("Closing scanner!");
this.controller.abort();
this.scanningCard = false;
},
lockTokens: async function () {
let sendAmount = this.sendData.amount;
// if unit is USD, multiply by 100
Expand Down
128 changes: 116 additions & 12 deletions src/pages/WalletPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
<div class="col-12 col-sm-11 col-md-8 text-center q-gutter-y-md">
<NoMintWarnBanner v-if="mints.length == 0" />
<BalanceView v-else :set-tab="setTab" />
<div
class="row items-center justify-center no-wrap q-mb-none q-mx-none q-px-none q-pt-lg q-pb-md"
>
<div class="col-2 q-mb-md q-mx-none">
<q-btn
align="center"
size="lg"
icon="qr_code_scanner"
outline
color="primary"
flat
@click="showCamera"
/>
</div>
<div class="col-2 q-mb-md q-mx-none">
<q-btn
align="center"
:disabled="!ndefSupported"
:loading="scanningCard"
size="lg"
icon="nfc"
outline
color="primary"
flat
@click="toggleScanner"
>
<q-tooltip>{{
ndefSupported ? "Read from NFC card" : "NDEF unsupported"
}}</q-tooltip>
<template v-slot:loading>
<q-spinner @click="toggleScanner"> </q-spinner>
</template>
</q-btn>
</div>
</div>
<div
class="row items-center justify-center no-wrap q-mb-none q-mx-none q-px-none q-pt-lg q-pb-md"
>
Expand All @@ -20,17 +55,6 @@
Receive</q-btn
>
</div>
<div class="col-2 q-mb-md q-mx-none">
<q-btn
align="center"
size="lg"
icon="qr_code_scanner"
outline
color="primary"
flat
@click="showCamera"
/>
</div>
<!-- button to showSendDialog -->
<div class="col-5 q-mb-md">
<q-btn
Expand Down Expand Up @@ -212,8 +236,8 @@ import { useCameraStore } from "src/stores/camera";
import { useP2PKStore } from "src/stores/p2pk";
import { useNWCStore } from "src/stores/nwc";
import { useNPCStore } from "src/stores/npubcash";

import ReceiveTokenDialog from "src/components/ReceiveTokenDialog.vue";
import { notifyError, notify } from "src/js/notify.ts";

export default {
mixins: [windowMixin],
Expand Down Expand Up @@ -267,6 +291,8 @@ export default {
baseURL: location.protocol + "//" + location.host + location.pathname,
credit: 0,
newName: "",
scanningCard: false,
ndefSupported: "NDEFReader" in globalThis,
};
},
computed: {
Expand Down Expand Up @@ -352,10 +378,88 @@ export default {
"checkPendingTokens",
"decodeRequest",
"generateNewMnemonic",
"redeem",
]),
...mapActions(useCameraStore, ["closeCamera", "showCamera"]),
...mapActions(useNWCStore, ["listenToNWCCommands"]),
...mapActions(useNPCStore, ["generateNPCConnection", "claimAllTokens"]),
knowThisMintOfTokenJson: function (tokenJson) {
const mintStore = useMintsStore();
// check if we have all mints
for (var i = 0; i < tokenJson.token.length; i++) {
if (
!mintStore.mints.map((m) => m.url).includes(token.getMint(tokenJson))
) {
return false;
}
}
return true;
},
lollerfirst marked this conversation as resolved.
Show resolved Hide resolved
toggleScanner: function () {
if (!this.scanningCard) {
try {
this.ndef = new window.NDEFReader();
this.controller = new AbortController();
const signal = this.controller.signal;
this.ndef
.scan({ signal })
.then(() => {
console.log("> Scan started");

this.ndef.addEventListener("readingerror", () => {
console.error("Argh! Cannot read data from the NFC tag.");
notifyError("Argh! Cannot read data from the NFC tag.");
this.controller.abort();
this.scanningCard = false;
});

this.ndef.addEventListener(
"reading",
({ message, serialNumber }) => {
notify(`Serial: ${serialNumber}`);
try {
const decodedTokenLink = new TextDecoder().decode(
message.records[0].data
);
const cashuIndex = decodedTokenLink.indexOf("cashu");
lollerfirst marked this conversation as resolved.
Show resolved Hide resolved
if (cashuIndex === -1) {
throw new Error("not a cashu token");
}
this.receiveData.tokensBase64 =
decodedTokenLink.substring(cashuIndex);
const tokenJson = token.decode(
this.receiveData.tokensBase64
);
if (tokenJson == undefined) {
throw new Error("unreadable token");
}
if (!this.knowThisMintOfTokenJson(tokenJson)) {
this.addMint({ url: token.getMint(tokenJson) });
}
this.redeem();
} catch (err) {
console.error(`Something went wrong! ${err}`);
notifyError(`Something went wrong! ${err}`);
}
this.controller.abort();
this.scanningCard = false;
}
);
this.scanningCard = true;
})
.catch((error) => {
console.error(`Argh! ${error}`);
notifyError(`Argh! ${error}`);
});
} catch (error) {
console.error(`Argh! ${error}`);
notifyError(`Argh! ${error}`);
}
} else {
this.controller.abort();
this.scanningCard = false;
}
},
// TOKEN METHODS
decodeToken: function (encoded_token) {
try {
Expand Down
Loading