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: widget privy example #355

Merged
merged 20 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/privy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Get APP ID from https://dashboard.privy.io/
VITE_PRIVY_APP_ID='your privy app id'
VITE_PRIVY_CLIENT_ID='your privy client id'
25 changes: 25 additions & 0 deletions examples/privy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.env

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
35 changes: 35 additions & 0 deletions examples/privy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# LI.FI Widget + Privy Example
This project shows an example of how to use the LI.FI Widget with the Privy wallet.

## Requirements
1. [A Privy app ID and client ID]('https://dashboard.privy.io')

## Installation
1. Clone this repo
2. Install dependencies `pnpm install`

## Configuration
Copy and rename `.env.example` to `.env`, and update the environment variables with yours.

## Run
Start the app by running `pnpm dev`

## Using chains from LI.FI
This example fetches a list of chains from LI.FI using the `useAvailableChains` hook, and they are passed to the Privy provider.

## Privy wallet model
On sign up, first Privy automatically creates an embedded EVM wallet, which is auto available to the widget via wagmi.

Multiple external wallets can be added by using the `useConnectWallet` hook.

## Syncing connectors and adapters
### EVM
The widget uses the wagmi library to interact with wallets, Privy supports wagmi with an Adapter, and we keep both libraries in sync by calling the
`useSyncWagmiConfig` in the `WalletProvider`

In the case that there are multiple EVM wallets for a user, we can set the active wallet by using the privy wagmi hook `useSetActiveWallet`.

Since the wagmi configs are in sync, the widget would automatically be set to the new active wallet.

### Solana
We sync the solana connections by listening for connection events in the `SolanaProvider`, and emitting those events when the Privy network connected or disconnected from solana.
28 changes: 28 additions & 0 deletions examples/privy/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import globals from 'globals'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
)
13 changes: 13 additions & 0 deletions examples/privy/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Privy + LI.FI Widget example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
43 changes: 43 additions & 0 deletions examples/privy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "privy",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@lifi/wallet-management": "^3.6.2",
"@lifi/widget": "^3.17.1",
"@mui/icons-material": "6.0.2",
"@mui/material": "^6.4.5",
"@privy-io/react-auth": "^2.4.5",
"@privy-io/wagmi": "^1.0.3",
"@solana/wallet-adapter-base": "^0.9.23",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/web3.js": "^1.98.0",
"@tanstack/react-query": "^5.66.8",
"mitt": "^3.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"viem": "^2.23.3",
"wagmi": "^2.14.11"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0",
"vite-plugin-node-polyfills": "^0.23.0"
}
}
1 change: 1 addition & 0 deletions examples/privy/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions examples/privy/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ChainId, LiFiWidget } from '@lifi/widget'
import { QueryClientProvider } from '@tanstack/react-query'
import { WalletHeader } from './components/WalletHeader'
import { queryClient } from './config/queryClient'
import { WalletProvider } from './providers/SyncedWalletProvider'

function App() {
return (
<QueryClientProvider client={queryClient}>
<WalletProvider>
<WalletHeader />
<LiFiWidget
integrator="vite-example"
config={{
theme: {
container: {
border: '1px solid rgb(234, 234, 234)',
borderRadius: '16px',
},
},
sdkConfig: {
rpcUrls: {
[ChainId.SOL]: [
// Replace with your private Solana RPC
'https://chaotic-restless-putty.solana-mainnet.quiknode.pro/',
'https://dacey-pp61jd-fast-mainnet.helius-rpc.com/',
],
},
},
}}
/>
</WalletProvider>
</QueryClientProvider>
)
}

export default App
1 change: 1 addition & 0 deletions examples/privy/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions examples/privy/src/components/AccountButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Avatar, Box, Button, Tooltip, Typography } from '@mui/material'
import { usePrivy } from '@privy-io/react-auth'
import React from 'react'
import { shortenAddress } from '../utils/account'
import { AccountMenu } from './AccountMenu'

export function AccountButton() {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
const open = Boolean(anchorEl)
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const { user } = usePrivy()

if (!user?.wallet?.address) {
return null
}

return (
<Box display="flex" gap={2}>
<Tooltip title="Account settings">
<Button
onClick={handleClick}
sx={{ ml: 2, color: 'black', display: 'flex', gap: 1 }}
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<Avatar
sx={{
width: 32,
height: 32,
background: 'white',
color: 'black',
}}
/>
<Typography variant="button">{shortenAddress(user.id)}</Typography>
</Button>
</Tooltip>
<AccountMenu anchorEl={anchorEl} open={open} handleClose={handleClose} />
</Box>
)
}
155 changes: 155 additions & 0 deletions examples/privy/src/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import AddLinkIcon from '@mui/icons-material/AddLink'
import AlternateEmailIcon from '@mui/icons-material/AlternateEmail'
import FingerPrintIcon from '@mui/icons-material/FingerPrint'
import Logout from '@mui/icons-material/Logout'
import WalletIcon from '@mui/icons-material/Wallet'
import {
Divider,
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
MenuList,
} from '@mui/material'
import {
type ConnectedSolanaWallet,
type ConnectedWallet,
useConnectWallet,
usePrivy,
useSolanaWallets,
useWallets,
} from '@privy-io/react-auth'
import { useSetActiveWallet } from '@privy-io/wagmi'
import { useWallet } from '@solana/wallet-adapter-react'
import { useAccount, useDisconnect } from 'wagmi'
import { emitter } from '../providers/SolanaProvider'
import { shortenAddress } from '../utils/account'

type AccountMenuProps = {
handleClose: () => void
anchorEl: HTMLElement | null
open: boolean
}

type ConnectedWalletType = ConnectedSolanaWallet | ConnectedWallet

export function AccountMenu({ handleClose, anchorEl, open }: AccountMenuProps) {
const { user, logout, ready, linkEmail, linkPasskey } = usePrivy()

// manage user wallets
const { connectWallet } = useConnectWallet({
onSuccess: ({ wallet }) => {
if (wallet.type === 'solana') {
emitter.emit('connect', wallet.meta.name)
}
},
})
const { setActiveWallet } = useSetActiveWallet()
const { disconnect } = useDisconnect()

// get user wallets
const { wallets, ready: walletsReady } = useWallets()
const { wallets: solanaWallets } = useSolanaWallets()
const allWallets = [...wallets, ...solanaWallets]

// get active addresses
const { address: activeEthAddress } = useAccount()
const { publicKey: activeSolanaAddress } = useWallet()

const handleLogout = () => {
logout()
disconnect()
emitter.emit('disconnect')
}

const handleSetActiveWallet = async (wallet: ConnectedWalletType) => {
if (isActiveWallet(wallet)) {
return
}
if (wallet.type === 'ethereum') {
return setActiveWallet(wallet)
}
if (wallet.type === 'solana') {
return emitter.emit('connect', wallet.meta.name)
}
}

const isActiveWallet = (wallet: ConnectedWalletType) => {
if (wallet.type === 'ethereum') {
return wallet.address === activeEthAddress
}
if (wallet.type === 'solana') {
return wallet.address === activeSolanaAddress?.toBase58()
}
}

const userHasPassKey = user?.linkedAccounts?.find(
(account) => account.type === 'passkey'
)

if (!user?.id || !ready) {
return null
}
return (
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
onClick={handleClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuList>
<ListSubheader>Wallets</ListSubheader>
{walletsReady &&
allWallets.map((wallet) => {
return (
<MenuItem
key={wallet.address}
disabled={isActiveWallet(wallet)}
onClick={() => handleSetActiveWallet(wallet)}
>
<ListItemIcon sx={{ minWidth: 36 }}>
<WalletIcon />
</ListItemIcon>
<ListItemText
primary={shortenAddress(wallet.address)}
secondary={isActiveWallet(wallet) ? 'Active' : null}
/>
</MenuItem>
)
})}

<Divider />
<MenuItem onClick={connectWallet}>
<ListItemIcon>
<AddLinkIcon fontSize="small" />
</ListItemIcon>
Connect another wallet
</MenuItem>
{!user.email && (
<MenuItem onClick={linkEmail}>
<ListItemIcon>
<AlternateEmailIcon fontSize="small" />
</ListItemIcon>
Link email
</MenuItem>
)}
<MenuItem onClick={linkPasskey}>
<ListItemIcon>
<FingerPrintIcon fontSize="small" />
</ListItemIcon>
Link {userHasPassKey ? 'another' : 'a'} passkey
</MenuItem>
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
</MenuList>
</Menu>
)
}
Loading