npm package: @lumiapassport/ui-kit
Description: React UI components and hooks for Mbark Wallet authentication and Account Abstraction
Latest version: 1.17.18

# @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

```bash
npm install @lumiapassport/ui-kit
# or
pnpm add @lumiapassport/ui-kit
# or
yarn add @lumiapassport/ui-kit
```

## Quick Start

The UI Kit requires `@tanstack/react-query` for query management & `i18next` + `react-i18next` for multilanguages support.

### 1. QueryClient Setup (Required)

First, create a query client:

```tsx
// queryClient.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {}
})
```

### 2. I18n config (Required) & typing (Optional)

i18next lib must be initiated for both passport & your app (namespace concept used to manage translations), so your app's language can be switched by Passport's lang selector.

There is no need to provide any translations for Passport ( has built-in default lang-set ), unless it's not required to expand supported languages by custom langs, but it is required to init i18next inside DApp.

First, create the following folder structure for your dapp's translations (or update existant, if needed):

example: common.json

```json
{
  "appname": "My App",
  "signin": "Sign IN"
}
```

```
src/
└── i18n/
    ├── locales/
    │   ├── en/
    │   ├──  ├── common.json
    │   ├──  ├── header.json
    │   ├──  ├── page1.json
    │   ├──  ├── page2.json
    │   ├──  └── ...restTranslationsJsons
    │   ├── /...restLocales/...
    └── index.ts
```

Where:

- `locales/**/*.json` files contain i18next translation maps. OPTIONAL Use dedicated files for easier translations maintainance.
- `index.ts` exports translation resources

> **IMPORTANT:** YOUR_APP_TRANSLATION_RESOURSES must be structured with namespaces (as shown), where "app" is example namespace

```tsx
import commonEn from './i18n/locales/en/common.json'
import headerEn from './i18n/locales/en/header.json'
import page1en from './locales/en/page1.json'
import page2en from './locales/en/page2.json'

export const YOUR_APP_TRANSLATION_RESOURSES = {
  // language: { namespace: jsonLocale }
  en: {
    app: {
      common: commonEn,
      header: headerEn,
      page1: page1en,
      page2: page2en
    }
  }
  // ...
} as const
```

> **Note:** If you don't need to translate your app then leave YOUR_APP_TRANSLATION_RESOURSES empty with no locales folder.

Decalre types (usualy at `src/i18next.d.ts`) so t-method provides intellisence typings for your translations. Default locale is recomended to be used for typing as shown

```tsx
import { YOUR_APP_TRANSLATION_RESOURSES } from './i18n'

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'app' //
    resources: {
      app: typeof YOUR_APP_TRANSLATION_RESOURSES.en.app
    }
  }
}
```

### 3. Wrap your app with providers & init i18n

> **Note:** Declare your specific dapp's high order useTranslation hook re-expoting namespaced react-i18next useTranslation, since now t-method is typed by your default locale.

```tsx
import { useTranslation } from 'react-i18next'

export function useT() {
  return useTranslation('app') // this will make t-method typed accordingly
}
```

```tsx
import {
  combineI18NResources,
  LOCAL_STORAGE_I18N_KEY,
  //
  LumiaPassportProvider,
  LumiaPassportSessionProvider,
  LumiaRainbowKitProvider
} from '@lumiapassport/ui-kit'
//
import i18n from 'i18next'
import { initReactI18next, useTranslation } from 'react-i18next'

import { YOUR_APP_TRANSLATION_RESOURSES } from './i18n'
import { queryClient } from './queryClient'

i18n.use(initReactI18next).init({
  resources: combineI18NResources(YOUR_APP_TRANSLATION_RESOURSES),
  lng: localStorage.getItem(LOCAL_STORAGE_I18N_KEY) || 'en', // passport saves language setup on change
  fallbackLng: 'en', // default
  defaultNS: 'app', // your app i18n-namespace, example: app
  namespace: 'app', // your app i18n-namespace, example: app
  debug: false
})

function Root() {
  const { t } = useT()

  return (
    <QueryClientProvider client={queryClient}>
      <LumiaPassportProvider
        projectId="your-project-id" // Get from Lumia Passport Dashboard
      >
        <LumiaRainbowKitProvider>
          <LumiaPassportSessionProvider>
            <header>
              <h1>{t('common.appname')}</h1>
            </header>

            <main>
              <span>{t('page1.title')}</span>
            </main>
          </LumiaPassportSessionProvider>
        </LumiaRainbowKitProvider>
      </LumiaPassportProvider>
    </QueryClientProvider>
  )
}
```

### 4. Add the Connect Button

```tsx
import { ConnectWalletButton } from '@lumiapassport/ui-kit'

function YourApp() {
  const { t } = useT()

  return (
    <div>
      <h1>{t('common.appname')}</h1>
      <ConnectWalletButton label={t('common.signin')} />
    </div>
  )
}
```

### 5. (Optional)

Custom unconnected button can be provided via ConnectButton prop.
Prop consumes standart HTMLButton component and will provide required onClick to it

```tsx
import { ConnectWalletButton } from '@lumiapassport/ui-kit'

function CustomButtonComponent(props: HTMLAttributes<HTMLButtonElement>) => {
  return (<button {...props}/>)
}

function YourApp() {
  const { t } = useT()

  return (
    <div>
      <h1>My App</h1>
      <ConnectWalletButton label={t('common.signin')} 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 `QueryClientProvider` from `@tanstack/react-query` before using `LumiaPassportProvider`, otherwise you'll get an error: "No QueryClient set, use QueryClientProvider to set one"

## Configuration Options

### Basic Configuration

```tsx
<LumiaPassportProvider
  projectId="your-project-id" // Required
  initialConfig={{
    network: {
      name: 'Lumia Beam',
      symbol: 'LUMIA',
      chainId: 2030232745, // Default chain for your dApp
      rpcUrl: 'https://beam-rpc.lumia.org',
      explorerUrl: 'https://beam-explorer.lumia.org',
      testnet: true,
    },
  }}
>
```

**Network Chain Priority:**

The SDK uses the following priority for determining the active chain:

1. **User's explicit selection** - If user manually switched chains in the UI, their choice is preserved (stored in localStorage)
2. **dApp config `network.chainId`** - Your configured default chain for first-time users
3. **SDK default** - Lumia Testnet (fallback if no config provided)

This ensures returning users keep their preferred chain while new users start on your configured network.

### Advanced Configuration

```tsx
<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,
      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,
      showActiveBalanceAsFiat: false,
    },

    // KYC configuration (if needed)
    kyc: {
      provider: 'sumsub',
      options: { levelName: 'basic-kyc', flowName: 'default' }
    },

    // Warnings
    warnings: {
      backupWarning: true,
      emailNotConnectedWarning: true,
    },

    // Network configuration
    // chainId sets default chain for new users (see "Network Chain Priority" above)
    network: {
      name: 'Lumia Beam',
      symbol: 'LUMIA',
      chainId: 2030232745, // Your dApp's default chain
      rpcUrl: 'https\:\/\/beam-rpc.lumia.org',
      explorerUrl: 'https\:\/\/beam-explorer.lumia.org',
      testnet: true,
      forceChain: false, // if true, passport is immediatly forced to switch itto provided chainId, chain selector via SeetingsMenu becomes anavailable
    },
  }}
  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);
    },
    onLumiaPassportChainChange: ({ chainId, previousChainId }) => {
      console.log('Chain Changed:', { previousChainId, chainId })
    },
  }}
>
```

## Using Hooks

> **Note:** The `useLumiaPassportSession` hook 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`, `useLumiaPassportAddress` etc. 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.

```tsx
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

- **useLumiaPassportActiveChainId** - Returns current ChainID
- **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

```tsx
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:

```tsx
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.

```tsx
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:**

```tsx
// 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:**

1. Checks if smart account contract exists on-chain
2. If exists: returns `null` immediately (no gas cost)
3. If not exists: sends minimal UserOperation (`to=0x0, value=0, data=0x`)
4. Factory deploys contract without user confirmation

### signTypedData - Sign EIP712 Structured Messages

Sign structured data according to [EIP-712](https://eips.ethereum.org/EIPS/eip-712) standard. This is commonly used for off-chain signatures in dApps (e.g., NFT marketplace orders, gasless transactions, permit signatures).

```tsx
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**:

1. **Owner Address (EOA)** - The address that signs messages/transactions
2. **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.sender` or 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 `verifyingContract` address (not zero address!)
- The `domain` parameters must match exactly between frontend and smart contract
- The `chainId` should 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

```tsx
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):**

```typescript
import { getUserOperationReceipt, sendUserOperationRaw } from '@lumiapassport/core'
import { getChainConfig } from '@lumiapassport/core/read'
import { recoverAddress } from 'viem'

const CHAIN_ID = 2030232745 // Lumia Testnet

// Get chain config for bundler URL
const chainConfig = getChainConfig(CHAIN_ID)
if (!chainConfig) throw new Error('Unsupported chain')

// 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 - REQUIRES chainId parameter
const submittedUserOpHash = await sendUserOperationRaw(userOp, { chainId: CHAIN_ID })

// Poll for receipt to get transaction hash and status
const waitForReceipt = async (hash: string, maxAttempts = 60, delayMs = 1000) => {
  for (let i = 0; i < maxAttempts; i++) {
    // REQUIRES bundlerUrl parameter
    const receipt = await getUserOperationReceipt(
      hash as `0x${string}`,
      chainConfig.bundlerUrl
    )
    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
}
```

**Network Configuration:**

| Network | Chain ID | Bundler URL |
|---------|----------|-------------|
| Lumia Testnet (Beam) | 2030232745 | https://api.lumiapassport.com/rundler |
| Lumia Mainnet (Prism) | 994873017 | https://api.lumiapassport.com/rundler-prism |

### useLumiaPassportOpen - Programmatic LumiaPassport Dialog Control

Control the Lumia Passport dialog programmatically.

```tsx
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>
  )
}
```

### ThemeToggle - Quick Theme Switcher

Pre-built theme toggle button component to use in combo with useLumiaPassportColorMode.

```tsx
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.

```tsx
// Configured by default, no additional setup needed
```

### Passkey (WebAuthn)

Secure biometric authentication with device passkeys.

```tsx
// Configured by default
// Users can register passkey after initial login
```

### Telegram Mini App

Authentication via Telegram for mini apps.

```tsx
// Configure via social providers:
initialConfig={{
  social: {
    enabled: true,
    providers: [
      { id: 'telegram', name: 'Telegram', enabled: true },
    ],
  },
}}
```

### External Wallet

Connect existing wallets (MetaMask, WalletConnect, etc.).

```tsx
// Configured by default
// Uses RainbowKit for wallet connections
```

## Styling

### Passport Dialog Window

Global Passport window view can be redefined via config prop by providing specific dialogClassName.

> **Note** Providing "dialogClassName" will disable default dialog window styles

```css
.passport-dialog-classname {
  background-color: rgba(14, 14, 14, 0.7);
  backdrop-filter: blur(10px);

  box-shadow:
    0 0 8px rgba(14, 14, 14, 0.1),
    inset 0 0 0 1px var(--lumia-passport-bd);
}
```

```tsx
import 'index.css'

<LumiaPassportProvider
  ...
  initialConfig={{
    ...
    ui: {
      ...
      dialogClassName: 'passport-dialog-classname',
      ...
    },
    ...
  }}/>
```

### Passport styles

Passport provides global hook for Light/Dark modes - useLumiaPassportColorMode - use it within yor application instead of any local mode declaration.

```tsx
import { useLumiaPassportColorMode } from '@lumiapassport/ui-kit'

function YourAnyComponent() {
  const { colorMode, setColorMode } = useLumiaPassportColorMode()

  return (
    <div>
      <p>Current theme: {colorMode}</p>
      <button onClick={() => setColorMode('light')}>Light</button>
      <button onClick={() => setColorMode('dark')}>Dark</button>
    </div>
  )
}
```

Passport styles are possible to match with your project via css-variables set (use separate for dark & light).
Values will be automatically applied according to selected colorMode.
The simpliest way to customize cssv is via app index.css file:

```css
.lumia-scope[data-lumia-passport-mode='light'],
.lumia-scope[data-lumia-passport-mode='dark'] {
  /* fixed cssv */
  --lumia-passport-maw: 384px;
  --lumia-passport-pd: 12px;
  --lumia-passport-gap: 10px;
  --lumia-passport-bdrs: 20px;
  --lumia-passport-element-bdrs: 10px;

  /** overlay */
  --lumia-passport-overlay: rgba(255, 255, 255, 0.8);
  --lumia-passport-backdrop-blur: 10px;

  /** surface backgrounds */
  --lumia-passport-bg: #ffffff;

  /** text */
  --lumia-passport-fg: #000000;
  --lumia-passport-fg-h: rgba(0, 0, 0, 0.6);
  --lumia-passport-fg-a: rgba(0, 0, 0, 0.4);
  --lumia-passport-fg-inverted: #ffffff;
  --lumia-passport-fg-muted: rgba(0, 0, 0, 0.6);

  /** backgrounds i.e. buttons bg etc */
  --lumia-passport-primary: #000000;
  --lumia-passport-primary-h: rgba(0, 0, 0, 0.8);
  --lumia-passport-primary-a: rgba(0, 0, 0, 0.6);

  --lumia-passport-secondary: #e4e4e4;
  --lumia-passport-secondary-h: rgba(228, 228, 228, 0.8);
  --lumia-passport-secondary-a: rgba(228, 228, 228, 0.6);

  /** borders */
  --lumia-passport-bd: #ebebeb;
  --lumia-passport-bd-intense: rgb(169, 169, 169);

  /** shadows */
  --lumia-passport-shadow-c: rgba(0, 0, 0, 0.1);

  /** highlight colors */
  --lumia-passport-info: #000000;
  --lumia-passport-bg-info: #e4e4e4;

  --lumia-passport-success: #000000;
  --lumia-passport-bg-success: #21ff51;

  --lumia-passport-warning: #000000;
  --lumia-passport-bg-warning: #e9fa00;

  --lumia-passport-error: #ffffff;
  --lumia-passport-bg-error: #d6204e;
}
```

## TypeScript Support

Full TypeScript support with exported types:

```tsx
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?

- 📖 [Documentation](https://docs.lumiapassport.com)
- 💬 [Discord Community](https://discord.gg/lumia)
- 🐛 [Report Issues](https://github.com/lumiachain/lumia-passport-sdk/issues)
- 📧 [Email Support](mailto:support@lumia.org)

## License

MIT License - see LICENSE file for details.

## Links

- [Website](https://lumiapassport.com)
- [GitHub](https://github.com/lumiachain/lumia-passport-sdk)
- [NPM Package](https://www.npmjs.com/package/@lumiapassport/ui-kit)