https://github.com/complexlity/immutable-planner-app
Step-by-step guide to integrate immutable passport
https://github.com/complexlity/immutable-planner-app
immutable reactjs
Last synced: 9 days ago
JSON representation
Step-by-step guide to integrate immutable passport
- Host: GitHub
- URL: https://github.com/complexlity/immutable-planner-app
- Owner: Complexlity
- Created: 2023-10-23T23:22:50.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-11-01T06:28:39.000Z (over 2 years ago)
- Last Synced: 2025-03-21T04:44:21.879Z (about 1 year ago)
- Topics: immutable, reactjs
- Language: JavaScript
- Homepage: https://immutable-planner-app.vercel.app
- Size: 2.61 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Immutable Passport Integration
In this guide, I will cover step by step the process of adding immutable passport authentication to an applilcation and creating transactions with it.
Before proceeding, Note that this could be done in plain html/javascript as well as all javascript frameworks including [svelte](https://svelte.dev/), [react](https://react.dev/), [vue](https://vuejs.org/), etc. For this guide, we would make use of [Nextjs](https://nextjs.org).
However, all the core concepts convered here are applicable to all of them.
## Pre-requisites
The follow this guide, ensure you have the following installed
- [npm/nodejs](https://nodejs.org/en)
- A Code Editor
## Getting Started
Run the following commands on your terminal to get started
```bash
git clone https://github.com/Complexlity/immutable-planner-app-starter immutable-planner-app
cd immutable-planner-app
npm install
npm run dev
```
Open in your browser

You can also find a [Live Example](https://immutable-planner-app-starter.vercel.app/)
## Register You Application On Immutable Hub
Create a new file `.env` and copy all the contents of [.env.example](.env.example) into it.
```.env
NEXT_PUBLIC_LOGOUT_URL=
NEXT_PUBLIC_CALLBACK_URL=
NEXT_PUBLIC_CLIENT_ID=
```
We need these three values to connect
- Logout URL
- Callback URL
- Client Id
Follow the steps below to get the required values
- Go to [hub.immutable.com](https://hub.immutable.com) and create an account.
- Initialize a project on Immutable zkEvm and a default Environment on Testnet. If you're unsure how to do that, Complete this [Quest 3 Guide](https://app.stackup.dev/quest_page/quest-3---create-an-immutable-passport)
- Add A passport Client

- Fill the form provided with the following steps

1. **Application** Type: Web application (remains unchanged). This represents where the application is intented to be run
2. **Client Name**: give your application any name. This is just an identifier.
3. **Logout URLs**: This is very **IMPORTANT**. It represents the url the user is redirected to after they logout of the application (In some applications,the default landing page). E.g `https://your-site-name.com/`.
Since we would be runnig the code locally on port `3001`. Enter http://localhost:3001 into the input box
4. **Callback URLs**: Also very **IMPORTANT**. When you try to login, it opens a popup direct to this url. This is where the logging in takes place. E.g `https://your-site-name.com/login`.
Since we are runnign the code on our dev server port `3001`, Enter http://localhost:3001/login into the input box
**IMPORTANT**: When you deploy, you also have to change these URLs to point to the site address.
Click **Create** once you have filled these values.

Copy the three values and replace them in the `.env` file.
## The Bug Before The Storm
In the course of writing this guide, I ran into a bug in the sdk where it looks for the `global` and `process`. If you ever encounter errors such as `global object missing` or `process missing`, simply add the code above to be run before all others.
```javascript
if (typeof global === 'undefined') {
window.global = window;
}
if (typeof process === 'undefined') {
window.process = { env: { NODE_ENV: 'production' } };
}
```
## Initialise the Passport object
The main package that enables all the passport functions is `@imtbl/sdk`. First we have to install this package into the project.
```bash
npm install @imtbl/sdk
```
To have access to immutable authentication, you have to import functions `config` and `passport` which will be used to create a new passport instance object.
```javascript
//import the needed functions
import { config, passport } from '@imtbl/sdk';
// Initialize the passport config
const passportConfig = {
baseConfig: new config.ImmutableConfiguration({
environment: config.Environment.SANDBOX
}),
// This is the client id obtained from the immutable hub
clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
// This is the callback url obtained from the immutable hub
redirectUri: process.env.CALLBACK_URL,
// This is the logour url obtained from the immutable hub
logoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URL,
audience: 'platform_api',
scope: 'openid offline_access email transact'
};
// Create a new passport instance
const passportInstance = typeof window !== 'undefined' ? new passport.Passport(passportConfig) : undefined
```
`typeof window === undefined`. This is a very important step for bundlers and in our Nextjs use case. This is intended to be run only on the browser so the window object would be undefined on the server.
In the [src folder]('/src') of the project, create a folder `store` and create a file `passportStore.js` in the newly created folder and copy the contents below into it
store/passportStore.js
import { createContext, useContext, useState, useReducer } from 'react';
import { config, passport } from '@imtbl/sdk';
const passportConfig = {
baseConfig: new config.ImmutableConfiguration({
environment: config.Environment.SANDBOX
}),
clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
redirectUri: process.env.NEXT_PUBLIC_CALLBACK_URL,
logoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URL,
audience: 'platform_api',
scope: 'openid offline_access email transact'
};
const passportInstance = typeof window !== 'undefined' ? new passport.Passport(passportConfig) : undefined
export const MyContext = createContext();
export function MyProvider({ children }) {
const [passportState] = useState(passportInstance);
return (
{children}
);
}
export function useMyContext() {
return useContext(MyContext);
}
Also replace the file contents in [src/pages/_app.js](src/pages/_app.js) with the code below
src/pages/_app.js
import '@/styles/globals.css'
import "@/styles/App.css";
import "@/styles/styles.css"
import { MyProvider } from '@/store/passportStore'
export default function App({ Component, pageProps }) {
return(
``
``
)
}
We have created a react context store and put the passport object. This is done so the same passport object is reusable in multiple components (as we would need it). In a different framework, you could as well do something similar though the syntaxes may differ
## Log In User With Passport
After initialising the passport object, we can login a user by running the two commands below
```javascript
const providerZkevm = passportInstance.connectEvm()
const accounts = await providerZkevm.request({ method: "eth_requestAccounts" })
```
First, we create a zkEVM provider. This initializes an object that can be used to interact directly with the blockchain using the details of the passportInstance
Secondly, we call an RPC named `eth_requestAccounts`. This is what trigger's the entire login process. It returns an array containing the addresses associated with the user
*Aside*: An RPC (Remote Procedure Call) is simply a defined method provided by the library (in our case) to interact with the ethereum blockchain. In the course of this guide, we would explore some other examples of it
After calling `eth_requestAccoutns`, a popup opens the `Callback Url` (In our case, `/login`)
In this route, we would handle the logging in inside the popup and return the data to the home page
```javascript
await passportInstance.loginCallback()
```
This is the single line of code used in the `/login` route and it should be made to be called on page load
- Plain javascript
```javascript
window.addEventlistener('load',() => {
await passportInstance.loginCallback()
})
```
- React
```javascript
useEffect(() => {
(async() => {
await passportInstance.logCallback()
})()
})
```
- Svelte
```javascript
onMount(async () => {
await passportInstance.loginCallback()
});
```
These are some different ways to handle it in different frameworks. The most important thing is to do so on page load
Update [src/components/NavBar.jsx](src/components/NavBar.jsx) to add the login function
src/components/NavBar.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import Head from "next/head";
import { useState } from 'react';
export default function NavButton() {
const {passportState: passportInstance, userInfo, dispatch } = useMyContext();
const [buttonState, setButtonState] = useState('Connect Passport')
const [isLoading, setIsLoading] = useState(false)
async function login() {
if (!passportInstance) return
setButtonState("...Connecting")
setIsLoading(true)
try {
console.log("I am connecting now")
const providerZkevm = passportInstance.connectEvm()
const accounts = await providerZkevm.request({ method: "eth_requestAccounts" })
// Set the address
dispatch({
type: 'add_user_info',
key: 'address',
value: accounts[0]
})
} catch (error) {
console.log("Something went wrong")
console.log({ error })
setButtonState('Connect Passport')
throw error
} finally {
setIsLoading(false)
}
setButtonState('Connected')
return
}
async function logout() {
// Logout Function Go Here
return
}
return (
<>
``
`Immutable Planner App`
``
``
``
``
`
{
buttonState === 'Connected'
?
<>
`
`{userInfo.email ?? "Hello world"} `p>`
`
{userInfo.address ?? "Hello world" }
``Logout`
>
: ``
{buttonState}
``
}
`
>
);
}
Create a file in [src/pages/](src/pages/) and call it `login.js`. This is where we would handle the loginCallback(). Also note that this url would match the `Callback Url` we have set in [hub.immutable.com](https://hub.immutable.com) while creating the passport client
src/pages/login.js
import { useEffect } from 'react';
import { useMyContext } from '@/store/passportStore';
export default function LoginPage() {
const { passportState: passportInstance, } = useMyContext();
useEffect(() => {
async function handleLoginCallback() {
if (!passportInstance) {
return
}
try {
console.log("login callback");
await passportInstance.loginCallback();
}
catch (err) {
console.error("login callback error", err);
}
}
handleLoginCallback()
}, []);
return (
`
);
}
Update [src/store/passportStore.js](src/store/passportStore.js) to store the user details
src/store/passportStore.js
import { createContext, useContext, useState, useReducer } from 'react';
import { config, passport } from '@imtbl/sdk';
const passportConfig = {
baseConfig: new config.ImmutableConfiguration({
environment: config.Environment.SANDBOX
}),
clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
redirectUri: process.env.NEXT_PUBLIC_CALLBACK_URL,
logoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URL,
audience: 'platform_api',
scope: 'openid offline_access email transact'
};
const passportInstance = typeof window !== 'undefined' ? new passport.Passport(passportConfig) : undefined
export const MyContext = createContext();
export function MyProvider({ children }) {
const [passportState] = useState(passportInstance);
const [userInfo, dispatch] = useReducer(reducer, {address: null, email: null, nickname: null, idToken: null, accessToken: null})
function reducer(state, action) {
const key = action.key
const value = action.value
switch (action.type) {
case "add_user_info": {
return {
...state,
[key]: value
}
}
default: return state
}
}
return (
{children}
);
}
export function useMyContext() {
return useContext(MyContext);
}
We added a user object to the store. This enables us re-use and update this object in different parts of the codebase without having to recreate it.
After updating the files, test the login functionality now.
## Getting Logged In User Details
The `passportInstance` object comes with more functions to get the details of the logged in user. These only work if there's user currently signed in.
In your code, ensure to call the `eth_requestAccounts` function and be sure it doesn't error before trying to fetch the user details
1. User's Email and Nickname
```js
const userInfo = await passportInstance.getUserInfo()
```
On success, the returns an object of the shape:
```js
{
email:
sub:
nickname:
}
```
You could then de-structure the object to get the nickname and the email.
```js
const email = userInfo.email
const nickname= userInfo.nickname
```
2. User's Access Token
Access tokens are used to re-authenticate the user. This value is important so the entire login process is not triggered every time the user reloads the page.
```js
const accessToken = await passportInstance.getAccessToken()
```
3. User's Id Token
This is an identifier for immutable passport users.
```js
const idToken = await passportInstance.getIdToken()
```
Now you cold fetch and insert these values on the front end. We would show the user email and eth address on the Navbar while the other details will be shown on the Immutable Widget
Update [src/components/NavBar.jsx](src/components/NavBar.jsx) with the code below
src/components/NavBar.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import Head from "next/head";
import Script from "next/script";
import { useReducer, useState } from 'react';
export default function NavButton() {
const {passportState: passportInstance, userInfo, dispatch } = useMyContext();
const [buttonState, setButtonState] = useState('Connect Passport')
const [isLoading, setIsLoading] = useState(false)
async function login() {
if (!passportInstance) return
setButtonState("...Connecting")
setIsLoading(true)
try {
console.log("I am connecting now")
const providerZkevm = passportInstance.connectEvm()
const accounts = await providerZkevm.request({ method: "eth_requestAccounts" })
// Set the address
dispatch({
type: 'add_user_info',
key: 'address',
value: accounts[0]
})
// Fetch user details
const user = await passportInstance.getUserInfo()
// Set the email
dispatch({
type: 'add_user_info',
key: 'email',
value: user.email
})
//set the nickname
dispatch({
type: 'add_user_info',
key: 'nickname',
value: user.nickname
})
// Fetch user access token
const accessToken = await passportInstance.getAccessToken()
// set the access token
dispatch({
type: 'add_user_info',
key: 'accessToken',
value: accessToken
})
// Fetch user's id token
const idToken = await passportInstance.getIdToken()
// set the id token
dispatch({
type: 'add_user_info',
key: 'idToken',
value: idToken
})
} catch (error) {
console.log("Something went wrong")
console.log({ error })
setButtonState('Connect Passport')
throw error
} finally {
setIsLoading(false)
}
setButtonState('Connected')
return
}
async function logout() {
// Logout Function Go Here
return
}
return (
<>
``
`Immutable Planner App`
``
``
``
``
`
{
buttonState === 'Connected'
?
<>
`
`{userInfo.email ?? "Hello world"} `p>`
`
{userInfo.address ?? "Hello world" }
``Logout`
>
: ``
{buttonState}
``
}
`
>
);
}
We're showing the user name and email. Also, we now show the logout button when the user is logged in.
Next, we need to populate the immutable widget with the required data. Update [src/components/widgets/ImmutableWidget.jsx](src/components/widgets/ImmutableWidget.jsx) with the code below
src/components/widgets/ImmutableWidget.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import { useRef, useState } from 'react';
export default function ImmutableWidget() {
const { passportState: passportInstance, userInfo } = useMyContext()
return (
`
``
`User Details`
`
`Id Token{userInfo.idToken ?? ""}`
`Access Token{userInfo.accessToken ?? ""}`
`Nickname{userInfo.nickname ?? "User has no nickname"}`
`
``
`
);
}
Now on the page, you should see the use details on the immutable widget
## Log Out A User
The `passportInstance` comes with a `logout` function which when called, logs the user out and redirect the page to the `Logout URLs` we specified while creating the passport client in [hub.immutable.com](https://hub.immutable.com)
Call `passportInstance.logout` in [src/components/NavBar.jsx](src/components/NavBar.jsx)
src/components/NavBar.jsx
....Rest of the code
async function logout() {
// Logout Function Go Here
await passportInstance.logout();
setButtonState('Connect Passport')
}
...Restof the code
And that's it. We are now able to login and logout the user.
## Interacting With The Blockchain using Passport
As stated [above](#log-in-user-with-passport), we could call other RPC function and interact with the blockchain once the user is signed in. The functions are called on the `providerZkevm` object and not the `passportInstance`
Here are some of them
1. Get Immutable X Gas Price
```js
const gasPrice = await providerZkevm.request({ method: 'eth_gasPrice' });
```
2. Get The Balance In an ETH address
```js
const userBalance = await providerZkevm.request({
method: 'eth_getBalance',
params: [
userInfo.address,
'latest'
]
});
```
3. Get Latest Block Number
```js
const latestBlockNumber = await providerZkevm.request({ method: 'eth_blockNumber' });
```
4. Get Chain Id
```js
const chainId = await providerZkevm.request({ method: 'eth_chainId' });
```
5. Get Transaction By Hash
This function fetch the transaction details of any transaction on the [Immutable Testnet Explorer](https://explorer.testnet.immutable.com/txs)
```js
const transaction = await provider.request({
method: 'eth_getTransactionByHash',
params: [
]
});
```
Substitute `` with any valid transaction on the immutable testnet and it would return it's value.
In our code, this function has been made to download the file as json the the user's computer
Update [src/componets/widgets/Immutable.jsx](src/components/widgets/ImmutableWidget.jsx)
src/components/widgets/ImmutableWidget.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import { useRef, useState } from 'react';
export default function ImmutableWidget() {
const { passportState: passportInstance, userInfo } = useMyContext()
const providerZkevm = passportInstance?.connectEvm()
const [isLoading, setIsLoading] = useState(false);
const[gasPrice, setGasPrice] = useState('');
const[userBalance, setUserBalance] = useState('');
const[latestBlockNumber, setLatestBlockNumber] = useState('');
const[chainId, setChainId] = useState('');
async function getGasPrice() {
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const gasPrice = await providerZkevm.request({ method: 'eth_gasPrice' });
setGasPrice(gasPrice)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getUserBalance() {
console.log({user: userInfo.address})
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const userBalance = await providerZkevm.request({
method: 'eth_getBalance',
params: [
userInfo.address,
'latest'
]
});
setUserBalance(userBalance)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getLatestBlockNumber() {
console.log({address: userInfo.address})
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const latestBlockNumber = await providerZkevm.request({ method: 'eth_blockNumber' });
setLatestBlockNumber(latestBlockNumber)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getChainId() {
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const chainId = await providerZkevm.request({ method: 'eth_chainId' });
setChainId(chainId)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getTransactionByHash(e) {
e.preventDefault()
let hash = e.target.hash.value
// if (!passportInstance || !userInfo.address) return
setIsLoading(true)
if (!hash) {
// Default hash value if not provided
hash = "0xa0d300ac90e69f3ba6274ca1a712219951b79ba6c0117f538fe16c016a701951"
}
try {
const transaction = await providerZkevm.request({
method: 'eth_getTransactionByHash',
params: [
hash
]
});
// Download file into user's machine as trasaction.json
const blob = new Blob([JSON.stringify(transaction, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'transaction.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.log(error)
alert("Something went wrong. Please try again")
}
finally {
setIsLoading(false)
}
}
return (
`
``
`User Details`
`
`Id Token{userInfo.idToken ?? ""}`
`Access Token{userInfo.accessToken ?? ""}`
`Nickname{userInfo.nickname ?? "User has no nickname"}`
`
``
``
``
`{isLoading ?`
``
``
``
` : null}`
Rpc Methods``
`
`
`Get Imx Gas Price`
`
{gasPrice}
`
`
`
`Get User Balance`
`
{userBalance}
`
`
`
``
Get Latest Block Number
``
`
`
`
`Get Chain Id`
`
`
``
`
`
Get Transaction By Hash
`
`
``
`Send`
`
``
`Tip: You can get example hashed from Immutable Explorer`
`
``
`
)
}
Fully adding all the RPC functions to the immutable widget. We're now able to call all of them once we've logged in using immutable passport
## Conclusion
We have seen how powerful the Immutable zkEvm passport is and how easy it is to integrate into any application and interact with the blockchain.
Using the techniques provided by this guide, you could build web application ranging from simple to complex and add the passport authentication in just minutes.
## Resources
- The Demp Project Full Source Code - [Github](https://github.com/Complexlity/immutable-planner-app)
- The Demo Project Live - [Immutable Planner App](https://immutable-planner-app.vercel.app)
- The Immutable Passport [Official Documentation](https://docs.immutable.com/docs/zkEVM/products/passport)
- The Writer Of this Awesome Guide - [Complexlity](https://github.com/complexlity)