-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
:feat add passport react context and hooks #2068
base: main
Are you sure you want to change the base?
Changes from 18 commits
bc72f13
736aabf
d72ecd3
8245bf6
0b7fc6d
df56bbf
a9b60d0
a418e26
2d97c60
ea500f3
84cfbc1
6442f79
7239164
81010a6
57e3c50
de02f09
5cc9dc6
2e8cf9b
f1274ba
bcd17fb
b5f63bc
c42f2dd
49b8f9a
08634f2
b5980b6
05bf6a5
85ed2fe
55afc6b
a1542bd
aaefc91
aa47ad0
8ae1029
4bf86e1
2fc813e
7a3f0bf
b7d419b
dbac851
efd65ed
cc0450f
0525454
973c147
c779ca9
b9b94e1
36f7c96
02cbcea
4383ba8
d2e9e3e
74bb0f5
8a55532
c708c61
c984dfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
'use client'; | ||
|
||
import { PassportProvider } from '@/context/passport2'; | ||
import { ReactNode } from 'react'; | ||
|
||
export default function Layout({ | ||
children, | ||
}: Readonly<{ | ||
children: ReactNode; | ||
}>) { | ||
return ( | ||
<PassportProvider>{children}</PassportProvider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
'use client'; | ||
|
||
import { passport } from '@imtbl/sdk'; | ||
|
||
export default function Page() { | ||
const { | ||
login, loginWithEthersjs, loginWithoutWallet, logout, | ||
} = passport.usePassport(); | ||
const { idToken } = passport.useIdToken(); | ||
const { accessToken } = passport.useAccessToken(); | ||
const { linkedAddresses } = passport.useLinkedAddresses(); | ||
const { userInfo } = passport.useUserInfo(); | ||
|
||
return ( | ||
<div className="flex flex-col items-center justify-center min-h-screen p-8"> | ||
<h1 className="text-3xl font-bold mb-8">Passport Identity Examples</h1> | ||
<div className="grid grid-cols-1 gap-4 text-center"> | ||
<div> | ||
<p className="mb-2"> | ||
<b>Access Token:</b> | ||
{' '} | ||
{accessToken} | ||
</p> | ||
<p className="mb-2"> | ||
<b>ID Token:</b> | ||
{' '} | ||
{idToken} | ||
</p> | ||
<p className="mb-2"> | ||
<b>Linked Addresses:</b> | ||
{' '} | ||
{linkedAddresses} | ||
</p> | ||
<p className="mb-2"> | ||
<b>User Info:</b> | ||
{' '} | ||
{JSON.stringify(userInfo, null, 2)} | ||
</p> | ||
</div> | ||
<button | ||
className="bg-black text-white py-2 px-4 rounded hover:bg-gray-800" | ||
onClick={login} | ||
type="button" | ||
> | ||
Login | ||
</button> | ||
<button | ||
className="bg-black text-white py-2 px-4 rounded hover:bg-gray-800" | ||
onClick={loginWithoutWallet} | ||
type="button" | ||
> | ||
Login Without Wallet | ||
</button> | ||
<button | ||
className="bg-black text-white py-2 px-4 rounded hover:bg-gray-800" | ||
onClick={loginWithEthersjs} | ||
type="button" | ||
> | ||
Login With EtherJS | ||
</button> | ||
<button | ||
className="bg-black text-white py-2 px-4 rounded hover:bg-gray-800" | ||
onClick={logout} | ||
type="button" | ||
> | ||
Logout | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { config, passport } from '@imtbl/sdk'; | ||
import { ReactNode } from 'react'; | ||
|
||
export function PassportProvider({ children }: { children: ReactNode }) { | ||
return ( | ||
<passport.PassportProvider | ||
config={{ | ||
baseConfig: { | ||
environment: config.Environment.SANDBOX, // or config.Environment.SANDBOX | ||
publishableKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY || '<YOUR_PUBLISHABLE_KEY>', // replace with your publishable API key from Hub | ||
}, | ||
clientId: process.env.NEXT_PUBLIC_CLIENT_ID || '<YOUR_CLIENT_ID>', // replace with your client ID from Hub | ||
redirectUri: 'http://localhost:3000/redirect', // replace with one of your redirect URIs from Hub | ||
logoutRedirectUri: 'http://localhost:3000/logout', // replace with one of your logout URIs from Hub | ||
audience: 'platform_api', | ||
scope: 'openid offline_access email transact', | ||
}} | ||
> | ||
{children} | ||
</passport.PassportProvider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,4 +106,4 @@ | |
"tests/**" | ||
] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,242 @@ | ||||
/* eslint-disable @typescript-eslint/naming-convention */ | ||||
import React, { | ||||
createContext, useContext, useEffect, useMemo, | ||||
useState, | ||||
} from 'react'; | ||||
import { Web3Provider } from '@ethersproject/providers'; | ||||
import { Passport } from '../Passport'; | ||||
import { PassportModuleConfiguration, UserProfile } from '../types'; | ||||
|
||||
type PassportContextType = { | ||||
passportInstance: Passport; | ||||
isLoggedIn: boolean; | ||||
login: () => Promise<string[]>; | ||||
logout: () => Promise<void>; | ||||
loginWithoutWallet: () => Promise<UserProfile | null>; | ||||
loginWithEthersjs: () => Promise<{ accounts: string[], provider: Web3Provider | null }>; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would provider be null here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll move provider onto the context but it's null when |
||||
isLoading: boolean; | ||||
error: Error | null; | ||||
accounts: string[]; | ||||
}; | ||||
|
||||
const PassportContext = createContext<PassportContextType>({ | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, its less finicky if you do
Works just as well as far as I know. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll do this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to consider the loginCallback too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah. I can add that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||
passportInstance: {} as Passport, | ||||
isLoggedIn: false, | ||||
login: () => { throw new Error('login must be used within a PassportProvider'); }, | ||||
logout: () => { throw new Error('logout must be used within a PassportProvider'); }, | ||||
loginWithoutWallet: () => { throw new Error('loginWithoutWallet must be used within a PassportProvider'); }, | ||||
loginWithEthersjs: () => { throw new Error('loginWithEthersjs must be used within a PassportProvider'); }, | ||||
isLoading: false, | ||||
error: null, | ||||
accounts: [], | ||||
}); | ||||
|
||||
type PassportProviderProps = { | ||||
config: PassportModuleConfiguration; | ||||
children: React.ReactNode; | ||||
}; | ||||
|
||||
export function PassportProvider({ children, config }: PassportProviderProps) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend calling this something like ReactProvider to avoid confusion with the provider on line 17 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Considering the usage side, maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normally you're going to be doing
so the double "passport" in
would be unnecessary IMO There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be useful if we could pass in the passport instance, rather than the config. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reason: when using checkout, you want to be able to pass the passport instance to checkout. Obviously you can still get it by calling |
||||
const [isLoggedIn, setLoggedIn] = useState(false); | ||||
const [isLoading, setIsLoading] = useState(false); | ||||
const [accounts, setAccounts] = useState<string[]>([]); | ||||
const [error, setError] = useState<Error | null>(null); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm no... I don't think you want to say this is Error or null when you're putting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. casted to Error in catch statements |
||||
const passportInstance = new Passport(config); | ||||
const v = useMemo(() => ({ | ||||
passportInstance, | ||||
isLoggedIn, | ||||
isLoading, | ||||
accounts, | ||||
error, | ||||
login: async () => { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why couple the login method to EVM? Any reason why we can't follow the existing interface? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going with our documentation to separate
I assume we want user to use the first one? what are you suggesting? name this function as connectEVM and the other one as login? |
||||
try { | ||||
setError(null); | ||||
setIsLoading(true); | ||||
const provider = passportInstance.connectEvm(); | ||||
const acc = await provider.request({ method: 'eth_requestAccounts' }); | ||||
setLoggedIn(true); | ||||
setAccounts(acc); | ||||
return accounts; | ||||
} catch (e: any) { | ||||
setError(e); | ||||
} finally { | ||||
setIsLoading(false); | ||||
} | ||||
return []; | ||||
}, | ||||
logout: async () => { | ||||
try { | ||||
setError(null); | ||||
setIsLoading(true); | ||||
await passportInstance.logout(); | ||||
setLoggedIn(false); | ||||
setAccounts([]); | ||||
} catch (e: any) { | ||||
setError(e); | ||||
} finally { | ||||
setIsLoading(false); | ||||
} | ||||
}, | ||||
loginWithoutWallet: async () => { | ||||
try { | ||||
setError(null); | ||||
setIsLoading(true); | ||||
const profile = await passportInstance.login(); | ||||
setLoggedIn(true); | ||||
return profile; | ||||
} catch (e: any) { | ||||
setError(e); | ||||
return null; | ||||
} finally { | ||||
setIsLoading(false); | ||||
} | ||||
}, | ||||
loginWithEthersjs: async () => { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't ethers be treated as a peer dependency? Isn't it safe to assume that partners intending to use ethers will already have it installed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passport sdk package already has There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I know that library is used internally only whereas you're exposing it here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use some of the modules from Ethers internally, but we do not expose Ethers to consumers of Passport. By exposing Ethers from Passport like this, we'll need to continue to support it indefinitely, or make a breaking change if we decide to remove it. This could also open up the door for partners to request that we add methods for other wallet connection libraries (e.g. Viem), so I think it's best if we just expose the raw Passport provider and let consumers wrap it themselves. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll try remove this and see what kind of experience it is with 3rd party provider. |
||||
try { | ||||
setError(null); | ||||
setIsLoading(true); | ||||
// eslint-disable-next-line new-cap | ||||
const provider = new Web3Provider(passportInstance.connectEvm()); | ||||
const acc = await provider.send('eth_requestAccounts', []); | ||||
setLoggedIn(true); | ||||
return { accounts: acc, provider }; | ||||
} catch (e: any) { | ||||
setError(e); | ||||
return { accounts: [], provider: null }; | ||||
} finally { | ||||
setIsLoading(false); | ||||
} | ||||
}, | ||||
}), [config, isLoggedIn]); | ||||
|
||||
return ( | ||||
<PassportContext.Provider value={v}> | ||||
{children} | ||||
</PassportContext.Provider> | ||||
); | ||||
} | ||||
|
||||
export function usePassport() { | ||||
const c = useContext(PassportContext); | ||||
if (!c) { | ||||
throw new Error('usePassport must be used within a PassportProvider'); | ||||
} | ||||
return c; | ||||
} | ||||
|
||||
export function useIdToken() { | ||||
const { passportInstance, isLoading, isLoggedIn } = usePassport(); | ||||
const [idToken, setIdToken] = useState<string | null>(null); | ||||
const [loading, setLoading] = useState(false); | ||||
const [error, setError] = useState<Error | null>(null); | ||||
|
||||
const getIdToken = () => { | ||||
if (isLoggedIn) { | ||||
setLoading(true); | ||||
passportInstance | ||||
.getIdToken() | ||||
.then((t) => setIdToken(t || null)) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we'd want to handle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is from calling oidcClient on this line:
So we don't really have control over. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm but in what scenario would this happen? If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be equivalent to an internal error / bug scenario. I can set the error in this case. however, it complex things a bit more because now we need to consider if we need to clear up all the other fields. |
||||
.catch((e) => setError(e)) | ||||
.finally(() => setLoading(false)); | ||||
} else { | ||||
setIdToken(null); | ||||
} | ||||
|
||||
return () => { | ||||
setIdToken(null); | ||||
}; | ||||
}; | ||||
useEffect(getIdToken, [passportInstance, isLoggedIn]); | ||||
return { | ||||
idToken, isLoading: isLoading || loading, error, refetch: getIdToken, | ||||
}; | ||||
} | ||||
|
||||
export function useAccessToken() { | ||||
const { passportInstance, isLoading, isLoggedIn } = usePassport(); | ||||
const [accessToken, setAccessToken] = useState<string | null>(null); | ||||
const [loading, setLoading] = useState(false); | ||||
const [error, setError] = useState<Error | null>(null); | ||||
|
||||
const getAccessToken = () => { | ||||
if (isLoggedIn) { | ||||
setLoading(true); | ||||
passportInstance | ||||
.getAccessToken() | ||||
.then((t) => setAccessToken(t || null)) | ||||
.catch((e) => setError(e)) | ||||
.finally(() => setLoading(false)); | ||||
} else { | ||||
setAccessToken(null); | ||||
} | ||||
|
||||
return () => { | ||||
setAccessToken(null); | ||||
}; | ||||
}; | ||||
useEffect(getAccessToken, [passportInstance, isLoggedIn]); | ||||
return { | ||||
accessToken, isLoading: isLoading || loading, error, refetch: getAccessToken, | ||||
}; | ||||
} | ||||
|
||||
export function useLinkedAddresses() { | ||||
const { passportInstance, isLoading, isLoggedIn } = usePassport(); | ||||
const [linkedAddresses, setLinkedAddresses] = useState<string[] | null>(null); | ||||
const [loading, setLoading] = useState(false); | ||||
const [error, setError] = useState<Error | null>(null); | ||||
|
||||
const getLinkedAddresses = () => { | ||||
if (isLoggedIn) { | ||||
setLoading(true); | ||||
passportInstance | ||||
.getLinkedAddresses() | ||||
.then((t) => setLinkedAddresses(t || null)) | ||||
.catch((e) => setError(e)) | ||||
.finally(() => setLoading(false)); | ||||
} else { | ||||
setLinkedAddresses(null); | ||||
} | ||||
|
||||
return () => { | ||||
setLinkedAddresses(null); | ||||
}; | ||||
}; | ||||
useEffect(getLinkedAddresses, [passportInstance, isLoggedIn]); | ||||
return { | ||||
linkedAddresses, isLoading: isLoading || loading, error, refetch: getLinkedAddresses, | ||||
}; | ||||
} | ||||
|
||||
export function useUserInfo() { | ||||
const { passportInstance, isLoading, isLoggedIn } = usePassport(); | ||||
const [userInfo, setUserInfo] = useState<Record<string, unknown> | null>(null); | ||||
const [loading, setLoading] = useState(false); | ||||
const [error, setError] = useState<Error | null>(null); | ||||
|
||||
const getUserInfo = () => { | ||||
if (isLoggedIn) { | ||||
setLoading(true); | ||||
passportInstance | ||||
.getUserInfo() | ||||
.then((t) => setUserInfo(t || null)) | ||||
.catch((e) => setError(e)) | ||||
.finally(() => setLoading(false)); | ||||
} else { | ||||
setUserInfo(null); | ||||
} | ||||
|
||||
return () => { | ||||
setUserInfo(null); | ||||
}; | ||||
}; | ||||
useEffect(getUserInfo, [passportInstance, isLoggedIn]); | ||||
return { | ||||
userInfo, isLoading: isLoading || loading, error, refetch: getUserInfo, | ||||
}; | ||||
} | ||||
|
||||
export function useAccounts() { | ||||
const { accounts, isLoading, error } = usePassport(); | ||||
return { accounts, isLoading, error }; | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's quite a bit of functionality within this module - can we please add some tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll need to address all the feedback first.