Security and Authentication page

Learn about security, authentication, and authorization by implementing Google Sign-In.

Overview

In this section, you will:

  • Authenticate with OAuth.
  • Implement Google Sign-In.
  • Memoize functions with useCallback.
  • Memoize values with useMemo.
  • Create an AuthProvider Context.

Objective 1: Add Google Sign-In

Screenshot of the Settings tab with a Google “Sign in” button. Screenshot of the Settings tab showing a sign-in user and a “Sign out” button.

Authenticating with OAuth

OAuth is a standard that allows an application to access resources hosted by another application through a secure authorization process.

Let’s break down some of the core concepts of OAuth:

  • Access Token: A token used by the application to access protected resources. It is obtained during the OAuth authorization flow and sent with each request to the resource server. Access tokens are typically short-lived and can be refreshed using a refresh token.

  • Refresh Token: A token used to obtain a new access token when the current access token expires. Refresh tokens are typically long-lived and can be used to obtain new access tokens without requiring the user to log in again.

  • Scopes: Scopes are permissions requested by the application during the OAuth authorization flow. They define what actions the application is allowed to perform using the access token. For example, an application might request only read access to a user’s Google Drive files, which would allow it to view (but not modify) the files.

  • Authorization Server: The server that authenticates the user and issues access tokens.

  • Resource Server: The server that hosts protected resources. It is responsible for validating access tokens and granting access to resources based on the token's permissions.

The use of refresh tokens rather than long-lived access tokens is primarily a security measure:

  • Reduced Exposure: If an access token is compromised, the attacker has a limited window of time to use it before it expires.

  • Revocation: If a user’s access is revoked, the refresh token can be invalidated, preventing the attacker from obtaining new access tokens.

  • Limited Scope: Refresh tokens can be issued with limited scopes, reducing the potential damage if they are compromised.

The OAuth flow typically involves the following steps:

  1. The user clicks a “Sign In” button in the application.
  2. The application redirects the user to the OAuth provider’s login page.
  3. The user logs in to the OAuth provider.
  4. The OAuth provider redirects the user back to the application with an authorization code.
  5. The application exchanges the authorization code for an access token.
  6. The application uses the access token to access the secured resources.

Implementing Google Sign-In

The @react-native-google-signin/google-signin package provides a simple way to integrate Google Sign-In into our React Native apps. It handles the OAuth flow, allowing users to sign in with their Google accounts and obtain an access token that can be used to access Google services on their behalf.

To use Google Sign-In in your React Native app, you must configure your application in the Google Cloud Console and obtain a client ID. This client ID is used to identify your application when requesting access tokens from Google.

  1. Create a new project in the Google Cloud Console.
  2. Enable APIs & services for your project.
  3. Navigate to the Credentials section and create a new OAuth client ID.
  4. Because we are using React Native, we will need to create an OAuth client ID for both a Web application and an Android application. For the Android application, use the command provided by the create form to generate a SHA-1 fingerprint.
  5. The client ID from the Web application will be used in the webClientId field when configuring the Google Sign-In package in your React Native app.

Now that we have a project set up in Google Cloud Console, we can start integrating Google Sign-In into our React Native app.

Configuring the package

It is mandatory to call the configure method before attempting to sign in. The configure method takes an object and is used to initialize your application for authentication with Google. The scopes parameter is an array of strings representing the permissions the application is requesting. The webClientId parameter is the client ID of th project that is created in the Google Cloud Console.

import { GoogleSignin } from "@react-native-google-signin/google-signin"

const googleOauthwebClientId = process.env.GOOGLE_OAUTH_CLIENT_ID

GoogleSignin.configure({
  scopes: ["openid", "profile", "email"],
  webClientId: googleOauthwebClientId,
})

Signing in

The signIn method is used to prompt a modal and allow the user to sign into their Google account from our application. This method returns a Promise that resolves to the user’s information if the sign-in is successful. This method is the first part of authorization OAuth flow. Once the user is signed in, we can use the getTokens method to retrieve an access token.

import {
  GoogleSignin,
  statusCodes,
} from "@react-native-google-signin/google-signin"

const signIn = async () => {
  try {
    const userInfo = await GoogleSignin.signIn()
    console.info(userInfo)
  } catch (error) {
    if (error.code === statusCodes.SIGN_IN_CANCELLED) {
      // User cancelled the login flow.
    } else if (error.code === statusCodes.IN_PROGRESS) {
      // Operation is in progress already.
    } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
      // Play services not available or outdated.
    } else {
      // Some other error happened.
    }
  }
}

export default signIn

Getting the current user

The getCurrentUser method is used to retrieve the current signed-in user’s information. This method returns a Promise that resolves to the user’s information if the user is signed in.

import { GoogleSignin } from "@react-native-google-signin/google-signin"

const getCurrentUser = async () => {
  const userInfo = await GoogleSignin.getCurrentUser()
  console.info(userInfo)
}

Signing out

The signOut method is used to sign the current user out and revoke the access token.

import { GoogleSignin } from "@react-native-google-signin/google-signin"

const signOut = async () => {
  await GoogleSignin.signOut()
}

Using the GoogleSigninButton

The GoogleSigninButton component is a pre-styled button that can be used to initiate the sign-in flow. The size, color, and whether the button is disabled can be customized using props. The onPress prop is used to specify the function to be called when the button is pressed.

import { GoogleSigninButton } from "@react-native-google-signin/google-signin"

function Button() {
  return (
    <GoogleSigninButton
      size={GoogleSigninButton.Size.Wide}
      color={GoogleSigninButton.Color.Dark}
      onPress={signIn}
      disabled={false}
    />
  )
}

Memoizing functions with useCallback

The useCallback Hook is used to memoize functions so that they are not recreated on every render. When using a provider, it is important to memoize functions so that they do not cause unnecessary re-renders of components that depend on them.

const memoizedCallback = useCallback(() => {
  // function logic
}, [dependencies])

In the example above, the memoizedCallback function will only be recreated when the dependencies array changes.

Memoizing values with useMemo

Similar to useCallback, the useMemo Hook is used to memoize values so that they are not recalculated on every render. It is useful for optimizing performance by avoiding unnecessary calculations.

The useCallback Hook is used to memoize a function, so that child components do not re-render when the function reference changes. The useMemo Hook on the other hand is used to memoize a value. We should use useMemo if the logic to calculate a value is expensive.

const memoizedValue = useMemo(() => {
  // value calculation logic
}, [dependencies])

Just like useCallback, the memoizedValue will only be recalculated when the dependencies array changes.

Creating an AuthProvider Context

It’s common to create an AuthProvider context that manages the authentication state of the application. It provides the authentication state and methods to sign in and sign out to the rest of the application using a React Context.

The AuthProvider typically includes methods to:

  • Sign in the user.
  • Sign out the user.
  • Get the current signed-in user’s information.

We can use an AuthProvider to lock down our application and only allow access to authenticated users.

function AuthProvider({ children }) {
  const [user, setUser] = useState(null)

  const signIn = useCallback(() => {
    // sign in logic
  }, [])

  const signOut = useCallback(() => {
    // sign out logic
  }, [])

  if (!user) {
    return <SignInScreen onSignIn={signIn} />
  }

  return (
    <AuthContext.Provider value={{ user, signOut }}>
      {children}
    </AuthContext.Provider>
  )
}

We would use this in the application like this:

<AuthProvider>
  <App />
</AuthProvider>

Setup 1

✏️ Install the new dependency:

npm install @react-native-google-signin/google-signin@11

✏️ Update jest-setup.ts to be:

import "@testing-library/react-native/extend-expect"

import "react-native-gesture-handler/jestSetup"
import "@react-native-google-signin/google-signin/jest/build/setup"

jest.mock("@react-navigation/native", () => {
  const actualNav = jest.requireActual("@react-navigation/native")
  return {
    ...actualNav,
    useNavigation: () => ({
      navigate: jest.fn(),
      setOptions: jest.fn(),
    }),
  }
})

jest.mock("./src/services/storage/storage", () =>
  require("./src/services/storage/storage.mock"),
)

jest.mock("@react-native-async-storage/async-storage", () =>
  require("@react-native-async-storage/async-storage/jest/async-storage-mock"),
)

const consoleError = console.error
console.error = (message, ...args) => {
  if (
    typeof message === "string" &&
    message.match(
      /Warning: An update to .+ inside a test was not wrapped in act\(\.\.\.\)\./,
    )
  ) {
    return
  }

  return consoleError(message, ...args)
}

✏️ Update src/App.tsx to be:

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { createStackNavigator } from "@react-navigation/stack"
import { Pressable } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import Icon from "react-native-vector-icons/Ionicons"

import Box from "./design/Box"
import ThemeProvider, { useTheme } from "./design/theme/ThemeProvider"
import Typography from "./design/Typography"
import CityList from "./screens/CityList"
import RestaurantDetails from "./screens/RestaurantDetails"
import RestaurantList from "./screens/RestaurantList"
import RestaurantOrder from "./screens/RestaurantOrder"
import Settings from "./screens/Settings"
import StateList from "./screens/StateList"
import AuthProvider from "./services/auth/AuthProvider"
import DataMigration from "./services/DataMigration"

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace ReactNavigation {
    // eslint-disable-next-line @typescript-eslint/no-empty-interface
    interface RootParamList extends RestaurantsStackParamList {}
  }
}

export type RestaurantsStackParamList = {
  StateList: undefined
  CityList: {
    state: {
      name: string
      short: string
    }
  }
  RestaurantList: {
    state: {
      name: string
      short: string
    }
    city: {
      name: string
      state: string
    }
  }
  RestaurantDetails: {
    slug: string
  }
  RestaurantOrder: {
    slug: string
  }
}

const RestaurantsStack = createStackNavigator<RestaurantsStackParamList>()
const RestaurantsNavigator: React.FC = () => {
  return (
    <RestaurantsStack.Navigator
      initialRouteName="StateList"
      screenOptions={{
        header: ({ route, navigation }) => {
          if (!navigation.canGoBack()) return null

          return (
            <Pressable onPress={navigation.goBack}>
              <Box
                padding="m"
                style={{ flexDirection: "row", gap: 8, alignItems: "center" }}
              >
                <Icon name="arrow-back" size={20} />
                <Typography variant="heading">
                  {/* @ts-ignore */}
                  {[route.params?.city?.name, route.params?.state?.name]
                    .filter(Boolean)
                    .join(", ")}
                </Typography>
              </Box>
            </Pressable>
          )
        },
      }}
    >
      <RestaurantsStack.Screen name="StateList" component={StateList} />
      <RestaurantsStack.Screen name="CityList" component={CityList} />
      <RestaurantsStack.Screen
        name="RestaurantList"
        component={RestaurantList}
      />
      <RestaurantsStack.Screen
        name="RestaurantDetails"
        component={RestaurantDetails}
      />
      <RestaurantsStack.Screen
        name="RestaurantOrder"
        component={RestaurantOrder}
      />
    </RestaurantsStack.Navigator>
  )
}

const AppTabs = createBottomTabNavigator()
const AppNavigator: React.FC = () => {
  const theme = useTheme()

  return (
    <AppTabs.Navigator
      initialRouteName="RestaurantsStack"
      screenOptions={({ route }) => ({
        headerStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        headerTitleStyle: {
          color: theme.palette.screen.contrast,
          ...theme.typography.title,
        },
        tabBarStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        tabBarActiveTintColor: theme.palette.primary.strong,
        tabBarInactiveTintColor: theme.palette.screen.contrast,
        tabBarIcon: ({ focused, color }) => {
          let icon = "settings"
          if (route.name === "Settings") {
            icon = focused ? "settings" : "settings-outline"
          } else if (route.name === "Restaurants") {
            icon = focused ? "restaurant" : "restaurant-outline"
          }

          return <Icon name={icon} size={20} color={color} />
        },
      })}
    >
      <AppTabs.Screen
        name="Restaurants"
        component={RestaurantsNavigator}
        options={{ title: "Place My Order" }}
      />
      <AppTabs.Screen
        name="Settings"
        component={Settings}
        options={{ title: "Settings" }}
      />
    </AppTabs.Navigator>
  )
}

// Exercise: Add `AuthProvider` to the App component.
const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ThemeProvider>
        <DataMigration>
          <NavigationContainer>
            <AppNavigator />
          </NavigationContainer>
        </DataMigration>
      </ThemeProvider>
    </SafeAreaView>
  )
}

export default App

✏️ Update src/env.d.ts to be:

/* eslint-disable @typescript-eslint/no-namespace */

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      readonly GOOGLE_OAUTH_CLIENT_ID: string
      readonly PMO_API: string
      readonly PMO_ASSETS: string
    }
  }
}

export {}

✏️ Update .env.example to be:

GOOGLE_OAUTH_CLIENT_ID=
PMO_API=http://localhost:7070
PMO_ASSETS=https://www.place-my-order.com/

✏️ Update .env to include your GOOGLE_OAUTH_CLIENT_ID key.

✏️ Terminate the existing dev server and start it again:

npm run start

✏️ Update src/screens/Settings/Settings.tsx to be:

import { GoogleSigninButton } from "@react-native-google-signin/google-signin"
import { StyleSheet, Switch, View } from "react-native"

import Loading from "../../components/Loading"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import { useThemeMode } from "../../design/theme"
import Typography from "../../design/Typography"
import { useAuthentication, useUser } from "../../services/auth"

const Settings: React.FC = () => {
  // Exercise: use the Hooks from the Auth service to get: error, isPending, signIn, signOut.
  const { mode, setMode } = useThemeMode()

  return (
    <Screen>
      <Card>
        {/*
          Exercise:
          - If the user is logged out: Render a button using the GoogleSigninButton.
          - If the user is logged in: Render a button using the signOut callback along with a welcome message.
          - If the Hook isPending: Render the Loading view.
          - If the Hook has an error: Render the error message.
        */}
        <Typography variant="heading"></Typography>
      </Card>
      <Card>
        <View style={styles.row}>
          <Typography variant="heading">Dark mode</Typography>
          <Switch
            onValueChange={(value) => setMode(value ? "dark" : "light")}
            value={mode === "dark"}
          />
        </View>
      </Card>
    </Screen>
  )
}

const styles = StyleSheet.create({
  row: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
  },
})

export default Settings

✏️ Create src/services/auth/AuthProvider.tsx and update it to be:

import {
  User as UserInfo,
  GoogleSignin,
} from "@react-native-google-signin/google-signin"
import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from "react"

import { AuthContext, AuthContextProvider, useAuthContext } from "./context"

const googleOauthwebClientId = process.env.GOOGLE_OAUTH_CLIENT_ID

GoogleSignin.configure({
  scopes: ["openid", "profile", "email"],
  webClientId: googleOauthwebClientId,
})

export interface AuthProviderProps {
  children: React.ReactNode
}

const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [error, setError] = useState<Error | undefined>()
  const [isPending, setIsPending] = useState<boolean>(true)
  const [userInfo, setUserInfo] = useState<UserInfo | undefined>()

  const signIn = async () => {
    // Exercise: Implement `signIn` with `useCallback`.
    try {
      setError(undefined)
      setIsPending(true)

      // Exercise: Use the GoogleSignin API to sign in the user.

      setIsPending(false)
      setUserInfo(userInfo)

      return userInfo.user
    } catch (error) {
      console.error("Call to GoogleSignin.signIn() failed with error:", error)

      setError(error as Error)
      setIsPending(false)
      setUserInfo(undefined)

      return false
    }
  }

  const signOut = async () => {
    // Exercise: Implement `signOut` with `useCallback`.
    try {
      setError(undefined)
      setIsPending(true)

      // Exercise: Use the GoogleSignin API to sign out the user.

      setIsPending(false)
      setUserInfo(undefined)

      return true
    } catch (error) {
      console.error("Call to GoogleSignin.signOut() failed with error:", error)

      setError(error as Error)
      setIsPending(false)

      return false
    }
  }

  useEffect(() => {
    async function run() {
      try {
        setError(undefined)
        setIsPending(true)

        // Exercise: When a sign in is successful, update the user.
      } catch (error) {
        console.error(
          "Call to GoogleSignin.getCurrentUser() failed with error:",
          error,
        )

        setError(error as Error)
        setIsPending(false)
      }
    }

    run()
  }, [])

  const value = useMemo<AuthContext>(
    () => ({
      signIn,
      signOut,
      error,
      isAuthenticated: userInfo
        ? true
        : userInfo === undefined
        ? false
        : undefined,
      isPending,
      user: userInfo?.user,
      scopes: userInfo?.scopes,
      idToken: userInfo?.idToken,
    }),
    [error, isPending, signIn, signOut, userInfo],
  )

  return <AuthContextProvider value={value}>{children}</AuthContextProvider>
}

export default AuthProvider

export function useAuthentication(): Pick<
  AuthContext,
  "error" | "isPending" | "signIn" | "signOut"
> {
  const { error, isPending, signIn, signOut } = useAuthContext()

  return { error, isPending, signIn, signOut }
}

export function useAuthenticated(): boolean | undefined {
  const { isAuthenticated } = useAuthContext()

  return isAuthenticated
}

export function useUser(): UserInfo["user"] | undefined {
  const { user } = useAuthContext()

  return user
}

✏️ Create src/services/auth/context.ts and update it to be:

import { User as UserInfo } from "@react-native-google-signin/google-signin"
import { createContext, useContext } from "react"

export interface AuthContext {
  /** Initiate the Google Auth flow. Return boolean success. */
  signIn: () => Promise<UserInfo["user"] | false>

  /** Log the user out from Google Auth. Return boolean success. */
  signOut: () => Promise<boolean>

  /** Error if any Google API returns an error. Undefined if no errors in the last API call. */
  error?: Error | undefined

  /** Boolean if the user is authenticated or not. Undefined if unknown. */
  isAuthenticated?: boolean

  /** Boolean if a Google API call is being made. */
  isPending: boolean

  /** Google Auth User object. Undefined if not signed in. */
  user?: UserInfo["user"]

  /** List of Google Auth scopes. Undefined if not signed in. */
  scopes?: UserInfo["scopes"]

  /** Google Auth Token. Undefined if not signed in. */
  idToken?: UserInfo["idToken"]
}

const Context = createContext<AuthContext | undefined>(undefined)

export const AuthContextProvider = Context.Provider

export function useAuthContext(): AuthContext {
  const value = useContext(Context)

  if (!value) {
    throw new Error("Missing AuthProvider.")
  }

  return value
}

✏️ Create src/services/auth/index.ts and update it to be:

export { default } from "./AuthProvider"
export * from "./AuthProvider"

Verify 1

✏️ Update src/screens/Settings/Settings.test.tsx to be:

import { NavigationContainer } from "@react-navigation/native"
import { fireEvent, render, screen } from "@testing-library/react-native"

import AuthProvider from "../../services/auth/AuthProvider"

import Settings from "./Settings"

const mockSetMode = jest.fn()

jest.mock("../../design/theme", () => ({
  ...jest.requireActual("../../design/theme"),
  useThemeMode: () => ({
    mode: "light",
    setMode: mockSetMode,
  }),
}))

describe("Screens/Settings", () => {
  it("renders", async () => {
    render(
      <AuthProvider>
        <NavigationContainer>
          <Settings />
        </NavigationContainer>
      </AuthProvider>,
    )
    expect(screen.getByText(/Loading…/i)).not.toBeNull()
  })

  it("switches to dark mode", () => {
    render(
      <AuthProvider>
        <NavigationContainer>
          <Settings />
        </NavigationContainer>
      </AuthProvider>,
    )

    const switchElement = screen.getByRole("switch")
    expect(switchElement.props.value).toBe(false)

    fireEvent(switchElement, "onValueChange", true)
    expect(mockSetMode).toHaveBeenCalledWith("dark")
  })
})

✏️ Create src/services/auth/AuthProvider.test.tsx and update it to be:

import { GoogleSigninButton } from "@react-native-google-signin/google-signin"
import { render, screen, fireEvent } from "@testing-library/react-native"
import { View, Text } from "react-native"

import Button from "../../design/Button"

import AuthProvider, {
  useAuthenticated,
  useAuthentication,
  useUser,
} from "./AuthProvider"

const oldFetch = global.fetch
const mockFetch = jest.fn()
beforeAll(() => {
  global.fetch = mockFetch
})
afterAll(() => {
  global.fetch = oldFetch
})

describe("Services/Auth/AuthProvider", () => {
  const TestComponent: React.FC = () => {
    const isAuthenticated = useAuthenticated()
    const { signIn, signOut } = useAuthentication()
    const user = useUser()

    return (
      <View>
        <Text>{user?.id}</Text>
        <Text>{user?.name}</Text>
        <Text>{user?.email}</Text>
        <Text>{user?.photo}</Text>
        <Text>{user?.givenName}</Text>
        <Text>{user?.familyName}</Text>
        {isAuthenticated && <Button onPress={signOut}>Sign out</Button>}
        {isAuthenticated === false && <GoogleSigninButton onPress={signIn} />}
      </View>
    )
  }

  it("renders with provider; signs in and signs out", async () => {
    render(
      <AuthProvider>
        <TestComponent />
      </AuthProvider>,
    )

    expect(await screen.findByText(/mockId/i)).toBeOnTheScreen()
    fireEvent.press(screen.getByText(/Sign Out/i))

    expect(
      await screen.findByText(/Mock Sign in with Google/i),
    ).toBeOnTheScreen()

    fireEvent.press(screen.getByText(/Mock Sign in with Google/i))
    expect(await screen.findByText(/Sign Out/i)).toBeOnTheScreen()
  })
})

Exercise 1

First, we need to finish implementing the AuthContext in AuthProvider:

  • Implement signIn and signOut using useCallback.
  • When a sign in is successful, update the user.

Next, in App.js

  • Add the AuthProvider to the App component.

Finally, in Settings.js:

  • Use the hooks from AuthContext to grab the user state, and signIn and signOut callbacks.
  • Implement a conditional to render the a button to Sign In or Sign Out based on the user’s state.

Solution 1

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/App.tsx to be:

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { createStackNavigator } from "@react-navigation/stack"
import { Pressable } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import Icon from "react-native-vector-icons/Ionicons"

import Box from "./design/Box"
import ThemeProvider, { useTheme } from "./design/theme/ThemeProvider"
import Typography from "./design/Typography"
import CityList from "./screens/CityList"
import RestaurantDetails from "./screens/RestaurantDetails"
import RestaurantList from "./screens/RestaurantList"
import RestaurantOrder from "./screens/RestaurantOrder"
import Settings from "./screens/Settings"
import StateList from "./screens/StateList"
import AuthProvider from "./services/auth/AuthProvider"
import DataMigration from "./services/DataMigration"

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace ReactNavigation {
    // eslint-disable-next-line @typescript-eslint/no-empty-interface
    interface RootParamList extends RestaurantsStackParamList {}
  }
}

export type RestaurantsStackParamList = {
  StateList: undefined
  CityList: {
    state: {
      name: string
      short: string
    }
  }
  RestaurantList: {
    state: {
      name: string
      short: string
    }
    city: {
      name: string
      state: string
    }
  }
  RestaurantDetails: {
    slug: string
  }
  RestaurantOrder: {
    slug: string
  }
}

const RestaurantsStack = createStackNavigator<RestaurantsStackParamList>()
const RestaurantsNavigator: React.FC = () => {
  return (
    <RestaurantsStack.Navigator
      initialRouteName="StateList"
      screenOptions={{
        header: ({ route, navigation }) => {
          if (!navigation.canGoBack()) return null

          return (
            <Pressable onPress={navigation.goBack}>
              <Box
                padding="m"
                style={{ flexDirection: "row", gap: 8, alignItems: "center" }}
              >
                <Icon name="arrow-back" size={20} />
                <Typography variant="heading">
                  {/* @ts-ignore */}
                  {[route.params?.city?.name, route.params?.state?.name]
                    .filter(Boolean)
                    .join(", ")}
                </Typography>
              </Box>
            </Pressable>
          )
        },
      }}
    >
      <RestaurantsStack.Screen name="StateList" component={StateList} />
      <RestaurantsStack.Screen name="CityList" component={CityList} />
      <RestaurantsStack.Screen
        name="RestaurantList"
        component={RestaurantList}
      />
      <RestaurantsStack.Screen
        name="RestaurantDetails"
        component={RestaurantDetails}
      />
      <RestaurantsStack.Screen
        name="RestaurantOrder"
        component={RestaurantOrder}
      />
    </RestaurantsStack.Navigator>
  )
}

const AppTabs = createBottomTabNavigator()
const AppNavigator: React.FC = () => {
  const theme = useTheme()

  return (
    <AppTabs.Navigator
      initialRouteName="RestaurantsStack"
      screenOptions={({ route }) => ({
        headerStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        headerTitleStyle: {
          color: theme.palette.screen.contrast,
          ...theme.typography.title,
        },
        tabBarStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        tabBarActiveTintColor: theme.palette.primary.strong,
        tabBarInactiveTintColor: theme.palette.screen.contrast,
        tabBarIcon: ({ focused, color }) => {
          let icon = "settings"
          if (route.name === "Settings") {
            icon = focused ? "settings" : "settings-outline"
          } else if (route.name === "Restaurants") {
            icon = focused ? "restaurant" : "restaurant-outline"
          }

          return <Icon name={icon} size={20} color={color} />
        },
      })}
    >
      <AppTabs.Screen
        name="Restaurants"
        component={RestaurantsNavigator}
        options={{ title: "Place My Order" }}
      />
      <AppTabs.Screen
        name="Settings"
        component={Settings}
        options={{ title: "Settings" }}
      />
    </AppTabs.Navigator>
  )
}

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ThemeProvider>
        <AuthProvider>
          <DataMigration>
            <NavigationContainer>
              <AppNavigator />
            </NavigationContainer>
          </DataMigration>
        </AuthProvider>
      </ThemeProvider>
    </SafeAreaView>
  )
}

export default App

✏️ Update src/screens/Settings/Settings.tsx to be:

import { GoogleSigninButton } from "@react-native-google-signin/google-signin"
import { StyleSheet, Switch, View } from "react-native"

import Loading from "../../components/Loading"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import { useThemeMode } from "../../design/theme"
import Typography from "../../design/Typography"
import { useAuthentication, useUser } from "../../services/auth"

const Settings: React.FC = () => {
  const { error, isPending, signIn, signOut } = useAuthentication()
  const user = useUser()
  const { mode, setMode } = useThemeMode()

  return (
    <Screen>
      <Card>
        {isPending ? (
          <Loading />
        ) : user ? (
          <>
            <Typography variant="heading">Welcome back</Typography>
            <Typography variant="body">
              {user.name || "Unknown name"}
            </Typography>
            <Button onPress={signOut}>Sign out</Button>
            {error ? (
              <Typography variant="body">Error: {error.message}</Typography>
            ) : null}
          </>
        ) : (
          <>
            <GoogleSigninButton onPress={signIn} style={{ width: "100%" }} />
            {error ? (
              <Typography variant="body">Error: {error.message}</Typography>
            ) : null}
          </>
        )}
      </Card>
      <Card>
        <View style={styles.row}>
          <Typography variant="heading">Dark mode</Typography>
          <Switch
            onValueChange={(value) => setMode(value ? "dark" : "light")}
            value={mode === "dark"}
          />
        </View>
      </Card>
    </Screen>
  )
}

const styles = StyleSheet.create({
  row: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
  },
})

export default Settings

✏️ Update src/services/auth/AuthProvider.tsx to be:

import {
  User as UserInfo,
  GoogleSignin,
} from "@react-native-google-signin/google-signin"
import { useCallback, useEffect, useMemo, useState } from "react"

import { AuthContext, AuthContextProvider, useAuthContext } from "./context"

const googleOauthwebClientId = process.env.GOOGLE_OAUTH_CLIENT_ID

GoogleSignin.configure({
  scopes: ["openid", "profile", "email"],
  webClientId: googleOauthwebClientId,
})

export interface AuthProviderProps {
  children: React.ReactNode
}

const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [error, setError] = useState<Error | undefined>()
  const [isPending, setIsPending] = useState<boolean>(true)
  const [userInfo, setUserInfo] = useState<UserInfo | undefined>()

  const signIn = useCallback(async () => {
    try {
      setError(undefined)
      setIsPending(true)

      const userInfo = await GoogleSignin.signIn()

      setIsPending(false)
      setUserInfo(userInfo)

      return userInfo.user
    } catch (error) {
      console.error("Call to GoogleSignin.signIn() failed with error:", error)

      setError(error as Error)
      setIsPending(false)
      setUserInfo(undefined)

      return false
    }
  }, [])

  const signOut = useCallback(async () => {
    try {
      setError(undefined)
      setIsPending(true)

      await GoogleSignin.signOut()

      setIsPending(false)
      setUserInfo(undefined)

      return true
    } catch (error) {
      console.error("Call to GoogleSignin.signOut() failed with error:", error)

      setError(error as Error)
      setIsPending(false)

      return false
    }
  }, [])

  useEffect(() => {
    async function run() {
      try {
        setError(undefined)
        setIsPending(true)

        const userInfo = await GoogleSignin.getCurrentUser()

        setIsPending(false)
        setUserInfo(userInfo || undefined)
      } catch (error) {
        console.error(
          "Call to GoogleSignin.getCurrentUser() failed with error:",
          error,
        )

        setError(error as Error)
        setIsPending(false)
      }
    }

    run()
  }, [])

  const value = useMemo<AuthContext>(
    () => ({
      signIn,
      signOut,
      error,
      isAuthenticated: userInfo
        ? true
        : userInfo === undefined
        ? false
        : undefined,
      isPending,
      user: userInfo?.user,
      scopes: userInfo?.scopes,
      idToken: userInfo?.idToken,
    }),
    [error, isPending, signIn, signOut, userInfo],
  )

  return <AuthContextProvider value={value}>{children}</AuthContextProvider>
}

export default AuthProvider

export function useAuthentication(): Pick<
  AuthContext,
  "error" | "isPending" | "signIn" | "signOut"
> {
  const { error, isPending, signIn, signOut } = useAuthContext()

  return { error, isPending, signIn, signOut }
}

export function useAuthenticated(): boolean | undefined {
  const { isAuthenticated } = useAuthContext()

  return isAuthenticated
}

export function useUser(): UserInfo["user"] | undefined {
  const { user } = useAuthContext()

  return user
}

Next steps

Next, with some of the new tricks we’ve learned we’ll also add Offline Support so our app is still usable during network outages.