From a3b9eca5ed4989420bc15ab78471c01a5201200e Mon Sep 17 00:00:00 2001 From: Johns Beharry Date: Sun, 28 Nov 2021 22:59:18 -0600 Subject: [PATCH] feat(app): support webln payments --- .../ExplorePage/partials/ProjectsRow.tsx | 2 +- src/Components/Login/Login_SuccessCard.tsx | 2 +- src/Components/Project/ProjectCard.tsx | 88 +++++++++++++----- src/Components/Shared/Navbar/Navbar.tsx | 8 +- src/Components/Tip/TipCard.tsx | 93 +++++++++++++++++-- src/redux/features/wallet.slice.ts | 22 ++++- 6 files changed, 179 insertions(+), 36 deletions(-) diff --git a/src/Components/ExplorePage/partials/ProjectsRow.tsx b/src/Components/ExplorePage/partials/ProjectsRow.tsx index b4d16ae..d6567ff 100644 --- a/src/Components/ExplorePage/partials/ProjectsRow.tsx +++ b/src/Components/ExplorePage/partials/ProjectsRow.tsx @@ -34,7 +34,7 @@ export default function ProjectsRow({ title, categoryId, projects }: Props) { document.addEventListener('mousemove', () => drag.current = true); const handleClick = (projectId: string) => { - projectId = '123123123'; + projectId = '1'; if (!drag.current) dispatch(openModal({ modalId: ModalId.Project, propsToPass: { projectId } })) } diff --git a/src/Components/Login/Login_SuccessCard.tsx b/src/Components/Login/Login_SuccessCard.tsx index 3d04e6a..b774fd3 100644 --- a/src/Components/Login/Login_SuccessCard.tsx +++ b/src/Components/Login/Login_SuccessCard.tsx @@ -16,7 +16,7 @@ export default function Login_SuccessCard({ onClose, direction, ...props }: Moda useEffect(() => { - dispatch(connectWallet()); + //dispatch(connectWallet()); const timeout = setTimeout(handleNext, 3000) return () => clearTimeout(timeout) }, [handleNext, dispatch]) diff --git a/src/Components/Project/ProjectCard.tsx b/src/Components/Project/ProjectCard.tsx index 9d1a15f..d851af4 100644 --- a/src/Components/Project/ProjectCard.tsx +++ b/src/Components/Project/ProjectCard.tsx @@ -1,31 +1,71 @@ +import { useEffect, useState } from 'react'; import { motion } from 'framer-motion' import { BiArrowBack } from 'react-icons/bi' import { BsJoystick } from 'react-icons/bs' +import { AiFillThunderbolt } from 'react-icons/ai'; import { MdLocalFireDepartment } from 'react-icons/md'; import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer'; -import { useQuery } from 'react-query'; +import { gql, useQuery } from "@apollo/client"; import { getProjectById } from '../../api'; import { useAppDispatch, useAppSelector } from '../../utils/hooks'; import { ModalId, openModal, scheduleModal } from '../../redux/features/modals.slice'; import { setProject } from '../../redux/features/project.slice'; +import { connectWallet } from '../../redux/features/wallet.slice'; import Button from 'src/Components/Shared/Button/Button'; +import { requestProvider } from 'webln'; +import mockData from "../../api/mockData.json"; +const PROJECT_BY_ID = gql` + query Project($projectId: Int!) { + getProject(id: $projectId) { + id + cover_image + thumbnail_image + title + website + votes_count + } + } + `; export default function ProjectCard({ onClose, direction, ...props }: ModalCard) { - - const { data: project, isLoading } = useQuery( - ['get-project', props.projectId], - () => getProjectById(props.projectId), + const { loading, error, data } = useQuery( + PROJECT_BY_ID, { - onSuccess: project => dispatch(setProject(project)) + variables: {projectId: parseInt(props.projectId)}, + onCompleted: data => dispatch(setProject(data.getProject)), } - ) - const { isWalletConnected } = useAppSelector(state => ({ isWalletConnected: state.wallet.isConnected })) + ); + + const [projectState, setProjectState] = useState({}); + + const { isWalletConnected, webln, project } = useAppSelector(state => ({ + isWalletConnected: state.wallet.isConnected, + webln: state.wallet.provider, + project: state.project, + })); + const dispatch = useAppDispatch(); - if (isLoading || !project) return <>; + console.log("project", props.projectId, project); + if (loading || !project) return <>; + + const onConnectWallet = async () => { + try { + const webln = await requestProvider(); + if(webln) { + dispatch(connectWallet(webln)); + alert("wallet connected!"); + } + // Now you can call all of the webln.* methods + } + catch(err: any) { + // Tell the user what went wrong + alert(err.message); + } + } const onTip = () => { @@ -65,37 +105,41 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard) >
- +
- +
-

{project.title}

- {project.website?.replace(/(^\w+:|^)\/\//, '')} +

{project?.title}

+ {project?.website?.replace(/(^\w+:|^)\/\//, '')}
- {project.category.title} + {project?.category.title} - {project.votes_count} + {project?.votes_count}
- + {isWalletConnected ? + + : + + }
-

{project.description}

-
- payments - lightining -
+

{project?.description}

- + {isWalletConnected ? + + : + + }

Screenshots

diff --git a/src/Components/Shared/Navbar/Navbar.tsx b/src/Components/Shared/Navbar/Navbar.tsx index e9d2cfd..79beeb1 100644 --- a/src/Components/Shared/Navbar/Navbar.tsx +++ b/src/Components/Shared/Navbar/Navbar.tsx @@ -25,7 +25,11 @@ export default function Navbar() { const [searchInput, setSearchInput] = useState(""); const inputRef = useRef(null) const dispatch = useAppDispatch() - const { isWalletConnected } = useAppSelector(state => ({ isWalletConnected: state.wallet.isConnected })) + + const { isWalletConnected, webln } = useAppSelector(state => ({ + isWalletConnected: state.wallet.isConnected, + webln: state.wallet.provider, + })); const toggleSearch = () => { if (!searchOpen) { @@ -78,7 +82,7 @@ export default function Navbar() { className="flex"> {isWalletConnected ? - + : } diff --git a/src/Components/Tip/TipCard.tsx b/src/Components/Tip/TipCard.tsx index 2e54ea7..6d58841 100644 --- a/src/Components/Tip/TipCard.tsx +++ b/src/Components/Tip/TipCard.tsx @@ -3,6 +3,10 @@ import React, { useState } from 'react'; import { AiFillThunderbolt } from 'react-icons/ai' import { IoClose } from 'react-icons/io5' import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer'; +import { useAppDispatch, useAppSelector } from '../../utils/hooks'; +import { gql, useQuery, useMutation } from "@apollo/client"; +import useWindowSize from "react-use/lib/useWindowSize"; +import Confetti from "react-confetti"; const defaultOptions = [ { text: '10 sat', value: 10 }, @@ -10,19 +14,88 @@ const defaultOptions = [ { text: '1k sats', value: 1000 }, ] -export default function TipCard({ onClose, direction, ...props }: ModalCard) { +enum PaymentStatus { + DEFAULT, + FETCHING_PAYMENT_DETAILS, + PAID, + AWAITING_PAYMENT, + PAYMENT_CONFIRMED, + NOT_PAID, + CANCELED +} - const [selectedOption, setSelectedOption] = useState(0); - const [input, setInput] = useState(); +const VOTE = gql` +mutation Mutation($projectId: Int!, $amountInSat: Int!) { + vote(project_id: $projectId, amount_in_sat: $amountInSat) { + id + amount_in_sat + payment_request + payment_hash + paid + } +} +`; + +const CONFIRM_VOTE = gql` +mutation Mutation($paymentRequest: String!, $preimage: String!) { + confirmVote(payment_request: $paymentRequest, preimage: $preimage) { + id + amount_in_sat + payment_request + payment_hash + paid + } +} +`; + +export default function TipCard({ onClose, direction, ...props }: ModalCard) { + const { width, height } = useWindowSize() + + const { isWalletConnected, webln } = useAppSelector(state => ({ + isWalletConnected: state.wallet.isConnected, + webln: state.wallet.provider, + })); + + const dispatch = useAppDispatch(); + + const [selectedOption, setSelectedOption] = useState(10); + const [voteAmount, setVoteAmount] = useState(10); + const [paymentStatus, setPaymentStatus] = useState(PaymentStatus.DEFAULT); + + const [vote, { data }] = useMutation(VOTE, { + onCompleted: (votingData) => { + setPaymentStatus(PaymentStatus.AWAITING_PAYMENT); + webln.sendPayment(votingData.vote.payment_request).then((res: any) => { + console.log("waiting for payment", res); + confirmVote({variables: { paymentRequest: votingData.vote.payment_request, preimage: res.preimage }}); + setPaymentStatus(PaymentStatus.PAID); + }) + .catch((err: any) => { + console.log(err); + setPaymentStatus(PaymentStatus.NOT_PAID); + }); + } + }); + + const [confirmVote, { data: confirmedVoteData }] = useMutation(CONFIRM_VOTE, { + onCompleted: (votingData) => { + setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED); + } + }); const onChangeInput = (event: React.ChangeEvent) => { setSelectedOption(-1); - setInput(Number(event.target.value)); + setVoteAmount(Number(event.target.value)); }; const onSelectOption = (idx: number) => { setSelectedOption(idx); - setInput(defaultOptions[idx].value); + setVoteAmount(defaultOptions[idx].value); + } + + const requestPayment = () => { + setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS); + vote({variables: { "amountInSat": voteAmount, "projectId": parseInt("1") }}); } return ( @@ -43,7 +116,7 @@ export default function TipCard({ onClose, direction, ...props }: ModalCard) {
{/* */} @@ -60,10 +133,16 @@ export default function TipCard({ onClose, direction, ...props }: ModalCard) { )}

1 sat = 1 vote

-
+ {paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && } ) } diff --git a/src/redux/features/wallet.slice.ts b/src/redux/features/wallet.slice.ts index 1781455..8664ecd 100644 --- a/src/redux/features/wallet.slice.ts +++ b/src/redux/features/wallet.slice.ts @@ -1,4 +1,5 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { requestProvider } from "webln"; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; interface StoreState { isConnected: boolean; @@ -6,6 +7,20 @@ interface StoreState { provider: any; } +const isWebLNConnected = () => { + // since webln spec expects webln.enable() to be called on each load + // and extensions like alby do not inject the webln object with true + // every refresh requires the user to re-enable, even if its been + // given premission previously. + // + // that is to say ... this function is quite useless + if (typeof window.webln === 'undefined') { + return false; + } else if (window.webln.enabled === true) { + return true; + } +} + const initialState = { isConnected: false, isLoading: false, @@ -16,8 +31,9 @@ export const walletSlice = createSlice({ name: "wallet", initialState, reducers: { - connectWallet(state) { - state.isConnected = true; + connectWallet(state, action: PayloadAction) { + state.isConnected = action.payload ? true : false; + state.provider = action.payload; }, }, });