From 7fd9f4ae85224d68cd88a4afba4fa5b386bd8598 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 25 May 2024 17:41:11 -0300 Subject: [PATCH 01/11] fix: remove window.webln as a requirement --- src/components/status-action-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index 61242fee0..92a54d7af 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -800,7 +800,7 @@ const StatusActionBar: React.FC = ({ /> )} - {(acceptsZaps && window.webln) && ( + {(acceptsZaps) && ( Date: Tue, 28 May 2024 22:16:30 -0300 Subject: [PATCH 02/11] fix: remove nostr Nostr Wallet Connect (NWC) --- src/api/hooks/nostr/useSignerStream.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index 9c6899930..945c09fd9 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'; import { WebsocketEvent } from 'websocket-ts'; import { useNostr } from 'soapbox/contexts/nostr-context'; -import { nwcRequestSchema } from 'soapbox/schemas/nostr'; function useSignerStream() { const { relay, pubkey, signer } = useNostr(); @@ -92,30 +91,11 @@ function useSignerStream() { } } - async function handleWalletEvent(event: NostrEvent) { - if (!relay || !pubkey || !signer) return; - - const decrypted = await signer.nip04!.decrypt(pubkey, event.content); - - const reqMsg = n.json().pipe(nwcRequestSchema).safeParse(decrypted); - if (!reqMsg.success) { - console.warn(decrypted); - console.warn(reqMsg.error); - return; - } - - await window.webln?.enable(); - await window.webln?.sendPayment(reqMsg.data.params.invoice); - } - async function handleEvent(event: NostrEvent) { switch (event.kind) { case 24133: await handleConnectEvent(event); break; - case 23194: - await handleWalletEvent(event); - break; } } @@ -149,7 +129,7 @@ function useSignerStream() { const signal = controller.signal; (async() => { - for await (const msg of relay.req([{ kinds: [24133, 23194], authors: [pubkey], limit: 0 }], { signal })) { + for await (const msg of relay.req([{ kinds: [24133], authors: [pubkey], limit: 0 }], { signal })) { if (msg[0] === 'EVENT') handleEvent(msg[2]); } })(); From 9dc4fbb62d19400dfc4668e4d782c91d68da8385 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 29 May 2024 17:50:29 -0300 Subject: [PATCH 03/11] feat(locales): add english zap messages --- src/locales/en.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index d869a77ba..aeca5d5df 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1637,5 +1637,8 @@ "video.pause": "Pause", "video.play": "Play", "video.unmute": "Unmute sound", - "who_to_follow.title": "People To Follow" -} \ No newline at end of file + "who_to_follow.title": "People To Follow", + "zap.send_to": "Send zaps to {target}", + "zap.unit": "Zap amount in sats", + "zap.comment_input.placeholder":"Optional comment" +} From b8dbf62a06c3897138da3e6dcd5eae3026341023 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 29 May 2024 17:59:23 -0300 Subject: [PATCH 04/11] feat(interactions-zap): get invoice from response header, use window.webln directly --- src/actions/interactions.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/actions/interactions.ts b/src/actions/interactions.ts index 60f1d686b..43152f8dd 100644 --- a/src/actions/interactions.ts +++ b/src/actions/interactions.ts @@ -314,18 +314,30 @@ const undislikeFail = (status: StatusEntity, error: unknown) => ({ skipLoading: true, }); -const zap = (status: StatusEntity, amount: number) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const zap = (status: StatusEntity, amount: number, comment:string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - dispatch(zapRequest(status)); + dispatch(zapRequest(status)); - api(getState).post(`/api/v1/statuses/${status.id}/zap`, { amount }).then(function(response) { + return api(getState).post(`/api/v1/statuses/${status.id}/zap`, { amount, comment: comment ?? '' }).then(async function(response) { + const invoice = response.headers['ln-invoice']; + if (!invoice) throw Error('Could not generate invoice'); + if (!window.webln) return invoice; + + try { + await window.webln?.enable(); + await window.webln?.sendPayment(invoice); dispatch(zapSuccess(status)); - }).catch(function(error) { - dispatch(zapFail(status, error)); - }); - }; + return undefined; + } catch (e) { // In case it fails we just return the invoice so the QR code can be created + console.log(e); + return invoice; + } + }).catch(function(e) { + console.log(e); + dispatch(zapFail(status, e)); + }); +}; const zapRequest = (status: StatusEntity) => ({ type: ZAP_REQUEST, From 1494d921aa872f16083e83143c5f89329962fef4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 29 May 2024 18:10:46 -0300 Subject: [PATCH 05/11] feat(handleZapClick): open the zap modal when zapping a user's post --- src/components/status-action-bar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index 92a54d7af..09d599c15 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -6,7 +6,7 @@ import { blockAccount } from 'soapbox/actions/accounts'; import { launchChat } from 'soapbox/actions/chats'; import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose'; import { editEvent } from 'soapbox/actions/events'; -import { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup, zap } from 'soapbox/actions/interactions'; +import { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation'; import { initMuteModal } from 'soapbox/actions/mutes'; @@ -195,9 +195,9 @@ const StatusActionBar: React.FC = ({ const handleZapClick: React.EventHandler = (e) => { if (me) { - dispatch(zap(status, 1337)); + dispatch(openModal('ZAP_PAY_REQUEST', { status, account: status.account })); } else { - onOpenUnauthorizedModal('ZAP'); + onOpenUnauthorizedModal('ZAP_PAY_REQUEST'); } }; From a6795dd2b39fac3bd3626c15d28f6137693c6243 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 29 May 2024 18:23:59 -0300 Subject: [PATCH 06/11] feat: create ZapPayRequestModal --- src/features/ui/components/modal-root.tsx | 2 ++ .../ui/components/modals/zap-pay-request.tsx | 32 +++++++++++++++++++ src/features/ui/util/async-components.ts | 1 + 3 files changed, 35 insertions(+) create mode 100644 src/features/ui/components/modals/zap-pay-request.tsx diff --git a/src/features/ui/components/modal-root.tsx b/src/features/ui/components/modal-root.tsx index 5fdc0225a..3f0743df2 100644 --- a/src/features/ui/components/modal-root.tsx +++ b/src/features/ui/components/modal-root.tsx @@ -42,6 +42,7 @@ import { UnauthorizedModal, VideoModal, EditRuleModal, + ZapPayRequestModal, } from 'soapbox/features/ui/util/async-components'; import ModalLoading from './modal-loading'; @@ -88,6 +89,7 @@ const MODAL_COMPONENTS: Record> = { 'SELECT_BOOKMARK_FOLDER': SelectBookmarkFolderModal, 'UNAUTHORIZED': UnauthorizedModal, 'VIDEO': VideoModal, + 'ZAP_PAY_REQUEST': ZapPayRequestModal, }; export type ModalType = keyof typeof MODAL_COMPONENTS | null; diff --git a/src/features/ui/components/modals/zap-pay-request.tsx b/src/features/ui/components/modals/zap-pay-request.tsx new file mode 100644 index 000000000..34c627f1b --- /dev/null +++ b/src/features/ui/components/modals/zap-pay-request.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { Modal } from 'soapbox/components/ui'; + +import ZapPayRequestForm from '../../../zap/components/zap-pay-request-form'; + +import type { Status as StatusEntity, Account as AccountEntity } from 'soapbox/types/entities'; + +interface IZapPayRequestModal { + account: AccountEntity; + status?: StatusEntity; + onClose:(type?: string) => void; +} + +const ZapPayRequestModal: React.FC = ({ account, status, onClose }) => { + const onClickClose = () => { + onClose('ZAP_PAY_REQUEST'); + }; + + const renderTitle = () => { + return ; + }; + + return ( + + + + ); +}; + +export default ZapPayRequestModal; diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 7d413bc5b..d358c3bd3 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -176,3 +176,4 @@ export const Relays = lazy(() => import('soapbox/features/admin/relays')); export const Rules = lazy(() => import('soapbox/features/admin/rules')); export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal')); export const AdminNostrRelays = lazy(() => import('soapbox/features/admin/nostr-relays')); +export const ZapPayRequestModal = lazy(() => import('soapbox/features/ui/components/modals/zap-pay-request')); From e32107b89925940447fc91648eb6d229cf524fd2 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 29 May 2024 18:27:22 -0300 Subject: [PATCH 07/11] feat: create ZapPayRequestForm --- .../zap/components/zap-pay-request-form.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/features/zap/components/zap-pay-request-form.tsx diff --git a/src/features/zap/components/zap-pay-request-form.tsx b/src/features/zap/components/zap-pay-request-form.tsx new file mode 100644 index 000000000..5242d5994 --- /dev/null +++ b/src/features/zap/components/zap-pay-request-form.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import { zap } from 'soapbox/actions/interactions'; +import { closeModal } from 'soapbox/actions/modals'; +import Account from 'soapbox/components/account'; +import { Stack, Button, Select } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; + +interface IZapPayRequestForm { + status?: StatusEntity; + account: AccountEntity; +} + +const messages = defineMessages({ + zap_button: { id: 'status.zap', defaultMessage: 'Zap' }, + zap_commentPlaceholder: { id: 'zap.comment_input.placeholder', defaultMessage: 'Optional comment' }, +}); + +const ZapPayRequestForm = ({ account, status }: IZapPayRequestForm) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const [zapComment, setZapComment] = useState(''); + const [zapAmount, setZapAmount] = useState(1); + + const handleSubmit = async (e?: React.FormEvent) => { + e?.preventDefault(); + if (status) { + const invoice = await dispatch(zap(status, zapAmount * 1000, zapComment)); + // If invoice is undefined it means the user has paid through his extension + // In this case, we simply close the modal + if (!invoice) { + dispatch(closeModal('ZAP_PAY_REQUEST')); + return; + } + // open QR code modal + } + }; + + const zapOptions = () => { + return ( + [ + , + , + , + , + , + ] + ); + }; + + return ( + + + setZapComment(e.target.value)} + placeholder={intl.formatMessage(messages.zap_commentPlaceholder)} + /> +