feat(app): support webln payments

This commit is contained in:
Johns Beharry
2021-11-28 22:59:18 -06:00
parent f4c5a9b5be
commit a3b9eca5ed
6 changed files with 179 additions and 36 deletions

View File

@@ -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 } }))
}

View File

@@ -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])

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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;
},
},
});