@lumiapassport/ui-kit
v1.14.1
Published
React UI components and hooks for Lumia Passport authentication and Account Abstraction
Readme
@lumiapassport/ui-kit
React UI components and hooks for Lumia Passport - a secure, user-friendly authentication and Account Abstraction wallet solution with MPC (Multi-Party Computation) key management.
Features
- 🔐 Secure Authentication - Multiple auth methods: Email, Passkey, Telegram, Wallet connect
- 🔑 MPC Key Management - Distributed key generation with iframe isolation
- 💼 Account Abstraction - ERC-4337 compliant smart contract wallets
- 🎨 Pre-built UI Components - Ready-to-use React components with customizable themes
- ⚡ Easy Integration - Just wrap your app with
LumiaPassportProvider
Installation
npm install @lumiapassport/ui-kit
# or
pnpm add @lumiapassport/ui-kit
# or
yarn add @lumiapassport/ui-kitQuick Start
1. Setup QueryClient (Required)
The UI Kit requires @tanstack/react-query for query management. First, create a query client:
// queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {}
})2. Wrap your app with providers
import {
//
LumiaPassportProvider,
LumiaPassportSessionProvider,
LumiaRainbowKitProvider
} from '@lumiapassport/ui-kit'
import { queryClient } from './queryClient'
function Root() {
return (
<QueryClientProvider client={queryClient}>
<LumiaPassportProvider
projectId="your-project-id" // Get from Lumia Passport Dashboard
>
<LumiaRainbowKitProvider>
<LumiaPassportSessionProvider>
<YourApp />
</LumiaPassportSessionProvider>
</LumiaRainbowKitProvider>
</LumiaPassportProvider>
</QueryClientProvider>
)
}3. Add the Connect Button
import { ConnectWalletButton } from '@lumiapassport/ui-kit'
function YourApp() {
return (
<div>
<h1>My App</h1>
<ConnectWalletButton label="Sign in" />
</div>
)
}3.1 (Optional)
Custom unconnected button can be provided via ConnectButton prop. Prop consumes standart HTMLButton component and will provide required onClick to it
import { ConnectWalletButton } from '@lumiapassport/ui-kit'
function CustomButtonComponent(props: HTMLAttributes<HTMLButtonElement>) => {
return (<button {...props}/>)
}
function YourApp() {
return (
<div>
<h1>My App</h1>
<ConnectWalletButton label="Sign in" ConnectButton={CustomButtonComponent} />
</div>
)
}That's it! The ConnectWalletButton provides a complete authentication UI with wallet management.
Note: Don't forget to wrap your app with
QueryClientProviderfrom@tanstack/react-querybefore usingLumiaPassportProvider, otherwise you'll get an error: "No QueryClient set, use QueryClientProvider to set one"
Configuration Options
Basic Configuration
<LumiaPassportProvider
projectId="your-project-id" // Required
initialConfig={{
network: {
name: 'Lumia Beam',
symbol: 'LUMIA',
chainId: 2030232745,
rpcUrl: 'https://beam-rpc.lumia.org',
explorerUrl: 'https://beam-explorer.lumia.org',
testnet: true,
},
}}
>Advanced Configuration
<LumiaPassportProvider
projectId="your-project-id"
initialConfig={{
// UI customization
preferedColorMode?: 'light', // 'light' | 'dark'
ui: {
title: 'Welcome to MyApp',
subtitle: 'Sign in to continue',
dialogClassName: 'string', // beta
authOrder: ['email', 'passkey', 'social'],
branding: {
tagline: 'Powered by MPC',
link: { text: 'Learn More', url: \'https\:\/\/example.com/docs\' },
},
},
// Authentication providers
email: {
enabled: true,
placeholder: 'Enter your email',
buttonText: 'Continue',
verificationTitle: 'Check your email',
},
passkey: {
enabled: true,
showCreateButton: true,
primaryButtonText: 'Sign in with Passkey',
},
social: {
enabled: true,
gridColumns: 2,
providers: [
{ id: 'Discord', name: 'Discord', enabled: true, comingSoon: false },
{ id: 'telegram', name: 'Telegram', enabled: true, comingSoon: false },
],
},
wallet: {
enabled: true,
supportedChains: [994873017, 2030232745],
requireSignature: true,
walletConnectProjectId: 'your-walletconnect-id',
},
// Features
features: {
mpcSecurity: true,
strictMode: false,
requestDeduplication: true,
kycNeeded: false,
displayNameNeeded: false,
},
// KYC configuration (if needed)
kyc: {
provider: 'sumsub',
options: { levelName: 'basic-kyc', flowName: 'default' }
},
// Warnings
warnings: {
backupWarning: true,
emailNotConnectedWarning: true,
},
// Network configuration
network: {
name: 'Lumia Beam',
symbol: 'LUMIA',
chainId: 2030232745,
rpcUrl: 'https\:\/\/beam-rpc.lumia.org',
explorerUrl: 'https\:\/\/beam-explorer.lumia.org',
testnet: true,
},
}}
callbacks={{
onLumiaPassportConnecting: ({ method, provider }) => {
console.log('Connecting with:', method, provider);
},
onLumiaPassportConnect: ({ address, session }) => {
console.log('Connected:', address);
},
onLumiaPassportAccount: ({ userId, address, session, hasKeyshare }) => {
console.log('Account ready:', userId);
},
onLumiaPassportUpdate: ({ providers }) => {
console.log('Profile updated:', providers);
},
onLumiaPassportDisconnect: ({ address, userId }) => {
console.log('Disconnected:', address);
},
onLumiaPassportError: ({ error, message }) => {
console.error('Error:', message);
},
onWalletReady: (status) => {
console.log('Wallet ready:', status.ready);
},
}}
>Using Hooks
Note: The
useLumiaPassportSessionhook is based on pure Zustand store so if you're already using useLumiaPassportSession hook please consider 2 options: 1) refactor state extarction so it uses zustand state extraction feature. 2) consider using dedicated LumiaPassport shared store values hooks:useLumiaPassportAccountSession,useLumiaPassportAddressetc. Otherwise you might experience excessive re-rendering issues as LumiaPassport shares its internal store and might update some state values which should not affect app render.
import { useLumiaPassportAccountSession, useLumiaPassportLoadingStatus } from '@lumiapassport/ui-kit'
function MyComponent() {
// const session = useLumiaPassportSession(s => s.session) - with prev hook & Zustand state extraction feature, please prefer this instead:
const session = useLumiaPassportAccountSession()
const { isSessionLoading } = useLumiaPassportLoadingStatus()
if (isSessionLoading) return <div>Loading...</div>
if (!session) {
return <div>Not authenticated. Please connect your wallet.</div>
}
return (
<div>
<p>Welcome!</p>
<p>User ID: {session.userId}</p>
<p>Wallet Address: {session.ownerAddress}</p>
<p>Smart Account: {session.accountAddress}</p>
</div>
)
}Lumia Passport shared store values hooks
- useLumiaPassportIsMobileView - Returns boolean indicating if UI is in mobile view mode
- useLumiaPassportAccountSession - Returns current user session object with userId, addresses, and auth info
- useLumiaPassportLoadingStatus - Returns
{ isSessionLoading, sessionStatus }for tracking authentication state - useLumiaPassportBalance - Returns wallet balance data:
{ walletBalance, fiatBalance, cryptoRate, fiatSymbol, cryptoSymbol } - useLumiaPassportIFrameReady - Returns boolean indicating if the MPC iframe is ready for operations
- useLumiaPassportAddress - Returns the current user's wallet address
- useLumiaPassportError - Returns any error that occurred during authentication or operations
- useLumiaPassportRecoveryUserId - Returns userId for account recovery flow
- useLumiaPassportHasServerVault - Returns boolean indicating if user has server-side keyshare backup
useSendTransaction - Send Transactions
import { useSendTransaction } from '@lumiapassport/ui-kit'
function TransactionExample() {
const { sendTransaction, isPending } = useSendTransaction()
const handleSend = async () => {
try {
const userOpHash = await sendTransaction({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: '1000000000000000000', // 1 ETH in wei
data: '0x' // Optional contract call data
})
console.log('UserOp hash:', userOpHash)
} catch (error) {
console.error('Transaction failed:', error)
}
}
return (
<div>
<button onClick={handleSend} disabled={isPending}>
{isPending ? 'Sending...' : 'Send Transaction'}
</button>
</div>
)
}sendUserOperation - Direct Transaction Submission
For direct control without using the React hook, you can use sendUserOperation function:
import { sendUserOperation, useLumiaPassportAccountSession } from '@lumiapassport/ui-kit'
function DirectTransactionExample() {
const session = useLumiaPassportAccountSession()
const handleSend = async () => {
if (!session) {
console.error('No active session')
return
}
try {
// Send transaction directly with full control
const userOpHash = await sendUserOperation(
session, // Required: session from useLumiaPassportAccountSession
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // to address
'1000000000000000000', // value in wei (1 ETH)
'0x', // data (optional contract call)
'standard', // fee type: 'economy' | 'standard' | 'fast'
'v0.7' // EntryPoint version
)
console.log('Transaction submitted:', userOpHash)
} catch (error) {
console.error('Transaction failed:', error)
}
}
return <button onClick={handleSend}>Send Transaction</button>
}When to use:
- ✅ Use
useSendTransaction()hook for React components (automatic session management) - ✅ Use
sendUserOperation()function for custom logic, utility functions, or non-React code
deployAccount - Deploy Smart Account (Optional)
Deploy the smart account contract immediately after registration. This is optional - accounts are automatically deployed on first transaction.
Smart behavior: Automatically checks if account is already deployed and skips transaction to save gas.
import { deployAccount, useLumiaPassportAccountSession } from '@lumiapassport/ui-kit'
function DeployAccountExample() {
const session = useLumiaPassportAccountSession()
const handleDeploy = async () => {
if (!session) return
try {
// Deploy account with minimal gas cost (skips if already deployed)
const userOpHash = await deployAccount(session, 'economy')
if (userOpHash) {
console.log('Account deployed:', userOpHash)
} else {
console.log('Account already deployed, skipped')
}
} catch (error) {
console.error('Deployment failed:', error)
}
}
return <button onClick={handleDeploy}>Deploy Account</button>
}Return values:
- Returns
userOpHash(string) - if deployment was needed and executed - Returns
null- if account already deployed (saves gas)
Advanced usage:
// Force deployment even if already deployed (not recommended)
const hash = await deployAccount(session, 'economy', { force: true })Why use this?
- ✅ Pre-deploy account before first real transaction
- ✅ No user consent required (safe minimal operation)
- ✅ Cleaner UX - separate deployment from first payment
- ✅ Smart gas savings - auto-skips if already deployed
- ⚠️ Optional - accounts auto-deploy on first transaction anyway
How it works:
- Checks if smart account contract exists on-chain
- If exists: returns
nullimmediately (no gas cost) - If not exists: sends minimal UserOperation (
to=0x0, value=0, data=0x) - Factory deploys contract without user confirmation
signTypedData - Sign EIP712 Structured Messages
Sign structured data according to EIP-712 standard. This is commonly used for off-chain signatures in dApps (e.g., NFT marketplace orders, gasless transactions, permit signatures).
import { signTypedData, useLumiaPassportAccountSession } from '@lumiapassport/ui-kit'
function SignatureExample() {
const session = useLumiaPassportAccountSession()
const handleSign = async () => {
if (!session) return
try {
// Define EIP712 typed data
const signature = await signTypedData(session, {
domain: {
name: 'MyDApp', // Your dApp name (must match contract)
version: '1', // Contract version
chainId: 994, // Lumia Prism Testnet
verifyingContract: '0x...' // Your contract address (REQUIRED in production!)
},
types: {
Order: [
{ name: 'tokenIds', type: 'uint256[]' },
{ name: 'price', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
},
primaryType: 'Order',
message: {
tokenIds: [1n, 2n, 3n],
price: 1000000000000000000n, // 1 token in wei
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour from now
}
})
console.log('Signature:', signature)
// Verify signature (optional)
const { recoverTypedDataAddress } = await import('viem')
const recoveredAddress = await recoverTypedDataAddress({
domain: {
/* same domain */
},
types: {
/* same types */
},
primaryType: 'Order',
message: {
/* same message */
},
signature
})
console.log('Signer:', recoveredAddress) // Should match session.ownerAddress
} catch (error) {
console.error('Signing failed:', error)
}
}
return <button onClick={handleSign}>Sign Message</button>
}Important Notes about ERC-4337 Smart Accounts:
In Account Abstraction (ERC-4337), there are two addresses:
- Owner Address (EOA) - The address that signs messages/transactions
- Smart Account Address - The contract wallet address
⚠️ Critical: The signature is created by the owner address (EOA), NOT the smart account address!
Compatibility with existing protocols:
- ✅ Works: Protocols that verify signatures off-chain (e.g., your backend verifies the owner EOA signature)
- ⚠️ May not work: Protocols designed for EOA wallets that store and verify against
msg.senderor wallet address- Example: Uniswap Permit2, some NFT marketplaces
- These protocols expect the signer address to match the wallet address
- With smart accounts: signer = owner EOA, wallet = smart account contract
- Solution: Use ERC-1271 signature validation in your smart contracts (allows contracts to validate signatures)
Domain Configuration:
- In production, use your actual
verifyingContractaddress (not zero address!) - The
domainparameters must match exactly between frontend and smart contract - The
chainIdshould match the network you're deploying to
Technical Details:
- Shows a MetaMask-like confirmation modal with structured message preview
- All BigInt values are supported in the message
- Signature can be verified using
viem.recoverTypedDataAddress()- will return owner EOA address
When to use signTypedData:
- ✅ Custom backend signature verification (you control the verification logic)
- ✅ Gasless transactions with meta-transaction relayers
- ✅ DAO voting and governance (off-chain signatures)
- ✅ Custom smart contracts with ERC-1271 support
- ⚠️ Be cautious with protocols designed exclusively for EOA wallets
prepareUserOperation - Prepare for Backend Submission
import { prepareUserOperation, useLumiaPassportAccountSession } from '@lumiapassport/ui-kit'
function BackendSubmissionExample() {
const session = useLumiaPassportAccountSession()
const handlePrepare = async () => {
if (!session) return
// Prepare and sign UserOp without sending to bundler
const { userOp, userOpHash } = await prepareUserOperation(
session,
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // to
'1000000000000000000', // 1 ETH in wei
'0x', // data
'standard', // fee type: 'economy' | 'standard' | 'fast'
'v0.7' // EntryPoint version
)
// Send to backend for validation and submission
await fetch('/api/submit-transaction', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userOp,
userOpHash,
ownerAddress: session.ownerAddress // for signature verification
})
})
}
return <button onClick={handlePrepare}>Prepare & Send to Backend</button>
}Backend example (using @lumiapassport/core):
import { getUserOperationReceipt, sendUserOperationRaw } from '@lumiapassport/core'
import { recoverAddress } from 'viem'
// Receive from frontend
const { userOp, userOpHash, ownerAddress } = await request.json()
// Verify signature
const recoveredAddress = await recoverAddress({
hash: userOpHash,
signature: userOp.signature
})
if (recoveredAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
throw new Error('Invalid signature')
}
// Submit to bundler - returns userOpHash
const submittedUserOpHash = await sendUserOperationRaw(userOp)
// Poll for receipt to get transaction hash and status
const waitForReceipt = async (userOpHash: string, maxAttempts = 60, delayMs = 1000) => {
for (let i = 0; i < maxAttempts; i++) {
const receipt = await getUserOperationReceipt(userOpHash as `0x${string}`)
if (receipt) {
return {
success: receipt.success,
transactionHash: receipt.receipt?.transactionHash,
blockNumber: receipt.receipt?.blockNumber
}
}
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
throw new Error('Transaction timeout')
}
const result = await waitForReceipt(submittedUserOpHash)
return {
success: result.success,
transactionHash: result.transactionHash,
blockNumber: result.blockNumber
}useLumiaPassportOpen - Programmatic LumiaPassport Dialog Control
Control the Lumia Passport dialog programmatically.
import { PageKey, useLumiaPassportOpen } from '@lumiapassport/ui-kit'
function CustomAuthButton() {
const { isOpen, open: openLumiaPassport, close } = useLumiaPassportOpen()
return (
<div>
<button onClick={() => openLumiaPassport(PageKey.AUTH)}>Sign In</button>
<button onClick={() => openLumiaPassport(PageKey.RECEIVE)}>Receive LUMIA</button>
<button onClick={close}>Close Dialog</button>
</div>
)
}useLumiaPassportColorMode - Theme Control
Control light/dark mode for Lumia Passport UI. Use hook to sync colorMode inside your App instead of any local colorMode states
import { useLumiaPassportColorMode } from '@lumiapassport/ui-kit'
function ThemeSelector() {
const { colorMode, setColorMode } = useLumiaPassportColorMode()
return (
<div>
<p>Current theme: {colorMode}</p>
<button onClick={() => setColorMode('light')}>Light</button>
<button onClick={() => setColorMode('dark')}>Dark</button>
</div>
)
}ThemeToggle - Quick Theme Switcher
Pre-built theme toggle button component to use in combo with useLumiaPassportColorMode.
import { ThemeToggle } from '@lumiapassport/ui-kit'
function AppHeader() {
return (
<header>
<h1>My App</h1>
<ThemeToggle />
</header>
)
}Authentication Methods
Email OTP
Users receive a one-time code via email.
// Configured by default, no additional setup neededPasskey (WebAuthn)
Secure biometric authentication with device passkeys.
// Configured by default
// Users can register passkey after initial loginTelegram Mini App
Authentication via Telegram for mini apps.
// Configure via social providers:
initialConfig={{
social: {
enabled: true,
providers: [
{ id: 'telegram', name: 'Telegram', enabled: true },
],
},
}}External Wallet
Connect existing wallets (MetaMask, WalletConnect, etc.).
// Configured by default
// Uses RainbowKit for wallet connectionsStyling
...to be updated
TypeScript Support
Full TypeScript support with exported types:
import type { AuthProvider, LumiaPassportConfig, User, WalletInfo } from '@lumiapassport/ui-kit'Examples
Check out the /examples directory for complete working examples:
- React + Vite - Modern React setup with Vite
- Next.js - Next.js App Router integration
- React + TypeScript - Full TypeScript example
Security
- 🔒 MPC Key Management - Keys are split between client and server using threshold cryptography
- 🏝️ Iframe Isolation - Sensitive operations run in isolated iframe context
- 🔐 No Private Key Exposure - Private keys never exist in complete form on client
- ✅ Non-custodial - Users maintain full control of their accounts
Need Help?
License
MIT License - see LICENSE file for details.
