I have made a lottery contract and now I want to connect it to the front-end which I will create with ReactJS. I have used Truffle framework for the deployment and tests for my contract.
npm i #truffle/contract
Then in utils/loadContract:
import contract from "#truffle/contract"
export const loadContract = async (name, provider) => {
// in truffle, contracts are in "contracts" directory
const res = await fetch(`/contracts/${name}.json`)
const Artifact = await res.json()
const _contract = contract(Artifact)
_contract.setProvider(provider)
let deployedContract = null
try {
deployedContract = await _contract.deployed()
} catch {
console.error("You are connected to the wrong network")
}
return deployedContract
}
Then load the contract inside app.jsx. First:
npm i #metamask/detect-provider
then write your code inside app.js
import detectEthereumProvider from '#metamask/detect-provider'
import { loadContract } from "./utils/load-contract";
import {useEffect, useState } from "react";
// in useEffect I load this state. So when your component is loaded, you would have access to web3Api.contract to call the contract methods
const [web3Api, setWeb3Api] = useState({
provider: null,
isProviderLoaded: false,
web3: null,
contract: null
})
// call this function inside useEFfect, if user connects to different account, you will update it
const setAccountListener = provider => {
provider.on("accountsChanged", _ => window.location.reload())
provider.on("chainChanged", _ => window.location.reload())
}
// useEffect is called before your component loaded
// load the contract, set the state, so when your compoent loaded, your state will be ready
useEffect(() => {
const loadProvider = async () => {
// if Metamask installed, this will detect its provider
const provider = await detectEthereumProvider()
// load the contract if provider exists
if (provider) {
// Lottery is not name of the file, it is NAME OF CONTRACT
const contract = await loadContract("Lottery", provider)
setAccountListener(provider)
setWeb3Api({
web3: new Web3(provider),
provider:provider,
// Now you set the contract.
// when your app.js loaded you will be able to call contract methods
contract:contract,
isProviderLoaded: true
})
} else {
setWeb3Api(api => ({...api, isProviderLoaded: true}))
console.error("Please, install Metamask.")
}
}
loadProvider()
}, [])
Related
I'm building an app with Next.js 13 and Supabase for the backend, and I've been stuck on figuring out the best/proper way to go about creating a context/provider for the current logged in user.
The flow to retrieve the user from Supabase is this:
Sign in with an OAuth Provider.
Grab the user ID from the session from the supabase onAuthState Changed hook.
Fetch the full user object from the supabase DB with the user ID mentioned above.
I have a supabase listener in my layout that listens for the auth state changes, and works well for setting and refreshing current session.
My initial approach was to add the fetchUser call from within the onAuthState changed hook, however I was running into late update hydration errors.
Taken directly from the examples, this is how the app looks:
// layout.tsx
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
});
const {
data: { session },
} = await supabase.auth.getSession();
return (
<html>
<head />
<body>
<NavMenu session={session} />
<SupabaseListener accessToken={session?.access_token} />
{children}
</body>
</html>
);
}
// supabase-listener.tsx
// taken directly from the supabase-auth-helpers library.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import supabase from "../lib/supabase/supabase-browser";
export default function SupabaseListener({
accessToken,
}: {
accessToken?: string;
}) {
const router = useRouter();
useEffect(() => {
supabase.auth.onAuthStateChange(async (event, session) => {
if (session?.access_token !== accessToken) {
router.refresh();
}
});
}, [accessToken, router]);
return null;
}
I basically just need to wrap my root layout with a LoggedInUserProvider, make the fetch user call somewhere in the initial page load, and set the state of the current logged in user provider.
The other approaches I tried was making the fetch user call from the root layout, and having a LoggedInUserListener client component that takes the user as a property and simply sets the state if the profile exists. This was causing improper set state errors.
Thank you so much.
Check out this PR for a better example of how to structure the application and add a provider for sharing a single instance of Supabase client-side, as well as the session from the server 👍
If you follow a similar pattern, then your additional query for the full user record should go immediately after you get the session in examples/nextjs-server-components/app/layout.tsx. You could then pass this as a prop to the <SupabaseProvider /> and share it across the application from context's value prop.
I am following your awesome auth-helpers example but my context from the provider keeps coming back as null for user details. Is there anything wrong with the code below or is there some isLoading logic that will work better for getting that data?
Also want to confirm, does the SupabaseProvider in the root layout pass down to all other child layout components?
'use client';
import type { Session } from '#supabase/auth-helpers-nextjs';
import { createContext, useContext, useState, useEffect } from 'react';
import type { TypedSupabaseClient } from 'app/layout';
import { createBrowserClient } from 'utils/supabase-client';
import { UserDetails, CompanyDetails } from 'models/types';
type MaybeSession = Session | null;
type SupabaseContext = {
supabase: TypedSupabaseClient;
session: MaybeSession;
userDetails: UserDetails | null;
isLoading: boolean;
};
// #ts-ignore
const Context = createContext<SupabaseContext>();
//TODO get stripe subscription data
export default function SupabaseProvider({
children,
session
}: {
children: React.ReactNode;
session: MaybeSession;
}) {
const [supabase] = useState(() => createBrowserClient());
const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
const [isLoading, setLoading] = useState(false);
// Hydrate user context and company data for a user
useEffect(() => {
const fetchUserDetails = async () => {
if (session && session.user) {
setLoading(true);
const { data } = await supabase
.from('users')
.select('*, organizations (*)')
.eq('id', session.user.id)
.single();
//TODO fix types
setUserDetails(data as any);
setLoading(false);
}
};
if (session) {
fetchUserDetails();
}
}, [session, supabase]);
return (
<Context.Provider value={{ supabase, session, userDetails, isLoading }}>
<>{children}</>
</Context.Provider>
);
}
export const useSupabase = () => useContext(Context);
I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.
I am trying to make a module for web3 authentication. For that I have a function which gets user's public address and makes a GraphqQL query to the back-end to see if it's present in the database.
export const useWeb3 = async () => {
const [ user, setUser ] = useState({})
if (!(window as any).ethereum) {
window.alert('Please install MetaMask first.');
return;
}
let web3: Web3 | undefined = undefined
if (!web3) {
try {
// Request account access if needed
await (window as any).ethereum.enable();
// We don't know window.web3 version, so we use our own instance of Web3
// with the injected provider given by MetaMask
web3 = new Web3((window as any).ethereum);
} catch (error) {
window.alert('You need to allow MetaMask.');
return [user];
}
}
const coinbase = await web3.eth.getCoinbase()
if (!coinbase) {
window.alert('Please activate MetaMask first.')
return [user]
}
console.log("COINBASE", coinbase)
const publicAddress = coinbase.toLowerCase();
// setLoading(true);
const { data } = useQuery(QUERY_USER_BY_PUBLIC_ADDRESS, {
variables: { publicAddress }
})
if(data) setUser(data)
return [user]
}
So I have a custom hook useWeb3(). I made it a hook so I could call useQuery inside it. I import useWeb3 inside the other component and when I try to use it like that:
const [user] = useWeb3()
It says Type
'Promise<{}[] | undefined>' is not an array type.ts(2461)
Welcome.component.tsx(34, 11): Did you forget to use 'await'?
The question is how can I implement the logic of taking public address and query back end with that inside a separate function so I could use it in other components?
If you just want to return the QUERY_USER_BY_PUBLIC_ADDRESS response data and aren't looking to have the results rendered as part of a React component, then you don't need to use the useQuery React hook here. Just use Apollo Client's query function directly, which returns a Promise:
...
// `client` here is your instantiated `ApolloClient` instance
const result = await client.query({
query: QUERY_USER_BY_PUBLIC_ADDRESS,
variables: { publicAddress }
});
...
I am working on a react app with redux. I did implement thunk actions to:
Create a web3modal instance
Register a ethersjs provider
Register a ethersjs signer
Everything is very basic and simple.
However whenever I try to make use of the on events (doesn't matter if provider oder contract), they simply won't fire.
I have 2 files:
walletSlice.ts which will handle all the redux action and reducer logic with #reduxjs/toolkit .
wallet-api.ts which has all the relevant functions to interact with the wallet.
The walletSlice.ts relevant part looks exactly like this:
export const connectWallet = createAsyncThunk(
'web3wallet/connectWallet',
async (arg, thunkApi) => {
const instance = await WalletAPI.registerWalletInstance();
provider = await WalletAPI.registerWalletProvider(instance);
signer = await WalletAPI.registerWalletSigner(provider);
return Promise.resolve();
}
);
The wallet-api.ts relevant parts look exactly like this:
import { ethers, providers } from 'ethers';
import Web3Modal from 'web3modal';
// get Web3Modal Instance
export async function registerWalletInstance(): Promise<Web3Modal> {
const providerOptions = {};
const web3Modal = new Web3Modal({
providerOptions,
});
const instance = await web3Modal.connect();
return Promise.resolve(instance);
}
/**
* register Wallet provider.
* Events on provider #see https://docs.ethers.io/v5/api/providers/provider/#Provider--event-methods
* Implementing the EIP-1193 Standard #see https://eips.ethereum.org/EIPS/eip-1193
*/
export async function registerWalletProvider(
instance: any
): Promise<providers.JsonRpcProvider> {
const provider = new ethers.providers.Web3Provider(instance);
// Subscribe to accounts change
provider.on('accountsChanged', (accounts: string[]) => {
console.log(accounts);
});
// Subscribe to chainId change
provider.on('chainChanged', (chainId: number) => {
console.log(chainId);
});
// Subscribe to provider connection
provider.on('connect', (info: { chainId: number }) => {
console.log(info);
});
// Subscribe to provider disconnection
provider.on('disconnect', (error: { code: number; message: string }) => {
console.log(error);
});
provider.on('error', (tx) => {
// Emitted when any error occurs
console.log({ tx });
});
return Promise.resolve(provider);
}
// register Wallet signer.
export async function registerWalletSigner(
provider: providers.JsonRpcProvider
): Promise<providers.JsonRpcSigner> {
const signer = provider.getSigner();
return Promise.resolve(signer);
}
None of the provider.on() events will fire. I've tried to change networks from rinkeby to polygon or mainnet, but nothing happens. When I disconnect from the site, nothing happens. It is the same with all provider events as shown in wallet-api.ts. I did try the same approach with another file called contract-api.ts. However the contract events won't fire either.
I tried to use the provider.on() events with useEffect() or useCallback(). Moved the code to a standalone tsx. But nothing happened.
await web3Modal.connect() already returns a provider;
try to use that one instead of new ethers.providers.Web3Provider(instance);
I am trying to mock my REST requests for a react/ts project when testing in Storybook using Axios. Even though I am setting the response to an array object, it still seems to be responding with a "Request failed with status code 404" status.
Here is my component making the REST call: TestPrompt.tsx
const onClickHandler = () => {
requestOrMock("http://localhost:9002/projectPlan/projectTasks?project=FAKEWID")
}
Here is the method my TestPrompt component is using to make the request: UtilityFunctions.ts
import axios from 'axios';
export const axiosMock = axios.create();
export const requestOrMock = async (uri: string) => {
const response = axiosMock.get(uri);
return response;
}
Here is my test that is mocking the response: Prompt.stories.tsx
import * as React from "react";
import {storiesOf} from '#storybook/react';
import TestPrompt from "../components/common/Prompt";
import MockAdapter from 'axios-mock-adapter';
import { axiosMock } from "../utils/utilityFunctions";
const mock = new MockAdapter(axiosMock);
const blankPromptRequestUri = "http://localhost:9002/projectPlan/projectTasks?project=FAKEWID";
const footballTeams = [
{
"descriptor": "New England Patriots",
"id": "NewEnglandPatriots"
},
{
"descriptor": "Seattle Seahawks",
"id": "SeattleSeahawks"
}
];
storiesOf('Components/MultiSelect', module)
.add('Prompt1', () => {
mock.onGet(blankPromptRequestUri).reply(200, footballTeams);
return (
<TestPrompt/>
);
})
When I click on this component in storybook, it sends out the request to the designated url, but it gets the 404 response rather than the footballTeams object I have specified. Any idea what I have done wrong? Thanks for your help.
If I get your problem correctly, you need to call onGet() of mock to setup the mock end point and then send a request to that end point.
mock.onGet("/teams").reply(200, footballTeams);
storiesOf('Components/MultiSelect', module)
.add('Prompt1', () => {
axios.get("/teams")
.then(res => console.log(res.data))
return (
<TestPrompt/>
);
})
The requests that were being made were being made relative to the host, so rather than "http://localhost:9002/projectPlan/projectTasks?project=FAKEWID" being sent, it was actually "/projectPlan/projectTasks?project=FAKEWID". It is likely that you will only need to pass in the routes here.