Skip to content

Commit

Permalink
feat: POC for NFC for Pay
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Burtey committed Feb 15, 2024
1 parent ada1c23 commit 043d4fa
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 4 deletions.
4 changes: 3 additions & 1 deletion apps/pay/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useRouter } from "next/navigation"

import CurrencyDropdown from "../components/Currency/currency-dropdown"
import { getClientSideGqlConfig } from "../config/config"
import NFCComponent from "../components/ParsePOSPayment/nfc"

const GET_NODE_STATS = gql`
query nodeIds {
Expand Down Expand Up @@ -46,6 +47,7 @@ function Home() {

return (
<Container>
<NFCComponent paymentRequest={"abcdef"} />
<br />
<Row>
<Col>
Expand All @@ -66,7 +68,7 @@ function Home() {
>
{error
? "Unavailable"
: loading
: loading
? "Loading..."
: data.globals.nodesIds[0]}
</p>
Expand Down
102 changes: 101 additions & 1 deletion apps/pay/components/ParsePOSPayment/Receive-Invoice.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
// TODO: remove eslint-disable, the logic likely needs to be reworked
import copy from "copy-to-clipboard"
import { getParams } from "js-lnurl"

import { useParams, useSearchParams } from "next/navigation"
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"
Expand All @@ -22,6 +24,7 @@ import { extractSearchParams, safeAmount } from "../../utils/utils"
import LoadingComponent from "../Loading"

import styles from "./parse-payment.module.css"
import NFCComponent from "./nfc"

interface Props {
recipientWalletCurrency?: string
Expand Down Expand Up @@ -214,6 +217,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
Expand Down Expand Up @@ -242,6 +338,8 @@ function ReceiveInvoice({ recipientWalletCurrency, walletId, state, dispatch }:
return <LoadingComponent />
}

console.log("invoice", invoice)

return (
<div className={styles.invoice_container}>
{recipientWalletCurrency === "USD" && (
Expand All @@ -254,6 +352,8 @@ function ReceiveInvoice({ recipientWalletCurrency, walletId, state, dispatch }:
</div>
)}
<div>
<NFCComponent paymentRequest={invoice?.paymentRequest} />

{data ? (
<>
<div
Expand Down
143 changes: 143 additions & 0 deletions apps/pay/components/ParsePOSPayment/nfc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useState, useEffect } from "react"

import { getParams } from "js-lnurl"

type Props = {
paymentRequest: string | undefined
}

function NFCComponent({ paymentRequest }: Props) {
const [hasNFCPermission, setHasNFCPermission] = useState(false)
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)) {
console.error("NFC is not supported")
return
}

console.log("NFC is supported, start reading")

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}.`)
})
}

useEffect(() => {
;(async () => {
if (!("permissions" in navigator)) {
console.error("Permissions API not supported")
return
}

const result = await navigator.permissions.query({ name: "nfc" })

console.log("result permission query", result)

if (result.state === "granted") {
setHasNFCPermission(true)
} else {
setHasNFCPermission(false)
}

result.onchange = () => {
if (result.state === "granted") {
setHasNFCPermission(true)
} else {
setHasNFCPermission(false)
}
}
})()
}, [setHasNFCPermission])

React.useEffect(() => {
console.log("hasNFCPermission", hasNFCPermission)

if (hasNFCPermission) {
handleNFCScan()
}
}, [hasNFCPermission])

React.useEffect(() => {
;(async () => {
if (!nfcMessage) {
console.error("no nfc message")
return
}

if (!paymentRequest) {
console.error("no invoice to redeem")
return
}

const lnurlParams = await getParams(nfcMessage)

if (!("tag" in lnurlParams && lnurlParams.tag === "withdrawRequest")) {
console.error("not a lnurl withdraw tag")
return
}

const { callback, k1 } = lnurlParams

const urlObject = new URL(callback)
const searchParams = urlObject.searchParams
searchParams.set("k1", k1)
searchParams.set("pr", 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, paymentRequest])

return (
<div>
{!hasNFCPermission && <button onClick={handleNFCScan}>Start NFC Scan</button>}
{nfcMessage && <div>LNURL: {nfcMessage}</div>}
</div>
)
}

export default NFCComponent
1 change: 1 addition & 0 deletions apps/pay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"graphql-ws": "^5.14.0",
"html2canvas": "^1.4.1",
"ioredis": "^5.3.1",
"js-lnurl": "^0.6.0",
"lnurl-pay": "^1.0.1",
"lodash.debounce": "^4.0.8",
"next": "^14.1.0",
Expand Down
Loading

0 comments on commit 043d4fa

Please sign in to comment.