@embarkai/ui-kit 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,977 @@
1
+ # @embarkai/ui-kit
2
+
3
+ React UI components and hooks for EmbarkAI - a secure, user-friendly authentication and Account Abstraction wallet solution with MPC (Multi-Party Computation) key management.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Secure Authentication** - Multiple auth methods: Email, Passkey, Telegram, Wallet connect
8
+ - 🔑 **MPC Key Management** - Distributed key generation with iframe isolation
9
+ - 💼 **Account Abstraction** - ERC-4337 compliant smart contract wallets
10
+ - 🎨 **Pre-built UI Components** - Ready-to-use React components with customizable themes
11
+ - ⚡ **Easy Integration** - Just wrap your app with `LumiaPassportProvider`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @embarkai/ui-kit
17
+ # or
18
+ pnpm add @embarkai/ui-kit
19
+ # or
20
+ yarn add @embarkai/ui-kit
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ The UI Kit requires `@tanstack/react-query` for query management & `i18next` + `react-i18next` for multilanguages support.
26
+
27
+ ### 1. QueryClient Setup (Required)
28
+
29
+ First, create a query client:
30
+
31
+ ```tsx
32
+ // queryClient.ts
33
+ import { QueryClient } from '@tanstack/react-query'
34
+
35
+ export const queryClient = new QueryClient({
36
+ defaultOptions: {}
37
+ })
38
+ ```
39
+
40
+ ### 2. I18n config (Required) & typing (Optional)
41
+
42
+ 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.
43
+
44
+ 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.
45
+
46
+ First, create the following folder structure for your dapp's translations (or update existant, if needed):
47
+
48
+ example: common.json
49
+
50
+ ```json
51
+ {
52
+ "appname": "My App",
53
+ "signin": "Sign IN"
54
+ }
55
+ ```
56
+
57
+ ```
58
+ src/
59
+ └── i18n/
60
+ ├── locales/
61
+ │ ├── en/
62
+ │ ├── ├── common.json
63
+ │ ├── ├── header.json
64
+ │ ├── ├── page1.json
65
+ │ ├── ├── page2.json
66
+ │ ├── └── ...restTranslationsJsons
67
+ │ ├── /...restLocales/...
68
+ └── index.ts
69
+ ```
70
+
71
+ Where:
72
+
73
+ - `locales/**/*.json` files contain i18next translation maps. OPTIONAL Use dedicated files for easier translations maintainance.
74
+ - `index.ts` exports translation resources
75
+
76
+ > **IMPORTANT:** YOUR_APP_TRANSLATION_RESOURSES must be structured with namespaces (as shown), where "app" is example namespace
77
+
78
+ ```tsx
79
+ import commonEn from './i18n/locales/en/common.json'
80
+ import headerEn from './i18n/locales/en/header.json'
81
+ import page1en from './locales/en/page1.json'
82
+ import page2en from './locales/en/page2.json'
83
+
84
+ export const YOUR_APP_TRANSLATION_RESOURSES = {
85
+ // language: { namespace: jsonLocale }
86
+ en: {
87
+ app: {
88
+ common: commonEn,
89
+ header: headerEn,
90
+ page1: page1en,
91
+ page2: page2en
92
+ }
93
+ }
94
+ // ...
95
+ } as const
96
+ ```
97
+
98
+ > **Note:** If you don't need to translate your app then leave YOUR_APP_TRANSLATION_RESOURSES empty with no locales folder.
99
+
100
+ 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
101
+
102
+ ```tsx
103
+ import { YOUR_APP_TRANSLATION_RESOURSES } from './i18n'
104
+
105
+ declare module 'i18next' {
106
+ interface CustomTypeOptions {
107
+ defaultNS: 'app' //
108
+ resources: {
109
+ app: typeof YOUR_APP_TRANSLATION_RESOURSES.en.app
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### 3. Wrap your app with providers & init i18n
116
+
117
+ > **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.
118
+
119
+ ```tsx
120
+ import { useTranslation } from 'react-i18next'
121
+
122
+ export function useT() {
123
+ return useTranslation('app') // this will make t-method typed accordingly
124
+ }
125
+ ```
126
+
127
+ ```tsx
128
+ import {
129
+ combineI18NResources,
130
+ LOCAL_STORAGE_I18N_KEY,
131
+ //
132
+ LumiaPassportProvider,
133
+ LumiaPassportSessionProvider,
134
+ LumiaRainbowKitProvider
135
+ } from '@embarkai/ui-kit'
136
+ //
137
+ import i18n from 'i18next'
138
+ import { initReactI18next, useTranslation } from 'react-i18next'
139
+
140
+ import { YOUR_APP_TRANSLATION_RESOURSES } from './i18n'
141
+ import { queryClient } from './queryClient'
142
+
143
+ i18n.use(initReactI18next).init({
144
+ resources: combineI18NResources(YOUR_APP_TRANSLATION_RESOURSES),
145
+ lng: localStorage.getItem(LOCAL_STORAGE_I18N_KEY) || 'en', // passport saves language setup on change
146
+ fallbackLng: 'en', // default
147
+ defaultNS: 'app', // your app i18n-namespace, example: app
148
+ namespace: 'app', // your app i18n-namespace, example: app
149
+ debug: false
150
+ })
151
+
152
+ function Root() {
153
+ const { t } = useT()
154
+
155
+ return (
156
+ <QueryClientProvider client={queryClient}>
157
+ <LumiaPassportProvider
158
+ projectId="your-project-id" // Get from Lumia Passport Dashboard
159
+ >
160
+ <LumiaRainbowKitProvider>
161
+ <LumiaPassportSessionProvider>
162
+ <header>
163
+ <h1>{t('common.appname')}</h1>
164
+ </header>
165
+
166
+ <main>
167
+ <span>{t('page1.title')}</span>
168
+ </main>
169
+ </LumiaPassportSessionProvider>
170
+ </LumiaRainbowKitProvider>
171
+ </LumiaPassportProvider>
172
+ </QueryClientProvider>
173
+ )
174
+ }
175
+ ```
176
+
177
+ ### 4. Add the Connect Button
178
+
179
+ ```tsx
180
+ import { ConnectWalletButton } from '@embarkai/ui-kit'
181
+
182
+ function YourApp() {
183
+ const { t } = useT()
184
+
185
+ return (
186
+ <div>
187
+ <h1>{t('common.appname')}</h1>
188
+ <ConnectWalletButton label={t('common.signin')} />
189
+ </div>
190
+ )
191
+ }
192
+ ```
193
+
194
+ ### 5. (Optional)
195
+
196
+ Custom unconnected button can be provided via ConnectButton prop.
197
+ Prop consumes standart HTMLButton component and will provide required onClick to it
198
+
199
+ ```tsx
200
+ import { ConnectWalletButton } from '@embarkai/ui-kit'
201
+
202
+ function CustomButtonComponent(props: HTMLAttributes<HTMLButtonElement>) => {
203
+ return (<button {...props}/>)
204
+ }
205
+
206
+ function YourApp() {
207
+ const { t } = useT()
208
+
209
+ return (
210
+ <div>
211
+ <h1>My App</h1>
212
+ <ConnectWalletButton label={t('common.signin')} ConnectButton={CustomButtonComponent} />
213
+ </div>
214
+ )
215
+ }
216
+ ```
217
+
218
+ That's it! The `ConnectWalletButton` provides a complete authentication UI with wallet management.
219
+
220
+ > **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"
221
+
222
+ ## Configuration Options
223
+
224
+ ### Basic Configuration
225
+
226
+ ```tsx
227
+ <LumiaPassportProvider
228
+ projectId="your-project-id" // Required
229
+ initialConfig={{
230
+ network: {
231
+ name: 'Lumia Beam',
232
+ symbol: 'LUMIA',
233
+ chainId: 2030232745, // Default chain for your dApp
234
+ rpcUrl: 'https://beam-rpc.lumia.org',
235
+ explorerUrl: 'https://beam-explorer.lumia.org',
236
+ testnet: true,
237
+ },
238
+ }}
239
+ >
240
+ ```
241
+
242
+ **Network Chain Priority:**
243
+
244
+ The SDK uses the following priority for determining the active chain:
245
+
246
+ 1. **User's explicit selection** - If user manually switched chains in the UI, their choice is preserved (stored in localStorage)
247
+ 2. **dApp config `network.chainId`** - Your configured default chain for first-time users
248
+ 3. **SDK default** - Lumia Testnet (fallback if no config provided)
249
+
250
+ This ensures returning users keep their preferred chain while new users start on your configured network.
251
+
252
+ ### Advanced Configuration
253
+
254
+ ```tsx
255
+ <LumiaPassportProvider
256
+ projectId="your-project-id"
257
+ initialConfig={{
258
+ // UI customization
259
+ preferedColorMode?: 'light', // 'light' | 'dark'
260
+
261
+ ui: {
262
+ title: 'Welcome to MyApp',
263
+ subtitle: 'Sign in to continue',
264
+
265
+ dialogClassName: 'string', // beta
266
+
267
+ authOrder: ['email', 'passkey', 'social'],
268
+
269
+ branding: {
270
+ tagline: 'Powered by MPC',
271
+ link: { text: 'Learn More', url: \'https\:\/\/example.com/docs\' },
272
+ },
273
+ },
274
+
275
+ // Authentication providers
276
+ email: {
277
+ enabled: true,
278
+ placeholder: 'Enter your email',
279
+ buttonText: 'Continue',
280
+ verificationTitle: 'Check your email',
281
+ },
282
+ passkey: {
283
+ enabled: true,
284
+ showCreateButton: true,
285
+ primaryButtonText: 'Sign in with Passkey',
286
+ },
287
+ social: {
288
+ enabled: true,
289
+ providers: [
290
+ { id: 'Discord', name: 'Discord', enabled: true, comingSoon: false },
291
+ { id: 'telegram', name: 'Telegram', enabled: true, comingSoon: false },
292
+ ],
293
+ },
294
+ wallet: {
295
+ enabled: true,
296
+ supportedChains: [994873017, 2030232745],
297
+ requireSignature: true,
298
+ walletConnectProjectId: 'your-walletconnect-id',
299
+ },
300
+
301
+ // Features
302
+ features: {
303
+ mpcSecurity: true,
304
+ strictMode: false,
305
+ requestDeduplication: true,
306
+ kycNeeded: false,
307
+ displayNameNeeded: false,
308
+ showActiveBalanceAsFiat: false,
309
+ },
310
+
311
+ // KYC configuration (if needed)
312
+ kyc: {
313
+ provider: 'sumsub',
314
+ options: { levelName: 'basic-kyc', flowName: 'default' }
315
+ },
316
+
317
+ // Warnings
318
+ warnings: {
319
+ backupWarning: true,
320
+ emailNotConnectedWarning: true,
321
+ },
322
+
323
+ // Network configuration
324
+ // chainId sets default chain for new users (see "Network Chain Priority" above)
325
+ network: {
326
+ name: 'Lumia Beam',
327
+ symbol: 'LUMIA',
328
+ chainId: 2030232745, // Your dApp's default chain
329
+ rpcUrl: 'https\:\/\/beam-rpc.lumia.org',
330
+ explorerUrl: 'https\:\/\/beam-explorer.lumia.org',
331
+ testnet: true,
332
+ forceChain: false, // if true, passport is immediatly forced to switch itto provided chainId, chain selector via SeetingsMenu becomes anavailable
333
+ },
334
+ }}
335
+ callbacks={{
336
+ onLumiaPassportConnecting: ({ method, provider }) => {
337
+ console.log('Connecting with:', method, provider);
338
+ },
339
+ onLumiaPassportConnect: ({ address, session }) => {
340
+ console.log('Connected:', address);
341
+ },
342
+ onLumiaPassportAccount: ({ userId, address, session, hasKeyshare }) => {
343
+ console.log('Account ready:', userId);
344
+ },
345
+ onLumiaPassportUpdate: ({ providers }) => {
346
+ console.log('Profile updated:', providers);
347
+ },
348
+ onLumiaPassportDisconnect: ({ address, userId }) => {
349
+ console.log('Disconnected:', address);
350
+ },
351
+ onLumiaPassportError: ({ error, message }) => {
352
+ console.error('Error:', message);
353
+ },
354
+ onWalletReady: (status) => {
355
+ console.log('Wallet ready:', status.ready);
356
+ },
357
+ onLumiaPassportChainChange: ({ chainId, previousChainId }) => {
358
+ console.log('Chain Changed:', { previousChainId, chainId })
359
+ },
360
+ }}
361
+ >
362
+ ```
363
+
364
+ ## Using Hooks
365
+
366
+ > **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.
367
+
368
+ ```tsx
369
+ import { useLumiaPassportAccountSession, useLumiaPassportLoadingStatus } from '@embarkai/ui-kit'
370
+
371
+ function MyComponent() {
372
+ // const session = useLumiaPassportSession(s => s.session) - with prev hook & Zustand state extraction feature, please prefer this instead:
373
+ const session = useLumiaPassportAccountSession()
374
+ const { isSessionLoading } = useLumiaPassportLoadingStatus()
375
+
376
+ if (isSessionLoading) return <div>Loading...</div>
377
+
378
+ if (!session) {
379
+ return <div>Not authenticated. Please connect your wallet.</div>
380
+ }
381
+
382
+ return (
383
+ <div>
384
+ <p>Welcome!</p>
385
+ <p>User ID: {session.userId}</p>
386
+ <p>Wallet Address: {session.ownerAddress}</p>
387
+ <p>Smart Account: {session.accountAddress}</p>
388
+ </div>
389
+ )
390
+ }
391
+ ```
392
+
393
+ ### Lumia Passport shared store values hooks
394
+
395
+ - **useLumiaPassportActiveChainId** - Returns current ChainID
396
+ - **useLumiaPassportIsMobileView** - Returns boolean indicating if UI is in mobile view mode
397
+ - **useLumiaPassportAccountSession** - Returns current user session object with userId, addresses, and auth info
398
+ - **useLumiaPassportLoadingStatus** - Returns `{ isSessionLoading, sessionStatus }` for tracking authentication state
399
+ - **useLumiaPassportBalance** - Returns wallet balance data: `{ walletBalance, fiatBalance, cryptoRate, fiatSymbol, cryptoSymbol }`
400
+ - **useLumiaPassportIFrameReady** - Returns boolean indicating if the MPC iframe is ready for operations
401
+ - **useLumiaPassportAddress** - Returns the current user's wallet address
402
+ - **useLumiaPassportError** - Returns any error that occurred during authentication or operations
403
+ - **useLumiaPassportRecoveryUserId** - Returns userId for account recovery flow
404
+ - **useLumiaPassportHasServerVault** - Returns boolean indicating if user has server-side keyshare backup
405
+
406
+ ### useSendTransaction - Send Transactions
407
+
408
+ ```tsx
409
+ import { useSendTransaction } from '@embarkai/ui-kit'
410
+
411
+ function TransactionExample() {
412
+ const { sendTransaction, isPending } = useSendTransaction()
413
+
414
+ const handleSend = async () => {
415
+ try {
416
+ const userOpHash = await sendTransaction({
417
+ to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
418
+ value: '1000000000000000000', // 1 ETH in wei
419
+ data: '0x' // Optional contract call data
420
+ })
421
+ console.log('UserOp hash:', userOpHash)
422
+ } catch (error) {
423
+ console.error('Transaction failed:', error)
424
+ }
425
+ }
426
+
427
+ return (
428
+ <div>
429
+ <button onClick={handleSend} disabled={isPending}>
430
+ {isPending ? 'Sending...' : 'Send Transaction'}
431
+ </button>
432
+ </div>
433
+ )
434
+ }
435
+ ```
436
+
437
+ ### sendUserOperation - Direct Transaction Submission
438
+
439
+ For direct control without using the React hook, you can use `sendUserOperation` function:
440
+
441
+ ```tsx
442
+ import { sendUserOperation, useLumiaPassportAccountSession } from '@embarkai/ui-kit'
443
+
444
+ function DirectTransactionExample() {
445
+ const session = useLumiaPassportAccountSession()
446
+
447
+ const handleSend = async () => {
448
+ if (!session) {
449
+ console.error('No active session')
450
+ return
451
+ }
452
+
453
+ try {
454
+ // Send transaction directly with full control
455
+ const userOpHash = await sendUserOperation(
456
+ session, // Required: session from useLumiaPassportAccountSession
457
+ '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // to address
458
+ '1000000000000000000', // value in wei (1 ETH)
459
+ '0x', // data (optional contract call)
460
+ 'standard', // fee type: 'economy' | 'standard' | 'fast'
461
+ 'v0.7' // EntryPoint version
462
+ )
463
+
464
+ console.log('Transaction submitted:', userOpHash)
465
+ } catch (error) {
466
+ console.error('Transaction failed:', error)
467
+ }
468
+ }
469
+
470
+ return <button onClick={handleSend}>Send Transaction</button>
471
+ }
472
+ ```
473
+
474
+ **When to use:**
475
+
476
+ - ✅ Use `useSendTransaction()` hook for React components (automatic session management)
477
+ - ✅ Use `sendUserOperation()` function for custom logic, utility functions, or non-React code
478
+
479
+ ### deployAccount - Deploy Smart Account (Optional)
480
+
481
+ Deploy the smart account contract immediately after registration. This is **optional** - accounts are automatically deployed on first transaction.
482
+
483
+ **Smart behavior:** Automatically checks if account is already deployed and skips transaction to save gas.
484
+
485
+ ```tsx
486
+ import { deployAccount, useLumiaPassportAccountSession } from '@embarkai/ui-kit'
487
+
488
+ function DeployAccountExample() {
489
+ const session = useLumiaPassportAccountSession()
490
+
491
+ const handleDeploy = async () => {
492
+ if (!session) return
493
+
494
+ try {
495
+ // Deploy account with minimal gas cost (skips if already deployed)
496
+ const userOpHash = await deployAccount(session, 'economy')
497
+
498
+ if (userOpHash) {
499
+ console.log('Account deployed:', userOpHash)
500
+ } else {
501
+ console.log('Account already deployed, skipped')
502
+ }
503
+ } catch (error) {
504
+ console.error('Deployment failed:', error)
505
+ }
506
+ }
507
+
508
+ return <button onClick={handleDeploy}>Deploy Account</button>
509
+ }
510
+ ```
511
+
512
+ **Return values:**
513
+
514
+ - Returns `userOpHash` (string) - if deployment was needed and executed
515
+ - Returns `null` - if account already deployed (saves gas)
516
+
517
+ **Advanced usage:**
518
+
519
+ ```tsx
520
+ // Force deployment even if already deployed (not recommended)
521
+ const hash = await deployAccount(session, 'economy', { force: true })
522
+ ```
523
+
524
+ **Why use this?**
525
+
526
+ - ✅ **Pre-deploy** account before first real transaction
527
+ - ✅ **No user consent** required (safe minimal operation)
528
+ - ✅ **Cleaner UX** - separate deployment from first payment
529
+ - ✅ **Smart gas savings** - auto-skips if already deployed
530
+ - ⚠️ **Optional** - accounts auto-deploy on first transaction anyway
531
+
532
+ **How it works:**
533
+
534
+ 1. Checks if smart account contract exists on-chain
535
+ 2. If exists: returns `null` immediately (no gas cost)
536
+ 3. If not exists: sends minimal UserOperation (`to=0x0, value=0, data=0x`)
537
+ 4. Factory deploys contract without user confirmation
538
+
539
+ ### signTypedData - Sign EIP712 Structured Messages
540
+
541
+ 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).
542
+
543
+ ```tsx
544
+ import { signTypedData, useLumiaPassportAccountSession } from '@embarkai/ui-kit'
545
+
546
+ function SignatureExample() {
547
+ const session = useLumiaPassportAccountSession()
548
+
549
+ const handleSign = async () => {
550
+ if (!session) return
551
+
552
+ try {
553
+ // Define EIP712 typed data
554
+ const signature = await signTypedData(session, {
555
+ domain: {
556
+ name: 'MyDApp', // Your dApp name (must match contract)
557
+ version: '1', // Contract version
558
+ chainId: 994, // Lumia Prism Testnet
559
+ verifyingContract: '0x...' // Your contract address (REQUIRED in production!)
560
+ },
561
+ types: {
562
+ Order: [
563
+ { name: 'tokenIds', type: 'uint256[]' },
564
+ { name: 'price', type: 'uint256' },
565
+ { name: 'deadline', type: 'uint256' }
566
+ ]
567
+ },
568
+ primaryType: 'Order',
569
+ message: {
570
+ tokenIds: [1n, 2n, 3n],
571
+ price: 1000000000000000000n, // 1 token in wei
572
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour from now
573
+ }
574
+ })
575
+
576
+ console.log('Signature:', signature)
577
+
578
+ // Verify signature (optional)
579
+ const { recoverTypedDataAddress } = await import('viem')
580
+ const recoveredAddress = await recoverTypedDataAddress({
581
+ domain: {
582
+ /* same domain */
583
+ },
584
+ types: {
585
+ /* same types */
586
+ },
587
+ primaryType: 'Order',
588
+ message: {
589
+ /* same message */
590
+ },
591
+ signature
592
+ })
593
+
594
+ console.log('Signer:', recoveredAddress) // Should match session.ownerAddress
595
+ } catch (error) {
596
+ console.error('Signing failed:', error)
597
+ }
598
+ }
599
+
600
+ return <button onClick={handleSign}>Sign Message</button>
601
+ }
602
+ ```
603
+
604
+ **Important Notes about ERC-4337 Smart Accounts:**
605
+
606
+ In Account Abstraction (ERC-4337), there are **two addresses**:
607
+
608
+ 1. **Owner Address (EOA)** - The address that signs messages/transactions
609
+ 2. **Smart Account Address** - The contract wallet address
610
+
611
+ ⚠️ **Critical:** The signature is created by the **owner address** (EOA), NOT the smart account address!
612
+
613
+ **Compatibility with existing protocols:**
614
+
615
+ - ✅ **Works:** Protocols that verify signatures off-chain (e.g., your backend verifies the owner EOA signature)
616
+ - ⚠️ **May not work:** Protocols designed for EOA wallets that store and verify against `msg.sender` or wallet address
617
+ - Example: Uniswap Permit2, some NFT marketplaces
618
+ - These protocols expect the signer address to match the wallet address
619
+ - With smart accounts: signer = owner EOA, wallet = smart account contract
620
+ - **Solution:** Use ERC-1271 signature validation in your smart contracts (allows contracts to validate signatures)
621
+
622
+ **Domain Configuration:**
623
+
624
+ - In production, use your actual `verifyingContract` address (not zero address!)
625
+ - The `domain` parameters must match exactly between frontend and smart contract
626
+ - The `chainId` should match the network you're deploying to
627
+
628
+ **Technical Details:**
629
+
630
+ - Shows a MetaMask-like confirmation modal with structured message preview
631
+ - All BigInt values are supported in the message
632
+ - Signature can be verified using `viem.recoverTypedDataAddress()` - will return owner EOA address
633
+
634
+ **When to use signTypedData:**
635
+
636
+ - ✅ Custom backend signature verification (you control the verification logic)
637
+ - ✅ Gasless transactions with meta-transaction relayers
638
+ - ✅ DAO voting and governance (off-chain signatures)
639
+ - ✅ Custom smart contracts with ERC-1271 support
640
+ - ⚠️ Be cautious with protocols designed exclusively for EOA wallets
641
+
642
+ ### prepareUserOperation - Prepare for Backend Submission
643
+
644
+ ```tsx
645
+ import { prepareUserOperation, useLumiaPassportAccountSession } from '@embarkai/ui-kit'
646
+
647
+ function BackendSubmissionExample() {
648
+ const session = useLumiaPassportAccountSession()
649
+
650
+ const handlePrepare = async () => {
651
+ if (!session) return
652
+
653
+ // Prepare and sign UserOp without sending to bundler
654
+ const { userOp, userOpHash } = await prepareUserOperation(
655
+ session,
656
+ '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // to
657
+ '1000000000000000000', // 1 ETH in wei
658
+ '0x', // data
659
+ 'standard', // fee type: 'economy' | 'standard' | 'fast'
660
+ 'v0.7' // EntryPoint version
661
+ )
662
+
663
+ // Send to backend for validation and submission
664
+ await fetch('/api/submit-transaction', {
665
+ method: 'POST',
666
+ headers: { 'Content-Type': 'application/json' },
667
+ body: JSON.stringify({
668
+ userOp,
669
+ userOpHash,
670
+ ownerAddress: session.ownerAddress // for signature verification
671
+ })
672
+ })
673
+ }
674
+
675
+ return <button onClick={handlePrepare}>Prepare & Send to Backend</button>
676
+ }
677
+ ```
678
+
679
+ **Backend example (using @embarkai/core):**
680
+
681
+ ```typescript
682
+ import { getUserOperationReceipt, sendUserOperationRaw } from '@embarkai/core'
683
+ import { getChainConfig } from '@embarkai/core/read'
684
+ import { recoverAddress } from 'viem'
685
+
686
+ const CHAIN_ID = 2030232745 // Lumia Testnet
687
+
688
+ // Get chain config for bundler URL
689
+ const chainConfig = getChainConfig(CHAIN_ID)
690
+ if (!chainConfig) throw new Error('Unsupported chain')
691
+
692
+ // Receive from frontend
693
+ const { userOp, userOpHash, ownerAddress } = await request.json()
694
+
695
+ // Verify signature
696
+ const recoveredAddress = await recoverAddress({
697
+ hash: userOpHash,
698
+ signature: userOp.signature
699
+ })
700
+
701
+ if (recoveredAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
702
+ throw new Error('Invalid signature')
703
+ }
704
+
705
+ // Submit to bundler - REQUIRES chainId parameter
706
+ const submittedUserOpHash = await sendUserOperationRaw(userOp, { chainId: CHAIN_ID })
707
+
708
+ // Poll for receipt to get transaction hash and status
709
+ const waitForReceipt = async (hash: string, maxAttempts = 60, delayMs = 1000) => {
710
+ for (let i = 0; i < maxAttempts; i++) {
711
+ // REQUIRES bundlerUrl parameter
712
+ const receipt = await getUserOperationReceipt(
713
+ hash as `0x${string}`,
714
+ chainConfig.bundlerUrl
715
+ )
716
+ if (receipt) {
717
+ return {
718
+ success: receipt.success,
719
+ transactionHash: receipt.receipt?.transactionHash,
720
+ blockNumber: receipt.receipt?.blockNumber
721
+ }
722
+ }
723
+ await new Promise((resolve) => setTimeout(resolve, delayMs))
724
+ }
725
+ throw new Error('Transaction timeout')
726
+ }
727
+
728
+ const result = await waitForReceipt(submittedUserOpHash)
729
+ return {
730
+ success: result.success,
731
+ transactionHash: result.transactionHash,
732
+ blockNumber: result.blockNumber
733
+ }
734
+ ```
735
+
736
+ **Network Configuration:**
737
+
738
+ | Network | Chain ID | Bundler URL |
739
+ |---------|----------|-------------|
740
+ | Lumia Testnet (Beam) | 2030232745 | https://api.lumiapassport.com/rundler |
741
+ | Lumia Mainnet (Prism) | 994873017 | https://api.lumiapassport.com/rundler-prism |
742
+
743
+ ### useLumiaPassportOpen - Programmatic LumiaPassport Dialog Control
744
+
745
+ Control the Lumia Passport dialog programmatically.
746
+
747
+ ```tsx
748
+ import { PageKey, useLumiaPassportOpen } from '@embarkai/ui-kit'
749
+
750
+ function CustomAuthButton() {
751
+ const { isOpen, open: openLumiaPassport, close } = useLumiaPassportOpen()
752
+
753
+ return (
754
+ <div>
755
+ <button onClick={() => openLumiaPassport(PageKey.AUTH)}>Sign In</button>
756
+ <button onClick={() => openLumiaPassport(PageKey.RECEIVE)}>Receive LUMIA</button>
757
+
758
+ <button onClick={close}>Close Dialog</button>
759
+ </div>
760
+ )
761
+ }
762
+ ```
763
+
764
+ ### ThemeToggle - Quick Theme Switcher
765
+
766
+ Pre-built theme toggle button component to use in combo with useLumiaPassportColorMode.
767
+
768
+ ```tsx
769
+ import { ThemeToggle } from '@embarkai/ui-kit'
770
+
771
+ function AppHeader() {
772
+ return (
773
+ <header>
774
+ <h1>My App</h1>
775
+ <ThemeToggle />
776
+ </header>
777
+ )
778
+ }
779
+ ```
780
+
781
+ ## Authentication Methods
782
+
783
+ ### Email OTP
784
+
785
+ Users receive a one-time code via email.
786
+
787
+ ```tsx
788
+ // Configured by default, no additional setup needed
789
+ ```
790
+
791
+ ### Passkey (WebAuthn)
792
+
793
+ Secure biometric authentication with device passkeys.
794
+
795
+ ```tsx
796
+ // Configured by default
797
+ // Users can register passkey after initial login
798
+ ```
799
+
800
+ ### Telegram Mini App
801
+
802
+ Authentication via Telegram for mini apps.
803
+
804
+ ```tsx
805
+ // Configure via social providers:
806
+ initialConfig={{
807
+ social: {
808
+ enabled: true,
809
+ providers: [
810
+ { id: 'telegram', name: 'Telegram', enabled: true },
811
+ ],
812
+ },
813
+ }}
814
+ ```
815
+
816
+ ### External Wallet
817
+
818
+ Connect existing wallets (MetaMask, WalletConnect, etc.).
819
+
820
+ ```tsx
821
+ // Configured by default
822
+ // Uses RainbowKit for wallet connections
823
+ ```
824
+
825
+ ## Styling
826
+
827
+ ### Passport Dialog Window
828
+
829
+ Global Passport window view can be redefined via config prop by providing specific dialogClassName.
830
+
831
+ > **Note** Providing "dialogClassName" will disable default dialog window styles
832
+
833
+ ```css
834
+ .passport-dialog-classname {
835
+ background-color: rgba(14, 14, 14, 0.7);
836
+ backdrop-filter: blur(10px);
837
+
838
+ box-shadow:
839
+ 0 0 8px rgba(14, 14, 14, 0.1),
840
+ inset 0 0 0 1px var(--lumia-passport-bd);
841
+ }
842
+ ```
843
+
844
+ ```tsx
845
+ import 'index.css'
846
+
847
+ <LumiaPassportProvider
848
+ ...
849
+ initialConfig={{
850
+ ...
851
+ ui: {
852
+ ...
853
+ dialogClassName: 'passport-dialog-classname',
854
+ ...
855
+ },
856
+ ...
857
+ }}/>
858
+ ```
859
+
860
+ ### Passport styles
861
+
862
+ Passport provides global hook for Light/Dark modes - useLumiaPassportColorMode - use it within yor application instead of any local mode declaration.
863
+
864
+ ```tsx
865
+ import { useLumiaPassportColorMode } from '@embarkai/ui-kit'
866
+
867
+ function YourAnyComponent() {
868
+ const { colorMode, setColorMode } = useLumiaPassportColorMode()
869
+
870
+ return (
871
+ <div>
872
+ <p>Current theme: {colorMode}</p>
873
+ <button onClick={() => setColorMode('light')}>Light</button>
874
+ <button onClick={() => setColorMode('dark')}>Dark</button>
875
+ </div>
876
+ )
877
+ }
878
+ ```
879
+
880
+ Passport styles are possible to match with your project via css-variables set (use separate for dark & light).
881
+ Values will be automatically applied according to selected colorMode.
882
+ The simpliest way to customize cssv is via app index.css file:
883
+
884
+ ```css
885
+ .lumia-scope[data-lumia-passport-mode='light'],
886
+ .lumia-scope[data-lumia-passport-mode='dark'] {
887
+ /* fixed cssv */
888
+ --lumia-passport-maw: 384px;
889
+ --lumia-passport-pd: 12px;
890
+ --lumia-passport-gap: 10px;
891
+ --lumia-passport-bdrs: 20px;
892
+ --lumia-passport-element-bdrs: 10px;
893
+
894
+ /** overlay */
895
+ --lumia-passport-overlay: rgba(255, 255, 255, 0.8);
896
+ --lumia-passport-backdrop-blur: 10px;
897
+
898
+ /** surface backgrounds */
899
+ --lumia-passport-bg: #ffffff;
900
+
901
+ /** text */
902
+ --lumia-passport-fg: #000000;
903
+ --lumia-passport-fg-h: rgba(0, 0, 0, 0.6);
904
+ --lumia-passport-fg-a: rgba(0, 0, 0, 0.4);
905
+ --lumia-passport-fg-inverted: #ffffff;
906
+ --lumia-passport-fg-muted: rgba(0, 0, 0, 0.6);
907
+
908
+ /** backgrounds i.e. buttons bg etc */
909
+ --lumia-passport-primary: #000000;
910
+ --lumia-passport-primary-h: rgba(0, 0, 0, 0.8);
911
+ --lumia-passport-primary-a: rgba(0, 0, 0, 0.6);
912
+
913
+ --lumia-passport-secondary: #e4e4e4;
914
+ --lumia-passport-secondary-h: rgba(228, 228, 228, 0.8);
915
+ --lumia-passport-secondary-a: rgba(228, 228, 228, 0.6);
916
+
917
+ /** borders */
918
+ --lumia-passport-bd: #ebebeb;
919
+ --lumia-passport-bd-intense: rgb(169, 169, 169);
920
+
921
+ /** shadows */
922
+ --lumia-passport-shadow-c: rgba(0, 0, 0, 0.1);
923
+
924
+ /** highlight colors */
925
+ --lumia-passport-info: #000000;
926
+ --lumia-passport-bg-info: #e4e4e4;
927
+
928
+ --lumia-passport-success: #000000;
929
+ --lumia-passport-bg-success: #21ff51;
930
+
931
+ --lumia-passport-warning: #000000;
932
+ --lumia-passport-bg-warning: #e9fa00;
933
+
934
+ --lumia-passport-error: #ffffff;
935
+ --lumia-passport-bg-error: #d6204e;
936
+ }
937
+ ```
938
+
939
+ ## TypeScript Support
940
+
941
+ Full TypeScript support with exported types:
942
+
943
+ ```tsx
944
+ import type { AuthProvider, LumiaPassportConfig, User, WalletInfo } from '@embarkai/ui-kit'
945
+ ```
946
+
947
+ ## Examples
948
+
949
+ Check out the `/examples` directory for complete working examples:
950
+
951
+ - **React + Vite** - Modern React setup with Vite
952
+ - **Next.js** - Next.js App Router integration
953
+ - **React + TypeScript** - Full TypeScript example
954
+
955
+ ## Security
956
+
957
+ - 🔒 **MPC Key Management** - Keys are split between client and server using threshold cryptography
958
+ - 🏝️ **Iframe Isolation** - Sensitive operations run in isolated iframe context
959
+ - 🔐 **No Private Key Exposure** - Private keys never exist in complete form on client
960
+ - ✅ **Non-custodial** - Users maintain full control of their accounts
961
+
962
+ ## Need Help?
963
+
964
+ - 📖 [Documentation](https://docs.lumiapassport.com)
965
+ - 💬 [Discord Community](https://discord.gg/lumia)
966
+ - 🐛 [Report Issues](https://github.com/lumiachain/lumia-passport-sdk/issues)
967
+ - 📧 [Email Support](mailto:support@lumia.org)
968
+
969
+ ## License
970
+
971
+ MIT License - see LICENSE file for details.
972
+
973
+ ## Links
974
+
975
+ - [Website](https://lumiapassport.com)
976
+ - [GitHub](https://github.com/lumiachain/lumia-passport-sdk)
977
+ - [NPM Package](https://www.npmjs.com/package/@embarkai/ui-kit)