Skip to content
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

Open
wants to merge 51 commits into
base: main
Choose a base branch
from

Conversation

shineli1984
Copy link
Collaborator

@shineli1984 shineli1984 commented Aug 5, 2024

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:

  • check SDK size
  • check login with etherjs hook interface
  • check hook return value types
  • logout should not return profile
  • code snippets for docs
  • add error in context
  • add loginCallback
  • add all zkevm methods from passport
  • add analytics
  • remove login with etherjs
  • tests
  • move to a different pacakge

Copy link

nx-cloud bot commented Aug 5, 2024

☁️ Nx Cloud Report

CI 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 targets

Sent with 💌 from NxCloud.

Copy link
Contributor

@keithbro-imx keithbro-imx left a 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);
Copy link
Contributor

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.

Copy link
Collaborator Author

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))
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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

Copy link
Collaborator Author

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 }>;
Copy link
Contributor

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?

Copy link
Collaborator Author

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>({
Copy link
Contributor

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.

Copy link
Collaborator Author

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 () => {
Copy link
Contributor

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?

Copy link
Collaborator Author

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

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 () => {
Copy link
Contributor

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?

Copy link
Collaborator Author

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?

Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Collaborator Author

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>({
Copy link
Contributor

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?

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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) {
Copy link
Contributor

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

Copy link
Collaborator Author

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?

Copy link
Contributor

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) {
Copy link
Contributor

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.

Copy link
Contributor

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

Comment on lines 58 to 77
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);
}
},
Copy link
Contributor

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

Copy link
Collaborator Author

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 };
}
Copy link
Contributor

@imx-mikhala imx-mikhala Aug 13, 2024

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

Copy link
Collaborator Author

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),
Copy link
Contributor

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:

Suggested change
linkExternalWallet: passportInstance.linkExternalWallet.bind(passportInstance),
linkExternalWallet: passportInstance.linkExternalWallet,

Copy link
Collaborator Author

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?

@shineli1984 shineli1984 marked this pull request as ready for review August 20, 2024 00:26
@shineli1984 shineli1984 requested review from a team as code owners August 20, 2024 00:26
Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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?

Comment on lines +49 to +54
const checkLogggedIn = useCallback(async () => {
const p = await passportInstance.login({
useCachedSession: true,
});
setLoggedIn(!!p);
}, [passportInstance]);
Copy link
Contributor

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.

Comment on lines +134 to +146
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 [];
},
Copy link
Contributor

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

Comment on lines +16 to +20
withWallet: boolean;
error: Error | null;
getIdToken: Passport['getIdToken'];
getAccessToken: Passport['getAccessToken'];
getAccounts: () => Promise<string[]>;
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

6 participants