diff --git a/package-lock.json b/package-lock.json index a4e0f22..5016bf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.104.0", "license": "MIT", "dependencies": { + "@cashu/cashu-ts": "^0.9.0", "@cookbook/solid-intl": "0.1.2", "@jukben/emoji-search": "3.0.0", "@kobalte/core": "0.11.0", @@ -491,6 +492,61 @@ "node": ">=6.9.0" } }, + "node_modules/@cashu/cashu-ts": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-0.9.0.tgz", + "integrity": "sha512-DacSnpv3dJIGzo6A1nHIp2guFuDcmoPB5CX9m0SXA60bQxoIa4srHKLkjxUZ8GFCD9RaI+60UZ2+hZS635Ro2w==", + "dependencies": { + "@gandlaf21/bolt11-decode": "^3.0.6", + "@noble/curves": "^1.0.0", + "@scure/bip32": "^1.3.2", + "@scure/bip39": "^1.2.1", + "buffer": "^6.0.3" + } + }, + "node_modules/@cashu/cashu-ts/node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/cashu-ts/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/cashu-ts/node_modules/@scure/base": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/cashu-ts/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@cookbook/solid-intl": { "version": "0.1.2", "license": "MIT", @@ -635,6 +691,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@gandlaf21/bolt11-decode": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@gandlaf21/bolt11-decode/-/bolt11-decode-3.1.1.tgz", + "integrity": "sha512-uorLVc3FAWO6USCaBHGixwUd7B86z4IVRa/TdLicsWi3Vm7QngYDIuUkOBemut0Qh/Qtsx47EjWMXS4WHel98A==", + "dependencies": { + "bech32": "^1.1.2", + "bn.js": "^4.11.8", + "buffer": "^6.0.3" + } + }, "node_modules/@internationalized/date": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.0.tgz", @@ -1043,6 +1109,30 @@ "@babel/core": "^7.0.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "license": "MIT", @@ -1050,6 +1140,11 @@ "node": ">=8" } }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/braces": { "version": "3.0.2", "license": "MIT", @@ -1091,6 +1186,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001534", "dev": true, @@ -1649,6 +1767,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/immutable": { "version": "4.3.4", "license": "MIT" diff --git a/package.json b/package.json index 6038837..ed692e7 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "vite-plugin-solid": "^2.5.0" }, "dependencies": { + "@cashu/cashu-ts": "0.9.0", "@cookbook/solid-intl": "0.1.2", "@jukben/emoji-search": "3.0.0", "@kobalte/core": "0.11.0", diff --git a/src/assets/icons/cashu.svg b/src/assets/icons/cashu.svg new file mode 100644 index 0000000..2da511d --- /dev/null +++ b/src/assets/icons/cashu.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/Cashu/Cashu.module.scss b/src/components/Cashu/Cashu.module.scss new file mode 100644 index 0000000..d13a338 --- /dev/null +++ b/src/components/Cashu/Cashu.module.scss @@ -0,0 +1,335 @@ +.cashu { + width: 100%; + min-height: 158px; + background-color: var(--background-header-input); + border-radius: var(--border-radius-small); + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px; + position: relative; + + .paymentOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--background-site); + opacity: 0.6; + display: flex; + justify-content: center; + align-items: center; + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 20px; + + .title { + display: flex; + justify-content: flex-start; + align-items: center; + + color: var(--text-primary); + + font-size: 15px; + font-weight: 700; + line-height: 16px; + } + + .headerActions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 2px; + + button { + .qrIcon { + width: 18px; + height: 18px; + display: inline-block; + margin: 0px; + background-color: var(--text-secondary); + -webkit-mask: url(../../assets/icons/qr_code.svg) no-repeat 0px / 18px; + mask: url(../../assets/icons/qr_code.svg) no-repeat 0px / 18px; + } + .copyIcon { + width: 18px; + height: 18px; + display: inline-block; + margin: 0px; + background-color: var(--text-secondary); + -webkit-mask: url(../../assets/icons/copy_border.svg) no-repeat 0px / 18px; + mask: url(../../assets/icons/copy_border.svg) no-repeat 0px / 18px; + } + + &:hover { + .qrIcon, .copyIcon { + background-color: var(--text-primary); + } + } + } + + .copyDone { + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + + .checkIcon { + width: 18px; + height: 18px; + display: inline-block; + margin: 0px; + background-color: var(--success-bright); + -webkit-mask: url(../../assets/icons/check.svg) no-repeat 0px / 18px; + mask: url(../../assets/icons/check.svg) no-repeat 0px / 18px; + } + } + } + } + + .body { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 8px; + + .description { + color: var(--text-primary); + + font-size: 15px; + font-weight: 400; + line-height: 18px; + } + + .amount { + color: var(--text-primary); + + font-size: 24px; + font-weight: 600; + line-height: 24px; + } + } + + .footer { + height: 36px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-end; + + .mint { + color: var(--text-secondary); + + font-size: 15px; + font-weight: 400; + line-height: 18px; + letter-spacing: 0.15px; + } + + .payAction { + height: 36px; + min-width: 120px; + display: flex; + button { + width: 100%; + height: 100%; + } + } + + .spent { + height: 36px; + min-width: 120px; + margin-right: 12px; + display: flex; + justify-content: flex-end; + align-items: flex-end; + } + } + + &.noBack { + background-color: unset; + } + + .cashuIcon { + width: 20px; + height: 20px; + background-image: url('../../assets/icons/cashu.svg'); + background-size: contain; + background-repeat: no-repeat; + } +} + +.cashuAlter { + width: 100%; + min-height: 158px; + background-color: none; + border-radius: var(--border-radius-small); + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 0; + position: relative; + + .paymentOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--background-site); + opacity: 0.6; + display: flex; + justify-content: center; + align-items: center; + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 20px; + + .title { + display: flex; + justify-content: flex-start; + align-items: center; + + color: white; + + font-size: 15px; + font-weight: 700; + line-height: 16px; + } + + .headerActions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 2px; + + button { + .qrIcon { + width: 18px; + height: 18px; + display: inline-block; + margin: 0px; + background-color: white; + -webkit-mask: url(../../assets/icons/qr_code.svg) no-repeat 0px / 18px; + mask: url(../../assets/icons/qr_code.svg) no-repeat 0px / 18px; + } + .copyIcon { + width: 18px; + height: 18px; + display: inline-block; + margin: 0px; + background-color: white; + -webkit-mask: url(../../assets/icons/copy_border.svg) no-repeat 0px / 18px; + mask: url(../../assets/icons/copy_border.svg) no-repeat 0px / 18px; + } + + &:hover { + .qrIcon, .copyIcon { + background-color: white; + } + } + } + + .copyDone { + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + + .checkIcon { + width: 18px; + height: 18px; + display: inline-block; + margin: 0px; + background-color: var(--success-bright); + -webkit-mask: url(../../assets/icons/check.svg) no-repeat 0px / 18px; + mask: url(../../assets/icons/check.svg) no-repeat 0px / 18px; + } + } + } + } + + .body { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 8px; + + .description { + color: white; + + font-size: 15px; + font-weight: 400; + line-height: 18px; + } + + .amount { + color: white; + + font-size: 24px; + font-weight: 600; + line-height: 24px; + } + } + + .footer { + height: 36px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-end; + + .expiryDate { + color: white; + + font-size: 15px; + font-weight: 400; + line-height: 18px; + letter-spacing: 0.15px; + } + .expiredDate { + color: white; + + font-size: 15px; + font-weight: 400; + line-height: 18px; + letter-spacing: 0.15px; + } + + .payAction { + height: 36px; + min-width: 120px; + button { + width: 100%; + height: 100%; + color: var(--accent); + background-color: white; + } + } + } + + &.noBack { + background-color: unset; + } + + .cashuIcon { + width: 20px; + height: 20px; + background-image: url('../../assets/icons/cashu.svg'); + background-size: contain; + background-repeat: no-repeat; + } +} diff --git a/src/components/Cashu/Cashu.tsx b/src/components/Cashu/Cashu.tsx new file mode 100644 index 0000000..668b93e --- /dev/null +++ b/src/components/Cashu/Cashu.tsx @@ -0,0 +1,185 @@ +import { Component, createEffect, createSignal, Match, onMount, Show, Switch } from 'solid-js'; +import { hookForDev } from '../../lib/devTools'; + +import styles from './Cashu.module.scss'; +import { createStore } from 'solid-js/store'; +import ButtonPrimary from '../Buttons/ButtonPrimary'; +import ButtonGhost from '../Buttons/ButtonGhost'; +import { useAppContext } from '../../contexts/AppContext'; +import Loader from '../Loader/Loader'; +import { logError } from '../../lib/logger'; +import { useIntl } from '@cookbook/solid-intl'; +import { cashuInvoice } from '../../translations'; +import { getDecodedToken, Token, TokenEntry } from "@cashu/cashu-ts"; +import { useAccountContext } from '../../contexts/AccountContext'; + + +const Cashu: Component< { id?: string, token: string, alternative?: boolean, noBack?: boolean } > = (props) => { + + const account = useAccountContext(); + const app = useAppContext(); + const intl = useIntl(); + + const [invoice, setInvoice] = createStore({ token: [] }); + const [cashuSpendable, setCashuSpendable] = createSignal(false); + + const [invoiceCopied, setInvoiceCopied] = createSignal(false); + + const [paymentInProgress, setPaymentInProgress] = createSignal(false); + + const checkMints = async (entries: TokenEntry[]) => { + let statuses: boolean[] = []; + + for (const entry of entries) { + const mint = app?.actions.getCashuMint(entry.mint); + if (!mint) continue; + + const spent = await mint.check({ proofs: entry.proofs.map((p) => ({ secret: p.secret })) }); + + const data = spent.spendable.map(s => s); + + statuses = [ ...statuses, ...data]; + } + + setCashuSpendable(() => !statuses.includes(false)); + } + + createEffect(() => { + if (invoice.token.length === 0) return; + + checkMints(invoice.token); + }); + + createEffect(() => { + try { + const dec: Token = getDecodedToken(props.token); + setInvoice(() => ({ ...dec })); + } catch (e) { + logError('Failed to decode cashu token: ', e); + } + }); + + createEffect(() => { + if (invoiceCopied()) { + setTimeout(() => { + setInvoiceCopied(() => false); + }, 1_000); + } + }) + + const amount = () => + `${invoice.token[0]?.proofs.reduce((acc, v) => acc + v.amount, 0) || 0} sats`; + + const description = () => invoice.memo || ''; + + const confirmPayment = () => app?.actions.openConfirmModal({ + title: intl.formatMessage(cashuInvoice.confirm.title), + description: intl.formatMessage(cashuInvoice.confirm.description, { amount: amount() }), + confirmLabel: intl.formatMessage(cashuInvoice.confirm.confirmLabel), + abortLabel: intl.formatMessage(cashuInvoice.confirm.abortLabel), + onAbort: app.actions.closeConfirmModal, + onConfirm: () => { + app.actions.closeConfirmModal(); + redeemCashu(); + }, + }); + + const redeemCashu = () => { + const lnurl = account?.activeUser?.lud16 ?? ''; + const url = `https://redeem.cashu.me?token=${encodeURIComponent(props.token)}&lightning=${encodeURIComponent( + lnurl, + )}&autopay=yes`; + + console.log('Redeem: ', url); + + window.open(url, 'blank_'); + }; + + const klass = () => { + let k = props.alternative ? styles.cashuAlter : styles.cashu; + if (props.noBack) { + k += ` ${styles.noBack}` + } + + return k; + } + + return ( +
+ +
+ +
+
+
+
+
+
{intl.formatMessage(cashuInvoice.title)}
+
+
+ + { + e.preventDefault(); + app?.actions.openCashuModal(props.token, () => { + app.actions.closeCashuModal(); + confirmPayment(); + }); + }} + shrink={true} + > +
+
+
+ +
} + > + { + e.preventDefault() + navigator.clipboard.writeText(props.token); + setInvoiceCopied(() => true); + }} + shrink={true} + > +
+
+ +
+
+
+
{description()}
+
{amount()}
+
+ +
+
+ + {intl.formatMessage(cashuInvoice.mint, { url: new URL(invoice.token[0]?.mint).hostname })} + +
+ + {intl.formatMessage(cashuInvoice.spent)} +
+ )} + > +
+ { + e.preventDefault(); + confirmPayment(); + }}> + {intl.formatMessage(cashuInvoice.redeem)} + +
+ + + + ); +} + +export default hookForDev(Cashu); diff --git a/src/components/CashuQrCodeModal/CashuQrCodeModal.module.scss b/src/components/CashuQrCodeModal/CashuQrCodeModal.module.scss new file mode 100644 index 0000000..742c4eb --- /dev/null +++ b/src/components/CashuQrCodeModal/CashuQrCodeModal.module.scss @@ -0,0 +1,120 @@ +.CashuQrCodeModal { + position: fixed; + min-width: 472px; + color: var(--text-primary); + background-color: var(--background-input); + border-radius: 8px; + + display: flex; + flex-direction: column; + padding: 20px 24px 28px 24px; + + .header { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 24px; + + .title { + color: var(--text-primary); + font-size: 20px; + font-weight: 700; + line-height: 20px; + } + + .close { + border: none; + outline: none; + padding: 0; + margin: 0; + box-shadow: none; + width: 20px; + height: 20px; + display: inline-block; + margin: 0px 0px; + background-color: var(--text-secondary); + -webkit-mask: url(../../assets/icons/close.svg) no-repeat center; + mask: url(../../assets/icons/close.svg) no-repeat center; + + &:hover { + background-color: var(--text-primary); + } + } + } +} + +.body { + display: flex; + flex-direction: column; + gap: 12px; + + .description { + color: var(--text-primary); + + font-size: 15px; + font-weight: 400; + line-height: 18px; + + display: flex; + justify-content: center; + align-items: center; + text-align: left; + width: 100%; + } + + .amount { + color: var(--text-primary); + + font-size: 24px; + font-weight: 600; + line-height: 24px; + + display: flex; + justify-content: center; + align-items: center; + width: 100%; + } + + .separator { + width: 100%; + height: 1px; + border: 1px solid var(--subtile-devider); + } +} + +.footer { + height: 36px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-top: 20px; + + .mint { + color: var(--text-secondary); + + font-size: 15px; + font-weight: 400; + line-height: 18px; + letter-spacing: 0.15px; + } + + .payAction { + height: 36px; + min-width: 120px; + button { + width: 100%; + height: 100%; + } + } +} + +.zapIcon { + width: 22px; + height: 22px; + display: inline-block; + margin-right: 9px; + background: var(--sidebar-section-icon-gradient); + -webkit-mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px; + mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px; +} diff --git a/src/components/CashuQrCodeModal/CashuQrCodeModal.tsx b/src/components/CashuQrCodeModal/CashuQrCodeModal.tsx new file mode 100644 index 0000000..4165d29 --- /dev/null +++ b/src/components/CashuQrCodeModal/CashuQrCodeModal.tsx @@ -0,0 +1,83 @@ +import { useIntl } from '@cookbook/solid-intl'; +// @ts-ignore +import { decode } from 'light-bolt11-decoder'; +import { Component, createEffect, Show } from 'solid-js'; +import { createStore, reconcile } from 'solid-js/store'; +import { emptyInvoice } from '../../constants'; +import { date, dateFuture } from '../../lib/dates'; +import { hookForDev } from '../../lib/devTools'; +import { humanizeNumber } from '../../lib/stats'; +import { cashuInvoice } from '../../translations'; +import { LnbcInvoice } from '../../types/primal'; +import ButtonPrimary from '../Buttons/ButtonPrimary'; +import Modal from '../Modal/Modal'; +import QrCode from '../QrCode/QrCode'; +import { getDecodedToken, Token } from "@cashu/cashu-ts"; + +import styles from './CashuQrCodeModal.module.scss'; + +const CashuQrCodeModal: Component<{ + id?: string, + open?: boolean, + cashu: string | undefined, + onPay?: () => void, + onClose?: () => void, +}> = (props) => { + const intl = useIntl(); + + const [invoice, setInvoice] = createStore({ token: []}); + + createEffect(() => { + if (props.cashu) { + const dec: Token = getDecodedToken(props.cashu); + setInvoice(reconcile(dec)); + } else { + setInvoice(reconcile({ token: [] })); + } + }); + + const amount = () => + `${invoice.token[0]?.proofs.reduce((acc, v) => acc + v.amount, 0) || 0} sats`; + + const description = () => invoice.memo || ''; + + return ( + +
+
+
+ {intl.formatMessage(cashuInvoice.title)} +
+ +
+ +
+
+ +
+ +
{description()}
+
{amount()}
+ +
+
+ +
+
+ + {intl.formatMessage(cashuInvoice.mint, { url: new URL(invoice.token[0]?.mint).hostname })} + +
+
+ + {intl.formatMessage(cashuInvoice.redeem)} + +
+
+
+
+ ); +} + +export default hookForDev(CashuQrCodeModal); diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index ce619be..2a24463 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -20,6 +20,7 @@ import CustomZap from '../CustomZap/CustomZap'; import NoteContextMenu from '../Note/NoteContextMenu'; import LnQrCodeModal from '../LnQrCodeModal/LnQrCodeModal'; import ConfirmModal from '../ConfirmModal/ConfirmModal'; +import CashuQrCodeModal from '../CashuQrCodeModal/CashuQrCodeModal'; export const [isHome, setIsHome] = createSignal(false); @@ -174,6 +175,13 @@ const Layout: Component = () => { onClose={app?.lnbc?.onCancel} /> + + void, }; -export type LnbcInfo = { +export type InvoiceInfo = { invoice: string, onPay?: () => void, onCancel?: () => void, @@ -57,9 +59,12 @@ export type AppContextStore = { showNoteContextMenu: boolean, noteContextMenuInfo: NoteContextMenuInfo | undefined, showLnInvoiceModal: boolean, - lnbc: LnbcInfo | undefined, + lnbc: InvoiceInfo | undefined, + showCashuInvoiceModal: boolean, + cashu: InvoiceInfo | undefined, showConfirmModal: boolean, confirmInfo: ConfirmInfo | undefined, + cashuMints: Map, actions: { openReactionModal: (noteId: string, stats: ReactionStats) => void, closeReactionModal: () => void, @@ -69,8 +74,11 @@ export type AppContextStore = { closeContextMenu: () => void, openLnbcModal: (lnbc: string, onPay: () => void) => void, closeLnbcModal: () => void, + openCashuModal: (cashu: string, onPay: () => void) => void, + closeCashuModal: () => void, openConfirmModal: (confirmInfo: ConfirmInfo) => void, closeConfirmModal: () => void, + getCashuMint: (url: string) => CashuMint | undefined, }, } @@ -90,8 +98,11 @@ const initialData: Omit = { noteContextMenuInfo: undefined, showLnInvoiceModal: false, lnbc: undefined, + showCashuInvoiceModal: false, + cashu: undefined, showConfirmModal: false, confirmInfo: undefined, + cashuMints: new Map(), }; export const AppContext = createContext(); @@ -165,6 +176,21 @@ export const AppProvider = (props: { children: JSXElement }) => { updateStore('lnbc', () => undefined); }; + + const openCashuModal = (cashu: string, onPay: () => void) => { + updateStore('showCashuInvoiceModal', () => true); + updateStore('cashu', () => ({ + invoice: cashu, + onPay, + onCancel: () => updateStore('showCashuInvoiceModal', () => false), + })) + }; + + const closeCashuModal = () => { + updateStore('showCashuInvoiceModal', () => false); + updateStore('cashu', () => undefined); + }; + const openConfirmModal = (confirmInfo: ConfirmInfo) => { updateStore('showConfirmModal', () => true); updateStore('confirmInfo', () => ({...confirmInfo })); @@ -179,6 +205,15 @@ export const AppProvider = (props: { children: JSXElement }) => { updateStore('showNoteContextMenu', () => false); }; + const getCashuMint = (url: string) => { + const formatted = new URL(url).toString(); + if (!store.cashuMints.has(formatted)) { + const mint = new CashuMint(formatted); + store.cashuMints.set(formatted, mint); + } + return store.cashuMints.get(formatted); + }; + // EFFECTS -------------------------------------- onMount(() => { @@ -227,6 +262,9 @@ export const AppProvider = (props: { children: JSXElement }) => { closeLnbcModal, openConfirmModal, closeConfirmModal, + openCashuModal, + closeCashuModal, + getCashuMint, } }); diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index 40d5637..9b19570 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -36,6 +36,7 @@ import PageCaption from '../components/PageCaption/PageCaption'; import { useMediaContext } from '../contexts/MediaContext'; import PageTitle from '../components/PageTitle/PageTitle'; import Lnbc from '../components/Lnbc/Lnbc'; +import Cashu from '../components/Cashu/Cashu'; type AutoSizedTextArea = HTMLTextAreaElement & { _baseScrollHeight: number }; @@ -840,6 +841,13 @@ const Messages: Component = () => { return test }; + const msgHasCashu = (msg: DirectMessage) => { + const r =/(\s+|\r\n|\r|\n|^)cashuA[a-zA-Z0-9]+/; + const test = r.test(msg.content); + + return test + }; + createEffect(() => { if (account?.hasPublicKey()) { profile?.actions.setProfileKey(account.publicKey) @@ -894,7 +902,7 @@ const Messages: Component = () => { } const renderMessage = (msg: DirectMessage, thread: DirectMessageThread) => { - if (!msgHasInvoice(msg)) { + if (!msgHasInvoice(msg) && !msgHasCashu(msg)) { return (
{ let sectionIndex = 0; tokens.forEach((t) => { - if (t.startsWith('lnbc')) { + if (t.startsWith('lnbc') || t.startsWith('cashuA')) { if (sections[sectionIndex]) sectionIndex++; sections[sectionIndex] = t; @@ -956,6 +964,15 @@ const Messages: Component = () => {
+ +
+ +
+
)} diff --git a/src/translations.ts b/src/translations.ts index 0f8e9c7..2c2d565 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -2199,3 +2199,54 @@ export const lnInvoice = { }, }; + +export const cashuInvoice = { + redeem: { + id: 'cashuInvoice.redeem', + defaultMessage: 'Reedem', + description: 'Reedem ecash action', + }, + pending: { + id: 'cashuInvoice.pending', + defaultMessage: 'Pending', + description: 'Pending ecash', + }, + spent: { + id: 'cashuInvoice.spent', + defaultMessage: 'Spent', + description: 'Spent ecash', + }, + title: { + id: 'cashuInvoice.title', + defaultMessage: 'Cashu Ecash', + description: 'Cashu Ecash title', + }, + mint: { + id: 'cashuInvoice.mint', + defaultMessage: 'Mint: {url}', + description: 'Mint url', + }, + confirm: { + title: { + id: 'cashuInvoice.confirm.title', + defaultMessage: 'Are you sure?', + description: 'Cashu invoice pay confirmation', + }, + description: { + id: 'cashuInvoice.confirm.description', + defaultMessage: 'Redeem {amount}', + description: 'Cashu Invoice confirm description', + }, + confirmLabel: { + id: 'cashuInvoice.confirm.confirmLabel', + defaultMessage: 'Yes, redeem', + description: 'Cashu Invoice confirm button label', + }, + abortLabel: { + id: 'cashuInvoice.confirm.abortLabel', + defaultMessage: 'Cancel', + description: 'Cashu Invoice confirm button label', + }, + }, + +};