diff --git a/package.json b/package.json index 80832fd..a891489 100644 --- a/package.json +++ b/package.json @@ -8,28 +8,32 @@ }, "license": "MIT", "dependencies": { - "bech32": "^1.1.3", - "bitcoinjs-lib": "^5.1.6", - "bn.js": "^5.0.0", - "buffer": "^5.4.3", - "classnames": "^2.2.6", + "@radix-ui/react-dialog": "latest", + "@radix-ui/react-slot": "latest", + "bech32": "latest", + "bitcoinjs-lib": "latest", + "bn.js": "latest", + "buffer": "latest", + "class-variance-authority": "latest", + "clsx": "latest", "coininfo": "https://github.com/cryptocoinjs/coininfo#dc3e6cc59e593ee7dbeb7c993485706e72d32743", - "date-fns": "^2.1.0", - "eslint": "^5.16.0", - "lodash": "^4.17.15", - "react": "^16.9.0", - "react-dom": "^16.9.0", - "react-ga": "^2.6.0", - "react-qr-reader": "^2.2.1", - "react-scripts": "3.1.1", - "safe-buffer": "^5.2.0", - "sass": "^1.37.5", - "secp256k1": "^3.7.1", - "serve": "^11.1.0" + "date-fns": "latest", + "lodash": "latest", + "lucide-react": "latest", + "react": "latest", + "react-dom": "latest", + "react-ga": "latest", + "react-qr-reader": "latest", + "react-scripts": "latest", + "safe-buffer": "latest", + "secp256k1": "latest", + "serve": "latest", + "tailwind-merge": "latest", + "tailwindcss-animate": "latest" }, "scripts": { - "start": "export NODE_OPTIONS=--openssl-legacy-provider && react-scripts start", - "build": "export NODE_OPTIONS=--openssl-legacy-provider && react-scripts build", + "start": "react-scripts start", + "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "deploy": "npm run build && cd build && now" @@ -49,7 +53,10 @@ "bitcoin" ], "devDependencies": { - "node": "^18.9.0", - "eslint": "^6.1.0" + "autoprefixer": "latest", + "eslint": "latest", + "node": "latest", + "postcss": "latest", + "tailwindcss": "latest" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/app.js b/src/app.js index dddb0bd..d546de3 100644 --- a/src/app.js +++ b/src/app.js @@ -1,14 +1,7 @@ // Core Libs & Utils -import React, { PureComponent } from 'react'; +import React, { useEffect, useState } from 'react'; import QrReader from 'react-qr-reader'; -import cx from 'classnames'; - -// Assets -import boltImage from './assets/images/bolt.png'; -import arrowImage from './assets/images/arrow.svg'; -import closeImage from './assets/images/close.svg'; -import qrcodeImage from './assets/images/qrcode.png'; -import githubImage from './assets/images/github.svg'; +import { Github, QrCode, RotateCcw, Search, Zap } from 'lucide-react'; // Utils import { formatDetailsKey } from './utils/keys'; @@ -31,630 +24,466 @@ import { LNURL_TAG_KEY, } from './constants/keys'; -// Styles -import './assets/styles/main.scss'; +import { Alert, AlertDescription, AlertTitle } from './components/ui/alert'; +import { Badge } from './components/ui/badge'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from './components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from './components/ui/dialog'; +import { Button } from './components/ui/button'; +import { Input } from './components/ui/input'; const INITIAL_STATE = { text: '', - error: {}, - hasError: false, + error: null, decodedInvoice: {}, + isLNURL: false, isLNAddress: false, isQRCodeOpened: false, isInvoiceLoaded: false, - isBitcoinAddrOpened: false, }; -export class App extends PureComponent { - state = INITIAL_STATE; +export function App() { + const [text, setText] = useState(INITIAL_STATE.text); + const [error, setError] = useState(INITIAL_STATE.error); + const [decodedInvoice, setDecodedInvoice] = useState(INITIAL_STATE.decodedInvoice); + const [isLNURL, setIsLNURL] = useState(INITIAL_STATE.isLNURL); + const [isLNAddress, setIsLNAddress] = useState(INITIAL_STATE.isLNAddress); + const [isQRCodeOpened, setIsQRCodeOpened] = useState(INITIAL_STATE.isQRCodeOpened); + const [isInvoiceLoaded, setIsInvoiceLoaded] = useState(INITIAL_STATE.isInvoiceLoaded); - componentDidMount() { + useEffect(() => { const invoiceOnURLParam = window.location.pathname; - - // Remove first `/` from pathname const cleanInvoice = invoiceOnURLParam.split('/')[1]; if (cleanInvoice && cleanInvoice !== '') { - this.setState(() => ({ text: cleanInvoice })); - this.getInvoiceDetails(cleanInvoice); + setText(cleanInvoice); + getInvoiceDetails(cleanInvoice); } - } + }, []); - clearInvoiceDetails = () => { - // Reset URL address + const clearInvoiceDetails = () => { const currentOrigin = window.location.origin; window.history.pushState({}, null, `${currentOrigin}`); - this.setState(() => ({ - ...INITIAL_STATE, - })); + setText(INITIAL_STATE.text); + setError(INITIAL_STATE.error); + setDecodedInvoice(INITIAL_STATE.decodedInvoice); + setIsLNURL(INITIAL_STATE.isLNURL); + setIsLNAddress(INITIAL_STATE.isLNAddress); + setIsQRCodeOpened(INITIAL_STATE.isQRCodeOpened); + setIsInvoiceLoaded(INITIAL_STATE.isInvoiceLoaded); }; - getInvoiceDetails = async (text) => { - // If this returns null is because there is no invoice to parse - if (!text) { - return this.setState(() => ({ - hasError: true, - decodedInvoice: {}, - isInvoiceLoaded: false, - error: { message: 'Please enter a valid request or address and try again.'}, - })); + const setErrorState = (message) => { + setError({ message }); + setDecodedInvoice({}); + setIsInvoiceLoaded(false); + }; + + const getInvoiceDetails = async (textValue) => { + if (!textValue) { + return setErrorState('Please enter a valid request or address and try again.'); } try { let response; - const parsedInvoiceResponse = await parseInvoice(text); + const parsedInvoiceResponse = await parseInvoice(textValue); - // If this returns null is because there is no invoice to parse if (!parsedInvoiceResponse) { - return this.setState(() => ({ - hasError: true, - decodedInvoice: {}, - isInvoiceLoaded: false, - error: { message: 'Please enter a valid request or address and try again.'}, - })); + return setErrorState('Please enter a valid request or address and try again.'); } - const { isLNURL, data, error, isLNAddress } = parsedInvoiceResponse; - - // If an error comes back from a nested operation in parsing it must - // propagate back to the end user - if (error && error.length > 0) { - return this.setState(() => ({ - hasError: true, - decodedInvoice: {}, - isInvoiceLoaded: false, - error: { message: error }, - })); + const { isLNURL: parsedIsLNURL, data, error: parseError, isLNAddress: parsedIsLNAddress } = parsedInvoiceResponse; + + if (parseError && parseError.length > 0) { + return setErrorState(parseError); } - // If data is null it means the parser could not understand the invoice if (!data) { - return this.setState(() => ({ - hasError: true, - decodedInvoice: {}, - isInvoiceLoaded: false, - error: { message: 'Could not parse/understand this invoice or request. Please try again.'}, - })); + return setErrorState('Could not parse/understand this invoice or request. Please try again.'); } - // Handle LNURLs differently - if (isLNURL) { - // If this is a Lightning Address, the contents have already been fetched - if (isLNAddress) { + if (parsedIsLNURL) { + if (parsedIsLNAddress) { response = data; } else { - // Otherwise this is an LNURL ready to be fetched response = await data; } } else { - // Handle normal invoices response = data; } if (response) { - // On successful response, set the request content on the addressbar - // if there isn't one already in there from before (user-entered) const currentUrl = window.location; const currentOrigin = window.location.origin; const currentPathname = window.location.pathname; const hasPathnameAlready = currentPathname && currentPathname !== ''; - // If there's a pathname already, we can just remove it and let the - // new pathname be entered if (hasPathnameAlready) { window.history.pushState({}, null, `${currentOrigin}`); } - window.history.pushState({}, null, `${currentUrl}${text}`); + window.history.pushState({}, null, `${currentUrl}${textValue}`); - this.setState(() => ({ - isLNURL, - error: {}, - isLNAddress, - hasError: false, - isInvoiceLoaded: true, - decodedInvoice: response, - })); + setIsLNURL(parsedIsLNURL); + setError(null); + setIsLNAddress(parsedIsLNAddress); + setIsInvoiceLoaded(true); + setDecodedInvoice(response); } - } catch(error) { - this.setState(() => ({ - error: error, - hasError: true, - decodedInvoice: {}, - isInvoiceLoaded: false, - })); + } catch (caughtError) { + setError(caughtError); + setDecodedInvoice({}); + setIsInvoiceLoaded(false); } - } - - handleChange = (event) => { - const { target: { value: text } } = event; - - this.setState(() => ({ - text, - error: {}, - hasError: false, - })); - } + }; - handleKeyPress = (event) => { - const { text } = this.state; + const handleChange = (event) => { + const { target: { value } } = event; + setText(value); + setError(null); + }; + const handleKeyPress = (event) => { if (event.key === 'Enter') { - this.getInvoiceDetails(text); + getInvoiceDetails(text); } - } - - handleQRCode = () => this.setState(prevState => ({ - isQRCodeOpened: !prevState.isQRCodeOpened - })) - - renderErrorDetails = () => { - const { hasError, error } = this.state; - - if (!hasError) return null; - - return ( -
+ {APP_TAGLINE}{' '} + + {APP_SUBTAGLINE} + +
+