Leveraging Particle Auth within RainbowKit

One of the most widely used methods of implementing Particle Auth is through RainbowKit. By leveraging RainbowKit as the underlying connection mechanism within your application, you can facilitate both Web2-based social logins with Particle Auth (through a custom wagmi connector powered by @particle-network/auth-core-modal) or Web3-native wallet connections. This unification of social logins and external wallet connections is also possible through Particle Connect, although if you’d like to use RainbowKit, including Particle Auth within the list of connection options is simple.


Implementing particleWagmiWallet

If you’re already using RainbowKit within your application, you can set up a custom wagmi connector, particleWagmiWallet alongside its respective derivatives, including particleGoogleWallet, particleTwitterWallet, and any other social login implementations that you’d like to include within your RainbowKit instance (through wallets within RainbowKit’s connectorsForWallets).

To build custom Particle Auth connectors of this nature, follow the walkthrough below.

Walkthrough

  1. First, set up the particleWagmiWallet connector. You’ll first need to create an additional file within your project to hold the custom connector, such as particleWagmiWallet.ts. This file should include the below snippet:

    particleWagmiWallet.ts
       import { type ConnectParam, type EIP1193Provider } from '@particle-network/auth-core';
       import type { EVMProvider } from '@particle-network/auth-core-modal/dist/context/evmProvider';
       import { ChainNotConfiguredError, createConnector, normalizeChainId } from '@wagmi/core';
       import { SwitchChainError, UserRejectedRequestError, getAddress, numberToHex, type ProviderRpcError } from 'viem';
    
       particleWagmiWallet.type = 'particleWallet' as const;
       export function particleWagmiWallet(param?: ConnectParam) {
           type Provider = EIP1193Provider;
           type Properties = any;
    
           return createConnector<Provider, Properties>((config) => ({
               id: 'particleWalletSDK',
               name: 'Particle Wallet',
               type: particleWagmiWallet.type,
               async connect({ chainId }: { chainId: number }) {
                   try {
                       const provider = await this.getProvider();
                       const accounts = (await (provider as EVMProvider).connect(param)).map((x) => getAddress(x));
    
                       provider.on('accountsChanged', this.onAccountsChanged);
                       provider.on('chainChanged', this.onChainChanged);
                       provider.on('disconnect', this.onDisconnect.bind(this));
    
                       // Switch to chain if provided
                       let currentChainId = await this.getChainId();
                       if (chainId && currentChainId !== chainId) {
                           const chain = await this.switchChain!({ chainId }).catch((error: any) => {
                               if (error.code === UserRejectedRequestError.code) throw error;
                               return { id: currentChainId };
                           });
                           currentChainId = chain?.id ?? currentChainId;
                       }
    
                       return { accounts, chainId: currentChainId };
                   } catch (error: any) {
                       if (error.code == 4011) throw new UserRejectedRequestError(error as Error);
                       throw error;
                   }
               },
               async disconnect() {
                   const provider = await this.getProvider();
    
                   provider.removeListener('accountsChanged', this.onAccountsChanged);
                   provider.removeListener('chainChanged', this.onChainChanged);
                   provider.removeListener('disconnect', this.onDisconnect.bind(this));
    
                   await (provider as any)?.disconnect?.();
               },
               async getAccounts() {
                   const provider = await this.getProvider();
                   return (
                       await provider.request({
                           method: 'eth_accounts',
                       })
                   ).map((x: string) => getAddress(x));
               },
               async getChainId() {
                   const provider = await this.getProvider();
                   const chainId = await provider.request({ method: 'eth_chainId' });
                   return normalizeChainId(chainId);
               },
               async getProvider() {
                   if (typeof window === 'undefined') {
                       return;
                   }
    
                   while (!(window as any).particle?.ethereum) {
                       await new Promise((resolve) => setTimeout(() => resolve(true), 100));
                   }
                   return (window as any).particle?.ethereum;
               },
               async isAuthorized() {
                   try {
                       const provider = await this.getProvider();
                       return (provider as any).isConnected();
                   } catch {
                       return false;
                   }
               },
               async switchChain({ chainId }: { chainId: number }) {
                   const chain = config.chains.find((chain) => chain.id === chainId);
                   if (!chain) throw new SwitchChainError(new ChainNotConfiguredError());
    
                   const provider = await this.getProvider();
                   const chainId_ = numberToHex(chain.id);
    
                   try {
                       await provider.request({
                           method: 'wallet_switchEthereumChain',
                           params: [{ chainId: chainId_ }],
                       });
                       return chain;
                   } catch (error) {
                       // Indicates chain is not added to provider
                       if ((error as ProviderRpcError).code === 4902) {
                           try {
                               await provider.request({
                                   method: 'wallet_addEthereumChain',
                                   params: [
                                       {
                                           chainId: chainId_,
                                           chainName: chain.name,
                                           nativeCurrency: chain.nativeCurrency,
                                           rpcUrls: [chain.rpcUrls.default?.http[0] ?? ''],
                                           blockExplorerUrls: [chain.blockExplorers?.default.url],
                                       },
                                   ],
                               });
                               return chain;
                           } catch (error) {
                               throw new UserRejectedRequestError(error as Error);
                           }
                       }
    
                       throw new SwitchChainError(error as Error);
                   }
               },
               onAccountsChanged(accounts: string[]) {
                   if (accounts.length === 0) config.emitter.emit('disconnect');
                   else
                       config.emitter.emit('change', {
                           accounts: accounts.map((x) => getAddress(x)),
                       });
               },
               onChainChanged(chain: string) {
                   const chainId = normalizeChainId(chain);
                   config.emitter.emit('change', { chainId });
               },
               async onDisconnect(_error: any) {
                   config.emitter.emit('disconnect');
    
                   const provider = await this.getProvider();
                   provider.removeListener('accountsChanged', this.onAccountsChanged);
                   provider.removeListener('chainChanged', this.onChainChanged);
                   provider.removeListener('disconnect', this.onDisconnect.bind(this));
               },
           }));
       }
    
    
  2. Initializing connector instances (such as particleGoogleWallet). With the particleWagmiWallet connector set up, you’ll need to establish a number of configured (derivative) connector instances to include specific social logins within your RainbowKit instance (these should be placed in the wallets array while configuring connectorsForWallets). An example of this is included below:

  1. Wrapping your ConnectButton instance with a social login patch. When using Particle Auth Core with RainbowKit, you’ll need to add a short snippet that fixes a few common issues that may arise when using social logins with wagmi. In our demo, we place this within the App.tsx file, which is then rendered in place of ConnectButton directly. Below is the snippet in question:

       import React, { useEffect } from 'react';
       import { useConnect as useParticleConnect } from '@particle-network/auth-core-modal';
       import { AuthCoreEvent, getLatestAuthType, isSocialAuthType, particleAuth } from '@particle-network/auth-core';
       import { ConnectButton } from '@rainbow-me/rainbowkit';
       import { useConnect, useDisconnect } from 'wagmi';
       import { particleWagmiWallet } from './particleWallet/particleWagmiWallet';
    
       function App() {
           const { connect } = useConnect();
           const { connectionStatus } = useParticleConnect();
           const { disconnect } = useDisconnect();
    
           useEffect(() => {
               if (connectionStatus === 'connected' && isSocialAuthType(getLatestAuthType())) {
                   connect({
                       connector: particleWagmiWallet({ socialType: getLatestAuthType() }),
                   });
               }
               const onDisconnect = () => disconnect();
               particleAuth.on(AuthCoreEvent.ParticleAuthDisconnect, onDisconnect);
               return () => particleAuth.off(AuthCoreEvent.ParticleAuthDisconnect, onDisconnect);
           }, [connect, connectionStatus, disconnect]);
    
           return <ConnectButton />;
       }
    
       export default App;
    

Demo repository available

To dive further into this process and view a practical example of implementation, take a look at the following demo repository:

1. Next.js example.

2. (create-react-app example coming soon)

Common issues

The following issues might be encountered in this process:

  1. Minification for Vercel deployments, generally seen as Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'sqrt').
    1. Occasionally, Vercel deployments are known to throw errors relating to the minifier you’re using, crashing the application upon startup. This is due to the usage of swcMinify within your next.config.js file. You’ll need to switch swcMinify from true to false, thus using Terser as the minifier rather than SWC.

📹 A full walkthrough video on leveraging Particle Auth within RainbowKit is available.

For an extended explanation and tutorial regarding the integration of Particle Auth within RainbowKit, we’ve created a video and GitHub template repository.