From 8fcabac4502f7bf7f388742e74510fc31687b8b9 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:45:39 +0100 Subject: [PATCH] Add zaps: NWC wallet connect + NIP-57 zap flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NWC client (nwc.ts): parse URI, encrypt/send kind 23194, await kind 23195 response - Lightning store: persist NWC URI to localStorage, zap() via NDKZapper + lnPay callback - ZapModal: amount presets (21/100/500/1000/5000 sats), custom amount, optional comment, paying/success/error states, prompts to Settings if no wallet connected - ⚡ zap button on NoteCard (action row) and ProfileView (header, next to follow) - Settings > Lightning Wallet section: paste NWC URI, connect/disconnect Co-Authored-By: Claude Sonnet 4.6 --- src/components/feed/NoteCard.tsx | 16 +++ src/components/profile/ProfileView.tsx | 40 ++++-- src/components/shared/SettingsView.tsx | 72 +++++++++++ src/components/zap/ZapModal.tsx | 162 +++++++++++++++++++++++++ src/lib/lightning/nwc.ts | 97 +++++++++++++++ src/stores/lightning.ts | 82 +++++++++++++ 6 files changed, 458 insertions(+), 11 deletions(-) create mode 100644 src/components/zap/ZapModal.tsx create mode 100644 src/lib/lightning/nwc.ts create mode 100644 src/stores/lightning.ts diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index 66f6963..c1b5759 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -7,6 +7,7 @@ import { useUIStore } from "../../stores/ui"; import { timeAgo, shortenPubkey } from "../../lib/utils"; import { publishReaction, publishReply } from "../../lib/nostr"; import { NoteContent } from "./NoteContent"; +import { ZapModal } from "../zap/ZapModal"; interface NoteCardProps { event: NDKEvent; @@ -35,6 +36,7 @@ export function NoteCard({ event }: NoteCardProps) { const [replyError, setReplyError] = useState(null); const [replySent, setReplySent] = useState(false); const replyRef = useRef(null); + const [showZap, setShowZap] = useState(false); const handleLike = async () => { if (!loggedIn || liked || liking) return; @@ -139,9 +141,23 @@ export function NoteCard({ event }: NoteCardProps) { > {liked ? "♥" : "♡"}{reactionCount !== null && reactionCount > 0 ? ` ${reactionCount}` : liked ? " liked" : " like"} + )} + {showZap && ( + setShowZap(false)} + /> + )} + {/* Inline reply box */} {showReply && (
diff --git a/src/components/profile/ProfileView.tsx b/src/components/profile/ProfileView.tsx index 9acd639..bfdd2f4 100644 --- a/src/components/profile/ProfileView.tsx +++ b/src/components/profile/ProfileView.tsx @@ -6,6 +6,7 @@ import { useProfile, invalidateProfileCache } from "../../hooks/useProfile"; import { fetchUserNotes, publishProfile } from "../../lib/nostr"; import { shortenPubkey } from "../../lib/utils"; import { NoteCard } from "../feed/NoteCard"; +import { ZapModal } from "../zap/ZapModal"; function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => void }) { const { profile, fetchOwnProfile } = useUserStore(); @@ -108,6 +109,7 @@ export function ProfileView() { const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const [followPending, setFollowPending] = useState(false); + const [showZap, setShowZap] = useState(false); const isFollowing = follows.includes(pubkey); @@ -161,17 +163,25 @@ export function ProfileView() { )} {!isOwn && loggedIn && ( - +
+ + +
)} @@ -219,6 +229,14 @@ export function ProfileView() {
)} + {showZap && ( + setShowZap(false)} + /> + )} + {/* Notes */} {loading &&
Loading notes…
} {!loading && notes.length === 0 &&
No notes found.
} diff --git a/src/components/shared/SettingsView.tsx b/src/components/shared/SettingsView.tsx index 7a99e7b..f341621 100644 --- a/src/components/shared/SettingsView.tsx +++ b/src/components/shared/SettingsView.tsx @@ -1,5 +1,7 @@ import { useState } from "react"; import { useUserStore } from "../../stores/user"; +import { useLightningStore } from "../../stores/lightning"; +import { isValidNwcUri } from "../../lib/lightning/nwc"; import { getNDK, getStoredRelayUrls, addRelay, removeRelay } from "../../lib/nostr"; function RelayRow({ url, onRemove }: { url: string; onRemove: () => void }) { @@ -121,6 +123,75 @@ function IdentitySection() { ); } +function WalletSection() { + const { nwcUri, setNwcUri, clearNwcUri } = useLightningStore(); + const [input, setInput] = useState(""); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + + const handleSave = () => { + const uri = input.trim(); + if (!uri) return; + if (!isValidNwcUri(uri)) { + setError("Invalid NWC URI. Must start with nostr+walletconnect://"); + return; + } + try { + setSaving(true); + setNwcUri(uri); + setInput(""); + setError(null); + } catch (err) { + setError(String(err)); + } finally { + setSaving(false); + } + }; + + return ( +
+

Lightning Wallet (NWC)

+ {nwcUri ? ( +
+
+ + Wallet connected + +
+

Your NWC connection is active. You can zap notes and profiles.

+
+ ) : ( +
+