From 1f11e5e1dccc50f16b4f8a2a4b669e9100924f56 Mon Sep 17 00:00:00 2001 From: Nicolas Burtey Date: Mon, 12 Feb 2024 15:51:31 -0600 Subject: [PATCH] feat: POC for NFC for Pay --- .../ParsePOSPayment/Receive-Invoice.tsx | 102 +++++++++++++++++- apps/pay/package.json | 1 + pnpm-lock.yaml | 54 +++++++++- 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/apps/pay/components/ParsePOSPayment/Receive-Invoice.tsx b/apps/pay/components/ParsePOSPayment/Receive-Invoice.tsx index 8ba2f355dd2..90492194564 100644 --- a/apps/pay/components/ParsePOSPayment/Receive-Invoice.tsx +++ b/apps/pay/components/ParsePOSPayment/Receive-Invoice.tsx @@ -2,7 +2,7 @@ // TODO: remove eslint-disable, the logic likely needs to be reworked import copy from "copy-to-clipboard" import { useRouter } from "next/router" -import React, { useCallback } from "react" +import React, { useCallback, useState } from "react" import Image from "react-bootstrap/Image" import OverlayTrigger from "react-bootstrap/OverlayTrigger" import Tooltip from "react-bootstrap/Tooltip" @@ -17,6 +17,8 @@ import { ACTION_TYPE } from "../../pages/_reducer" import PaymentOutcome from "../PaymentOutcome" import { Share } from "../Share" +import { getParams } from "js-lnurl" + import { safeAmount } from "../../utils/utils" import styles from "./parse-payment.module.css" @@ -210,6 +212,99 @@ function ReceiveInvoice({ recipientWalletCurrency, walletId, state, dispatch }: } } + const [nfcMessage, setNfcMessage] = useState("") + + const decodeNDEFRecord = (record) => { + // Ensure that the record's data is an instance of ArrayBuffer + if (record.data instanceof ArrayBuffer) { + const decoder = new TextDecoder(record.encoding || "utf-8") + return decoder.decode(record.data) + } else { + // If it's not an ArrayBuffer, it might be a DataView or another typed array. + // In that case, we can create a new Uint8Array from the buffer of the DataView. + const decoder = new TextDecoder(record.encoding || "utf-8") + return decoder.decode(new Uint8Array(record.data.buffer)) + } + } + + const handleNFCScan = () => { + if ("NDEFReader" in window) { + const ndef = new NDEFReader() + ndef + .scan() + .then(() => { + console.log("NFC scan started successfully.") + + ndef.onreading = (event) => { + console.log("NFC tag read.") + const record = event.message.records[0] + const text = decodeNDEFRecord(record) + + if (text.toLowerCase().includes("lnurl")) { + setNfcMessage(text) + // Handle your "lnurl" logic here... + } + } + + ndef.onreadingerror = () => { + console.log("Cannot read data from the NFC tag. Try another one?") + } + }) + .catch((error) => { + console.log(`Error! Scan failed to start: ${error}.`) + }) + } else { + console.log("NFC is not supported") + } + } + + React.useEffect(() => { + console.log("nfcMessage", nfcMessage) + + setNfcMessage( + "lnurlw://boltcard.tiankii.app/v1/lnurl/b1pizbxx0ikdivim5tpt9csy9kezxi?p=960C0DDCE939D1295C301D6B1A65BE78&c=CDFC90874BCE5AF2", + ) + }, []) + + React.useEffect(() => { + ;(async () => { + if (nfcMessage) { + const lnurlParams = await getParams(nfcMessage) + + console.log("lnurlParams", lnurlParams) + + if (!("tag" in lnurlParams && lnurlParams.tag === "withdrawRequest")) { + console.error("not a lnurl withdraw tag") + return + } + + if (!invoice?.paymentRequest) { + console.error("no invoice to redeem") + return + } + + const { callback, k1 } = lnurlParams + + const urlObject = new URL(callback) + const searchParams = urlObject.searchParams + searchParams.set("k1", k1) + searchParams.set("pr", invoice?.paymentRequest) + + const url = urlObject.toString() + + const result = await fetch(url) + if (result.ok) { + const lnurlResponse = await result.json() + if (lnurlResponse?.status?.toLowerCase() !== "ok") { + console.error(lnurlResponse, "error with redeeming") + } + } else { + console.error(result.text(), "error with submitting withdrawalRequest") + } + } + })() + }, [nfcMessage, invoice]) + const copyInvoice = () => { if (!invoice?.paymentRequest) { return @@ -242,6 +337,8 @@ function ReceiveInvoice({ recipientWalletCurrency, walletId, state, dispatch }: ) } + console.log("invoice", invoice) + return (
{recipientWalletCurrency === "USD" && ( @@ -254,6 +351,9 @@ function ReceiveInvoice({ recipientWalletCurrency, walletId, state, dispatch }:
)}
+ + {nfcMessage &&
LNURL: {nfcMessage}
} + {data ? ( <>
=10.13.0'} dev: false + /@types/aes-js@3.1.4: + resolution: {integrity: sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==} + dev: false + /@types/aria-query@5.0.4: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: true @@ -13870,6 +13877,10 @@ packages: '@babel/types': 7.23.6 dev: true + /@types/base64-js@1.3.2: + resolution: {integrity: sha512-Q2Xn2/vQHRGLRXhQ5+BSLwhHkR3JVflxVKywH0Q6fVoAiUE8fFYL2pE5/l2ZiOiBDfA8qUqRnSxln4G/NFz1Sg==} + dev: false + /@types/basic-auth@1.1.7: resolution: {integrity: sha512-bFR3Ld3Fty5ayg45sqr3RI4e/GTXyp2W8jzMmw3WOC8RuQ19TrpsZE4y3jcw9iGSZj5f9mH6e+2biPeFUDovww==} dependencies: @@ -17872,7 +17883,6 @@ packages: node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: true /cross-inspect@1.0.0: resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} @@ -18189,7 +18199,6 @@ packages: /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - dev: true /decompress-response@3.3.0: resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} @@ -20893,6 +20902,11 @@ packages: dependencies: to-regex-range: 5.0.1 + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: false + /finalhandler@1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} @@ -24052,6 +24066,22 @@ packages: nopt: 6.0.0 dev: true + /js-lnurl@0.6.0: + resolution: {integrity: sha512-U4hnInqlHVM9DyYMnOLk0IqlD293A7GVen8JBNWbXrq7C1IigQpTfoal+Fgz/eTZOsYtIEFEOyW9mKgFD/Oc0w==} + dependencies: + '@types/aes-js': 3.1.4 + '@types/base64-js': 1.3.2 + aes-js: 3.1.2 + base64-js: 1.5.1 + bech32: 1.1.4 + buffer: 5.7.1 + cross-fetch: 3.1.8 + query-string: 6.14.1 + safe-buffer: 5.2.1 + transitivePeerDependencies: + - encoding + dev: false + /js-md5@0.7.3: resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==} dev: false @@ -27553,6 +27583,16 @@ packages: strict-uri-encode: 1.1.0 dev: true + /query-string@6.14.1: + resolution: {integrity: sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==} + engines: {node: '>=6'} + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -29021,6 +29061,11 @@ packages: - utf-8-validate dev: true + /split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + dev: false + /split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -29186,6 +29231,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + dev: false + /string-env-interpolation@1.0.1: resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} dev: true