feat: rewrote the way wallet service work

moved the wallet setup logic to a dedicated service that remembers if user connected before and connectes automatically on refreshs
This commit is contained in:
MTG2000
2022-01-17 16:05:42 +02:00
parent 3f8ec31b10
commit c6020935dc
11 changed files with 135 additions and 58 deletions

View File

@@ -3,27 +3,29 @@ import Navbar from "src/Components/Navbar/Navbar";
import ExplorePage from "src/pages/ExplorePage";
import ModalsContainer from "src/Components/Modals/ModalsContainer/ModalsContainer";
import { useAppDispatch, useAppSelector, useResizeListener } from './utils/hooks';
import { connectWallet } from 'src/redux/features/wallet.slice';
import { setIsMobileScreen } from "src/redux/features/theme.slice";
import { Wallet_Service } from "./services";
function App() {
const { isWalletConnected, webln } = useAppSelector(state => ({
const { isWalletConnected } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
}));
const dispatch = useAppDispatch();
useEffect(() => {
if (typeof window.webln != "undefined") {
window.webln.enable().then((res: any) => {
dispatch(connectWallet(window.webln));
console.log("called:webln.enable()", res);
}).catch((err: any) => {
console.log("error:webln.enable()", err);
});
}
}, [dispatch]);
// if (typeof window.webln != "undefined") {
// alert('hi')
// window.webln.enable().then((res: any) => {
// dispatch(connectWallet(window.webln));
// console.log("called:webln.enable()", res);
// }).catch((err: any) => {
// console.log("error:webln.enable()", err);
// });
// }
setTimeout(() => {
Wallet_Service.init();
}, 2000)
}, []);
useResizeListener(() => {
// dispatch(setIsMobileScreen(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)));

View File

@@ -41,7 +41,7 @@ export default function Badge(
size = 'md',
className,
href,
shadow = 'sm',
shadow = 'none',
children,
isLoading,
onRemove,
@@ -50,7 +50,7 @@ export default function Badge(
: PropsWithChildren<Props>) {
const classes = `
rounded-48 shadow-${shadow} inline-block relative align-middle
rounded-48 shadow-${shadow} border inline-block relative align-middle
${badgrColor[color]}
${badgeSize[size]}
${className}

View File

@@ -28,9 +28,8 @@ export default function Navbar() {
const inputRef = useRef<HTMLInputElement>(null)
const dispatch = useAppDispatch()
const { isWalletConnected, webln } = useAppSelector(state => ({
const { isWalletConnected } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
}));
const toggleSearch = () => {

View File

@@ -1,5 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Wrapper from './utils/Wrapper';
import {
ApolloClient,
InMemoryCache,
@@ -8,7 +9,6 @@ import {
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Wrapper from './utils/Wrapper';
const client = new ApolloClient({
uri: 'https://xenodochial-goldstine-d09942.netlify.app/.netlify/functions/graphql',

View File

@@ -10,6 +10,7 @@ import { ALL_CATEGORIES_PROJECTS_QUERY, ALL_CATEGORIES_PROJECTS_RES } from "./qu
export default function ProjectsSection() {
const { data, loading } = useQuery<ALL_CATEGORIES_PROJECTS_RES>(ALL_CATEGORIES_PROJECTS_QUERY);
console.log(data, loading);
if (loading || !data) return <div className='mt-32 lg:mt-48'>
{Array(3).fill(0).map((_, idx) => <ProjectsRowSkeleton key={idx} />)}

View File

@@ -1,18 +1,16 @@
import { motion } from 'framer-motion'
import { BsJoystick } from 'react-icons/bs'
import { MdClose, MdLocalFireDepartment } from 'react-icons/md';
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { ModalCard } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { useQuery } from "@apollo/client";
import { useAppDispatch, useAppSelector } from 'src/utils/hooks';
import { openModal, scheduleModal } from 'src/redux/features/modals.slice';
import { setProject } from 'src/redux/features/project.slice';
import { connectWallet } from 'src/redux/features/wallet.slice';
import Button from 'src/Components/Button/Button';
import { requestProvider } from 'webln';
import { PROJECT_BY_ID_QUERY, PROJECT_BY_ID_RES, PROJECT_BY_ID_VARS } from './query'
import { AiFillThunderbolt } from 'react-icons/ai';
import ProjectCardSkeleton from './ProjectCard.Skeleton'
import TipButton from 'src/Components/TipButton/TipButton';
import { Wallet_Service } from 'src/services'
interface Props extends ModalCard {
@@ -33,9 +31,8 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
}
);
const { isWalletConnected, webln, project, isMobileScreen } = useAppSelector(state => ({
const { isWalletConnected, project, isMobileScreen } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
project: state.project.project,
isMobileScreen: state.theme.isMobileScreen
}));
@@ -46,24 +43,11 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
return <ProjectCardSkeleton onClose={onClose} direction={direction} isPageModal={props.isPageModal} />;
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);
}
Wallet_Service.connectWallet()
}
const onTip = (tip?: number) => {
if (!isWalletConnected) {
dispatch(scheduleModal({ Modal: 'TipCard', props: { tipValue: tip } }))
dispatch(openModal({
@@ -116,7 +100,7 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
{isWalletConnected ?
<TipButton onTip={onTip} />
:
<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>
<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 Tip</Button>
}
</div>
</div>
@@ -126,7 +110,7 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
{isWalletConnected ?
<TipButton fullWidth onTip={onTip} />
:
<Button size='md' fullWidth className="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>
<Button size='md' fullWidth className="bg-gray-200 hover:bg-gray-100 mb-24" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Tip</Button>
}
</div>
<div className="mt-40">

View File

@@ -3,10 +3,11 @@ import React, { useState } from 'react';
import { AiFillThunderbolt } from 'react-icons/ai'
import { IoClose } from 'react-icons/io5'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { useAppDispatch, useAppSelector } from 'src/utils/hooks';
import { gql, useQuery, useMutation } from "@apollo/client";
import { useAppSelector } from 'src/utils/hooks';
import { gql, useMutation } from "@apollo/client";
import useWindowSize from "react-use/lib/useWindowSize";
import Confetti from "react-confetti";
import { Wallet_Service } from 'src/services';
const defaultOptions = [
{ text: '10 sat', value: 10 },
@@ -14,6 +15,7 @@ const defaultOptions = [
{ text: '1k sats', value: 1000 },
]
enum PaymentStatus {
DEFAULT,
FETCHING_PAYMENT_DETAILS,
@@ -55,20 +57,19 @@ interface Props extends ModalCard {
export default function TipCard({ onClose, direction, tipValue, ...props }: Props) {
const { width, height } = useWindowSize()
const { isWalletConnected, webln } = useAppSelector(state => ({
const { isWalletConnected } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
}));
const dispatch = useAppDispatch();
const [selectedOption, setSelectedOption] = useState(10);
const [voteAmount, setVoteAmount] = useState<number>(tipValue ?? 10);
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.DEFAULT);
const [vote, { data }] = useMutation(VOTE, {
onCompleted: (votingData) => {
onCompleted: async (votingData) => {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const webln = await Wallet_Service.getWebln()
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 } });
@@ -84,6 +85,9 @@ export default function TipCard({ onClose, direction, tipValue, ...props }: Prop
const [confirmVote, { data: confirmedVoteData }] = useMutation(CONFIRM_VOTE, {
onCompleted: (votingData) => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
setTimeout(() => {
onClose?.();
}, 2000);
}
});
@@ -112,7 +116,7 @@ export default function TipCard({ onClose, direction, tipValue, ...props }: Prop
className="modal-card max-w-[343px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>Upvote Project</h2>
<h2 className='text-h5 font-bold'>Tip this Project</h2>
<div className="mt-32 ">
<label className="block text-gray-700 text-body4 mb-2 ">
Enter Amount
@@ -143,7 +147,7 @@ export default function TipCard({ onClose, direction, tipValue, ...props }: Prop
{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
Tip
</button>
</div>
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <Confetti width={width} height={height} />}

View File

@@ -1,12 +1,13 @@
import { requestProvider } from "webln";
import { WebLNProvider } from "webln";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface StoreState {
isConnected: boolean;
isLoading: boolean;
provider: any;
type StoreState = {
isConnected: boolean,
}
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
@@ -23,7 +24,6 @@ const isWebLNConnected = () => {
const initialState = {
isConnected: false,
isLoading: false,
provider: null,
} as StoreState;
@@ -31,9 +31,8 @@ export const walletSlice = createSlice({
name: "wallet",
initialState,
reducers: {
connectWallet(state, action: PayloadAction<any>) {
state.isConnected = action.payload ? true : false;
state.provider = action.payload;
connectWallet(state) {
state.isConnected = true;
},
},
});

5
src/services/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import Wallet_Service from './wallet.service'
export {
Wallet_Service
}

View File

@@ -0,0 +1,83 @@
import { requestProvider, MissingProviderError, WebLNProvider } from 'webln';
import { store } from '../redux/store'
import { connectWallet as connectWalletStore } from '../redux/features/wallet.slice'
class _Wallet_Service {
isConnected = false;
webln: WebLNProvider | null = null;
async getWebln() {
if (!this.isConnected) await this.connectWallet();
return this.webln as WebLNProvider;
}
init() {
const connectedPreviously = localStorage.getItem('wallet-connected')
if (connectedPreviously)
this.connectWallet();
}
async connectWallet() {
try {
const webln = await requestProvider();
store.dispatch(connectWalletStore())
localStorage.setItem('wallet-connected', 'yes')
this.webln = webln;
this.isConnected = false;
}
catch (err: any) {
// Default error message
let message = `Couldn't connect wallet`;
// If they didn't have a provider, point them to Joule
if (err.constructor === MissingProviderError) {
message = "Check out https://lightningjoule.com to get a WebLN provider";
}
localStorage.removeItem('wallet-connected')
// Show the error (though you should probably use something better than alert!)
alert(message);
}
}
}
const Wallet_Service = new _Wallet_Service()
export default Wallet_Service;
// export async function initWallet() {
// const connectedPreviously = localStorage.getItem('wallet-connected')
// if (connectedPreviously)
// connectWallet();
// }
// export async function connectWallet() {
// try {
// const webln = await requestProvider();
// store.dispatch(connectWalletStore(webln))
// localStorage.setItem('wallet-connected', 'yes')
// }
// catch (err: any) {
// // Default error message
// let message = `Couldn't connect wallet`;
// // If they didn't have a provider, point them to Joule
// if (err.constructor === MissingProviderError) {
// message = "Check out https://lightningjoule.com to get a WebLN provider";
// }
// localStorage.removeItem('wallet-connected')
// // Show the error (though you should probably use something better than alert!)
// alert(message);
// }
// }

View File

@@ -1,7 +1,7 @@
import { store } from '../redux/store';
import { QueryClient, QueryClientProvider } from 'react-query'
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from '../redux/store';
import 'react-multi-carousel/lib/styles.css';
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";