-
Notifications
You must be signed in to change notification settings - Fork 24
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?
Conversation
☁️ Nx Cloud ReportCI is running/has finished running commands for commit c984dfb. As they complete they will appear below. Click to see the status, the terminal output, and the build insights. 📂 See all runs for this CI Pipeline Execution ✅ Successfully ran 5 targetsSent with 💌 from NxCloud. |
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.
Some initial comments, but i'll try this in a real app and provide feedback
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 comment
The 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 any
in to it.
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.
casted to Error in catch statements
setLoading(true); | ||
passportInstance | ||
.getIdToken() | ||
.then((t) => setIdToken(t || null)) |
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 think we'd want to handle t
being undefined here in some way. What would cause this? And what would we expect to get back from useIdToken
in this scenario?
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.
this is from calling oidcClient on this line:
const oidcUser = await this.userManager.getUser(); |
So we don't really have control over.
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.
Hmm but in what scenario would this happen? If isLoggedIn
is true and getIdToken
returns undefined, should that set an error for example? Consider the result of useIdToken - it would be { idToken: null, error: null, loading: false }
- not helpful
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.
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.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move provider onto the context but it's null when provider.send('eth_requestAccounts', [])
rejects.
accounts: string[]; | ||
}; | ||
|
||
const PassportContext = createContext<PassportContextType>({ |
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.
Alternatively, its less finicky if you do
const PassportContext = createContext<PassportContextType|null>(null);
Works just as well as far as I know.
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 do this.
isLoading, | ||
accounts, | ||
error, | ||
login: async () => { |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going with our documentation to separate
- login: https://docs.immutable.com/products/zkEVM/passport/identity/login#2-initialise-the-provider-and-login-users
- and login without wallet: https://docs.immutable.com/products/zkEVM/passport/identity/login#2-initialise-the-provider-and-login-users
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?
setIsLoading(false); | ||
} | ||
}, | ||
loginWithEthersjs: async () => { |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Passport sdk package already has @ethersproject/providers
as an dependency tho. Should I change it to peer?
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.
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 comment
The 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 comment
The 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.
accounts: string[]; | ||
}; | ||
|
||
const PassportContext = createContext<PassportContextType>({ |
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.
Do we need to consider the loginCallback too?
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.
yeah. I can add that
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.
Added
children: React.ReactNode; | ||
}; | ||
|
||
export function PassportProvider({ children, config }: PassportProviderProps) { |
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'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 comment
The reason will be displayed to describe this comment to others. Learn more.
Considering the usage side, maybe PassportReactProvider
?
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.
Normally you're going to be doing
import { passport } from '@imtbl/sdk';
so the double "passport" in
passport.PassportReactProvider
would be unnecessary IMO
children: React.ReactNode; | ||
}; | ||
|
||
export function PassportProvider({ children, config }: PassportProviderProps) { |
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.
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 comment
The 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 usePassport
but it makes it more tricky as you would want to initialise checkout and passport together
login: async (withoutWallet: boolean = false) => { | ||
try { | ||
setError(null); | ||
setIsLoading(true); | ||
if (withoutWallet) { | ||
const p = await passportInstance.login(); | ||
setProfile(p); | ||
setLoggedIn(true); | ||
} else { | ||
const provider = getPassportProvider(); | ||
const acc = await provider.request({ method: 'eth_requestAccounts' }); | ||
setLoggedIn(true); | ||
setAccounts(acc); | ||
} | ||
} catch (e) { | ||
setError(e as Error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}, |
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.
The purpose of the login
method is to authenticate the user without initialising their wallet, and login
is intended to be rollup agnostic so we shouldn't be doing anything zkEvm specific in here (calling getPassportProvider()
or provider.request()
.
We'll probably want to expose the useCachedSession
argument for the login method here as well, as consumers typically call login({ useCachedSession: true });
on page load to determine if the user has an existing session
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'm only supporting zkevm here. I'll change the component name to reflect on that.
In that case, I think it makes more sense to have a login for both auth + wallet, which is our preferred use case? Then treating auth without wallet as edge case.
export function useProfile() { | ||
const { profile, isLoading, error } = usePassport(); | ||
return { profile, 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.
These three hooks seem redundant if they can just get the values from usePassport
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.
This is mostly for convenience.
loginCallback: async () => { | ||
await passportInstance.loginCallback(); | ||
}, | ||
linkExternalWallet: passportInstance.linkExternalWallet.bind(passportInstance), |
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.
Is the bind
call here necessary? It's not clear to me why we can't just call linkExternalWallet
directly:
linkExternalWallet: passportInstance.linkExternalWallet.bind(passportInstance), | |
linkExternalWallet: passportInstance.linkExternalWallet, |
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.
linkExternalWallet
uses this
in its function body tho. Without the bind, it'll work?
This reverts commit 4383ba8.
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.
Regarding the package.json:
- Could the "d" command be updated to be a rollup config build without the NODE_ENV=production, and have the rollup.config.js to conditionally use typescript or swc when rebuilding to match the change that was merged in yesterday?
- The unplugin-swc dependency will then need to be added to this package
- The exports object needs to be set up for the conditional types for live types locally, matching the other package.jsons that were updated
- I'll drop the tsconfig change needed here; the customConditions value also needs to be added to support the live types.
You can use the passport sdk package.json/rollup.config/tsconfig.json as a point of reference
passportInstance: Passport; | ||
isLoggedIn: boolean; | ||
isLoading: boolean; | ||
withWallet: boolean; |
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.
What's the use case for withWallet
? If the consumer specifies whether they want the wallet initialised or not, then the consumer would already know if the wallet has been initialised
export function ZkEvmReactProvider({ children, config }: ZkEvmReactProviderProps) { | ||
setPassportClientId(config.clientId); | ||
track('passport', 'ZkEvmProviderInitialised'); | ||
const [isLoggedIn, setLoggedIn] = useState(false); |
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.
Can we update the setter to setIsLoggedIn
please?
const checkLogggedIn = useCallback(async () => { | ||
const p = await passportInstance.login({ | ||
useCachedSession: true, | ||
}); | ||
setLoggedIn(!!p); | ||
}, [passportInstance]); |
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.
This has the potential to cause re-rendering issues for consumers. My assumption is that we expect marketplaces to consume the provider like this:
function MyComponent() {
const { isLoading, isLoggedIn } = reactPassport.usePassport();
if (isLoading) {
return <LoadingAnimation />;
}
if (isLoggedIn) {
return <SomeComponentForAuthenticatedUsers />;
}
return <PromptUserToLogin />;
}
In the above scenario, if login
triggers a token refresh, PromptUserToLogin
would render momentarily. Once the token refresh finishes and login
returns true, SomeComponentForAuthenticatedUsers
would then render.
getAccounts: async () => { | ||
setIsLoading(true); | ||
try { | ||
const provider = passportInstance.connectEvm(); | ||
const accounts = await provider.request({ method: 'eth_requestAccounts' }); | ||
return accounts; | ||
} catch (e: any) { | ||
setError(e as Error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
return []; | ||
}, |
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.
eth_requestAccounts
initialises the wallet, so if we're going to keep the withWallet
property, then this should also call setWithWallet
withWallet: boolean; | ||
error: Error | null; | ||
getIdToken: Passport['getIdToken']; | ||
getAccessToken: Passport['getAccessToken']; | ||
getAccounts: () => Promise<string[]>; |
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.
Can I suggest that we expose the connectEvm method instead of getAccounts
like this? This interface would make it difficult to support something like app-chains in the future
Summary
Add passport react context and hooks to reduce the code user need to write in a react app.
Detail and impact of the change
Added
Passport context and hooks for react app
todo: