mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-23 00:04:20 +01:00
feat(app): support webln payments
This commit is contained in:
@@ -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 } }))
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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<any>(
|
||||
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<any>({});
|
||||
|
||||
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)
|
||||
|
||||
>
|
||||
<div className="relative h-[152px]">
|
||||
<img className="w-full h-full object-cover" src={project.cover_image} alt="" />
|
||||
<img className="w-full h-full object-cover" src={project?.cover_image} alt="" />
|
||||
<button className="w-[48px] h-[48px] bg-white absolute top-1/2 left-32 -translate-y-1/2 rounded-full hover:bg-gray-200 text-center" onClick={onClose}><BiArrowBack className=' inline-block text-body1' /></button>
|
||||
</div>
|
||||
<div className="p-24">
|
||||
<div className="flex gap-24 items-center h-[93px]">
|
||||
<div className="flex-shrink-0 w-[93px] h-[93px] rounded-md overflow-hidden">
|
||||
<img className="w-full h-full object-cover" src={project.thumbnail_image} alt="" />
|
||||
<img className="w-full h-full object-cover" src={project?.thumbnail_image} alt="" />
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-between self-stretch'>
|
||||
<h3 className="text-h3 font-regular">{project.title}</h3>
|
||||
<a className="text-blue-400 font-regular text-body4" target='_blank' rel="noreferrer" href={project.website}>{project.website?.replace(/(^\w+:|^)\/\//, '')}</a>
|
||||
<h3 className="text-h3 font-regular">{project?.title}</h3>
|
||||
<a className="text-blue-400 font-regular text-body4" target='_blank' rel="noreferrer" href={project?.website}>{project?.website?.replace(/(^\w+:|^)\/\//, '')}</a>
|
||||
<div>
|
||||
<span className="chip-small font-light text-body5 py-4 px-12 mr-8"> {project.category.title}</span>
|
||||
<span className="chip-small font-light text-body5 py-4 px-12 mr-8"> {project?.category.title}</span>
|
||||
|
||||
<span className="chip-small bg-warning-50 font-light text-body5 py-4 px-12"><MdLocalFireDepartment className='inline-block text-fire transform text-body4 align-middle' /> {project.votes_count}</span>
|
||||
<span className="chip-small bg-warning-50 font-light text-body5 py-4 px-12"><MdLocalFireDepartment className='inline-block text-fire transform text-body4 align-middle' /> {project?.votes_count}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 hidden md:flex ml-auto gap-16">
|
||||
<Button color='primary' size='md' className=" my-16">Play <BsJoystick /></Button>
|
||||
<Button onClick={onTip} size='md' className="border border-warning-100 bg-warning-50 hover:bg-warning-50 active:bg-warning-100 my-16">Tip <MdLocalFireDepartment className='text-fire' /></Button>
|
||||
{isWalletConnected ?
|
||||
<Button onClick={onTip} size='md' className="border border-warning-100 bg-warning-50 hover:bg-warning-50 active:bg-warning-100 my-16">Tip <MdLocalFireDepartment className='text-fire' /></Button>
|
||||
:
|
||||
<Button onClick={onConnectWallet} size='md' className="border border-gray-200 bg-gray-100 hover:bg-gray-50 active:bg-gray-100 my-16">Connect Wallet to Vote</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-40 text-body4 leading-normal">{project.description}</p>
|
||||
<div className="flex gap-16 mt-24 flex-wrap">
|
||||
<span className="chip-small bg-red-100 text-red-800 font-regular"> payments </span>
|
||||
<span className="chip-small bg-primary-100 text-primary-800 font-regular"> lightining </span>
|
||||
</div>
|
||||
<p className="mt-40 text-body4 leading-normal">{project?.description}</p>
|
||||
<div className="md:hidden">
|
||||
<Button color='primary' size='md' fullWidth className="w-full mt-24 mb-16">Play <BsJoystick /></Button>
|
||||
<Button size='md' fullWidth className="w-full bg-yellow-100 hover:bg-yellow-50 mb-24" onClick={onTip}>Vote <MdLocalFireDepartment className='text-fire' /></Button>
|
||||
{isWalletConnected ?
|
||||
<Button size='md' fullWidth className="w-full bg-yellow-100 hover:bg-yellow-50 mb-24" onClick={onTip}>Vote <MdLocalFireDepartment className='text-fire' /></Button>
|
||||
:
|
||||
<Button size='md' fullWidth className="w-full bg-gray-200 hover:bg-gray-100 mb-24" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Vote</Button>
|
||||
}
|
||||
</div>
|
||||
<div className="mt-40">
|
||||
<h3 className="text-h5 font-bold mb-16">Screenshots</h3>
|
||||
|
||||
@@ -25,7 +25,11 @@ export default function Navbar() {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(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">
|
||||
<Button color='primary' size='md' className="lg:px-40">Submit App️</Button>
|
||||
{isWalletConnected ?
|
||||
<Button className="ml-16 py-12 px-16 lg:px-20" onClick={onWithdraw}>2.2k Sats <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
|
||||
<Button className="ml-16 py-12 px-16 lg:px-20">Connected <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
|
||||
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
|
||||
}
|
||||
</motion.div>
|
||||
|
||||
@@ -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<number>();
|
||||
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<number>(10);
|
||||
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(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<HTMLInputElement>) => {
|
||||
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) {
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
className="input-field"
|
||||
value={input} onChange={onChangeInput}
|
||||
value={voteAmount} onChange={onChangeInput}
|
||||
type="number"
|
||||
placeholder="e.g 5 sats" />
|
||||
{/* <IoCopy className='input-icon' /> */}
|
||||
@@ -60,10 +133,16 @@ export default function TipCard({ onClose, direction, ...props }: ModalCard) {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-body6 mt-12 text-gray-500">1 sat = 1 vote</p>
|
||||
<button className="btn btn-primary w-full mt-32" onClick={onClose}>
|
||||
{paymentStatus === PaymentStatus.FETCHING_PAYMENT_DETAILS && <p className="text-body6 mt-12 text-gray-500">Please wait while we the fetch payment details.</p>}
|
||||
{paymentStatus === PaymentStatus.NOT_PAID && <p className="text-body6 mt-12 text-red-500">You did not confirm the payment. Please try again.</p>}
|
||||
{paymentStatus === PaymentStatus.PAID && <p className="text-body6 mt-12 text-green-500">The invoice was paid! Please wait while we confirm it.</p>}
|
||||
{paymentStatus === PaymentStatus.AWAITING_PAYMENT && <p className="text-body6 mt-12 text-yellow-500">Please confirm the payment in the prompt...</p>}
|
||||
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <p className="text-body6 mt-12 text-green-500">Imagine confetti here</p>}
|
||||
<button className="btn btn-primary w-full mt-32" onClick={requestPayment}>
|
||||
Upvote
|
||||
</button>
|
||||
</div>
|
||||
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <Confetti width={width} height={height} />}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<any>) {
|
||||
state.isConnected = action.payload ? true : false;
|
||||
state.provider = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user