diff --git a/core/wallet-ui-components/src/components/wallet-picker.ts b/core/wallet-ui-components/src/components/wallet-picker.ts deleted file mode 100644 index fbd7e8332..000000000 --- a/core/wallet-ui-components/src/components/wallet-picker.ts +++ /dev/null @@ -1,995 +0,0 @@ -// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { css } from 'lit' -import { BaseElement } from '../internal/base-element' -import { cssToString } from '../utils' -import { WalletPickerEntry } from '@canton-network/core-types' - -export type { - WalletPickerEntry, - WalletPickerResult, -} from '@canton-network/core-types' - -const SUBSTITUTABLE_CSS = cssToString([ - BaseElement.styles, - css` - * { - box-sizing: border-box; - font-family: var(--wg-theme-font-family); - color: var(--wg-theme-text-color); - } - - .root { - background-color: var(--wg-theme-background-color); - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - } - - .view-container { - display: flex; - flex-direction: column; - height: 100%; - } - - .header { - height: 40px; - padding: 0 24px; - display: flex; - align-items: center; - border-bottom: 1px solid var(--wg-theme-border-color); - } - - .header-logo { - width: 28px; - height: 28px; - } - - .view-title { - font-size: 20px; - font-weight: 600; - padding: 16px 24px 12px; - color: var(--wg-theme-text-color); - } - - .view-title-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 16px 24px 12px; - } - - .view-title-row .view-title { - padding: 0; - } - - .back-link { - border: none; - background: transparent; - padding: 0; - color: var(--wg-theme-text-color); - text-decoration: none; - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 14px; - font-weight: 400; - line-height: 1; - cursor: pointer; - white-space: nowrap; - } - - .back-link:hover { - color: var(--wg-theme-text-color); - } - - .back-link:focus-visible { - outline: 2px solid var(--wg-theme-accent-color); - outline-offset: 2px; - border-radius: 4px; - } - - .back-link .icon { - display: inline-flex; - align-items: center; - } - - .back-link svg { - width: 10px; - height: 10px; - } - - .wallet-list { - flex: 1; - overflow-y: auto; - padding: 4px 12px 0; - } - - .wallet-card { - display: flex; - align-items: center; - gap: 12px; - padding: 14px 16px; - border-radius: 8px; - border: 1px solid var(--wg-theme-border-color); - background: var(--wg-theme-surface-color); - cursor: pointer; - transition: all 0.15s ease; - width: 100%; - text-align: left; - margin-bottom: 8px; - } - - .wallet-card:hover { - background: var(--wg-theme-surface-hover); - border-color: var(--wg-theme-accent-color); - } - - .wallet-card:focus-visible { - outline: 2px solid var(--wg-theme-accent-color); - outline-offset: 2px; - } - - .wallet-card:active { - transform: scale(0.99); - } - - .wallet-icon { - width: 32px; - height: 32px; - border-radius: 8px; - background: var(--wg-theme-icon-bg); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - overflow: hidden; - } - - .wallet-icon img { - width: 32px; - height: 32px; - border-radius: 8px; - object-fit: cover; - } - - .wallet-icon svg { - width: 22px; - height: 22px; - color: var(--wg-theme-text-secondary); - } - - .wallet-name { - flex: 1; - min-width: 0; - font-size: 15px; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .wallet-remove-btn { - border: none; - background: transparent; - color: var(--wg-theme-text-secondary); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: color 0.15s ease; - flex-shrink: 0; - padding: 0; - width: 16px; - height: 16px; - } - - .wallet-remove-btn:hover { - color: var(--wg-theme-error-color); - } - - .wallet-remove-btn:focus-visible { - outline: 2px solid var(--wg-theme-accent-color); - outline-offset: 4px; - border-radius: 4px; - } - - .wallet-remove-btn svg { - width: 16px; - height: 16px; - } - - .custom-url-section { - padding: 8px 12px 16px; - } - - .custom-url-label { - position: relative; - display: flex; - align-items: center; - gap: 6px; - width: 100%; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--wg-theme-text-color); - padding: 0 4px 8px; - } - - .custom-url-label .info-wrap { - display: inline-flex; - align-items: center; - } - - .custom-url-label .info-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - color: var(--wg-theme-text-secondary); - border: none; - background: transparent; - padding: 0; - cursor: pointer; - } - - .custom-url-label .info-icon:focus-visible { - outline: 2px solid var(--wg-theme-accent-color); - border-radius: 999px; - } - - .custom-url-label .info-tooltip { - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - z-index: 20; - width: max-content; - max-width: min(320px, 90vw); - padding: 8px 10px; - border: none; - border-radius: 10px; - background: var(--wg-theme-primary-color); - color: var(--wg-theme-primary-text-color); - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.22); - font-size: 12px; - font-weight: 500; - line-height: 1.4; - text-transform: none; - letter-spacing: normal; - white-space: normal; - opacity: 0; - visibility: hidden; - pointer-events: none; - transition: opacity 0.12s ease; - } - - .custom-url-label .info-wrap:hover .info-tooltip { - opacity: 1; - visibility: visible; - } - - .custom-url-row { - display: flex; - gap: 8px; - align-items: center; - } - - .custom-url-input { - flex: 1; - padding: 10px 14px; - border: 1px solid var(--wg-theme-border-color); - border-radius: 8px; - font-size: 14px; - outline: none; - background: var(--wg-theme-surface-color); - color: var(--wg-theme-text-color); - } - - .custom-url-input:focus { - border-color: var(--wg-theme-accent-color); - box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15); - } - - .custom-url-input::placeholder { - color: var(--wg-theme-text-secondary); - } - - .btn-add { - background: var(--wg-theme-primary-color); - color: var(--wg-theme-primary-text-color); - border: none; - border-radius: 20px; - padding: 10px 24px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background 0.15s; - white-space: nowrap; - } - - .btn-add:hover { - background: var(--wg-theme-primary-hover); - } - - .btn-add:disabled { - opacity: 0.5; - cursor: default; - } - - .status-view { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 48px 24px; - gap: 16px; - text-align: center; - flex: 1; - } - - .status-view h3 { - margin: 0; - font-size: 16px; - font-weight: 600; - } - - .status-view p { - margin: 0; - font-size: 14px; - color: var(--wg-theme-text-secondary); - } - - .spinner { - width: 36px; - height: 36px; - border: 3px solid var(--wg-theme-border-color); - border-top-color: var(--wg-theme-accent-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; - } - - @keyframes spin { - to { - transform: rotate(360deg); - } - } - - .success-icon { - color: var(--wg-theme-success-color); - } - - .error-icon { - color: var(--wg-theme-error-color); - } - - .btn-row { - display: flex; - gap: 8px; - margin-top: 8px; - } - - .btn-primary { - background: var(--wg-theme-primary-color); - color: var(--wg-theme-primary-text-color); - border: none; - border-radius: 8px; - padding: 10px 24px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background 0.15s; - } - - .btn-primary:hover { - background: var(--wg-theme-primary-hover); - } - - .btn-secondary { - background: transparent; - color: var(--wg-theme-text-secondary); - border: 1px solid var(--wg-theme-border-color); - border-radius: 8px; - padding: 10px 24px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - } - - .empty-state { - color: var(--wg-theme-text-secondary); - } - `, -]) - -/** - * — a wallet selection component modelled after PartyLayer's - * WalletModal. Designed for popup rendering (same pattern as ). - * - * IMPORTANT: Because the popup serialises this class via .toString() and runs it - * inside a blob URL, every helper the class uses must be either: - * (a) a method / property on the class itself, or - * (b) a string literal inlined where it is used. - * Top-level module constants are NOT available at runtime in the popup. - * - * Communication: - * - Reads wallet entries from localStorage key `splice_wallet_picker_entries` - * - Posts a WalletPickerResult to window.opener via postMessage on selection - * - * States: list → connecting → connected | error - */ -export class WalletPicker extends HTMLElement { - static styles = SUBSTITUTABLE_CSS - - private readonly RECENT_KEY = 'splice_wallet_picker_recent' - - private root: HTMLElement - private entries: WalletPickerEntry[] = [] - private recentGateways: { name: string; rpcUrl: string }[] = [] - private state: 'list' | 'connecting' | 'connected' | 'error' = 'list' - private selectedEntry: WalletPickerEntry | null = null - private errorMessage = '' - - private wcUri: string | null = null - private wcQrDataUrl: string | null = null - - private readonly onOpenerStatusMessage = (event: MessageEvent): void => { - if (event.origin !== window.location.origin) return - - const data = event.data - if (data?.messageType !== 'SPLICE_WALLET_PICKER_CONNECT_STATUS') return - - if (data.status === 'connected') { - this.setConnected() - return - } - - if (data.status === 'error') { - const message = - typeof data.message === 'string' && data.message.length > 0 - ? data.message - : 'Failed to connect wallet' - this.setError(message) - } - } - - constructor() { - super() - this.attachShadow({ mode: 'open' }) - - const ctor = this.constructor as typeof HTMLElement & { - styles?: string - } - if (ctor.styles) { - const style = document.createElement('style') - style.textContent = ctor.styles - this.shadowRoot!.appendChild(style) - } - - this.root = document.createElement('div') - this.root.className = 'root' - - this.loadEntries() - this.recentGateways = this.loadRecentGateways() - } - - // ── localStorage helpers (inlined so they survive .toString() serialisation) ── - - private loadRecentGateways(): { name: string; rpcUrl: string }[] { - try { - const raw = localStorage.getItem(this.RECENT_KEY) - if (raw) return JSON.parse(raw) - } catch { - // ignore - } - return [] - } - - private saveRecentGateway(entry: { name: string; rpcUrl: string }): void { - const recent = this.loadRecentGateways().filter( - (r) => r.rpcUrl !== entry.rpcUrl - ) - recent.unshift(entry) - this.recentGateways = recent.slice(0, 5) - localStorage.setItem( - this.RECENT_KEY, - JSON.stringify(this.recentGateways) - ) - } - - private removeRecentGateway(rpcUrl: string): void { - this.recentGateways = this.loadRecentGateways().filter( - (r) => r.rpcUrl !== rpcUrl - ) - - if (this.recentGateways.length === 0) { - localStorage.removeItem(this.RECENT_KEY) - } else { - localStorage.setItem( - this.RECENT_KEY, - JSON.stringify(this.recentGateways) - ) - } - - this.render() - } - - private loadEntries(): void { - const stored = localStorage.getItem('splice_wallet_picker_entries') - if (!stored) return - try { - this.entries = JSON.parse(stored) - } catch { - this.entries = [] - } - } - - private getAllEntries(): WalletPickerEntry[] { - // Merge all entries into a single flat list: - // 1. Registered entries (extensions + gateways from discovery) - // 2. Recent gateways not already in the registered list - const knownUrls = new Set( - this.entries - .filter((e) => e.type === 'remote' && e.url) - .map((e) => e.url) - ) - - const recentEntries: WalletPickerEntry[] = this.recentGateways - .filter((r) => !knownUrls.has(r.rpcUrl)) - .map((r) => ({ - providerId: 'remote:' + r.rpcUrl, - name: r.name, - type: 'remote' as const, - url: r.rpcUrl, - reuseGlobalWalletPopup: true, - })) - - return [...this.entries, ...recentEntries] - } - - private isRemovableEntry(entry: WalletPickerEntry): boolean { - if (entry.type !== 'remote' || !entry.url) { - return false - } - - const isRegisteredEntry = this.entries.some( - (knownEntry) => - knownEntry.type === 'remote' && knownEntry.url === entry.url - ) - const isManualEntry = this.recentGateways.some( - (recentEntry) => recentEntry.rpcUrl === entry.url - ) - - return isManualEntry && !isRegisteredEntry - } - - // ── Actions ───────────────────────────────────────────── - - private selectWallet(entry: WalletPickerEntry): void { - this.selectedEntry = entry - this.state = 'connecting' - this.render() - - if (window.opener) { - window.opener.postMessage( - { - messageType: 'SPLICE_WALLET_PICKER_RESULT', - providerId: entry.providerId, - name: entry.name, - walletType: entry.type, - url: entry.url, - reuseGlobalWalletPopup: entry.reuseGlobalWalletPopup, - }, - '*' - ) - } - } - - private connectCustomUrl(rpcUrl: string): void { - const trimmed = rpcUrl.trim() - if (!trimmed) return - - this.selectWallet({ - providerId: 'remote:' + trimmed, - name: trimmed, - type: 'remote', - url: trimmed, - reuseGlobalWalletPopup: true, - }) - } - - public setConnected(): void { - this.state = 'connected' - this.render() - setTimeout(() => { - if (window.opener) window.close() - }, 1200) - } - - public setError(message: string): void { - this.errorMessage = message - this.state = 'error' - this.render() - } - - private goBackToList(): void { - this.selectedEntry = null - this.errorMessage = '' - this.state = 'list' - this.render() - } - - private createBackButton(): HTMLButtonElement { - const backBtn = this.el('button', '', { - class: 'back-link', - type: 'button', - 'aria-label': 'Back', - }) - - const icon = this.el('span', '', { class: 'icon' }) - icon.innerHTML = - '' - - backBtn.append(icon, this.el('span', 'Back')) - backBtn.addEventListener('click', () => this.goBackToList()) - - return backBtn - } - - // ── Rendering ────────────────────────────────────────── - - private renderHeader(): HTMLElement { - const header = this.el('div', '', { class: 'header' }) - - // Canton logo (base64 data URL - embedded for zero-config deployment) - // Note that it has to be inlined here else it wont be avialbel at runtime - const logo = this.el('img', '', { - class: 'header-logo', - src: 'data:image/png;base64,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', - alt: 'Canton', - }) - header.appendChild(logo) - - return header - } - - private renderWalletCard(entry: WalletPickerEntry): HTMLElement { - const card = this.el('div', '', { - class: 'wallet-card', - role: 'button', - tabindex: '0', - 'aria-label': `Connect to ${entry.name}`, - }) - - const icon = this.el('div', '', { class: 'wallet-icon' }) - if (entry.icon) { - const img = this.el('img', '', { src: entry.icon, alt: entry.name }) - icon.appendChild(img) - } else { - icon.innerHTML = - entry.type === 'browser' - ? '' - : '' - } - card.appendChild(icon) - - card.appendChild(this.el('span', entry.name, { class: 'wallet-name' })) - card.addEventListener('click', () => this.selectWallet(entry)) - - if (this.isRemovableEntry(entry) && entry.url) { - const removeButton = this.el('button', '', { - class: 'wallet-remove-btn', - type: 'button', - 'aria-label': `Remove custom wallet ${entry.name}`, - title: `Remove custom wallet ${entry.name}`, - }) - removeButton.innerHTML = - '' - removeButton.addEventListener('click', (event: Event) => { - event.stopPropagation() - this.removeRecentGateway(entry.url!) - }) - card.appendChild(removeButton) - } - - return card - } - - private renderList(): HTMLElement { - const container = this.el('div', '', { - class: 'view-container', - }) - - // container.appendChild(this.renderHeader()) - - const title = this.el('div', 'Connect a Wallet', { - class: 'view-title', - }) - container.appendChild(title) - - const allEntries = this.getAllEntries() - - const list = this.el('div', '', { class: 'wallet-list' }) - - if (allEntries.length === 0) { - const empty = this.el('div', '', { class: 'status-view' }) - empty.appendChild( - this.el('h3', 'No wallets available', { class: 'empty-state' }) - ) - empty.appendChild( - this.el( - 'p', - 'Install a Canton wallet extension or enter a Wallet Gateway URL below.' - ) - ) - list.appendChild(empty) - } else { - for (const entry of allEntries) { - list.appendChild(this.renderWalletCard(entry)) - } - } - - container.appendChild(list) - - // Custom URL section - const customSection = this.el('div', '', { - class: 'custom-url-section', - }) - - const label = this.el('div', '', { class: 'custom-url-label' }) - label.appendChild(document.createTextNode('CUSTOM WALLET')) - - const infoIcon = this.el('button', '', { - class: 'info-icon', - type: 'button', - 'aria-label': 'Wallet API help', - }) - infoIcon.innerHTML = - '' - - const infoTooltip = this.el( - 'div', - 'Wallet not listed above? Enter its Wallet API. The wallet must support CIP-103.', - { - class: 'info-tooltip', - role: 'tooltip', - } - ) - const infoWrap = this.el('span', '', { class: 'info-wrap' }) - infoWrap.append(infoIcon, infoTooltip) - - label.append(infoWrap) - customSection.appendChild(label) - - const row = this.el('div', '', { class: 'custom-url-row' }) - const input = this.el('input', '', { - class: 'custom-url-input', - type: 'text', - placeholder: 'Wallet API URL', - }) - const addBtn = this.el('button', 'Connect', { class: 'btn-add' }) - - const doConnect = () => { - const value = (input as HTMLInputElement).value - if (value.trim()) { - this.connectCustomUrl(value) - } - } - - addBtn.addEventListener('click', doConnect) - input.addEventListener('keydown', (e: Event) => { - if ((e as KeyboardEvent).key === 'Enter') doConnect() - }) - - row.append(input, addBtn) - customSection.appendChild(row) - container.appendChild(customSection) - - return container - } - - private renderConnecting(): HTMLElement { - const container = this.el('div', '', { - class: 'view-container', - }) - container.appendChild(this.renderHeader()) - container.appendChild( - this.el('div', 'Connecting...', { class: 'view-title' }) - ) - - const view = this.el('div', '', { class: 'status-view' }) - - if (this.wcUri) { - if (this.wcQrDataUrl) { - const qrImg = this.el('img', '', { - src: this.wcQrDataUrl, - alt: 'QR Code', - }) - qrImg.style.cssText = - 'display:block;margin:0 auto 12px;width:200px;height:200px;border-radius:8px;' - view.appendChild(qrImg) - } - - view.appendChild( - this.el( - 'h3', - this.wcQrDataUrl - ? 'Or paste this URI in your wallet' - : 'Paste this URI in your wallet' - ) - ) - - const code = this.el('code', this.wcUri) - code.style.cssText = - 'display:block;word-break:break-all;font-size:11px;' + - 'background:var(--wg-theme-background-color, #111);' + - 'padding:12px;border-radius:6px;margin:8px 0;' + - 'max-height:120px;overflow:auto;user-select:all;cursor:pointer;' - view.appendChild(code) - - const uri = this.wcUri - const copyBtn = this.el('button', 'Copy URI') - copyBtn.style.cssText = - 'padding:8px 16px;border-radius:4px;border:none;' + - 'background:#646cff;color:white;cursor:pointer;font-size:14px;margin-top:4px;' - copyBtn.addEventListener('click', () => { - navigator.clipboard.writeText(uri) - copyBtn.innerText = 'Copied!' - setTimeout(() => { - copyBtn.innerText = 'Copy URI' - }, 2000) - }) - view.appendChild(copyBtn) - } else { - view.appendChild(this.el('div', '', { class: 'spinner' })) - view.appendChild( - this.el( - 'h3', - 'Connecting to ' + (this.selectedEntry?.name || '') + '...' - ) - ) - view.appendChild( - this.el( - 'p', - this.selectedEntry?.type === 'remote' - ? 'Approve the connection in the wallet popup' - : 'Approve the connection in your extension' - ) - ) - } - - container.appendChild(view) - return container - } - - private renderConnected(): HTMLElement { - const container = this.el('div', '', { - class: 'view-container', - }) - container.appendChild(this.renderHeader()) - container.appendChild( - this.el('div', 'Connected', { class: 'view-title' }) - ) - - const view = this.el('div', '', { class: 'status-view' }) - - const icon = this.el('div', '', { class: 'success-icon' }) - icon.innerHTML = - '' - view.appendChild(icon) - - view.appendChild( - this.el( - 'h3', - 'Connected to ' + (this.selectedEntry?.name || 'wallet') - ) - ) - container.appendChild(view) - return container - } - - private renderError(): HTMLElement { - const container = this.el('div', '', { - class: 'view-container', - }) - container.appendChild(this.renderHeader()) - container.appendChild( - this.el('div', 'Connection Failed', { class: 'view-title' }) - ) - - const view = this.el('div', '', { class: 'status-view' }) - - const icon = this.el('div', '', { class: 'error-icon' }) - icon.innerHTML = - '' - view.appendChild(icon) - - view.appendChild(this.el('h3', 'Failed to connect')) - view.appendChild( - this.el('p', this.errorMessage || 'An unexpected error occurred') - ) - - const btnRow = this.el('div', '', { class: 'btn-row' }) - const retryBtn = this.el('button', 'Try Again', { - class: 'btn-primary', - type: 'button', - }) - retryBtn.addEventListener('click', () => this.goBackToList()) - const cancelBtn = this.el('button', 'Cancel', { - class: 'btn-secondary', - type: 'button', - }) - cancelBtn.addEventListener('click', () => window.close()) - btnRow.append(retryBtn, cancelBtn) - view.appendChild(btnRow) - - container.appendChild(view) - return container - } - - render(): void { - let content: HTMLElement - switch (this.state) { - case 'connecting': - content = this.renderConnecting() - break - case 'connected': - content = this.renderConnected() - break - case 'error': - content = this.renderError() - break - default: - content = this.renderList() - } - - if (this.shadowRoot) { - Array.from(this.shadowRoot.childNodes).forEach((node) => { - if (!(node instanceof HTMLStyleElement)) { - this.shadowRoot!.removeChild(node) - } - }) - this.shadowRoot.appendChild(content) - } - } - - connectedCallback(): void { - window.addEventListener('message', this.onOpenerStatusMessage) - this.render() - - // Listen for WalletConnect URI from the adapter via postMessage - window.addEventListener('message', (e) => { - if (e.data?.type === 'wc-uri' && typeof e.data.uri === 'string') { - this.wcUri = e.data.uri - this.wcQrDataUrl = e.data.qrDataUrl ?? null - if (this.state === 'connecting') this.render() - } - }) - } - - disconnectedCallback(): void { - window.removeEventListener('message', this.onOpenerStatusMessage) - } - - // ── DOM helpers ───────────────────────────────────────── - - private el( - tag: K, - text?: string, - attrs: Record = {} - ): HTMLElementTagNameMap[K] { - const element = document.createElement(tag) - if (text) element.innerText = text - for (const [key, val] of Object.entries(attrs)) { - element.setAttribute(key, val) - } - return element - } -} - -customElements.define('swk-wallet-picker', WalletPicker) diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/card/index.ts b/core/wallet-ui-components/src/components/wallet-picker/components/card/index.ts new file mode 100644 index 000000000..b623e2b9e --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/card/index.ts @@ -0,0 +1,120 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { WalletPickerEntry } from '@canton-network/core-types' +import { CSSResultGroup, html, LitElement, nothing } from 'lit' +import { customElement, property } from 'lit/decorators.js' +import { BaseElement } from '../../../../internal/base-element' +import styles from './styles' + +@customElement('wallet-picker-card') +export class WalletPickerCard extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + @property({ type: Object }) + entry!: WalletPickerEntry + + @property({ type: Boolean }) + isRemovable: boolean = true + + private validate() { + if (!this.entry) throw Error('Property `entry` must be passed') + } + + connectedCallback(): void { + this.validate() + } + + renderIcon() { + return this.entry.icon + ? html` ${this.entry.name} ` + : html` + ${this.entry.type === 'browser' + ? html` + ` + : html` + + + `} + ` + } + + renderRemoveButton() { + if (!this.isRemovable || !this.entry.url) return nothing + return html` + + ` + } + + render() { + return html` +
+
${this.renderIcon()}
+ ${this.entry.name} + ${this.renderRemoveButton()} +
+ ` + } + + private handleSelect() { + this.dispatchEvent( + new CustomEvent('select', { + detail: this.entry, + }) + ) + // TODO: trigger this.selectWallet(entry) + } + + private handleRemove(event: Event) { + event.stopPropagation() + this.dispatchEvent(new CustomEvent('remove')) + // TODO: trigger this.removeRecentGateway(entry.url!) + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/card/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/components/card/styles.ts new file mode 100644 index 000000000..032506340 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/card/styles.ts @@ -0,0 +1,103 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' +import commonStyles from '../../styles' + +export default css` + ${commonStyles} + + .wallet-remove-btn { + border: none; + background: transparent; + color: var(--wg-theme-text-secondary); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: color 0.15s ease; + flex-shrink: 0; + padding: 0; + width: 16px; + height: 16px; + } + + .wallet-remove-btn:hover { + color: var(--wg-theme-error-color); + } + + .wallet-remove-btn:focus-visible { + outline: 2px solid var(--wg-theme-accent-color); + outline-offset: 4px; + border-radius: 4px; + } + + .wallet-remove-btn svg { + width: 16px; + height: 16px; + } + + .wallet-card { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: 8px; + border: 1px solid var(--wg-theme-border-color); + background: var(--wg-theme-surface-color); + cursor: pointer; + transition: all 0.15s ease; + width: 100%; + text-align: left; + margin-bottom: 8px; + } + + .wallet-card:hover { + background: var(--wg-theme-surface-hover); + border-color: var(--wg-theme-accent-color); + } + + .wallet-card:focus-visible { + outline: 2px solid var(--wg-theme-accent-color); + outline-offset: 2px; + } + + .wallet-card:active { + transform: scale(0.99); + } + + .wallet-icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: var(--wg-theme-icon-bg); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; + } + + .wallet-icon img { + width: 32px; + height: 32px; + border-radius: 8px; + object-fit: cover; + } + + .wallet-icon svg { + width: 22px; + height: 22px; + color: var(--wg-theme-text-secondary); + } + + .wallet-name { + flex: 1; + min-width: 0; + font-size: 15px; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +` diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/connected/index.ts b/core/wallet-ui-components/src/components/wallet-picker/components/connected/index.ts new file mode 100644 index 000000000..ff3a11e92 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/connected/index.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CSSResultGroup, html, LitElement } from 'lit' +import { customElement, property } from 'lit/decorators.js' +import { BaseElement } from '../../../../internal/base-element' +import styles from './styles' + +@customElement('wallet-picker-connected') +export class WalletPickerConnected extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + @property({ type: String }) + entryName: string = '' + + render() { + return html` +
+ + + +
+

Connected to ${this.entryName || 'wallet'}

+ ` + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/connected/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/components/connected/styles.ts new file mode 100644 index 000000000..4c78df8c1 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/connected/styles.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' +import commonStyles from '../../styles' + +export default css` + ${commonStyles} + + .success-icon { + color: var(--wg-theme-success-color); + } +` diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/connecting/index.ts b/core/wallet-ui-components/src/components/wallet-picker/components/connecting/index.ts new file mode 100644 index 000000000..b72ed1ed4 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/connecting/index.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { WalletPickerEntry } from '@canton-network/core-types' +import { CSSResultGroup, html, LitElement } from 'lit' +import { customElement, property, state } from 'lit/decorators.js' +import { BaseElement } from '../../../../internal/base-element' +import styles from './styles' + +@customElement('wallet-picker-connecting') +export class WalletPickerConnecting extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + @property() + wcUri = '' + + @property() + wcQrDataUrl = '' + + @property({ type: Object }) + entry: WalletPickerEntry | null = null + + @state() + copyButtonClicked = false + + render() { + return this.wcUri + ? html` + ${this.wcQrDataUrl && + html` QR Code `} +

+ ${this.wcQrDataUrl + ? 'Or paste this URI in your wallet' + : 'Paste this URI in your wallet'} +

+ ${this.wcUri} + + ` + : html` +
+

Connecting to ${this.entry?.name || ''}...

+

+ ${this.entry?.type === 'remote' + ? 'Approve the connection in the wallet popup' + : 'Approve the connection in your extension'} +

+ ` + } + + private handleCopy() { + navigator.clipboard.writeText(this.wcUri) + this.copyButtonClicked = true + setTimeout(() => { + this.copyButtonClicked = false + }, 2000) + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/connecting/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/components/connecting/styles.ts new file mode 100644 index 000000000..244c81eee --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/connecting/styles.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' +import commonStyles from '../../styles' + +export default css` + ${commonStyles} + + code { + display: block; + word-break: break-all; + font-size: 11px; + background: var(--wg-theme-background-color, #111); + padding: 12px; + border-radius: 6px; + margin: 8px 0; + max-height: 120px; + overflow: auto; + user-select: all; + cursor: pointer; + } + + img { + display: block; + margin: 0 auto 12px; + width: 200px; + height: 200px; + border-radius: 8px; + } + + button { + padding: 8px 16px; + border-radius: 4px; + border: none; + background: #646cff; + color: white; + cursor: pointer; + font-size: 14px; + margin-top: 4px; + } + + .spinner { + width: 36px; + height: 36px; + border: 3px solid var(--wg-theme-border-color); + border-top-color: var(--wg-theme-accent-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } +` diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/error/index.ts b/core/wallet-ui-components/src/components/wallet-picker/components/error/index.ts new file mode 100644 index 000000000..10c1f5c89 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/error/index.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CSSResultGroup, html, LitElement } from 'lit' +import { customElement, property } from 'lit/decorators.js' +import { BaseElement } from '../../../../internal/base-element' +import styles from './styles' + +@customElement('wallet-picker-error') +export class WalletPickerError extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + @property() message = '' + + render() { + return html` +
+ + + + + +
+

Failed to connect

+

${this.message || 'An unexpected error occurred'}

+ +
+ + + +
+ ` + } + + private goBackToList() { + this.dispatchEvent(new CustomEvent('retry')) + } + + private cancel() { + window.close() + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/error/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/components/error/styles.ts new file mode 100644 index 000000000..c95591536 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/error/styles.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' +import commonStyles from '../../styles' + +export default css` + ${commonStyles} + + .error-icon { + color: var(--wg-theme-error-color); + } + + .btn-row { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .btn-primary { + background: var(--wg-theme-primary-color); + color: var(--wg-theme-primary-text-color); + border: none; + border-radius: 8px; + padding: 10px 24px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; + } + + .btn-primary:hover { + background: var(--wg-theme-primary-hover); + } + + .btn-secondary { + background: transparent; + color: var(--wg-theme-text-secondary); + border: 1px solid var(--wg-theme-border-color); + border-radius: 8px; + padding: 10px 24px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + } +` diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/header/index.ts b/core/wallet-ui-components/src/components/wallet-picker/components/header/index.ts new file mode 100644 index 000000000..564349f8f --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/header/index.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CSSResultGroup, html, LitElement } from 'lit' +import { customElement } from 'lit/decorators.js' +import { BaseElement } from '../../../../internal/base-element' +import styles from './styles' + +@customElement('wallet-picker-header') +export class WalletPickerHeader extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + constructor() { + super() + } + + render() { + return html` +
+ +
+ ` + } + + private get logo() { + return 'data:image/png;base64,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' + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/header/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/components/header/styles.ts new file mode 100644 index 000000000..07b4c0b90 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/header/styles.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' +import commonStyles from '../../styles' + +export default css` + ${commonStyles} + + .header { + height: 40px; + padding: 0 24px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--wg-theme-border-color); + } + + .header-logo { + width: 28px; + height: 28px; + } +` diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/list/index.ts b/core/wallet-ui-components/src/components/wallet-picker/components/list/index.ts new file mode 100644 index 000000000..12fcb4a87 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/list/index.ts @@ -0,0 +1,86 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { WalletPickerEntry } from '@canton-network/core-types' +import { CSSResultGroup, html, LitElement } from 'lit' +import { customElement, property } from 'lit/decorators.js' +import { BaseElement } from '../../../../internal/base-element' +import styles from './styles' + +@customElement('wallet-picker-list') +export class WalletPickerList extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + @property({ type: Array }) + entries: WalletPickerEntry[] = [] + + render() { + return html` +
+ ${ + this.entries.length + ? this.entries.map( + (entry) => html` + + ` + ) + : html` +
+

+ No wallets available +

+

+ Install a Canton wallet extension or enter + a Wallet Gateway URL below. +

+
+ ` + } +
+
+
+ CUSTOM WALLET + + + + +
+
+ + +
+
` + } + + private handleConnect() { + // TODO: call doConnect() + } + + private handleInputKeydown(event: Event) { + if ((event as KeyboardEvent).key === 'Enter') this.handleConnect() + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/components/list/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/components/list/styles.ts new file mode 100644 index 000000000..9709c35b0 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/components/list/styles.ts @@ -0,0 +1,139 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' +import commonStyles from '../../styles' + +export default css` + ${commonStyles} + + .wallet-list { + flex: 1; + overflow-y: auto; + padding: 4px 12px 0; + } + + .custom-url-section { + padding: 8px 12px 16px; + } + + .custom-url-label { + position: relative; + display: flex; + align-items: center; + gap: 6px; + width: 100%; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--wg-theme-text-color); + padding: 0 4px 8px; + } + + .custom-url-label .info-wrap { + display: inline-flex; + align-items: center; + } + + .custom-url-label .info-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + color: var(--wg-theme-text-secondary); + border: none; + background: transparent; + padding: 0; + cursor: pointer; + } + + .custom-url-label .info-icon:focus-visible { + outline: 2px solid var(--wg-theme-accent-color); + border-radius: 999px; + } + + .custom-url-label .info-tooltip { + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + z-index: 20; + width: max-content; + max-width: min(320px, 90vw); + padding: 8px 10px; + border: none; + border-radius: 10px; + background: var(--wg-theme-primary-color); + color: var(--wg-theme-primary-text-color); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.22); + font-size: 12px; + font-weight: 500; + line-height: 1.4; + text-transform: none; + letter-spacing: normal; + white-space: normal; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.12s ease; + } + + .custom-url-label .info-wrap:hover .info-tooltip { + opacity: 1; + visibility: visible; + } + + .custom-url-row { + display: flex; + gap: 8px; + align-items: center; + } + + .custom-url-input { + flex: 1; + padding: 10px 14px; + border: 1px solid var(--wg-theme-border-color); + border-radius: 8px; + font-size: 14px; + outline: none; + background: var(--wg-theme-surface-color); + color: var(--wg-theme-text-color); + } + + .custom-url-input:focus { + border-color: var(--wg-theme-accent-color); + box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15); + } + + .custom-url-input::placeholder { + color: var(--wg-theme-text-secondary); + } + + .btn-add { + background: var(--wg-theme-primary-color); + color: var(--wg-theme-primary-text-color); + border: none; + border-radius: 20px; + padding: 10px 24px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; + } + + .btn-add:hover { + background: var(--wg-theme-primary-hover); + } + + .btn-add:disabled { + opacity: 0.5; + cursor: default; + } + + .empty-state { + color: var(--wg-theme-text-secondary); + } +` diff --git a/core/wallet-ui-components/src/components/wallet-picker/index.ts b/core/wallet-ui-components/src/components/wallet-picker/index.ts new file mode 100644 index 000000000..707384ae6 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/index.ts @@ -0,0 +1,294 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CSSResultGroup, html, LitElement } from 'lit' +import { BaseElement } from '../../internal/base-element' +import { WalletPickerEntry } from '@canton-network/core-types' +import { customElement } from 'lit/decorators.js' +import styles from './styles' +import './components/list' +import './components/connecting' +import './components/connected' +import './components/error' + +export type { + WalletPickerEntry, + WalletPickerResult, +} from '@canton-network/core-types' + +/** + * — a wallet selection component modelled after PartyLayer's + * WalletModal. Designed for popup rendering (same pattern as ). + * + * IMPORTANT: Because the popup serialises this class via .toString() and runs it + * inside a blob URL, every helper the class uses must be either: + * (a) a method / property on the class itself, or + * (b) a string literal inlined where it is used. + * Top-level module constants are NOT available at runtime in the popup. + * + * Communication: + * - Reads wallet entries from localStorage key `splice_wallet_picker_entries` + * - Posts a WalletPickerResult to window.opener via postMessage on selection + * + * States: list → connecting → connected | error + */ +@customElement('swk-wallet-picker') +export class WalletPicker extends LitElement { + static styles: CSSResultGroup = [BaseElement.styles, styles] + + private readonly RECENT_KEY = 'splice_wallet_picker_recent' + + private entries: WalletPickerEntry[] = [] + private recentGateways: { name: string; rpcUrl: string }[] = [] + private state: 'list' | 'connecting' | 'connected' | 'error' = 'list' + private selectedEntry: WalletPickerEntry | null = null + private errorMessage = '' + + private wcUri: string | null = null + private wcQrDataUrl: string | null = null + + private readonly onOpenerStatusMessage = (event: MessageEvent): void => { + if (event.origin !== window.location.origin) return + + const data = event.data + if (data?.messageType !== 'SPLICE_WALLET_PICKER_CONNECT_STATUS') return + + if (data.status === 'connected') { + this.setConnected() + return + } + + if (data.status === 'error') { + const message = + typeof data.message === 'string' && data.message.length > 0 + ? data.message + : 'Failed to connect wallet' + this.setError(message) + } + } + + // constructor() { + // super() + // const ctor = this.constructor as typeof HTMLElement & { + // styles?: string + // } + // if (ctor.styles) { + // const style = document.createElement('style') + // style.textContent = ctor.styles + // this.shadowRoot!.appendChild(style) + // } + // } + + // ── localStorage helpers (inlined so they survive .toString() serialisation) ── + + private loadRecentGateways(): { name: string; rpcUrl: string }[] { + try { + const raw = localStorage.getItem(this.RECENT_KEY) + if (raw) return JSON.parse(raw) + } catch { + // ignore + } + return [] + } + + private removeRecentGateway(rpcUrl: string): void { + this.recentGateways = this.loadRecentGateways().filter( + (r) => r.rpcUrl !== rpcUrl + ) + + if (this.recentGateways.length === 0) { + localStorage.removeItem(this.RECENT_KEY) + } else { + localStorage.setItem( + this.RECENT_KEY, + JSON.stringify(this.recentGateways) + ) + } + + this.render() + } + + private get viewTitle() { + switch (this.state) { + case 'connected': + return 'Connected' + case 'connecting': + return 'Connecting...' + case 'error': + return 'Connection Failed' + default: + return 'Connect a Wallet' + } + } + + private loadEntries(): void { + const stored = localStorage.getItem('splice_wallet_picker_entries') + if (!stored) return + try { + this.entries = JSON.parse(stored) + } catch { + this.entries = [] + } + } + + private getAllEntries(): WalletPickerEntry[] { + // Merge all entries into a single flat list: + // 1. Registered entries (extensions + gateways from discovery) + // 2. Recent gateways not already in the registered list + const knownUrls = new Set( + this.entries + .filter((e) => e.type === 'remote' && e.url) + .map((e) => e.url) + ) + + const recentEntries: WalletPickerEntry[] = this.recentGateways + .filter((r) => !knownUrls.has(r.rpcUrl)) + .map((r) => ({ + providerId: 'remote:' + r.rpcUrl, + name: r.name, + type: 'remote' as const, + url: r.rpcUrl, + reuseGlobalWalletPopup: true, + })) + + return [...this.entries, ...recentEntries] + } + + private isRemovableEntry(entry: WalletPickerEntry): boolean { + if (entry.type !== 'remote' || !entry.url) { + return false + } + + const isRegisteredEntry = this.entries.some( + (knownEntry) => + knownEntry.type === 'remote' && knownEntry.url === entry.url + ) + const isManualEntry = this.recentGateways.some( + (recentEntry) => recentEntry.rpcUrl === entry.url + ) + + return isManualEntry && !isRegisteredEntry + } + + // ── Actions ───────────────────────────────────────────── + + private selectWallet(entry: WalletPickerEntry): void { + this.selectedEntry = entry + this.state = 'connecting' + this.render() + + if (window.opener) { + window.opener.postMessage( + { + messageType: 'SPLICE_WALLET_PICKER_RESULT', + providerId: entry.providerId, + name: entry.name, + walletType: entry.type, + url: entry.url, + reuseGlobalWalletPopup: entry.reuseGlobalWalletPopup, + }, + '*' + ) + } + } + + private connectCustomUrl(rpcUrl: string): void { + const trimmed = rpcUrl.trim() + if (!trimmed) return + + this.selectWallet({ + providerId: 'remote:' + trimmed, + name: trimmed, + type: 'remote', + url: trimmed, + reuseGlobalWalletPopup: true, + }) + } + + public setConnected(): void { + this.state = 'connected' + this.render() + setTimeout(() => { + if (window.opener) window.close() + }, 1200) + } + + public setError(message: string): void { + this.errorMessage = message + this.state = 'error' + this.render() + } + + private goBackToList(): void { + this.selectedEntry = null + this.errorMessage = '' + this.state = 'list' + this.render() + } + + private handleWalletConnectURIChange(event: MessageEvent) { + if ( + event.data?.type === 'wc-uri' && + typeof event.data.uri === 'string' + ) { + this.wcUri = event.data.uri + this.wcQrDataUrl = event.data.qrDataUrl ?? null + if (this.state === 'connecting') this.render() + } + } + + render() { + const allEntries = this.getAllEntries() + + return html` +
+ +
${this.viewTitle}
+ + ${this.state === 'list' || !this.state + ? html`` + : html` +
+ ${this.state === 'connecting' && + html``} + ${this.state === 'connected' && + html``} + ${this.state === 'error' && + html``} +
+ `} +
+ ` + } + + connectedCallback(): void { + super.connectedCallback() + this.loadEntries() + this.recentGateways = this.loadRecentGateways() + window.addEventListener('message', this.onOpenerStatusMessage) + + window.addEventListener('errorRetry', this.goBackToList) + + // Listen for WalletConnect URI from the adapter via postMessage + window.addEventListener('message', this.handleWalletConnectURIChange) + } + + disconnectedCallback(): void { + super.disconnectedCallback() + window.removeEventListener('message', this.onOpenerStatusMessage) + + window.removeEventListener('errorRetry', this.goBackToList) + + window.removeEventListener('message', this.handleWalletConnectURIChange) + } +} diff --git a/core/wallet-ui-components/src/components/wallet-picker/styles.ts b/core/wallet-ui-components/src/components/wallet-picker/styles.ts new file mode 100644 index 000000000..af1964c58 --- /dev/null +++ b/core/wallet-ui-components/src/components/wallet-picker/styles.ts @@ -0,0 +1,53 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { css } from 'lit' + +const commonStyles = css` + * { + box-sizing: border-box; + font-family: var(--wg-theme-font-family); + color: var(--wg-theme-text-color); + } + + .status-view { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 24px; + gap: 16px; + text-align: center; + flex: 1; + } + + .status-view h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + } + + .status-view p { + margin: 0; + font-size: 14px; + color: var(--wg-theme-text-secondary); + } +` +export default commonStyles + +export const componentStyles = css` + ${commonStyles} + + .view-container { + display: flex; + flex-direction: column; + height: 100%; + } + + .view-title { + font-size: 20px; + font-weight: 600; + padding: 16px 24px 12px; + color: var(--wg-theme-text-color); + } +` diff --git a/core/wallet-ui-components/src/index.ts b/core/wallet-ui-components/src/index.ts index 775450fb8..faca28b49 100644 --- a/core/wallet-ui-components/src/index.ts +++ b/core/wallet-ui-components/src/index.ts @@ -9,7 +9,7 @@ export * from './components/copy-button.js' export * from './components/error-page.js' export * from './components/loading-state.js' export * from './components/pagination.js' -export * from './components/wallet-picker.js' +export * from './components/wallet-picker/index.js' export * from './components/form-input.js' export * from './components/idp-card.js' export * from './components/idp-form.js' diff --git a/core/wallet-ui-components/src/shims.d.ts b/core/wallet-ui-components/src/shims.d.ts new file mode 100644 index 000000000..ecb646148 --- /dev/null +++ b/core/wallet-ui-components/src/shims.d.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { WalletPicker } from './components/wallet-picker' +import { WalletPickerCard } from './components/wallet-picker/components/card' +import { WalletPickerConnected } from './components/wallet-picker/components/connected' +import { WalletPickerConnecting } from './components/wallet-picker/components/connecting' +import { WalletPickerError } from './components/wallet-picker/components/error' +import { WalletPickerList } from './components/wallet-picker/components/list' + +declare global { + interface HTMLElementTagNameMap { + 'wallet-picker-error': WalletPickerError + 'wallet-picker-list': WalletPickerList + 'wallet-picker-connecting': WalletPickerConnecting + 'wallet-picker-connected': WalletPickerConnected + 'wallet-picker-card': WalletPickerCard + 'swk-wallet-picker': WalletPicker + } +} diff --git a/core/wallet-ui-components/src/windows/popup.ts b/core/wallet-ui-components/src/windows/popup.ts index efb84dd49..98cdaf131 100644 --- a/core/wallet-ui-components/src/windows/popup.ts +++ b/core/wallet-ui-components/src/windows/popup.ts @@ -1,6 +1,8 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { CSSResultGroup } from 'lit' + interface PopupOptions { title?: string target?: string @@ -12,7 +14,7 @@ interface PopupOptions { interface StyledElement { new (): HTMLElement - styles: string + styles: string | CSSResultGroup } let globalPopupInstance: WindowProxy | undefined @@ -81,7 +83,9 @@ class PopupInstance { const { title = 'Custom Popup' } = options || {} // Extract and safely escape styles for use in template literal within