Implemented basic zap function, starting on listening for zaps

This commit is contained in:
austinkelsay 2024-03-27 18:12:17 -05:00
parent 6eb4edf617
commit 6ef8f2cb88
4 changed files with 111 additions and 39 deletions
src
components/content/carousels
hooks
pages/details

@ -1,7 +1,5 @@
import React, { useState, useEffect, use } from 'react';
import React, { useState, useEffect } from 'react';
import { Carousel } from 'primereact/carousel';
import { useRouter } from 'next/router';
import { useImageProxy } from '@/hooks/useImageProxy';
import { parseEvent } from '@/utils/nostr';
import { useNostr } from '@/hooks/useNostr';
import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate';
@ -27,11 +25,8 @@ const responsiveOptions = [
export default function CoursesCarousel() {
const [processedCourses, setProcessedCourses] = useState([]);
const [screenWidth, setScreenWidth] = useState(null);
const [courses, setCourses] = useState([]);
const router = useRouter();
const { fetchCourses, events } = useNostr();
const { returnImageProxy } = useImageProxy();
useEffect(() => {
if (events && events.courses && events.courses.length > 0) {
@ -41,35 +36,6 @@ export default function CoursesCarousel() {
}
}, [events]);
useEffect(() => {
// Update the state to the current window width
setScreenWidth(window.innerWidth);
const handleResize = () => {
// Update the state to the new window width when it changes
setScreenWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Remove the event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []); // The empty array ensures this effect only runs once, similar to componentDidMount
const calculateImageDimensions = () => {
if (screenWidth >= 1200) {
// Large screens
return { width: 426, height: 240 };
} else if (screenWidth >= 768 && screenWidth < 1200) {
// Medium screens
return { width: 344, height: 194 };
} else {
// Small screens
return { width: screenWidth - 120, height: (screenWidth - 120) * (9 / 16) };
}
};
useEffect(() => {
const processCourses = courses.map(course => {
const { id, content, title, summary, image, published_at } = parseEvent(course);

@ -1,14 +1,27 @@
import React from "react";
import React, {useEffect, useState} from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useResponsiveImageDimensions from "@/hooks/useResponsiveImageDimensions";
import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useNostr } from "@/hooks/useNostr";
const CourseTemplate = (course) => {
const [zaps, setZaps] = useState([]);
const router = useRouter();
const { returnImageProxy } = useImageProxy();
const { width, height } = useResponsiveImageDimensions();
const {events, fetchZapsForEvent} = useNostr();
useEffect(() => {
if (events && events.zaps) {
console.log('zaps:', events.zaps);
setZaps(events.zaps);
} else {
fetchZapsForEvent(course.id);
}
}, [events]);
return (
<div style={{width: width < 768 ? "auto" : width}} onClick={() => router.push(`/details/${course.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg">
<div style={{maxWidth: width, minWidth: width}} className="max-tab:h-auto max-mob:h-auto">

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
import { SimplePool, nip19, verifyEvent } from "nostr-tools";
import { SimplePool, nip19, verifyEvent, nip57 } from "nostr-tools";
import axios from "axios";
import { useToast } from "./useToast";
@ -20,7 +20,8 @@ export const useNostr = () => {
resources: [],
workshops: [],
courses: [],
streams: []
streams: [],
zaps: []
});
const { showToast } = useToast();
@ -53,6 +54,9 @@ export const useNostr = () => {
try {
const sub = pool.current.subscribeMany(relays, filter, {
onevent: async (event) => {
if (event.kind === 9735) {
console.log('event:', event);
}
const shouldInclude = await hasRequiredTags(event.tags);
if (shouldInclude) {
setEvents(prevData => ({
@ -75,6 +79,84 @@ export const useNostr = () => {
}
};
// zaps
// 1. get the author from the content
// 2. get the author's kind0
// 3. get the author's lud16 if available
// 4. Make a get request to the lud16 endpoint and ensure that allowNostr is true
// 5. Create zap request event and sign it
// 6. Send to the callback url as a get req with the nostr event as a query param
// 7. get the invoice back and pay it with webln
// 8. listen for the zap receipt event and update the UI
const zapEvent = async (event) => {
const kind0 = await fetchKind0([{ authors: [event.pubkey], kinds: [0] }], {});
if (Object.keys(kind0).length === 0) {
console.error('Error fetching kind0');
return;
}
if (kind0?.lud16) {
const lud16Username = kind0.lud16.split('@')[0];
const lud16Domain = kind0.lud16.split('@')[1];
const lud16Url = `https://${lud16Domain}/.well-known/lnurlp/${lud16Username}`;
const response = await axios.get(lud16Url);
if (response.data.allowsNostr) {
const zapReq = nip57.makeZapRequest({
profile: event.pubkey,
event: event.id,
amount: 1000,
relays: relays,
comment: 'Plebdevs Zap'
});
console.log('zapReq:', zapReq);
const signedEvent = await window?.nostr.signEvent(zapReq);
const callbackUrl = response.data.callback;
const zapRequestAPICall = `${callbackUrl}?amount=${1000}&nostr=${encodeURI(JSON.stringify(signedEvent))}`;
const invoiceResponse = await axios.get(zapRequestAPICall);
if (invoiceResponse?.data?.pr) {
const invoice = invoiceResponse.data.pr;
const enabled = await window?.webln?.enable();
console.log('webln enabled:', enabled);
const payInvoiceResponse = await window?.webln?.sendPayment(invoice);
console.log('payInvoiceResponse:', payInvoiceResponse);
} else {
console.error('Error fetching invoice');
showToast('error', 'Error', 'Error fetching invoice');
}
}
} else if (kind0?.lud06) {
// handle lnurlpay
} else {
showToast('error', 'Error', 'User has no Lightning Address or LNURL');
return;
}
}
const fetchZapsForEvent = async (eventId) => {
const filter = [{ kinds: [9735] }];
const hasRequiredTags = async (eventData) => {
const hasEtag = eventData.some(([tag, value]) => tag === "e" && value === eventId);
return hasEtag;
};
fetchEvents(filter, 'zaps', hasRequiredTags);
}
// Fetch resources, workshops, courses, and streams with appropriate filters and update functions
const fetchResources = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
@ -288,6 +370,8 @@ export const useNostr = () => {
fetchCourses,
fetchWorkshops,
// fetchStreams,
zapEvent,
fetchZapsForEvent,
getRelayStatuses,
events
};

@ -27,10 +27,18 @@ export default function Details() {
const [author, setAuthor] = useState(null);
const { returnImageProxy } = useImageProxy();
const { fetchSingleEvent, fetchKind0 } = useNostr();
const { fetchSingleEvent, fetchKind0, zapEvent } = useNostr();
const router = useRouter();
const handleZapEvent = async () => {
if (!event) return;
const response = await zapEvent(event);
console.log('zap response:', response);
}
useEffect(() => {
if (router.isReady) {
const { slug } = router.query;
@ -112,6 +120,7 @@ export default function Details() {
label="Zap"
severity="success"
outlined
onClick={handleZapEvent}
pt={{
button: {
icon: ({ context }) => ({