Request Additional Attributes from Affinidi Vault

Learn how to update the sample NextJS app to make use of the other attributes from the Affinidi Vault.

In this guide, we’ll explore how to use the data points returned by Affinidi Login in our NextJS App.

Since we are using Affinidi Login to enable passwordless authentication in our app, we can also use Affinidi Login to get other data points from Affinidi Vault to enable seamless customer onboarding.

The data points are returned in our app as IDtoken user claims.

Before you begin
  1. Set up Affinidi Vault account. Follow the guide below if you haven’t set it up yet.
Set up Affinidi Vault

Set up an Affinidi Vault account using the Web Vault or install the Mobile Vault (for Android).

The same setup steps for Mobile Vault.

  1. Click on Get started if you are creating a new account, or click on Restore from Backup if you have an existing backup of your Affinidi Vault. Provide the Passphrase to secure your Affinidi Vault.

You have the option to enable Biometrics to unlock your Affinidi Vault easily instead of using Passphrase.

Affinidi Vault Setup
  1. Enter your email address to register with the Affinidi Vault. An OTP will be sent to this email for verification.
Affinidi Vault Passphrase
  1. Enter the OTP sent to the email you have provided for verification to complete the setup.
Affinidi Vault Email Verification

After successfully providing the OTP, you are redirected to the Affinidi Vault dashboard.

  1. Install the Affinidi CLI. Follow the guide below if it hasn’t been installed.
Set up Affinidi CLI
  1. Download and install NodeJS on your machine if you haven’t set it up yet.
  1. Install Affinidi CLI using Node Package Manager (npm).
npm install -g @affinidi/cli
  1. Verify that the installation is successful.
affinidi --version
  1. Generate the NextJS sample app using Affinidi CLI.
affinidi generate app --framework=nextJS --library=nextauthJS --provider=affinidi --path=affinidi-sample-app

Make sure to run the command in your desired directory. The command will generate the NextJS sample app in the affinidi-sample-app directory.

Exploring the NextJS Sample App

Running the app with just the default Login Configuration and without customisation will show the front end below.

Basic NextJS App

It features the capability to log in to a web application passwordlessly. The NextJS Sample code uses NextAuth JS for authentication.

Affinidi Login Sample

In src/lib/auth, we’ll find all the application’s NextAuth JS-related logic.

The auth-provider.ts contains the definition of the Login Provider with the id affinidi, the same name we have configured in our redirect uri in the login configuration.

In the auth-options.ts, we’ll find the JWT and the sessions generated from the IDtoken received from Affinidi Login after successful user authentication.

We’ll also learn how to obtain the user’s email and country information from the Affinidi Vault. These are all returned as part of the IDtoken under the custom array.

What We’ll Be Building

Using the default NextJS sample code, we will modify it to fetch the additional attributes from the Affinidi Vault and use them in the web application.

Basic NextJS App

In this example, we will customise the app to use the email, profile picture, country, and name.

As an added customisation, the country will be displayed as a flashbar with the flag of the country name. We’ll use a free API from https://flagcdn.com/ to display the flag of the country name obtained from the Affinidi Vault.

For example, the URL https://flagcdn.com/sg.svg will return Singapore’s flag.

This means we can make it dynamic by identifying the two-letter country code based on ISO 3166. Refer to this link for the IBAN country codes.

Requesting Additional User Attributes

To request additional user attributes in the sample code, we first need to identify which data points the app will use.

The example presentation definition below will make the Email, Given Name, Last Name, Country, and Profile Picture available.

{
  "id": "combined_vp_token",
  "submission_requirements": [
    {
      "rule": "pick",
      "min": 1,
      "from": "A"
    }
  ],
  "input_descriptors": [
    {
      "id": "email_vc",
      "name": "Email VC",
      "purpose": "Check if VC data contains necessary fields",
      "group": [
        "A"
      ],
      "constraints": {
        "fields": [
          {
            "path": [
              "$.type"
            ],
            "purpose": "Check if VC type is correct",
            "filter": {
              "type": "array",
              "contains": {
                "type": "string",
                "pattern": "^Email$"
              }
            }
          },
          {
            "path": [
              "$.credentialSubject.email"
            ],
            "purpose": "Check if VC contains email field",
            "filter": {
              "type": "string"
            }
          },
          {
            "path": [
              "$.issuer"
            ],
            "purpose": "Check if VC Issuer is Trusted",
            "filter": {
              "type": "string",
              "pattern": "^did:key:zQ3shtMGCU89kb2RMknNZcYGUcHW8P6Cq3CoQyvoDs7Qqh33N"
            }
          }
        ]
      }
    },
    {
      "id": "Given Name_vc",
      "name": "Given Name VC",
      "purpose": "Check if VC data contains necessary fields",
      "group": [
        "A"
      ],
      "constraints": {
        "fields": [
          {
            "path": [
              "$.type"
            ],
            "purpose": "Check if VC type is correct",
            "filter": {
              "type": "array",
              "contains": {
                "type": "string",
                "pattern": "^HITGivenName$"
              }
            }
          },
          {
            "path": [
              "$.credentialSubject.Given Name"
            ],
            "purpose": "given Name",
            "filter": {
              "type": "string"
            }
          }
        ]
      }
    },
    {
      "id": "familyName_vc",
      "name": "familyName VC",
      "purpose": "Check if VC data contains necessary fields",
      "group": [
        "A"
      ],
      "constraints": {
        "fields": [
          {
            "path": [
              "$.type"
            ],
            "purpose": "Check if VC type is correct",
            "filter": {
              "type": "array",
              "contains": {
                "type": "string",
                "pattern": "^HITFamilyName$"
              }
            }
          },
          {
            "path": [
              "$.credentialSubject.familyName"
            ],
            "purpose": "family Name",
            "filter": {
              "type": "string"
            }
          }
        ]
      }
    },
    {
      "id": "picture_vc",
      "name": "picture VC",
      "purpose": "Check if VC data contains necessary fields",
      "group": [
        "A"
      ],
      "constraints": {
        "fields": [
          {
            "path": [
              "$.type"
            ],
            "purpose": "Check if VC type is correct",
            "filter": {
              "type": "array",
              "contains": {
                "type": "string",
                "pattern": "^HITPicture$"
              }
            }
          },
          {
            "path": [
              "$.credentialSubject.picture"
            ],
            "purpose": "picture",
            "filter": {
              "type": "string"
            }
          }
        ]
      }
    },
    {
      "id": "country_vc",
      "name": "country VC",
      "purpose": "Check if VC data contains necessary fields",
      "group": [
        "A"
      ],
      "constraints": {
        "fields": [
          {
            "path": [
              "$.type"
            ],
            "purpose": "Check if VC type is correct",
            "filter": {
              "type": "array",
              "contains": {
                "type": "string",
                "pattern": "^HITCountry$"
              }
            }
          },
          {
            "path": [
              "$.credentialSubject.country"
            ],
            "purpose": "country",
            "filter": {
              "type": "string"
            }
          }
        ]
      }
    }
  ]
}

We’ll also use the ID Token Mapping below to set each datapoint into a specific idTokenClaim field.

[
    {
      "sourceField": "$.type",
      "idTokenClaim": "$.custom[0].type",
      "inputDescriptorId": "email_vc"
    },
    {
      "sourceField": "$.credentialSubject.email",
      "idTokenClaim": "$.email",
      "inputDescriptorId": "email_vc"
    },
    {
      "sourceField": "$.credentialSubject.Given Name",
      "idTokenClaim": "$.given_name",
      "inputDescriptorId": "Given Name_vc"
    },
    {
      "sourceField": "$.credentialSubject.familyName",
      "idTokenClaim": "$.family_name",
      "inputDescriptorId": "familyName_vc"
    },
    {
      "sourceField": "$.credentialSubject.picture",
      "idTokenClaim": "$.picture",
      "inputDescriptorId": "picture_vc"
    },
    {
      "sourceField": "$.credentialSubject.country",
      "idTokenClaim": "$.address.country",
      "inputDescriptorId": "country_vc"
    }
  ]

These idTokenClaim mappings set the path of the ID Token Claims of each datapoint into the OIDC Standard Claims path instead of having it within the custom property of the ID Token.

Here’s what it will look like once we fetch the data from the Affinidi Vault using NextAuth JS.

{
  "acr": "0",
  "address": {
    "country": "Singapore"
  },
  "at_hash": "L2t8qWh20VtF3WDf4nw2DQ",
  "aud": [
    "8ac7b51b-d28c-46b7-b519-7be51d78cc0e"
  ],
  "auth_time": 1718268164,
  "custom": [
    {
      "type": [
        "VerifiableCredential",
        "Email"
      ]
    },
    {
      "did": "did:key:XOZoLlHEe3ceIBWbv92dlL52BBp42BhgbxGUYc9gH8ZVZ6b4v"
    }
  ],
  "email": "john.doe@example.com",
  "exp": 1718269066,
  "family_name": "Doe",
  "given_name": "John",
  "iat": 1718268166,
  "iss": "https://72e93195-2d73-4068-a73b-6a1c9250fe19.apse1.login.affinidi.io",
  "jti": "fd33e4a3-d1a6-43b4-b38e-0624517d15f5",
  "picture": "https://images.pexels.com/photos/3785079/pexels-photo-3785079.jpeg?auto=compress&cs=tinysrgb&w=800",
  "rat": 1718268154,
  "sid": "cd73a7bc-dba1-4ba0-a3c8-652dc545d12c",
  "sub": "did:key:XOZoLlHEe3ceIBWbv92dlL52BBp42BhgbxGUYc9gH8ZVZ6b4v"
}

Notice that the data points are now defined in each idToken Claim. Learn more about ID Tokens and Standard Claims here.

Since our Presentation Definition has been updated to include the Given Name, Last Name, Country, and Profile Picture, and we’ve also updated the IDToken Mapping, we need to ensure that we are fetching these details from the correct key name in the sample code.

Fetching the ID Token Claims

In this step, we will be updating the code related to NextAuthJS to derive the Affinidi Vault Data from the ID Token Claim.

Updating the UserInfo Object Type

First, we must ensure that the type UserInfo has all the attributes our application will need. Here’s what it’ll look like after adding the new attributes:

File: src/types/types.ts

export type UserInfo = {
  email?: string;
  country?: string;
  familyName?: string;
  givenName?: string;
  picture?: string;
}

After updating the UserInfo, we can now use the new fields as containers for the data points that we will derive from the ID Token Claims.

Adding the Keys from the ID Token

Let’s look back at the JWT callback property in auth-options.ts. Since we’ve determined that the email, country, familyName, Given Name, and picture will not be available in the custom array, we have to define where they should be available.

The code snippet below shows which specific keys we can use to get those data points.

File: src/lib/auth/auth-provider.ts

...
export const PROVIDER_ATTRIBUTES_KEY = "custom";
export const PROVIDER_ADDRESS = 'address'
export const COUNTRY = 'country'
export const PROVIDER_EMAIL = 'email'
export const PROVIDER_FAMILY_NAME = 'family_name'
export const PROVIDER_GIVEN_NAME = 'given_name'
export const PROVIDER_PICTURE = 'picture'
...
Fetching from the ID Token u sing the Keys

In the auth-options.ts, we can update it to fetch the data points from the keys we’ve defined in the previous step.

File: src/lib/auth/auth-options.ts

      ...
      let user: UserInfo = {}
	  
      // Fetch the custom data points
      user.email = (profile as any)?.[PROVIDER_EMAIL] // email key
      user.Given Name = (profile as any)?.[PROVIDER_GIVEN_NAME] // given_name key
      user.familyName = (profile as any)?.[PROVIDER_FAMILY_NAME] // family_name key
      user.picture = (profile as any)?.[PROVIDER_PICTURE] // picture key
      user.country = (profile as any)?.[PROVIDER_ADDRESS].[COUNTRY] // address key which has country attribute
      
      const profileItems = (profile as any)?.[PROVIDER_ATTRIBUTES_KEY];
      if (profile && profileItems) {
        let userDID: string;
        userDID = profileItems.find(
          (item: any) => typeof item.did === "string"
        )?.did;

        token = {
          ...token,
          user,
          ...(userDID && { userId: userDID }),
        };

      }
      ...

Notice that we no longer need to fetch the email and country within the custom array. What is left in the custom array is only the DID.

Using the Additional Attributes in the App

The user-info data containing the attributes we will fetch from the Affinidi Vault can be obtained after successful sign-in by calling the get-user-info endpoint. This OAuth 2.0 protected resource endpoint can be accessed only if a user is successfully authenticated using the Auth Provider (in our case, its Affinidi Login).

File: src/lib/hooks/use-fetch-user-info-query.ts

...
export const useFetchUserInfoQuery = () => {
  return useQuery<any, ErrorResponse, { userId: string; user?: UserInfo }>(
    ["userInfo"],
    async () => {
      const response = await fetch(`${hostUrl}/api/auth/get-user-info`, {
        method: "GET",
      });
      if (!response.ok) {
        throw new Error("Unable to get user info. Are you authenticated?");
      }
      return await response.JSON();
    },
    { retry: false }
  );
};

The get-user-info endpoint is declared in useFetchUserInfoQuery function and is made available via useAuthentication() in src/lib/hooks/use-authentication.ts.

File: src/lib/hooks/use-authentication.ts

import { useFetchUserInfoQuery } from "src/lib/hooks/use-fetch-user-info-query";

export function useAuthentication() {
  const { data, status } = useFetchUserInfoQuery();

  return {
    isLoading: status === "loading",
    isAuthenticated: status === "success",
    userId: data?.userId,
    user: data?.user,
  };
}

By calling the useAuthentication(), we can use our app’s user data fetched via Affinidi Login. Snippet below shows where the useAuthentication() is being used within the app.

NextJS Use Authentiucation
Implementing Dynamic Banner by Country

Now that we have made the added attributes available for our app, we can use them to provide customisation based on what is fetched from the Affinidi Vault.

Let’s implement the country name and display the flag. We’ll implement it in the WelcomeBanner.tsx, where the JSX components are currently configured as part of the banner.

The code snippet below shows that if the country obtained from the Affinidi Vault is germany, it will display the flag of Germany along with the Banner greeting ‘Welcome to our Germany website’ along with the flag associated with the country.

const WelcomeBanner: FC = () => {
  const [isClosed, setIsClosed] = useState(false);

  const { country } = useLocalContent();

  if (isClosed) return null;

  return (
    <S.BannerContainer>
      <S.Banner direction="row">
        <Box
          direction="row"
          justifyContent="flex-start"
          flex={2}
          alignItems="center"
        >
          {country && country.toLowerCase().includes("germany") && (
            <Image
              src={GermanFlagIcon.src}
              alt="german flag"
              width={32}
              height={32}
            />
          )}
          <S.BannerTitle>Welcome to our {country} website</S.BannerTitle>
        </Box>

        <S.CloseButton
          justifyContent="flex-end"
          onClick={() => setIsClosed(true)}
        >
          <Image src={close.src} alt="close" width={12} height={12} />
        </S.CloseButton>
      </S.Banner>
    </S.BannerContainer>
  )
}

Although the code snippet above adjusts the content, we can enhance it further by making it more dynamic. We’ll use a free API from https://flagcdn.com/ to display the flag of the country name obtained from the Affinidi Vault.

We simply need to add the 2-letter country code in the API to use it. For instance, https://flagcdn.com/sg.svg will return Singapore’s flag.

The datapoint contains the full country name; we need to create a mapping of the country name to its 2-letter country code. To do that, we can create a utility code that contains an array of country names to code mapping called countryListAllIsoData. See the sample snippet below:

File: src/components/Utils/CountryCodes.ts

export const countryListAllIsoData = [
    { code: "AF", name: "Afghanistan" },
    { code: "AL", name: "Albania" },
    { code: "DZ", name: "Algeria" },
    { code: "AS", name: "American Samoa" },
    { code: "AD", name: "Andorra" },
    { code: "AO", name: "Angola" },
    { code: "AI", name: "Anguilla" },
    { code: "AQ", name: "Antarctica" },
    { code: "AG", name: "Antigua and Barbuda" },
	...    

Back to the WelcomeBanner.tsx, we can now use the countryListAllIsoData. The code snippet below has been updated to use the flagcdn api where we have appended the two-letter country code to get the flag image.

import { countryListAllIsoData } from "../Utils/CountryCodes";
import { useLocalContent } from "src/lib/hooks/use-local-content";

const WelcomeBanner: FC = () => {
  const [isClosed, setIsClosed] = useState(false);

  const { country } = useLocalContent();
  // find the country data from the countryListAllIsoData
  const countryIsoData = countryListAllIsoData.find(c => c.name === country)
  if (isClosed) return null;

  return (
    <S.BannerContainer>
      <S.Banner direction="row">
        <Box
          direction="row"
          justifyContent="flex-start"
          flex={2}
          alignItems="center"
        >
          {countryIsoData && (
            <Image
              src={`https://flagcdn.com/${countryIsoData?.code.toLowerCase()}.svg`}
              alt={countryIsoData.name}
              width={32}
              height={32}
            />
          )}

          <S.BannerTitle>Welcome to our {country} website</S.BannerTitle>
        </Box>

        <S.CloseButton
          justifyContent="flex-end"
          onClick={() => setIsClosed(true)}
        >
          <Image src={close.src} alt="close" width={12} height={12} />
        </S.CloseButton>
      </S.Banner>
    </S.BannerContainer>
  )
}

With the code changes on WelcomeBanner.tsx, we can test out and see the welcome banner displaying the flag of the country.

Basic NextJS App
Implementing Greeting with the User’s Name

In this step, we will be using the name of the user from the Affinidi Vault to be displayed upon successful sign-in.

File: src/components/LocalLandingPage/LocalLandingPage.tsx

...
const LocalLandingPage = () => {
  const { country } = useLocalContent();

  return (
    <>
      <WelcomeBanner />

      <Box direction="row">
        <S.ContentContainer justifyContent="center">
          <S.Title>
            Personal liability insurance <span>for {country}</span>
          </S.Title>

          <S.Content>Get coverage in 2 minutes. Cancel monthly.</S.Content>

          <S.ButtonContainer direction="row">
            <S.Button variant="primary">Get Covered</S.Button>
            <S.Button variant="secondary">Learn More</S.Button>
          </S.ButtonContainer>
        </S.ContentContainer>

        <S.Logo direction="row" justifyContent="flex-end" flex={1}>
          <Image
            src={logo.src}
            alt="logo"
            width={777}
            height={487}
            style={{ objectFit: "cover" }}
          />
        </S.Logo>
      </Box>

      <TilesSection />
    </>
  );
};

export default LocalLandingPage;

In the current code, only the name of the Country gets displayed upon login. To add some personalisation, we can add a greeting by displaying the name of the user. The code logic above currently gets the country from the function useLocalContent().

The use-local-content.ts code shows that it returns 2 parameters namely country and user.

File: src/lib/hooks/use-local-content.ts

import { useAuthentication } from "src/lib/hooks/use-authentication";

export const useLocalContent = () => {
  const { isAuthenticated, user } = useAuthentication();

  return {
    country: isAuthenticated && user?.country,
    user: user,
  };
};

This means we can also use the user variable to get the details from the UserInfo.

Back to the LocalLandingPage.tsx, we can simply add the user parameter from the destructured return paramters of useLocalContent()

Here’s the updated code snippet for the LocalLandingPage.tsx where we’ve added the greeting “Hi <User’s Name>”

const LocalLandingPage = () => {
  const { user, country } = useLocalContent();

  return (
    <>
      <WelcomeBanner />
      <S.Title>Hi {user?.Given Name}</S.Title>
      <Box direction="row">
      
        <S.ContentContainer justifyContent="center">
          <S.Title>
            Personal liability insurance <span>for {country}</span>
          </S.Title>

          <S.Content>Get coverage in 2 minutes. Cancel monthly.</S.Content>

          <S.ButtonContainer direction="row">
            <S.Button variant="primary">Get Covered</S.Button>
            <S.Button variant="secondary">Learn More</S.Button>
          </S.ButtonContainer>
        </S.ContentContainer>

        <S.Logo direction="row" justifyContent="flex-end" flex={1}>
          <Image
            src={logo.src}
            alt="logo"
            width={777}
            height={487}
            style={{ objectFit: "cover" }}
          />
        </S.Logo>
      </Box>

      <TilesSection />
    </>
  );
};

export default LocalLandingPage;

With the code changes, we can now see the personalised greeting with the name of the user “John”.

Basic NextJS App
Implementing Profile Image Display in NavBar

In this step, we will be implementing the display of the profile picture from the Affinidi Vault to the NavBar upon successful sign-in.

The code that handles the NavBar display is the NavBar.tsx.

File: src/components/NavBar/NavBar.tsx

...
import { useAuthentication } from "src/lib/hooks/use-authentication";
import { useLocalContent } from "src/lib/hooks/use-local-content";

import * as S from "./NavBar.styled";

const NavBar: FC = () => {
  const [isSignInPage, setIsSignInPage] = useState(false);
  const [confirmLogOut, setConfirmLogOut] = useState(false);
  const { user, isAuthenticated, isLoading } = useAuthentication();

  const { country } = useLocalContent();

  useEffect(() => {
    if (window.location.href.includes("/sign-in")) {
      setIsSignInPage(true);
    } else {
      setIsSignInPage(false);
    }
  }, []);

  useEffect(() => {
    if (confirmLogOut) {
      const timeoutId = setTimeout(() => {
        setConfirmLogOut(false);
      }, 5000);

      return () => clearTimeout(timeoutId);
    }
  }, [confirmLogOut]);

  async function handleLogOut() {
    if (!confirmLogOut) {
      setConfirmLogOut(true);
      return;
    }

    await signOut();
  }

  return (
    <S.Container
      justifyContent="space-between"
      alignItems="center"
      direction="row"
    >
      <S.Title $isLocal={country} onClick={() => (window.location.href = "/")}>
        INSURANCE
      </S.Title>

      {!isSignInPage && (
        <>
          <S.NavigationContainer
            justifyContent="space-between"
            alignItems="flex-end"
            direction="row"
            $isLocal={country}
          >
            <S.NavTabs>Home</S.NavTabs>
            <S.NavTabs>Products</S.NavTabs>
            <S.NavTabs>Service</S.NavTabs>
            <S.NavTabs>Pricing</S.NavTabs>
            <S.NavTabs>Contact Us</S.NavTabs>
          </S.NavigationContainer>

          <Box style={{ minWidth: 200 }} alignItems="end">
            {isLoading && <S.Loading $isLocal={country}>Loading...</S.Loading>}

            {!isLoading && !isAuthenticated && (
              <Box justifyContent="end" alignItems="center" direction="row">
                <S.Button
                  variant="primary"
                  onClick={() => (window.location.href = "/sign-in")}
                >
                  Log In
                </S.Button>
                <S.Button
                  variant="secondary"
                  onClick={() => (window.location.href = "/sign-in")}
                >
                  Sign Up
                </S.Button>
              </Box>
            )}

            {!isLoading && isAuthenticated && (
              <S.Account
                onClick={handleLogOut}
                direction="row"
                alignItems="center"
                justifyContent="end"
                gap={16}
              >
                {!confirmLogOut && (
                  <S.Avatar $isLocal={country}>
                    <Image src={IconPersonFilled} alt="avatar" />
                  </S.Avatar>
                )}
                <S.Email $isLocal={country}>
                  {confirmLogOut ? "Log out" : user?.email || "My Account"}
                </S.Email>
              </S.Account>
            )}
          </Box>
        </>
      )}
    </S.Container>
  );
};

export default NavBar;

The code above handles the display of the Navigation Bar, which contains three components found at the top of the web page. Along those components that are displayed in the app is the user’s email, which indicates that the user is logged in. On its left side, it shows a stock image of the profile picture, which is rendered with a default image.

Basic NextJS App

To display the profile picture from the Affinidi Vault, we need to identify where we can get the UserInfo data from the code. We can use the destructured returned parameters of useAuthentication(), including the user parameters. This means we can obtain the profile picture by calling user.picture.

The snippet below shows where the profile picture icon image gets configured in the NavBar via IconPersonFilled in the Image tag.

...
{!isLoading && isAuthenticated && (
  <S.Account
    onClick={handleLogOut}
    direction="row"
    alignItems="center"
    justifyContent="end"
    gap={16}
  >
    {!confirmLogOut && (
      <S.Avatar $isLocal={country}>
        <Image src={IconPersonFilled} alt="avatar" />
      </S.Avatar>
    )}
    
    <S.Email $isLocal={country}>
      {confirmLogOut ? "Log out" : user?.email || "My Account"}
    </S.Email>
  </S.Account>
)}
...            

To simplify it and to handle scenarios when there is no profile image from the Affinidi Vault available, we can modify the Image tage with this:

<Image src={user?.picture || IconPersonFilled} alt="avatar" width={'60'} height={'60'} style={{borderRadius: '50%'}}/>

The updated code of NavBar.tsx should like like this:

import { FC, useEffect, useState } from "react";
import { signOut } from "next-auth/react";
import Image from "next/image";

import Box from "src/components/Box/Box";
import IconPersonFilled from "public/images/icon-person-filled.svg";

import { useAuthentication } from "src/lib/hooks/use-authentication";
import { useLocalContent } from "src/lib/hooks/use-local-content";

import * as S from "./NavBar.styled";
import Link from "next/link";

const NavBar: FC = () => {
  const [isSignInPage, setIsSignInPage] = useState(false);
  const [confirmLogOut, setConfirmLogOut] = useState(false);
  const { user, isAuthenticated, isLoading } = useAuthentication();

  const { country } = useLocalContent();

  useEffect(() => {
    if (window.location.href.includes("/sign-in")) {
      setIsSignInPage(true);
    } else {
      setIsSignInPage(false);
    }
  }, []);

  useEffect(() => {
    if (confirmLogOut) {
      const timeoutId = setTimeout(() => {
        setConfirmLogOut(false);
      }, 5000);

      return () => clearTimeout(timeoutId);
    }
  }, [confirmLogOut]);

  async function handleLogOut() {
    if (!confirmLogOut) {
      setConfirmLogOut(true);
      return;
    }

    await signOut();
  }

  return (
    <S.Container
      justifyContent="space-between"
      alignItems="center"
      direction="row"
    >
      <S.Title $isLocal={country} onClick={() => (window.location.href = "/")}>
        INSURANCE
      </S.Title>

      {!isSignInPage && (
        <>
          <S.NavigationContainer
            justifyContent="space-between"
            alignItems="flex-end"
            direction="row"
            $isLocal={country}
          >
            <S.NavTabs>Home</S.NavTabs>
            <S.NavTabs>Products</S.NavTabs>
            <S.NavTabs>Service</S.NavTabs>
            <S.NavTabs>Pricing</S.NavTabs>
            <S.NavTabs>Contact Us</S.NavTabs>
          </S.NavigationContainer>

          <Box style={{ minWidth: 200 }} alignItems="end">
            {isLoading && <S.Loading $isLocal={country}>Loading...</S.Loading>}

            {!isLoading && !isAuthenticated && (
              <Box justifyContent="end" alignItems="center" direction="row">
                <S.Button
                  variant="primary"
                >
                  <Link href='/sign-in'>Log In</Link>
                </S.Button>
                <S.Button
                  variant="secondary"
                >
                  <Link href='/sign-in'>Sign Up</Link>
                </S.Button>
              </Box>
            )}

            {!isLoading && isAuthenticated && (
              <S.Account
                onClick={handleLogOut}
                direction="row"
                alignItems="center"
                justifyContent="end"
                gap={16}
              >
                {!confirmLogOut && (
                  <S.Avatar $isLocal={country}>
                    <Image src={user?.picture || IconPersonFilled} alt="avatar" width={'60'} height={'60'} style={{borderRadius: '50%'}}/>
                  </S.Avatar>
                )}
                <S.Email $isLocal={country}>
                  {confirmLogOut ? "Log out" : user?.email || "My Account"}
                </S.Email>
              </S.Account>
            )}
          </Box>
        </>
      )}
    </S.Container>
  );
};

export default NavBar;

With this customisation, the profile picture should now be displayable upon successful login to the web app.

Basic NextJS App

Download the Customised NextJS App

You can get the full source code of all the customisation mentioned in this guide here.