npm package: @lukso/up-modal
Description: Connect modal for LUKSO Universal Profiles
Author: LUKSO
Latest version: 0.19.1

<p align="center">
  <br>
  <img src="https://cdn.jsdelivr.net/npm/@lukso/up-modal/src/images/apple-touch-icon.png" alt="LUKSO" width="180" />
</p>

<h1 align="center">@lukso/up-modal</h1>

<p align="center">
  <a href="https://www.npmjs.com/package/@lukso/up-modal"><img src="https://img.shields.io/npm/v/%40lukso%2Fup-modal?style=flat-square&color=2563eb" alt="npm version" /></a>
  <a href="https://www.npmjs.com/package/@lukso/up-modal"><img src="https://img.shields.io/npm/dm/%40lukso%2Fup-modal?style=flat-square&color=2563eb" alt="npm downloads" /></a>
  <a href="./LICENSE.md"><img src="https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square" alt="license" /></a>
</p>

<p align="center">Connect modal for LUKSO Universal Profiles. Provides a responsive connection dialog with support for desktop and mobile. Modal works with apps that use [Wagmi](https://wagmi.sh), a library to interact Ethereum. Wagmi handles the underlying connection management, so you can use its APIs alongside the modal.</p>

<p align="center">
  <img src="https://cdn.jsdelivr.net/npm/@lukso/up-modal/src/images/modal-light.png" alt="Light theme" width="320" />
  &nbsp;&nbsp;
  <img src="https://cdn.jsdelivr.net/npm/@lukso/up-modal/src/images/modal-dark.png" alt="Dark theme" width="320" />
</p>

## Table of contents

- [What gets auto-detected](#what-gets-auto-detected)
- [Installation](#installation)
- [Minimal setup](#minimal-setup)
- [API reference](#api-reference)
- [Advanced](#advanced)
- [Framework integrations](#framework-integrations)
- [Customization](#customization)
- [Internationalization](#internationalization)
- [License](#license)

## What gets auto-detected

Modal automatically configures:

| App              | How                                                     |
| ---------------- | ------------------------------------------------------- |
| **UP Mobile**    | WalletConnect (deep link on mobile, QR code on desktop) |
| **UP Extension** | Browser extension, detected via EIP-6963                |
| **EOA wallets**  | Other EIP-6963 wallets (MetaMask, Coinbase, etc.)       |

## Installation

```bash
# pnpm
pnpm add @lukso/up-modal

# npm
npm install @lukso/up-modal

# yarn
yarn add @lukso/up-modal
```

Also install the required peer dependencies:

```bash
npm install @wagmi/core viem
```

## Minimal setup

### Initialize modal

```typescript
import { setupLuksoConnector } from '@lukso/up-modal'

const connector = await setupLuksoConnector({
  walletConnect: {
    projectId: 'YOUR_REOWN_PROJECT_ID', // get yours at https://cloud.reown.com
  },
})
```

### Open the Sign In modal

```typescript
connector.showSignInModal()
```

### Open the Sign Up modal

```typescript
connector.showSignUpModal()
```

<img src="https://cdn.jsdelivr.net/npm/@lukso/up-modal/src/images/sign-up.png" alt="Light theme" width="320" />

## API reference

### `setupLuksoConnector`

Returns `Promise<LuksoConnector>`. Call once at app initialization. Example with custom options:

```typescript
import { luksoTestnet } from 'viem/chains'

const connector = await setupLuksoConnector({
  theme: 'dark',
  walletConnect: {
    projectId: 'YOUR_REOWN_PROJECT_ID', // get yours at https://cloud.reown.com
  },
  chains: {
    additional: [luksoTestnet], // add LUKSO Testnet
  },
  connectors: {
    eoa: false,
  },
  onConnect: (event) => console.log('Connected:', event.detail),
  onError: (event) => console.error('Error:', event.detail),
  onClose: () => console.log('Modal closed'),
})
```

#### Config options

| Option                    | Type                          | Default       | Description                                                       |
| ------------------------- | ----------------------------- | ------------- | ----------------------------------------------------------------- |
| `theme`                   | `'light' \| 'dark' \| 'auto'` | `'light'`     | Modal theme                                                       |
| `onConnect`               | `(e: CustomEvent) => void`    | —             | Called on successful connection                                   |
| `onError`                 | `(e: CustomEvent) => void`    | —             | Called on connection error                                        |
| `onClose`                 | `() => void`                  | —             | Called when modal is closed                                       |
| `walletConnect.enabled`   | `boolean`                     | `true`        | Enable WalletConnect                                              |
| `walletConnect.projectId` | `string`                      | LUKSO default | WalletConnect project ID                                          |
| `chains.defaultChainId`   | `number`                      | `42`          | Default chain (LUKSO mainnet)                                     |
| `chains.additional`       | `Chain[]` (from `viem`)       | `[]`          | Extra chains alongside LUKSO mainnet (e.g. `[luksoTestnet]`)      |
| `chains.skipChainSwitch`  | `boolean`                     | `false`       | Skip automatic chain switch on connect                            |
| `storage.key`             | `string`                      | `'up-wagmi'`  | localStorage key prefix for wagmi state                           |
| `connectors.upMobile`     | `boolean`                     | `true`        | Show UP Mobile connector button                                   |
| `connectors.upExtension`  | `boolean`                     | `true`        | Show UP Extension connector button                                |
| `connectors.eoa`          | `boolean`                     | `true`        | Show EOA wallets divider and button                               |
| `wagmiConfig`             | `Config` (from `@wagmi/core`) | auto-created  | Pass your own wagmi config                                        |

#### `LuksoConnector` methods

| Method              | Description                                                                          |
| ------------------- | ------------------------------------------------------------------------------------ |
| `showSignInModal()` | Opens the sign-in modal (creates `<connect-modal>` in `document.body` on first call) |
| `showSignUpModal()` | Opens the sign-up modal for creating a new Universal Profile                         |
| `closeModal()`      | Closes the modal                                                                     |
| `setTheme(theme)`   | Updates the theme (`'light' \| 'dark' \| 'auto'`) — works while modal is open        |
| `destroyModal()`    | Closes the modal and removes the element from the DOM                                |
| `wagmiConfig`       | The underlying wagmi `Config` instance for advanced use                              |

## Advanced

### Using your own wagmi config

```typescript
import { setupLuksoConnector } from '@lukso/up-modal'
import { myWagmiConfig } from './wagmi'

const connector = await setupLuksoConnector({
  wagmiConfig: myWagmiConfig,
})
```

### Watching connection state

```typescript
import { watchConnection, getConnection } from '@wagmi/core'

const { wagmiConfig } = connector

// One-time read
const connection = getConnection(wagmiConfig)

// Subscribe to changes
const stopWatching = watchConnection(wagmiConfig, {
  onChange: (connection) => {
    console.log(connection.address, connection.chainId)
  },
})
```

### Multi-chain support

In the default auto-setup configuration (when you don't provide a custom `wagmiConfig`), LUKSO mainnet is always included and cannot be removed from the supported networks. Any additional chains are added alongside it via `chains.additional`.

The following example adds Ethereum mainnet so users can connect from either network:

```typescript
import { mainnet } from 'viem/chains'
import { setupLuksoConnector } from '@lukso/up-modal'

const connector = await setupLuksoConnector({
  chains: {
    additional: [mainnet],
  },
})
```

Testnets are also supported, just pass them in an array. Example of adding LUKSO Testnet and Ethereum Sepolia:

```typescript
import { setupLuksoConnector } from '@lukso/up-modal'
import { luksoTestnet, sepolia } from 'viem/chains'

const connector = await setupLuksoConnector({
  chains: {
    additional: [luksoTestnet, sepolia],
  },
})
```

> If a wallet connects on a chain that is not in this list, the modal will emit an `on-error` event and the connection will be rejected — since wagmi has no RPC transport configured for that chain and any calls would fail.

## Framework integrations

### Vue 3

Full working [example](https://github.com/universal-everything/up-modal-vue-example).

```html
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { setupLuksoConnector, type LuksoConnector } from '@lukso/up-modal'

const connector = ref<LuksoConnector | null>(null)

onMounted(async () => {
  connector.value = await setupLuksoConnector({ theme: 'light' })
})
</script>

<template>
  <button :disabled="!connector" @click="connector?.showSignInModal()">
    Connect
  </button>
</template>
```

### Nuxt

**Plugin** (`plugins/lukso.client.ts`):

```typescript
import { setupLuksoConnector } from '@lukso/up-modal'
import type { LuksoConnector } from '@lukso/up-modal'

let connector: LuksoConnector | null = null

export default defineNuxtPlugin(async () => {
  if (process.server) return

  connector = await setupLuksoConnector({
    theme: 'light',
    onConnect: (event) => console.log('Connected:', event.detail),
  })
})

export { connector }
```

**Component**:

```html
<template>
  <button @click="connector?.showSignInModal()">Connect Wallet</button>
</template>

<script setup lang="ts">
  import { connector } from '~/plugins/lukso.client'
</script>
```

### React

Full working [example](https://github.com/universal-everything/up-modal-react-example).

```tsx
import { setupLuksoConnector } from '@lukso/up-modal'
import type { LuksoConnector } from '@lukso/up-modal'
import { useEffect, useState } from 'react'

export function ConnectButton() {
  const [connector, setConnector] = useState<LuksoConnector | null>(null)

  useEffect(() => {
    setupLuksoConnector({
      theme: 'light',
      onConnect: (event) => console.log('Connected:', event.detail),
    }).then(setConnector)
  }, [])

  return (
    <button disabled={!connector} onClick={() => connector?.showSignInModal()}>
      Connect Wallet
    </button>
  )
}
```

**Watching connection state** (requires `@wagmi/core`):

```typescript
import { watchConnection } from '@wagmi/core'

const stopWatching = watchConnection(connector.wagmiConfig, {
  onChange: (connection) => {
    console.log('Address:', connection.address)
  },
})
```

### Svelte

**Initialization** (`src/lib/connector.svelte.ts`):

```typescript
import { setupLuksoConnector } from '@lukso/up-modal'
import type { LuksoConnector } from '@lukso/up-modal'

export const connector: { current: LuksoConnector | null } = $state({
  current: null,
})

export async function initConnector() {
  connector.current = await setupLuksoConnector({
    theme: 'light',
    onConnect: (e) => console.log('Connected:', e.detail),
  })
}
```

**Component** (`ConnectButton.svelte`):

```svelte
<script lang="ts">
  import { connector, initConnector } from '$lib/connector.svelte'

  $effect(() => {
    initConnector()
  })
</script>

<button onclick={() => connector.current?.showSignInModal()}>
  Connect Wallet
</button>
```

## Customization

The modal supports visual customization via CSS variables. Set them on `:root` or any ancestor of the `<connect-modal>` element.

### CSS Variables

| Variable                     | Description                | Default                                       |
| ---------------------------- | -------------------------- | --------------------------------------------- |
| `--up-modal-font-family`     | Modal font family          | `Inter, ui-sans-serif, system-ui, sans-serif` |
| `--up-modal-font-color`      | Modal text color (light)   | `#243542`                                     |
| `--up-modal-font-dark-color` | Modal text color (dark)    | `#f5f8fa`                                     |
| `--up-modal-btn-color`       | Button background (light)  | `#ffffff`                                     |
| `--up-modal-btn-dark-color`  | Button background (dark)   | `#243542`                                     |
| `--up-modal-btn-text`        | Button text & icon (light) | `#243542`                                     |
| `--up-modal-btn-dark-text`   | Button text & icon (dark)  | `#ffffff`                                     |
| `--up-modal-btn-radius`      | Button border radius       | `12px` (large) / `10px` (medium)              |
| `--up-modal-border-radius`   | Modal border radius        | `12px`                                        |
| `--up-modal-bg`              | Modal background (light)   | `#f8fafb`                                     |
| `--up-modal-dark-bg`         | Modal background (dark)    | `#121b21`                                     |

### Example with custom brand colors

```html
<style>
  :root {
    --up-modal-font-family: 'Roboto', sans-serif;
    --up-modal-font-color: #1e3a5f;
    --up-modal-btn-color: #4285f4;
    --up-modal-btn-text: #ffffff;
    --up-modal-bg: #eff6ff;
    --up-modal-border-radius: 20px;
  }
</style>
```

## Internationalization

The modal ships with English (`en_US`) translations by default. You can customize any text string or add support for other languages.

The i18n features require [`@lukso/core`](https://www.npmjs.com/package/@lukso/core), a separate LUKSO utilities package. Install it alongside `@lukso/up-modal`:

```bash
npm install @lukso/core
```

### Translation keys

| Key                                             | Default (English)                                                                                           |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `connect_modal_title`                           | Let's log you in                                                                                            |
| `connect_modal_description`                     | Log in with your Universal Profile                                                                          |
| `connect_modal_connectors_passkey_wallet`       | Passkey Wallet                                                                                              |
| `connect_modal_connectors_up_mobile`            | Mobile Application                                                                                          |
| `connect_modal_connectors_up_browser_extension` | Browser Extension                                                                                           |
| `connect_modal_or`                              | Or                                                                                                          |
| `connect_modal_or_info`                         | Log in with a different wallet                                                                              |
| `connect_modal_other_connectors`                | Connect Wallet                                                                                              |
| `connect_modal_eoa_title`                       | Connect your Wallet                                                                                         |
| `connect_modal_installed`                       | INSTALLED                                                                                                   |
| `connect_modal_failed_to_load`                  | Failed to load...                                                                                           |
| `connect_modal_try_again`                       | Try again                                                                                                   |
| `connect_modal_qr_code_title`                   | Scan to log in                                                                                              |
| `connect_modal_qr_code_description`             | Scan the below QR code with the Universal Profile mobile app to log in                                      |
| `connect_modal_close`                           | Close modal                                                                                                 |
| `connect_modal_go_back`                         | Go back                                                                                                     |
| `sign_up_modal_title`                           | Create Universal Profile!                                                                                   |
| `sign_up_modal_description`                     | Choose the device on which you want to create a profile. You can use either to interact with supported apps |
| `sign_up_modal_use_mobile`                      | Install the Mobile Application                                                                              |
| `sign_up_modal_install_extension`               | Install the Browser Extension                                                                               |

### Changing the locale

The modal uses the global `IntlService` from [`@lukso/core`](https://www.npmjs.com/package/@lukso/core). If your app already sets up an intl service, the modal will use it automatically. Otherwise it creates a local English instance.

```typescript
import { createIntlService, setIntlService } from '@lukso/core/services/intl'

// Create a service with your translations
const intl = createIntlService({
  locale: 'de-DE',
  messages: {
    connect_modal_title: 'Wir melden Sie an',
    connect_modal_description: 'Melden Sie sich mit Ihrem Universalprofil an.',
    // ... other keys
  },
})

// Set it globally — the modal picks it up automatically
setIntlService(intl)
```

### Overriding individual strings

If you only need to change a few labels, set the global service with your full message map. Any keys you omit will fall back to the built-in English translations.

```typescript
import enMessages from '@lukso/core/translations/en_US.json'

const intl = createIntlService({
  locale: 'en-US',
  messages: {
    ...enMessages,
    connect_modal_title: 'Sign in to MyApp',
    connect_modal_description: 'Connect your Universal Profile to continue',
  },
})

setIntlService(intl)
```

### Switching locale at runtime

```typescript
import { getIntlService } from '@lukso/core/services/intl'
import germanMessages from '@lukso/core/translations/de_DE.json'

const intl = getIntlService()
intl?.setLocale('de-DE', germanMessages)
// The modal re-renders automatically
```

## License

Apache-2.0 — see [LICENSE.md](./LICENSE.md) for details.