damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README | LICENSE

commit e4d581b9cebaac9bcd7135275d6a2594bfd2d06d
parent 28eaba297319056b05d3ff62183d32f71cfc993b
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Wed, 31 Jan 2024 02:11:03 +0000

Improve error handling UI/UX on LN checkout

This commit adds a mechanism to display errors nicely to the user.

All (or most) functions related to the checkout process were wrapped in
try-catch statements that display error nicely with support contact info
and a reference number.

Detailed error messages were written containing tips for the users.

Testing
--------

Test 1: Went through the entire LN flow locally, and it worked without errors
Test 2: Went through the entire LN flow, turning the server on and off and the wi-fi on/off in each step. Saw the nice messages appearing.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Link: 20240131021044.86169-3-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mpackage-lock.json | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackage.json | 1+
Msrc/components/sections/PurpleCheckout.tsx | 192++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
3 files changed, 413 insertions(+), 50 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-slot": "^1.0.2", @@ -596,6 +597,34 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", + "integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", @@ -686,6 +715,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -730,6 +795,48 @@ } } }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", @@ -807,6 +914,29 @@ } } }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", @@ -1287,6 +1417,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -1882,6 +2023,11 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2720,6 +2866,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -3034,6 +3188,14 @@ "tslib": "^2.4.0" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -4242,6 +4404,73 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4997,6 +5226,47 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/usehooks-ts": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.9.2.tgz", diff --git a/package.json b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-slot": "^1.0.2", diff --git a/src/components/sections/PurpleCheckout.tsx b/src/components/sections/PurpleCheckout.tsx @@ -13,6 +13,17 @@ import { QRCodeSVG } from 'qrcode.react'; import { useInterval } from 'usehooks-ts' import Lnmessage from 'lnmessage' import { DAMUS_TESTFLIGHT_URL } from "@/lib/constants"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/AlertDialog"; export function PurpleCheckout() { @@ -24,6 +35,7 @@ export function PurpleCheckout() { const [continueShowQRCodes, setContinueShowQRCodes] = useState<boolean>(false) // Whether the user wants to show a QR code for the final step const [lnInvoicePaid, setLNInvoicePaid] = useState<boolean | undefined>(undefined) // Whether the ln invoice has been paid const [waitingForInvoice, setWaitingForInvoice] = useState<boolean>(false) // Whether we are waiting for a response from the LN node about the invoice + const [error, setError] = useState<string|null>(null) // An error message to display to the user // MARK: - Functions @@ -31,45 +43,69 @@ export function PurpleCheckout() { if (!pubkey) { return } - const profile = await getProfile(pubkey) - setProfile(profile) + try { + const profile = await getProfile(pubkey) + setProfile(profile) + } + catch (e) { + console.error(e) + setError("Failed to get profile info from the relay. Please wait a few minutes and refresh the page. If the problem persists, please contact support.") + } } const fetchProductTemplates = async () => { - const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/products", { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - }) - const data = await response.json() - setProductTemplates(data) + try { + const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/products", { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + }) + const data = await response.json() + setProductTemplates(data) + } + catch (e) { + console.error(e) + setError("Failed to get product list from our servers, please try again later in a few minutes. If the problem persists, please contact support.") + } } const refreshLNCheckout = async (id?: string) => { if (!lnCheckout && !id) { return } - const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout/" + (id || lnCheckout?.id), { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - }) - const data: LNCheckout = await response.json() - setLNCheckout(data) + try { + const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout/" + (id || lnCheckout?.id), { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + }) + const data: LNCheckout = await response.json() + setLNCheckout(data) + } + catch (e) { + console.error(e) + setError("Failed to get checkout info from our servers, please wait a few minutes and try to refresh this page. If the problem persists, please contact support.") + } } const selectProduct = async (productTemplateName: string) => { - const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout", { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ product_template_name: productTemplateName }) - }) - const data: LNCheckout = await response.json() - setLNCheckout(data) + try { + const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout", { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ product_template_name: productTemplateName }) + }) + const data: LNCheckout = await response.json() + setLNCheckout(data) + } + catch (e) { + console.error(e) + setError("Failed to begin the checkout process. Please wait a few minutes, refresh this page, and try again. If the problem persists, please contact support.") + } } const checkLNInvoice = async () => { @@ -77,22 +113,32 @@ export function PurpleCheckout() { if (!lnCheckout?.invoice?.bolt11) { return } - const ln = new Lnmessage({ - // The public key of the node you would like to connect to - remoteNodePublicKey: lnCheckout.invoice.connection_params.nodeid, - // The websocket proxy address of the node - wsProxy: `wss://${lnCheckout.invoice.connection_params.ws_proxy_address}`, - // The IP address of the node - ip: lnCheckout.invoice.connection_params.address, - // Protocol to use when connecting to the node - wsProtocol: 'wss:', - port: 9735, - }) - // TODO: This is a workaround due to a limitation in LNMessage URL formatting: (https://github.com/aaronbarnardsound/lnmessage/issues/52) - ln.wsUrl = `wss://${lnCheckout.invoice.connection_params.ws_proxy_address}/${lnCheckout.invoice.connection_params.address}` - await ln.connect() - setWaitingForInvoice(true) // Indicate that we are waiting for a response from the LN node + let ln = null try { + ln = new Lnmessage({ + // The public key of the node you would like to connect to + remoteNodePublicKey: lnCheckout.invoice.connection_params.nodeid, + // The websocket proxy address of the node + wsProxy: `wss://${lnCheckout.invoice.connection_params.ws_proxy_address}`, + // The IP address of the node + ip: lnCheckout.invoice.connection_params.address, + // Protocol to use when connecting to the node + wsProtocol: 'wss:', + port: 9735, + }) + // TODO: This is a workaround due to a limitation in LNMessage URL formatting: (https://github.com/aaronbarnardsound/lnmessage/issues/52) + ln.wsUrl = `wss://${lnCheckout.invoice.connection_params.ws_proxy_address}/${lnCheckout.invoice.connection_params.address}` + await ln.connect() + setWaitingForInvoice(true) // Indicate that we are waiting for a response from the LN node + } + catch (e) { + console.error(e) + setError("Failed to connect to the Lightning node. Please refresh this page, and try again in a few minutes. If the problem persists, please contact support.") + return + } + + try { + if (!ln) { return } const res: any = await ln.commando({ method: 'waitinvoice', params: { label: lnCheckout.invoice.label }, @@ -100,20 +146,32 @@ export function PurpleCheckout() { }) setWaitingForInvoice(false) // Indicate that we are no longer waiting for a response from the LN node setLNInvoicePaid(!res.error) + if (res.error) { + console.error(res.error) + setError("The lightning payment failed. If you haven't paid yet, please start a new checkout from the beginning and try again. If you have already paid, please copy the reference ID shown below and contact support.") + } } catch (e) { setWaitingForInvoice(false) // Indicate that we are no longer waiting for a response from the LN node + console.error(e) + setError("There was an error checking the lightning payment status. If you haven't paid yet, please wait a few minutes, refresh the page, and try again. If you have already paid, please copy the reference ID shown below and contact support.") } } const tellServerToCheckLNInvoice = async () => { - const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout/" + lnCheckout?.id + "/check-invoice", { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - }) - const data: LNCheckout = await response.json() - setLNCheckout(data) + try { + const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout/" + lnCheckout?.id + "/check-invoice", { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + }) + const data: LNCheckout = await response.json() + setLNCheckout(data) + } + catch (e) { + console.error(e) + setError("Failed to finalize checkout. Please try refreshing the page. If the error persists, please copy the reference ID shown below and contact support.") + } } const pollState = async () => { @@ -176,6 +234,40 @@ export function PurpleCheckout() { // MARK: - Render return (<> + <AlertDialog open={error != null} onOpenChange={(open) => { if (!open) setError(null) }}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle><div className="text-red-700">Error</div></AlertDialogTitle> + <AlertDialogDescription> + <div className="text-left"> + {error} + </div> + <div className="text-black/40 text-xs text-left mt-4 mb-6"> + You can contact support by sending an email to <Link href="mailto:support@damus.io" className="text-damuspink-600 underline">support@damus.io</Link> + </div> + {lnCheckout && lnCheckout.id && ( + <div className="flex items-center justify-between rounded-md bg-gray-200"> + <div className="text-xs text-gray-400 font-normal px-4 py-2"> + Reference: + </div> + <div className="w-full text-xs text-gray-500 font-normal px-4 py-2 overflow-x-scroll"> + {lnCheckout?.id} + </div> + <button + className="text-sm text-gray-500 font-normal px-4 py-2 active:text-gray-500/30 hover:text-gray-500/80 transition" + onClick={() => navigator.clipboard.writeText(lnCheckout?.id || "")} + > + <Copy /> + </button> + </div> + )} + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <Button variant="default" onClick={() => setError(null)}>OK</Button> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> <div className="bg-black overflow-hidden relative" >