Expo ReactNative Firebase Get authenticated user and profile in one go - firebase

I am new to Expo and ReactNative. At the moment, I am developing a mobile app that utilizes Firebase Auth and Firestore for authentication and user profile. From googling, I found a solution to use ContextProvider for the Firebase Auth. I tried to adjust the code to obtain the profile at the same time.
This is the code for AuthenticatedUserProvider.js
import React, { useState, createContext } from 'react';
export const AuthenticatedUserContext = createContext({});
export const AuthenticatedUserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [profile, setProfile] = useState(null);
return (
<AuthenticatedUserContext.Provider value={{ user, setUser, profile, setProfile }}>
{children}
</AuthenticatedUserContext.Provider>
);
};
And I read the profile on the RotNavigator.js below
import React, { useContext, useEffect, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import * as eva from '#eva-design/eva';
import { ApplicationProvider, IconRegistry } from '#ui-kitten/components';
import { EvaIconsPack } from '#ui-kitten/eva-icons';
import { auth, fs } from '../config/firebase';
import { AuthenticatedUserContext } from './AuthenticatedUserProvider';
import AuthStack from './AuthStack';
import HomeStack from './HomeStack';
import { default as theme } from '../orange-theme.json';
export default function RootNavigator() {
const { user, setUser, profile, setProfile } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// onAuthStateChanged returns an unsubscribe
const unsubscribeAuth = auth.onAuthStateChanged(async authenticatedUser => {
try {
if (authenticatedUser) {
await setUser(authenticatedUser)
const response = await fs.collection("members").doc(authenticatedUser.uid).get();
if (response.exists) {
let data = response.data();
// console.log(data);
await setProfile(data);
// console.log(profile);
}
} else {
await setUser(null);
await setProfile(null);
}
setIsLoading(false);
} catch (error) {
console.log(error);
}
});
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size='large' />
</View>
);
}
return (
<>
<IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={{ ...eva.light, ...theme }}>
<NavigationContainer>
{user ? <HomeStack /> : <AuthStack />}
</NavigationContainer>
</ApplicationProvider >
</>
);
}
The code looks like working except that if I log in, sometimes I saw an error on profile = null and cannot read the profile but then it refreshed by itself and back to run properly.
Can someone suggest the correct way to display the profile from Firestore?
The reason I do it like this is I want an efficient way of displaying the profile since HomeStack has 3 screens that would need to display some parts of the profile on each screen.
Thank you for your help.

Related

Is it possible to implement expo-auth-session in rtk query?

I'm trying to move this logic to redux toolkit query in order to reduce logic from the screens and allow business logic to be available in redux side. However I'm having hard time to figure out how to move this to redux. Any suggestion would be appreciated
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { ResponseType } from 'expo-auth-session';
import * as Google from 'expo-auth-session/providers/google';
import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider, signInWithCredential } from 'firebase/auth';
import { Button } from 'react-native';
// Initialize Firebase
initializeApp({
/* Config */
});
WebBrowser.maybeCompleteAuthSession();
export default function App() {
const [request, response, promptAsync] = Google.useIdTokenAuthRequest(
{
clientId: 'Your-Web-Client-ID.apps.googleusercontent.com',
},
);
React.useEffect(() => {
if (response?.type === 'success') {
const { id_token } = response.params;
const auth = getAuth();
const credential = GoogleAuthProvider.credential(id_token);
signInWithCredential(auth, credential);
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}

Navgigation.navigate(param) with PushNotification in react native and react navigation

I want to redirect my user on the page I choose when they click on my push notification. I know how to get parameters from firebase.
Push Notification package : https://github.com/zo0r/react-native-push-notification
React navigation : https://reactnavigation.org/docs/getting-started/
Index.tsx :
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import PushNotification from "react-native-push-notification";
import { useNavigation } from '#react-navigation/native';
PushNotification.configure({
onNotification: function (notification) {
console.log(notification)
screenToRedirect = notification.data.screen //from firebase key => value
//here I'd like to do something like this :
const navigation = useNavigation();
navigation.navigate(screenToRedirect )
},
requestPermissions: Platform.OS === 'ios'
});
AppRegistry.registerComponent(appName, () => App);
It tells me:
Invalid hook call. Hooks can only be called inside of the body of a function component. Problem : I can't put the PushNotification.configure inside a component (it is mentioned in the doc).
You can't use hooks outside react components. You can try this solution: https://reactnavigation.org/docs/navigating-without-navigation-prop/
SOLUTION
I just added my principal menu in the root navigation. So the navigationRef return my principal menu, example :
export const Drawer = createDrawerNavigator();
export const MyDrawer = () => {
return(
<Drawer.Navigator initialRouteName="Accueil">
<Drawer.Screen name="Accueil" component={Home} />
<Drawer.Screen name="Qui sommes-nous ?" component={Who} />
<Drawer.Screen name="Nos Services" component={Services} />
<Drawer.Screen name="Nos Biens" component={Bien} />
</Drawer.Navigator>
)
}
export const Bottom = createBottomTabNavigator();
export const MyBottom = () => {
return(
<Bottom.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused
? 'home'
: 'folder';
} else if (route.name === 'Mon Profil') {
iconName = focused ? 'alert-circle' : 'aperture-sharp';
}
// You can return any component that you like here!
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
})}
>
<Bottom.Screen name="Home" component={MyDrawer} options={{ headerShown: false }}/>
<Bottom.Screen name="Mon Profil" component={Profil} options={{ headerShown: false }}/>
</Bottom.Navigator>
)
}
export const navigationRef = createNavigationContainerRef()
export function navigate(name, params) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
const RootStack = createNativeStackNavigator();
export default function AppMenu() {
return (
<NavigationContainer ref={navigationRef} independent={true}>
<RootStack.Navigator initialRouteName="Accueil">
// Here, I return My Bottom which is the principal menu that return itself MyDrawer menu
<RootStack.Screen name="Home2" component={MyBottom} options={{ headerShown: false }}/>
</RootStack.Navigator>
</NavigationContainer>
);
}
And now My App.tsx return AppMenu.
Here's how I solved this problem:
1- First create a RootNavigation,js file
2- Add the below code in that file
import {createNavigationContainerRef} from '#react-navigation/native';
export const navigationRef = createNavigationContainerRef();
export function navigate(name, params) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
3- Add navigationRef in navigation container
import {navigationRef} from './RootNavigation';
<NavigationContainer ref={navigationRef}>
<MainStack />
</NavigationContainer>
4- Now you can use this ref outside of NavigationContainer as well if you have initialized your push notifications in app.js
import * as RootNavigation from './src/navigation/RootNavigation';
***if you are using one signal for push notifications then you can navigate to your screen in setNotificationOpenedHandler method***
//Method for handling notifications opened
OneSignal.setNotificationOpenedHandler(notification => {
RootNavigation.navigate("your_screen_name");
});

Next auth credentials

I'm trying to do a credentials auth with next-auth. I have to use a custom sign-in page and I absolutely can't make it work for approximately one entire week.
I have :
// [...nextauth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from '#api/axios';
const options = {
providers: [
Providers.Credentials({
async authorize(credentials) {
const { data: user, status } = await axios.post('/users/authentication', credentials);
if (user && status === 200) {
return user;
} else {
throw new Error('error message');
}
}
})
],
pages: {
signIn: '/profil/authentication/login',
error: '/profil/authentication/login'
},
session: {
jwt: true,
maxAge: 30 * 24 * 60 * 60 // 30 days
},
debug: true
};
export default (req, res) => NextAuth(req, res, options);
and :
// profil/authentication/login
import { signOut, useSession } from 'next-auth/client';
import AuthenticationForm from '#components/auth/authenticationForm';
import Layout from '#components/layout';
const Login = () => {
const [session] = useSession();
const intl = useIntl();
return (
<Layout>
{!session && <AuthenticationForm />}
{session && (
<>
Signed in as {session.user.email}
<br />
<button onClick={signOut}>Sign out</button>
</>
)}
</Layout>
);
};
export default Login;
// authenticationForm.js
import { signIn, csrfToken } from 'next-auth/client';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import PasswordInput from '#components/auth/passwordInput';
import Button from '#components/form/button';
import TextInput from '#components/form/textInput';
const AuthenticationForm = ({ csrf }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleChangeUsername = ({ target: { value } }) => setUsername(value);
const handleChangePassword = ({ target: { value } }) => setPassword(value);
const handleLogin = () => {
signIn('credentials', {
username,
password,
callbackUrl: `${window.location.origin}/profil`
})
.then((res) => {
console.log('form::res -> ', res);
router.back();
})
.catch((e) => {
console.log('form::e -> ', e);
setError('login error');
});
};
useEffect(() => {
if (router.query.error) {
setError(router.query.error);
setUsername(router.query.username);
}
}, [router]);
return (
<form onSubmit={handleLogin}>
<TextInput
name="username"
value={username}
onChange={handleChangeUsername}
/>
<PasswordInput handleChange={handleChangePassword} />
{error && <div>{error}</div>}
<Button type="submit">
connexion
</Button>
<input name="csrfToken" type="hidden" defaultValue={csrf} />
</form>
);
};
AuthenticationForm.getInitialProps = async (context) => {
return {
csrf: await csrfToken(context)
};
};
export default AuthenticationForm;
And for sure a NEXTAUTH_URL=http://localhost:3000 in .env.local.
If I go on /profil/authentication/login, I see my form and when I click connect, I always some errors like : "Failed to fetch", nothing more, or :
[next-auth][error][client_fetch_error] (2) ["/api/auth/csrf", TypeError: Failed to fetch]
https://next-auth.js.org/errors#client_fetch_error
Even if I try to delete all the csrf handling in my form and let sign-in "do it alone yea".
I'm really stuck with this lib and I most likely will change for another one but I would like to know what am I doing wrong? Is there a FULL example with custom sign-in page and errors handled on the same sign-in page. This is so basic that I can't understand why I don't find one easily.
#Tralgar
I think that problem is related to CSRF policy on your backend, if you are on localhost then localhost:3000 and localhost:2000 is like two different domains. Just make sure if you have your frontend domain in your backend cors policy (if on localhost it must be with a port)
I was able to fix the error by deleting the .next build folder and creating a new build by running npm run build

Deleting account/user in firebase using REST API

I trying to delete an account in firebase using REST API, axios and React Native?
Look at my code:
import React, {Component} from 'react'
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native'
import axios from 'axios'
class App extends Component {
deleteAccount = () => {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:delete?key=[API_KEY]', {
"idToken":"[FIREBASE_ID_TOKEN]"
})
}
render() {
return(
<View style={styles.container}>
<TouchableOpacity onPress={this.deleteAccount}>
<Text>Button</Text>
</TouchableOpacity>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}
})
export default App
PS.: Maybe, I'm puting the wrong FIREBASE_ID_TOKEN
Where can I get the right FIREBASE_ID_TOKEN?
Reference: https://firebase.google.com/docs/reference/rest/auth?hl=pt-br#section-delete-account
Thanks!
The ID token you need to use comes from User#getIdToken(). This function returns a Promise containing the user's ID token that you then send off to the Identity Toolkit API.
Make sure to replace [API_KEY] with the Web API Key that you'll find on the project settings page.
deleteAccount = () => {
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true)
.then((userIdToken) => {
return axios.post('https://identitytoolkit.googleapis.com/v1/accounts:delete?key=[API_KEY]', {
"idToken": userIdToken
});
})
.then(function (response) {
console.log(response);
// TODO: update UI that account was deleted
})
.catch(function (error) {
console.log(error);
// TODO: update UI that operation failed
});
}

"Cannot read property of undefined" being thrown by react-apollo used with React-NextJS

My NextJS project has been giving me grief over Graph QL these days. I've been trying to implement an Apollo client solution to retrieve data from a remote GraphQL server into a custom component. But no matter which solution I try, I always end up with this error. Here's my current react-apollo implementation:
// /lib/with-apollo-client.js
import React from "react";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";
import initApollo from "./init-apollo";
export default App => {
return class WithData extends React.Component {
static displayName = `WithData(${App.displayName})`;
static async getInitialProps(ctx) {
const { Component, router } = ctx;
const apollo = initApollo({});
ctx.ctx.apolloClient = apollo;
let appProps = {};
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx);
}
// Run all GraphQL queries in the component tree
// and extract the resulting data
if (!process.browser) {
try {
// Run all GraphQL queries
await getDataFromTree(
<App
{...appProps}
Component={Component}
router={router}
apolloClient={apollo}
/>
);
} catch (error) {
console.error("Error while running `getDataFromTree`", error);
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind();
}
// Extract query data from the Apollo store
const apolloState = apollo.cache.extract();
return {
...appProps,
apolloState
};
}
constructor(props) {
super(props);
this.apolloClient = initApollo(props.apolloState);
}
render() {
return <App {...this.props} apolloClient={this.apolloClient} />;
}
};
};
// /lib/init-apollo.js
import { ApolloClient, InMemoryCache, HttpLink } from 'apollo-boost'
import fetch from 'isomorphic-unfetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
function create (initialState) {
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: new HttpLink({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
}),
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo (initialState) {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create(initialState)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState)
}
return apolloClient
}
The component I'm retrieving data into looks like this:
// /components/PostsList2.jsx
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
export const allUsersQuery = gql`
query allUsers($first: Int!, $skip: Int!) {
allUsers(orderBy: createdAt_DESC, first: $first, skip: $skip) {
id
firstName
createdAt
}
_allUsersMeta {
count
}
}
`
export const allUsersQueryVars = {
skip: 0,
first: 10
}
export default function PostsList2 () {
return (
<Query query={allUsersQuery} variables={allUsersQueryVars}>
{({ loading, error, data: { allUsers, _allUsersMeta }, fetchMore }) => {
if (error) return <aside>Error loading users!</aside>
if (loading) return <div>Loading</div>
const areMorePosts = allUsers.length < _allUsersMeta.count
return (
<section>
<ul>
{allUsers.map((user, index) => (
<li key={user.id}>
<div>
<span>{index + 1}. </span>
<div>{user.firstName}</div>
</div>
</li>
))}
</ul>
{areMorePosts ? (
<button onClick={() => loadMorePosts(allUsers, fetchMore)}>
{' '}
{loading ? 'Loading...' : 'Show More'}{' '}
</button>
) : (
''
)}
</section>
)
}}
</Query>
)
}
function loadMorePosts (allUsers, fetchMore) {
fetchMore({
variables: {
skip: allUsers.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new users results to the old one
allUsers: [...previousResult.allUsers, ...fetchMoreResult.allUsers]
})
}
})
}
Since this is a NextJS project, there's also an _app.jsx that I've wrapped in a special provider component:
// /pages._app.jsx
/* eslint-disable max-len */
import '../static/styles/fonts.scss';
import '../static/styles/style.scss';
import '../static/styles/some.css';
import CssBaseline from '#material-ui/core/CssBaseline';
import { ThemeProvider } from '#material-ui/styles';
import jwt from 'jsonwebtoken';
import withRedux from 'next-redux-wrapper';
import App, {
Container,
} from 'next/app';
import Head from 'next/head';
import React from 'react';
import { Provider } from 'react-redux';
import makeStore from '../reducers';
import mainTheme from '../themes/main-theme';
import getSessIDFromCookies from '../utils/get-sessid-from-cookies';
import getLanguageFromCookies from '../utils/get-language-from-cookies';
import getUserTokenFromCookies from '../utils/get-user-token-from-cookies';
import removeFbHash from '../utils/remove-fb-hash';
import withApolloClient from '../lib/with-apollo-client'
import { ApolloProvider } from 'react-apollo'
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let userToken;
let sessID;
let language;
if (ctx.isServer) {
ctx.store.dispatch({ type: 'UPDATEIP', payload: ctx.req.headers['x-real-ip'] });
userToken = getUserTokenFromCookies(ctx.req);
sessID = getSessIDFromCookies(ctx.req);
language = getLanguageFromCookies(ctx.req);
const dictionary = require(`../dictionaries/${language}`);
ctx.store.dispatch({ type: 'SETLANGUAGE', payload: dictionary });
if(ctx.res) {
if(ctx.res.locals) {
if(!ctx.res.locals.authenticated) {
userToken = null;
sessID = null;
}
}
}
if (userToken && sessID) { // TBD: validate integrity of sessID
const userInfo = jwt.verify(userToken, process.env.JWT_SECRET);
ctx.store.dispatch({ type: 'ADDUSERINFO', payload: userInfo });
}
ctx.store.dispatch({ type: 'ADDSESSION', payload: sessID }); // component will be able to read from store's state when rendered
}
const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};
return { pageProps };
}
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles);
}
// Register serviceWorker
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/serviceWorker.js'); }
// Handle FB's ugly redirect URL hash
removeFbHash(window, document);
}
render() {
const { Component, pageProps, store, apolloClient } = this.props;
return (
<Container>
<Head>
// redacted for brevity
</Head>
<ThemeProvider theme={mainTheme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<ApolloProvider client={apolloClient}>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</ApolloProvider>
</ThemeProvider>
</Container>
);
}
}
export default withApolloClient(withRedux(makeStore)(MyApp));
So with this setup, when I compile and run my app, it throws the following:
TypeError: Cannot read property 'allUsers' of undefined
I'm really lost! The repo is up at https://github.com/amitschandillia/proost/tree/master/web.

Resources