MetaMask on page refresh with Web3.js and pure JS - javascript

I am trying to have the MetaMask wallet to stay connected upon the page refresh. However, I could not find any info on that matter on the world-wide-web. Sad. Also, from what I've managed to find, there was supposedly a MM privacy update in 2019, which cut out the injection method...so, is there a way to do it natively with pure JS?
The code so far:
const getWeb3 = async () => {
return new Promise(async (resolve, reject) => {
const web3 = new Web3(window.ethereum)
try {
await window.ethereum.request({ method: "eth_requestAccounts" })
resolve(web3)
} catch (error) {
reject(error)
}
})
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("connect_button").addEventListener("click", async ({ target }) => {
const web3 = await getWeb3()
const walletAddress = await web3.eth.requestAccounts()
const walletShort = walletAddress.toString()
const walletBalanceInWei = await web3.eth.getBalance(walletAddress[0])
const walletBalanceInEth = Math.round(Web3.utils.fromWei(walletBalanceInWei) * 100) / 100
target.setAttribute("hidden", "hidden")
document.getElementById("wallet_balance").innerText = walletBalanceInEth
document.getElementById("wallet_info").removeAttribute("hidden")
document.getElementById("address_shrt").innerText = walletShort.slice(0,3) + '...' + walletShort.slice(-3)
})
})
I have no idea about react whatsoever, so react guides are kinda gibberish to me. Any useful links or directions I could follow at least? Thanks in advance!

What you are trying to do is impossible by design. You have to re-request the address view permission every time your page refreshes (for privacy reasons).

u will stay connected on page refresh with these codes in react,next,next etc.
import { useState, useEffect } from "react";
export default function Home() {
const [ismetamask, setIsmetamask] = useState(false); // is metamask installed ?
const [accountaddress, setAccountaddress] = useState([]);
useEffect(() => {
if (window) {
//sometimes window is not loaded, so wait for windows loading
if (typeof window.ethereum !== "undefined") {
console.log("MetaMask is installed!");
setIsmetamask(true);
// check if metamask is already connected
if (window.ethereum._state.accounts.length > 0) {
// metamask is already connected
ethereum.request({ method: "eth_requestAccounts"});
setAccountaddress(window.ethereum._state.accounts[0]);
} else {
// metamask not connected yet
}
// trigger when account change: logout or login
ethereum.on("accountsChanged", function (accounts) {
if (window.ethereum._state.accounts.length > 0) {
setAccountaddress(window.ethereum._state.accounts[0]);
}else{
setAccountaddress([]);
}
});
} else {
console.log("metamask not installed");
}
} else {
console.log("window not loaded yet");
}
}, []);
const signinMetamask = async () => {
// const accounts =
await ethereum.request({ method: "eth_requestAccounts" });
// const account = accounts[0];
};
return (
<>
{ismetamask ? (
<>
{accountaddress.length < 1 ? (
<>
<button
onClick={() => {
signinMetamask();
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Connect Metamask
</button>
</>
) : (
<>user: {accountaddress}</>
)}
</>
) : (
<>metamask not installed</>
)}
</>
);
}

Related

React Native => getting userLocationAddress upon loading splash screen

I am writing react-native app and I am trying to get user location address upon loading splash screen. It works but I am not sure whether I am doing things in a right way. Should I need to put verifyPermissions() and fetchLocationUserAddress() in useEffect hook? How would you approach if you have the task to refactor this code? The code is below. This is the first loaded screen in my app.
export default function SplashScreen({navigation}) {
const [locationPermissionInformation, requestPermission] = useForegroundPermissions()
const dispatch = useDispatch()
const verifyPermissions = async () => {
if (locationPermissionInformation.status === PermissionStatus.UNDETERMINED) {
const permissions = await requestPermission()
return permissions.granted
}
if (locationPermissionInformation.status === PermissionStatus.DENIED) {
Alert.alert('You need to grant access to use this application!')
return false;
}
return true;
}
const locationHandler = async () => {
const awaitPermission = await verifyPermissions();
if (!awaitPermission) {
return;
}
const locationUser = await getCurrentPositionAsync();
console.log(locationUser)
let lat = locationUser.coords.latitude
let lon = locationUser.coords.longitude
fetchLocationUserAddress(lat,lon)
dispatch(setLocation({lat,lon}))
}
const fetchLocationUserAddress = (lat,long) => {
fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${lat},${long}&key=AIzaSyCbJ4Xt40x94OBFcSQat5BI00dDf8ZwF4s`)
.then((response) => response.json())
.then((json) => {
console.log(json)
dispatch(setFormattedLocation(json.results[0].formatted_address))
})
.catch((error) => console.error(error))
}
const onSplashScreenLoadedHandler = async (e) => {
await locationHandler()
// e.preventDefault();
setTimeout(() => {
navigation.navigate("PrivacyPolicy");
}, 2000);
}
return (
<View style={styles.container}>
<Image
style={styles.flag}
source={require("../../media/6328868-removebg-preview1.png")}
/>
<Image
onLoadEnd={onSplashScreenLoadedHandler}
style={styles.logo}
source={require("../../media/road-runner-logo-vector-2440181.png")}
/>
</View>
);
I am new in react native world and want to get some advice from programmers with more experience.

Why does my component fail to construct Url?

I'm fairly new to React development and hope someone can help me with this problem. I'm coding along with a YouTube video https://www.youtube.com/watch?v=XtMThy8QKqU&t=10138s (2:55:00 shows what it is supposed to do)and for some reason I can't find the mistake I'm making. When I test my app on localhost the window in which the trailer is supposed to play is only displayed when I click certain movie covers but not when I click on others. my other problem is that it will never actually play a trailer. The console displays the error you can hopefully see here [1]: https://i.stack.imgur.com/vC6Sh.jpg
import movieTrailer from "movie-trailer";
import React, { useEffect, useState } from "react";
import YouTube from "react-youtube";
import axios from "./axios";
import "./Row.css"
const base_url = "https://image.tmdb.org/t/p/original/";
function Row({ title, fetchUrl, isLargeRow }) {
const [movies, setMovies] = useState([]);
const [trailerUrl, setTrailerUrl] = useState("");
//A snippet of code which runs based on a specific condition or variable
useEffect(() => {
// if brackets are blank [] it means run once when row loads, and don't run again
async function fetchData() {
const request = await axios.get(fetchUrl);
// console.log(request.data.results);
setMovies(request.data.results)
return request;
// async function fetchData() {
// try{
// const request = await axios.get(fetchUrl);
// console.log(request);
// return request;
// }
// catch (error){
// console.log(error);
// }
}
fetchData();
}, [fetchUrl]);
const opts = {
height: '390',
width: '100%',
playerVars: {
// https://developers.google.com/youtube/player_parameters
autoplay: 1,
},
};
//console.log(movies);
const handleClick = (movie) => {
if (trailerUrl){
setTrailerUrl('');
} else {
movieTrailer(movie?.name || "")
.then ((url) => {
const urlParams = new URLSearchParams(new URL(url).search);
setTrailerUrl(urlParams.get("v"));
}).catch(error => console.log(error));
}
};
return(
<div className="row">
<h2>{title}</h2>
<div className="row__posters">
{movies.map(movie => (
<img
key={movie.id}
onClick={() => handleClick(movie)}
className= {`row__poster ${isLargeRow && "row__posterLarge"}`}
src={`${base_url}${isLargeRow ? movie.poster_path : movie.backdrop_path}`} alt={movie.name}/>
))}
</div>
{trailerUrl && <YouTube videoId="{trailerUrl}" opts={opts} /> }
</div>
)
}
export default Row
Invalid property name in movie
Taking a look at the tmdb docs it will show you what the properties of each object has. In this case, there is no name. Try using something like movie.title
In your handleClick() function you could use movie?.title.
Trying to use movie.name will give back a null value. Which errors out movieTrailer() and you get no YouTube url back.
Create handle function like this and the call it in your return function and use however you want... mainly should be used by using onClick method

How to Sign and call anchor (solana) smart contract from web app

I want to call a very simple anchor smart contract from web app after signing with a wallet, I have the code for the smart contract and the web client javascript to get the sign. I am using https://github.com/project-serum/sol-wallet-adapter for interacting with the user's wallet for the signing but am flexible.
I suspect that I need to copy Anchor's IDL/json file to my web app and then call some JS functions.
I am not sure on how to call the smart contract's INCREMENT method? Can someone help me with the steps involved and the JS code?
Smart contract code
const assert = require("assert");
const anchor = require("#project-serum/anchor");
const { SystemProgram } = anchor.web3;
describe("basic-2", () => {
const provider = anchor.AnchorProvider.local();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
// Counter for the tests.
const counter = anchor.web3.Keypair.generate();
// Program for the tests.
const program = anchor.workspace.Basic2;
it("Creates a counter", async () => {
await program.rpc.create(provider.wallet.publicKey, {
accounts: {
counter: counter.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [counter],
});
let counterAccount = await program.account.counter.fetch(counter.publicKey);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() === 0);
});
it("Updates a counter", async () => {
await program.rpc.increment({
accounts: {
counter: counter.publicKey,
authority: provider.wallet.publicKey,
},
});
const counterAccount = await program.account.counter.fetch(
counter.publicKey
);
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey));
assert.ok(counterAccount.count.toNumber() == 1);
});
});
Client side code
import React, { useEffect, useMemo, useState } from 'react';
import './App.css';
import Wallet from '../../';
import {
Connection,
SystemProgram,
Transaction,
clusterApiUrl,
} from '#solana/web3.js';
function toHex(buffer: Buffer) {
return Array.prototype.map
.call(buffer, (x: number) => ('00' + x.toString(16)).slice(-2))
.join('');
}
function App(): React.ReactElement {
const [logs, setLogs] = useState<string[]>([]);
function addLog(log: string) {
setLogs((logs) => [...logs, log]);
}
const network = clusterApiUrl('devnet');
const [providerUrl, setProviderUrl] = useState('https://www.sollet.io');
const connection = useMemo(() => new Connection(network), [network]);
const urlWallet = useMemo(
() => new Wallet(providerUrl, network),
[providerUrl, network],
);
const injectedWallet = useMemo(() => {
try {
return new Wallet(
(window as unknown as { solana: unknown }).solana,
network,
);
} catch (e) {
console.log(`Could not create injected wallet`, e);
return null;
}
}, [network]);
const [selectedWallet, setSelectedWallet] = useState<
Wallet | undefined | null
>(undefined);
const [, setConnected] = useState(false);
useEffect(() => {
if (selectedWallet) {
selectedWallet.on('connect', () => {
setConnected(true);
addLog(
`Connected to wallet ${selectedWallet.publicKey?.toBase58() ?? '--'}`,
);
});
selectedWallet.on('disconnect', () => {
setConnected(false);
addLog('Disconnected from wallet');
});
void selectedWallet.connect();
return () => {
void selectedWallet.disconnect();
};
}
}, [selectedWallet]);
// =========== Need to modify this I think ==================
async function sendTransaction() {
try {
const pubkey = selectedWallet?.publicKey;
if (!pubkey || !selectedWallet) {
throw new Error('wallet not connected');
}
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: pubkey,
toPubkey: pubkey,
lamports: 100,
}),
);
addLog('Getting recent blockhash');
transaction.recentBlockhash = (
await connection.getRecentBlockhash()
).blockhash;
addLog('Sending signature request to wallet');
transaction.feePayer = pubkey;
const signed = await selectedWallet.signTransaction(transaction);
addLog('Got signature, submitting transaction');
const signature = await connection.sendRawTransaction(signed.serialize());
addLog('Submitted transaction ' + signature + ', awaiting confirmation');
await connection.confirmTransaction(signature, 'singleGossip');
addLog('Transaction ' + signature + ' confirmed');
} catch (e) {
console.warn(e);
addLog(`Error: ${(e as Error).message}`);
}
}
async function signMessage() {
try {
if (!selectedWallet) {
throw new Error('wallet not connected');
}
const message =
'Please sign this message for proof of address ownership.';
addLog('Sending message signature request to wallet');
const data = new TextEncoder().encode(message);
const signed = await selectedWallet.sign(data, 'hex');
addLog('Got signature: ' + toHex(signed.signature));
} catch (e) {
console.warn(e);
addLog(`Error: ${(e as Error).message}`);
}
}
return (
<div className="App">
<h1>Wallet Adapter Demo</h1>
<div>Network: {network}</div>
<div>
Waller provider:{' '}
<input
type="text"
value={providerUrl}
onChange={(e) => setProviderUrl(e.target.value.trim())}
/>
</div>
{selectedWallet && selectedWallet.connected ? (
<div>
<div>Wallet address: {selectedWallet.publicKey?.toBase58()}.</div>
<button onClick={sendTransaction}>Send Transaction</button>
<button onClick={signMessage}>Sign Message</button>
<button onClick={() => selectedWallet.disconnect()}>
Disconnect
</button>
</div>
) : (
<div>
<button onClick={() => setSelectedWallet(urlWallet)}>
Connect to Wallet
</button>
<button onClick={() => setSelectedWallet(injectedWallet)}>
Connect to Injected Wallet
</button>
</div>
)}
<hr />
<div className="logs">
{logs.map((log, i) => (
<div key={i}>{log}</div>
))}
</div>
</div>
);
}
export default App;
If you want to do this with #project-serum/anchor, you need install the dependency and import them:
import * as anchor from '#project-serum/anchor';
import { Program } from '#project-serum/anchor';
import { Basic2 } from '<path to your basic2.json idl>';
And then do something similar to this snippet in your code:
anchor.setProvider(new AnchorProvider(connection, wallet, AnchorProvider.defaultOptions()));
const program = anchor.workspace.Basic2 as Program<Basic2>;
await program.methods.increment()
.accounts({
counter: counter.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
The way it is done in your "Smart contract code" above is the old-school way and has been superseded with this one.

Routing with Spotify API

I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below:
The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone
App.js
const code = new URLSearchParams(window.location.search).get("code")
const App = () => {
return (
<>
{code ?
<Router>
<Link to="/tracks">
<div style={{ marginBottom: "3rem" }}>
<p>Tracks</p>
</div>
</Link>
<Link to="/">
<div style={{ marginBottom: "3rem" }}>
<p>Home</p>
</div>
</Link>
<Switch>
<Route exact path="/">
<Profile code={code} />
</Route>
<Route path="/tracks">
<TopTracks code={code} />
</Route>
</Switch>
</Router> : <Login />}
</>
)
}
TopTracks.js
const spotifyApi = new SpotifyWebApi({
client_id: "some client id"
})
const TopTracks = ({ code }) => {
const accessToken = useAuth(code)
console.log(accessToken) // undefined in console
console.log(code) // the correct code as provided by spotify
useEffect(() => {
if (accessToken) {
spotifyApi.setAccessToken(accessToken)
return
}
}, [accessToken])
'useAuth' custom Hook
export default function useAuth(code) {
const [accessToken, setAccessToken] = useState()
const [refreshToken, setRefreshToken] = useState()
const [expiresIn, setExpiresIn] = useState()
useEffect(() => {
axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
setExpiresIn(res.data.expiresIn)
window.history.pushState({}, null, "/")
})
.catch((err) => {
// window.location = "/"
console.log("login error", err)
})
}, [code])
You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.
You can persist these tokens using localStorage, IndexedDB or another database mechanism.
For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.
To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.
Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.
// SpotifyAuthContext.js
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => throw new Error("context not loaded"),
refreshAccessToken: () => throw new Error("context not loaded"),
get hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({children}) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return; // do nothing, no tokens available
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Usage:
// TopTracks.js
import useSpotify from '...'
const TopTracks = () => {
const { api, refreshableCall } = useSpotify()
const [ tracks, setTracks ] = useState([])
const [ error, setError ] = useState(null)
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
.then((res) => {
if (disposed) return
setTracks(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setTracks([])
setError(err)
});
return () => disposed = true
});
if (error != null) {
return <span class="error">{error.message}</span>
}
if (tracks.length === 0) {
return <span class="warning">No tracks found.</span>
}
return (<ul>
{tracks.map((track) => {
const artists = track.artists
.map(artist => artist.name)
.join(', ')
return (
<li key={track.id}>
<a href={track.preview_url}>
{track.name} - {artists}
</a>
</li>
)
}
</ul>)
}
// Login.js
import useSpotify from '...'
const Login = () => {
const { exchangeCode } = useSpotify()
const [ error, setError ] = useState(null)
const code = new URLSearchParams(window.location.search).get("code")
useEffect(() => {
if (!code) return // no code. do nothing.
// if here, code available for login
let disposed = false
exchangeCode(code)
.then(() => {
if (disposed) return
setError(null)
window.history.pushState({}, null, "/")
})
.catch(error => {
if (disposed) return
console.error(error)
setError(error)
})
return () => disposed = true
}, [code])
if (error !== null) {
return <span class="error">{error.message}</span>
}
if (code) {
// TODO: Render progress bar/spinner/throbber for "Signing in..."
return /* ... */
}
// if here, no code & no error. Show login button
// TODO: Render login button
return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'
const MyRouter = () => {
const { hasToken } = useSpotify()
if (!hasToken) {
// No access token available, show login screen
return <Login />
}
// Access token available, show main content
return (
<Router>
// ...
</Router>
)
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'
const App = () => {
return (
<SpotifyAuthContextProvider>
<MyRouter />
</SpotifyAuthContextProvider>
);
}

How can I Autogenerate tabs and also the contents dynamically from firebase realtime database using Javascript

Hello I am new to react native and cant figure out this problem please help me I am working in a project .In my project I want all the tabs and tab content to be autogenerated dynamically from my firebase database
I need that all these nodes (Living room, kitchen, bedroom, etc.) to be the names of the tab and all tabs should show its own content directly from firebase like Living room tab will show app_1 and app_2 similarly Kitchen and bedroom will also autogenerate these directly from database
Like this image:
const HorScrollView = () => {
const [homeId, setHomeId] = useState(0);
const [roomList, setRoomList] =useState(["Loading Rooms...."]);
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve,reject) => {
database().ref(`/USERS/${user.uid}/home_id`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
};
const roomListProvider = ()=>{
return new Promise((resolve,reject) => {
database().ref(`/HOMES/${homeId}/rooms`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
}
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
return (
<View style={styles.scrollViewContainer} >
<ScrollView horizontal>
{roomList.map((roomlist) => (
<Pressable key={roomlist}>
<Text style={styles.scrollViewText} >{roomlist}
</Text>
</Pressable>
))}
</ScrollView>
</View>
);
};
There's a UI library called antd which can help you do this. Use a <Table/> tag for this, then as props pass the value of the columns attribute to be the titles that are coming through, and then the dataSource attribute will be the data under those specific columns. Read the documentation for more.
While you can use the <Tabs> component from antd to achieve what you want, your current code has some bugs regarding how it handles user state and the asynchronous calls.
Taking a look at these lines:
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve, reject) => {
database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => {
resolve(snapshot.val());
});
});
};
Here there are two main problems:
You make use of auth().currentUser but this isn't guaranteed to contain the user object that you expect as it may still be resolving with the server (where it will be null) or the user may be signed out (also null).
You incorrectly chain the promise by wrapping it in a Promise constructor (known as the Promise constructor anti-pattern) where the errors of the original promise will never reach the reject handler leading to crashes.
To fix the user state problem, you should make use of onAuthStateChanged and look out for when the user signs in/out/etc.
function useCurrentUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
// returns [firebase.auth.User | null, boolean]
return [user || null, userLoading];
}
// in your component
const [user, userLoading] = useCurrentUser();
To fix the PCAPs, you'd use:
const homeidProvider = () => {
return database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const roomListProvider = () => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
Because these functions don't depend on state changes, you should place them outside your component and pass the relevant arguments into them.
Next, these lines should be inside a useEffect call where error handling and unmounting the component should be handled as appropriate:
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
should be swapped out with:
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
return;
}
let disposed = false;
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
You should also track the status of your component:
Status
Meaning
"loading"
data is loading
"error"
something went wrong
"signed-out"
no user logged in
"ready"
data is ready for display
Rolling this together:
import { Tabs, Spin, Alert, Card } from 'antd';
const { TabPane } = Tabs;
const { Meta } = Card;
function useUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
return [user || null, userLoading];
}
const getUserHomeId = (uid) => {
return database()
.ref(`/USERS/${uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const getHomeRoomData = (homeId) => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
const RoomView = () => {
const [homeId, setHomeId] = useState(0);
const [status, setStatus] = useState("loading")
const [roomList, setRoomList] = useState([]);
const [user, userLoading] = useUser();
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
setStatus("signed-out");
return;
}
let disposed = false;
setStatus("loading");
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
setStatus("ready");
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
setStatus("error");
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
switch (status) {
case "loading":
return <Spin tip="Loading rooms..." />
case "error":
return <Alert
message="Error"
description="An unknown error has occurred"
type="error"
/>
case "signed-out":
return <Alert
message="Error"
description="User is signed out"
type="error"
/>
}
if (roomList.length === 0) {
return <Alert
message="No rooms found"
description="You haven't created any rooms yet"
type="info"
/>
}
return (
<Tabs defaultActiveKey={roomList[0].key}>
{
roomList.map(room => {
let tabContent;
if (room.apps.length == 0) {
tabContent = "No apps found in this room";
} else {
tabContent = room.apps.map(app => {
<Card style={{ width: 300, marginTop: 16 }} key={app.key}>
<Meta
avatar={
<Avatar src="https://via.placeholder.com/300x300?text=Icon" />
}
title={app.name}
description={app.description}
/>
</Card>
});
}
return <TabPane tab={room.title} key={room.key}>
{tabContent}
</TabPane>
})
}
</Tabs>
);
};

Categories