Is it possible to specify a Resource name in React Admin with a path variable?
Problem
When the user authenticate, if admin, should see a list of email accounts. Clicking on the email it redirects to the path experts/${email}/requests.
To map this path I need to define a new Resource for each email. The problem of this is that I have to do it before the authentication.
What I would like to have, is a path variable for the resource name. e.g. experts/:email/requests.
Example of the current implementation
function App() {
const [emails, setEmails] = useState<string[]>([]);
useEffect(() => {
(async () => {
const experts = await firebase.firestore().collection("experts").get();
const docIds: string[] = experts.docs.map((doc) => doc.id);
setEmails(docIds);
})();
}, []);
return (
<Admin
authProvider={authProvider}
dataProvider={dataProvider}
>
{(props) => {
if (props.admin) {
return [
emails.map((email) => {
return (
<Resource
key={email}
options={{ label: `${email}` }}
name={`experts/${email}/requests`}
list={RequestList}
edit={RequestEdit}
/>
);
}),
<Resource name={`experts`} list={ExpertList} />,
];
} else {
return [
<Resource
options={{ label: `${props.email}` }}
name={`experts/${props.email}/requests`}
list={RequestList}
edit={RequestEdit}
/>,
];
}
}}
</Admin>
);
}
Instead of a path variable (not possible with React Admin) I decided to override the login of the Firebase authProvider to fetch the data right after the user log in.
The timeout is needed because it can happen that the localStorage is setup after the first rendering.
let authProvider = FirebaseAuthProvider(firebaseConfig, options);
const login = authProvider.login;
authProvider = {
...authProvider,
login: async (params) => {
try {
const user = await login(params);
const experts = await firebase.firestore().collection("experts").get();
const docIds: string[] = experts.docs.map((doc) => doc.id);
localStorage.setItem("emails", JSON.stringify(docIds));
await timeout(300);
return user;
} catch (error) {
console.error("error", error);
}
},
};
Related
I'm learning React as I need to write an AWS app using Cognito. This series of videos is very helpful (https://www.youtube.com/watch?v=R-3uXlTudSQ&list=PLDckhLrNepPR8y-9mDXsLutiwsLhreOk1&index=3&t=300s) but it doesn't explain how you redirect your app after you've logged in.
my App.js is this:
export default () => {
return (
<Account>
<Status />
<Signup />
<Login />
<ForgotPassword />
<Settings />
</Account>
);
};
The Settings component will only appear for an authenticated user. However, once you've logged in it doesn't appear until you refresh the page. How do I get it to show the settings page without having to refresh the page?
The settings component is:
export default () => {
return (
<Account>
<Status />
<Signup />
<Login />
<ForgotPassword />
<Settings />
<SearchParms/>
</Account>
);
};
And the Accounts component is this:
import React, { createContext } from "react";
import { CognitoUser, AuthenticationDetails } from "amazon-cognito-identity-js";
import Pool from "../UserPool";
const AccountContext = createContext();
const Account = props => {
const getSession = async () =>
await new Promise((resolve, reject) => {
const user = Pool.getCurrentUser();
if (user) {
user.getSession(async (err, session) => {
if (err) {
reject();
} else {
const attributes = await new Promise((resolve, reject) => {
user.getUserAttributes((err, attributes) => {
if (err) {
reject(err);
} else {
const results = {};
for (let attribute of attributes) {
const { Name, Value } = attribute;
results[Name] = Value;
}
resolve(results);
}
});
});
resolve({
user,
...session,
...attributes
});
}
});
} else {
reject();
}
});
const authenticate = async (Username, Password) =>
await new Promise((resolve, reject) => {
Username = "nick.wright#maintel.co.uk";
Password = "C411m3di4**&";
const user = new CognitoUser({ Username, Pool });
//const authDetails = new AuthenticationDetails({ Username, Password });
const authDetails = new AuthenticationDetails({ Username, Password });
user.authenticateUser(authDetails, {
onSuccess: data => {
console.log("onSuccess:", data);
resolve(data);
},
onFailure: err => {
console.error("onFailure:", err);
reject(err);
},
newPasswordRequired: data => {
console.log("newPasswordRequired:", data);
resolve(data);
}
});
});
const logout = () => {
const user = Pool.getCurrentUser();
if (user) {
user.signOut();
}
};
return (
<AccountContext.Provider
value={{
authenticate,
getSession,
logout
}}
>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };
In Settings I have
import React, { useState, useEffect, useContext } from "react";
import { AccountContext } from "./Accounts";
import ChangePassword from "./ChangePassword";
import ChangeEmail from "./ChangeEmail";
// eslint-disable-next-line import/no-anonymous-default-export
export default () => {
const [loggedIn, setLoggedIn] = useState(false);
const { getSession } = useContext(AccountContext);
useEffect(() => {
getSession().then(() => {
setLoggedIn(true);
}).catch((err) => console.log("Catch", err) )
}, [getSession]);;
return (
<div>
{loggedIn && (
<>
<h1>Settings</h1>
<ChangePassword />
<ChangeEmail />
</>
)}
</div>
);
};
and at this line:
const { getSession } = useContext(AccountContext);
I'm getting an "AccountContext is not defined" error.
I haven't been able to find any online examples that solve this issue. Is there a way of dynamically showing/hiding each element when the login button is clicked.
In this case, there's no need to define getSession, authenticate and logout on the context. You can put those functions inside a auth folder and call them whenever you need wihout having to define them in the context. What you need to define in the context is whether the user is logged in or not, because that's the information that you want to share in your whole application. Regarding the AccountContext is not defined, I don't see any issues from what you have shared. https://codesandbox.io/s/adoring-sun-kz30yr?file=/src/Settings.js
We link our website in a Tiktok profile similar to https://SKKNBYKIM.COM in this profile https://www.tiktok.com/#kimkardashian. When using the Tiktok mobile app and our website is clicked in a Tiktok profile, sometimes our BeatLoader loading component is displayed indefinitely. However, sometimes the website loads successfully (there doesn't seem to be a pattern).
However, if you visit the Tiktok profile on web browser and click the link to our website in the profile, it always loads. Also, the profile always loads the link when clicked directly through web browser, mobile browser, and from an Instagram profile link.
I suspect when the website doesn't load, it might be due to being unable to authenticate on Firebase since the line {(currUser && !isPullingUser) is never becoming true (verified by changing the BeatLoader component to something else and seeing it was displayed indefinitely as well). Is there any way to determine what the console looks like when the website is opened through the Tiktok app or some other way to diagnose what might be causing the problem?
/* global chrome */
import React, { useEffect } from "react";
import { fade, makeStyles } from "#material-ui/core/styles";
import UserChest from "../../components/chest/userChest";
import Box from "#material-ui/core/Box";
import {
BrowserRouter as Router,
useHistory,
useParams,
} from "react-router-dom";
import { db, firebase } from "../../api/firebase/firebase";
import ProductHeader from "../../components/headers/productHeader";
import { BeatLoader } from "react-spinners";
function Home(props) {
const classes = useStyles();
const [currUser, setCurrUser] = React.useState(null);
const [me, setMe] = React.useState(null);
const [allUsers, setAllUsers] = React.useState([]);
const [isMe, setIsMe] = React.useState(true);
const [isPullingUser, setIsPullingUser] = React.useState(false);
const [isPullingMe, setIsPullingMe] = React.useState(true);
let { username } = useParams();
const history = useHistory();
const handleFirstTimeSignUp = async (user) => {
await db.collection("users").doc(user.email).update({
isFirstSignIn: false,
});
};
const selectUser = () => {
setIsMe(false);
};
/*
getToken creates an https request that automatically authenticates
a user on our chrome extension
*/
const getToken = async (uid) => {
let callToken = firebase.functions().httpsCallable("generateLoginToken")(
uid
);
await callToken
.then((result) => {
// Read result of the Cloud Function.
let token = result.data;
var editorExtensionId = "OUR_CHROME_EXTENSION_EDITOR_ID";
chrome.runtime?.sendMessage(
editorExtensionId,
{ token: token },
function (response) {
if (response?.status === "success") {
console.log("success: auth token sent", response);
} else {
console.log("failure: sending auth token failed", response);
}
}
);
})
.catch((error) => {
console.log(error);
});
};
const getUserbyUsername = async username => {
let user = {};
const snapshot = await db
.collection("users")
.where("username", "==", username)
.get();
snapshot.forEach((doc) => {
user = doc.data();
});
return user;
};
useEffect(() => {
setIsPullingUser(true)
if (username) {
let unsubscribeUser;
(async () => {
let currentUser =
username !== "home"
? await getUserbyUsername(username)
: firebase.auth().currentUser;
if (username === 'home' && !currentUser) {
history.replace("/login")
}
let user = null;
let email = currentUser?.email;
// eslint-disable-next-line no-undef
const queryUser = db.collection("users").doc(email);
unsubscribeUser = queryUser.onSnapshot(
(doc) => {
let userPulled = doc.data();
if (userPulled) {
user = userPulled;
// if(user.username === username || !username)
setCurrUser(user);
setIsPullingUser(false)
}
}, (error) =>
console.log(error)
);
})();
return () => {
unsubscribeUser();
};
} else {
setCurrUser(null);
setIsPullingUser(false)
}
}, [username]);
useEffect(() => {
let currentUser = firebase.auth().currentUser;
if (currentUser && currentUser.uid) {
getToken(currentUser.uid);
let user = null;
let email = currentUser?.email;
// eslint-disable-next-line no-undef
async function getUser(doc) {
let userPulled = doc.data();
if (userPulled) {
user = userPulled;
setMe(user);
setIsPullingUser(false);
setIsPullingMe(false);
if (userPulled.isFirstSignIn) {
handleFirstTimeSignUp(user).then((r) =>
console.log("welcome to company name!")
);
}
} else {
history.push('/username')
}
}
const queryUser = db.collection("users").doc(email)
const unsubscribeUser = queryUser.onSnapshot(getUser, (error) =>
console.log(error)
);
return () => {
console.log('unsubscribe email')
unsubscribeUser();
};
} else {
setIsPullingMe(false);
}
}, []);
return (
<div style={{ minHeight: "100vh" }}>
{(currUser && !isPullingUser) ? (
<div>
<ProductHeader
me={me}
user={currUser}
selectUser={selectUser}
allUsers={allUsers}
tab={"home"}
/>
{ !isPullingMe &&
<UserChest
me={me}
isMe={isMe}
user={currUser}
/>
}
</div>
) :
<Box
style={{ margin: 10 }}
justifyContent={"center"}
alignItems={"center"}
>
<BeatLoader color={"pink"} size={18} margin={2} />
</Box>
}
</div>
);
}
export default Home;
I'm trying to make my React web application native using React Native Webview. In order to implement native social oauth, I made a screen that requires users sign in and after the users sign in, a webView with my web application url is rendered:
const App = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
// Check if the app has valid tokens
}, []);
return (
<AuthContext.Provider
value={{
isLoggedIn,
login: () => setIsLoggedIn(true),
logout: () => setIsLoggedIn(false),
}}>
{isLoggedIn ? (
<SafeAreaView style={styles.container}>
<MyWebView />
</SafeAreaView>
) : (
<LoginScreen />
)}
</AuthContext.Provider>
);
};
If users navigate to a page in my domain, my app renders it in MyWebView, otherwise my app opens the os browser with the external url. Also, I want my app to confirm and set isLoggedIn to false so that the app stops rendering MyWebView and goes back to LoginScreen, if users try to navigate to ${BASE_URL}/logout, :
const MyWebView = () => {
const { logout: appLogout } = useContext(AuthContext);
const askForLogout = () => {
const title = 'Logout';
const message = 'Do you want to sign out?';
return new Promise((resolve, reject) => {
Alert.alert(
title,
message,
[
{ text: 'cancel', onPress: () => resolve(false) },
{ text: 'OK', onPress: () => resolve(true) },
],
{ cancelable: false },
);
reject('Error');
});
};
const onLogout = async () => {
try {
const shouldLogout = await askForLogout();
if (!shouldLogout) {
return;
}
await logout(); // This expires tokens
appLogout();
} catch (e) {
alertError();
}
};
return (
<WebView
source={{ uri: BASE_URL }}
sharedCookiesEnabled={true}
startInLoadingState={true}
renderLoading={() => <ActivityIndicator />}
onShouldStartLoadWithRequest={navState => {
const { url } = navState;
if (url.startsWith(BASE_URL)) {
if (url.includes('/logout')) {
onLogout();
return false;
}
return true;
} else {
// External Links
Linking.openURL(url);
return false;
}
}}
/>
);
};
export default MyWebView;
The problem is, onShouldStartLoadWithRequest sometimes (almost always) does not detect navigations using history.push(path) so my app cannot detect whether users navigate to /logout and call the logout function. onNavigationStateChange can detect history.push(path), but with onNavigationStateChange my app cannot stops external links from being rendered in MyWebView (This is bad because some external links use insecure HTTP, causing NSURLErrorDomain)
Any help would be really appreciated. Thank you!
I have the following code:
const queryClient = new QueryClient({
defaultOptions: {
useErrorBoundary: true,
queries: {
suspense: true,
useErrorBoundary: true,
retry: 0,
}
}
});
const useUsers = () => {
return useQuery("users", async () => {
const users = await fetchUsers();
console.log(users);
return users;
})
};
function UserList() {
const { data, isFetching, error, status } = useUsers();
const { users } = data;
// return some render with users
}
My fetchUsers method:
export function fetchUsers(fields = ['id', 'name']) {
console.info("fetch users");
return request(`${process.env.REACT_APP_SERVER_URL}/graphql`, gql`
query getUsers {
users {
${fields},nice
}
}`);
}
My App.js:
<QueryClientProvider client={queryClient}>
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
There was an error!{" "}
<pre style={{ whiteSpace: "normal" }}>{error.message}</pre>
</div>
)}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<UserList/>
</ErrorBoundary>
</QueryClientProvider>
I expect to see the ErrorBoundary running when I type unexisting graphql field (aka nice) - but my react app crashes with:
Error: Cannot query field "nice" on type "User". Did you mean "name"?
Why the error boundary don't catch this error? Any idea what I'm missing?
Have you set useErrorBoundary: true to turn on that option, because I’m not seeing that on your code
I could use your input on a quick question about Component loads.
The Goal
Return the <Login /> Component if the user isn't logged in, and the App if they are.
Expected Behavior
When a user is logged in, they see the App.
Observed Behavior
The <Login /> Component flickers (renders) for a moment, then the user sees the App.
My goal is to eliminate this flicker!
Code Samples
Index.js
export default function Index() {
let [isLoading, setIsLoading] = useState(true)
const router = useRouter()
// User object comes in from an Auth Context Provider
const { user } = useContext(AuthContext)
const { email } = user
useEffect(() => {
if (user) {
setIsLoading(false)
}
}, [])
// Returns the App if logged in, login screen if not
const getLoggedIn = () => {
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
}
return (
<Box className="App">
{ isLoading
? <div className={classes.root}>
<LinearProgress />
</div>
: getLoggedIn()
}
</Box>
)
}
Auth Context
Note: I'm using Firebase for auth.
// Listens to auth state changes when App mounts
useEffect(() => {
// Calls setUser state update method on callback
const unsubscribe = onAuthStateChange(setUser)
return () => {
unsubscribe()
}
}, [])
// Brings data from auth to Auth Context user state via callback
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
Stack
"next": "^8.1.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
Thanks so much for taking a look.
I had this exact problem and resolved it by storing the user in local storage
then on app start up do this:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')))
and it'll use the details from localstorage and you wont see a flicker
(it's because onauthstate takes longer to kick in)
So I figured out a sort of 'hacky' way around this. One needs to set the value of the boolean on which the initial load of the App depends...
const getLoggedIn = () => {
// Right here
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
...before making any asynchronous calls in the AuthContext. Like this:
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
// sets loggedIn to true to prevent flickering to login screen on load
callback({ loggedIn: true })
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
I hope this helps someone.