Request Additional Attributes from 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
- Set up Affinidi Vault account. Follow the guide below if you haven’t set it up yet.
- Install the Affinidi CLI. Follow the guide below if it hasn’t been installed.
- 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.
Important Note
The downloadable sample application is provided only as a guide to quickly explore and learn how to integrate the components of Affinidi Trust Network into your application. This is NOT a Production-ready implementation. Do not deploy this to a production environment.Exploring the NextJS Sample App
Running the app with just the default Login Configuration and without customisation will show the front end below.
It features the capability to log in to a web application passwordlessly. The NextJS Sample code uses NextAuth JS for authentication.
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.
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.
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.
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”.
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.
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.
Download the Customised NextJS App
You can get the full source code of all the customisation mentioned in this guide here.
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.