rspace-online/modules/rcart/components/folk-payment-page.ts

673 lines
25 KiB
TypeScript

/**
* <folk-payment-page> — Public payment page for QR code payment requests.
*
* Three-tab layout:
* 1. Card — Email → Transak iframe for fiat-to-crypto
* 2. Wallet — EIP-6963 wallet discovery → ERC-20 transfer
* 3. EncryptID — Passkey auth → derive EOA → sign tx
*
* Polls GET /api/payments/:id for status updates.
*/
class FolkPaymentPage extends HTMLElement {
private shadow: ShadowRoot;
private space = 'default';
private paymentId = '';
private payment: any = null;
private loading = true;
private error = '';
private activeTab: 'card' | 'wallet' | 'encryptid' = 'card';
private pollTimer: ReturnType<typeof setInterval> | null = null;
// Card tab state
private cardEmail = '';
private cardLoading = false;
private transakUrl = '';
// Wallet tab state
private walletProviders: any[] = [];
private walletDiscovery: any = null;
private walletConnected = false;
private walletAccount = '';
private walletSending = false;
private walletTxHash = '';
private walletError = '';
private selectedProviderUuid = '';
// EncryptID tab state
private eidSigning = false;
private eidTxHash = '';
private eidError = '';
// Editable amount (for amountEditable payments)
private customAmount = '';
// QR state
private qrDataUrl = '';
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.space = this.getAttribute('space') || 'default';
this.paymentId = this.getAttribute('payment-id') || '';
this.loadPayment();
this.startPolling();
this.discoverWallets();
}
disconnectedCallback() {
this.stopPolling();
this.walletDiscovery?.stop?.();
}
private getApiBase(): string {
const path = window.location.pathname;
const match = path.match(/^(\/[^/]+)?\/rcart/);
return match ? match[0] : '/rcart';
}
private async loadPayment() {
this.loading = true;
this.render();
try {
const res = await fetch(`${this.getApiBase()}/api/payments/${this.paymentId}`);
if (!res.ok) throw new Error('Payment not found');
this.payment = await res.json();
this.generateQR();
} catch (e) {
this.error = e instanceof Error ? e.message : 'Failed to load payment';
}
this.loading = false;
this.render();
}
private startPolling() {
this.pollTimer = setInterval(async () => {
if (!this.paymentId) return;
try {
const res = await fetch(`${this.getApiBase()}/api/payments/${this.paymentId}`);
if (res.ok) {
const updated = await res.json();
if (updated.status !== this.payment?.status) {
this.payment = updated;
this.render();
}
}
} catch { /* ignore poll errors */ }
}, 5000);
}
private stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
private async generateQR() {
try {
const QRCode = await import('qrcode');
const payUrl = `${window.location.origin}/${this.space}/rcart/pay/${this.paymentId}`;
this.qrDataUrl = await QRCode.toDataURL(payUrl, { margin: 2, width: 200 });
} catch { /* QR generation optional */ }
}
// ── Wallet discovery (EIP-6963) ──
private async discoverWallets() {
try {
const { WalletProviderDiscovery } = await import('../../../src/encryptid/eip6963');
this.walletDiscovery = new WalletProviderDiscovery();
this.walletDiscovery.onProvidersChanged((providers: any[]) => {
this.walletProviders = providers;
this.render();
});
this.walletDiscovery.start();
} catch { /* no wallet support */ }
}
// ── Card tab: Transak ──
private async startTransak() {
if (!this.cardEmail) return;
this.cardLoading = true;
this.render();
try {
const res = await fetch(`${this.getApiBase()}/api/payments/${this.paymentId}/transak-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: this.cardEmail }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create session');
this.transakUrl = data.widgetUrl;
// Listen for Transak postMessage events
window.addEventListener('message', this.handleTransakMessage);
} catch (e) {
this.error = e instanceof Error ? e.message : String(e);
}
this.cardLoading = false;
this.render();
}
private handleTransakMessage = (e: MessageEvent) => {
if (!e.data?.event_id) return;
if (e.data.event_id === 'TRANSAK_ORDER_SUCCESSFUL' || e.data.event_id === 'TRANSAK_ORDER_COMPLETED') {
const orderId = e.data.data?.id || e.data.data?.orderId;
this.updatePaymentStatus('paid', 'transak', null, orderId);
window.removeEventListener('message', this.handleTransakMessage);
}
};
// ── Wallet tab: EIP-6963 ──
private async connectWallet(uuid: string) {
const provider = this.walletDiscovery?.getProvider(uuid);
if (!provider) return;
this.selectedProviderUuid = uuid;
this.walletError = '';
try {
const { ExternalSigner } = await import('../../../src/encryptid/external-signer');
const signer = new ExternalSigner(provider.provider);
// Switch to correct chain
await signer.switchChain(this.payment.chainId);
const accounts = await signer.getAccounts();
if (accounts.length === 0) throw new Error('No accounts');
this.walletAccount = accounts[0];
this.walletConnected = true;
} catch (e) {
this.walletError = e instanceof Error ? e.message : String(e);
}
this.render();
}
private async sendWalletPayment() {
if (!this.walletConnected || !this.walletAccount) return;
const provider = this.walletDiscovery?.getProvider(this.selectedProviderUuid);
if (!provider) return;
this.walletSending = true;
this.walletError = '';
this.render();
try {
const { ExternalSigner } = await import('../../../src/encryptid/external-signer');
const signer = new ExternalSigner(provider.provider);
const p = this.payment;
const effectiveAmount = this.getEffectiveAmount();
if (!effectiveAmount || effectiveAmount === '0') throw new Error('Enter an amount');
let txHash: string;
if (p.token === 'ETH') {
// Native ETH transfer
const weiAmount = BigInt(Math.round(parseFloat(effectiveAmount) * 1e18));
txHash = await signer.sendTransaction({
from: this.walletAccount,
to: p.recipientAddress,
value: '0x' + weiAmount.toString(16),
chainId: String(p.chainId),
});
} else {
// ERC-20 transfer (USDC: 6 decimals)
const usdcAddress = p.usdcAddress;
if (!usdcAddress) throw new Error('USDC not supported on this chain');
const decimals = p.token === 'USDC' ? 6 : 18;
const rawAmount = BigInt(Math.round(parseFloat(effectiveAmount) * (10 ** decimals)));
// transfer(address to, uint256 amount) — selector: 0xa9059cbb
const recipient = p.recipientAddress.slice(2).toLowerCase().padStart(64, '0');
const amountHex = rawAmount.toString(16).padStart(64, '0');
const data = `0xa9059cbb${recipient}${amountHex}`;
txHash = await signer.sendTransaction({
from: this.walletAccount,
to: usdcAddress,
value: '0x0',
data,
chainId: String(p.chainId),
});
}
this.walletTxHash = txHash;
await this.updatePaymentStatus('paid', 'wallet', txHash);
} catch (e) {
this.walletError = e instanceof Error ? e.message : String(e);
}
this.walletSending = false;
this.render();
}
// ── EncryptID tab: Passkey-derived EOA ──
private async payWithEncryptID() {
this.eidSigning = true;
this.eidError = '';
this.render();
try {
// 1. Authenticate with passkey + PRF
const { authenticatePasskey } = await import('../../../src/encryptid/webauthn');
const { deriveEOAFromPRF } = await import('../../../src/encryptid/eoa-derivation');
const authResult = await authenticatePasskey();
if (!authResult.prfOutput) throw new Error('Passkey PRF not supported — try a different browser or device');
const eoa = deriveEOAFromPRF(new Uint8Array(authResult.prfOutput));
// 2. Sign transaction with viem
const { privateKeyToAccount } = await import('viem/accounts');
const { createWalletClient, http } = await import('viem');
const { base, baseSepolia, mainnet } = await import('viem/chains');
const chainMap: Record<number, any> = {
8453: base,
84532: baseSepolia,
1: mainnet,
};
const chain = chainMap[this.payment.chainId];
if (!chain) throw new Error(`Unsupported chain: ${this.payment.chainId}`);
const hexKey = ('0x' + Array.from(eoa.privateKey).map(b => b.toString(16).padStart(2, '0')).join('')) as `0x${string}`;
const account = privateKeyToAccount(hexKey);
const client = createWalletClient({
account,
chain,
transport: http(),
});
const p = this.payment;
const effectiveAmount = this.getEffectiveAmount();
if (!effectiveAmount || effectiveAmount === '0') throw new Error('Enter an amount');
let txHash: `0x${string}`;
if (p.token === 'ETH') {
txHash = await client.sendTransaction({
account,
to: p.recipientAddress as `0x${string}`,
value: BigInt(Math.round(parseFloat(effectiveAmount) * 1e18)),
chain,
});
} else {
// ERC-20 transfer
const usdcAddress = p.usdcAddress;
if (!usdcAddress) throw new Error('USDC not supported on this chain');
const decimals = p.token === 'USDC' ? 6 : 18;
const rawAmount = BigInt(Math.round(parseFloat(effectiveAmount) * (10 ** decimals)));
const recipient = p.recipientAddress.slice(2).toLowerCase().padStart(64, '0');
const amountHex = rawAmount.toString(16).padStart(64, '0');
const data = `0xa9059cbb${recipient}${amountHex}` as `0x${string}`;
txHash = await client.sendTransaction({
account,
to: usdcAddress as `0x${string}`,
value: 0n,
data,
chain,
});
}
// Zero the private key
eoa.privateKey.fill(0);
this.eidTxHash = txHash;
await this.updatePaymentStatus('paid', 'encryptid', txHash);
} catch (e) {
this.eidError = e instanceof Error ? e.message : String(e);
}
this.eidSigning = false;
this.render();
}
/** Get the effective payment amount — custom amount for editable payments, or the preset amount. */
private getEffectiveAmount(): string {
const p = this.payment;
if (p.amountEditable && this.customAmount) return this.customAmount;
if (p.amount && p.amount !== '0') return p.amount;
return this.customAmount || '0';
}
// ── Status update ──
private async updatePaymentStatus(status: string, method: string, txHash?: string | null, transakOrderId?: string | null) {
try {
await fetch(`${this.getApiBase()}/api/payments/${this.paymentId}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status,
paymentMethod: method,
txHash: txHash || undefined,
transakOrderId: transakOrderId || undefined,
}),
});
await this.loadPayment();
} catch { /* will be picked up by polling */ }
}
// ── Render ──
private render() {
this.shadow.innerHTML = `
<style>${this.getStyles()}</style>
<div class="payment-page">
${this.loading ? '<div class="loading">Loading payment...</div>' :
this.error && !this.payment ? `<div class="error">${this.esc(this.error)}</div>` :
this.payment ? this.renderPayment() : '<div class="error">Payment not found</div>'}
</div>`;
this.bindEvents();
}
private renderPayment(): string {
const p = this.payment;
const isPaid = p.status === 'paid' || p.status === 'confirmed';
const isExpired = p.status === 'expired';
const isCancelled = p.status === 'cancelled';
const isTerminal = isPaid || isExpired || isCancelled;
const chainNames: Record<number, string> = { 8453: 'Base', 84532: 'Base Sepolia', 1: 'Ethereum' };
const chainName = chainNames[p.chainId] || `Chain ${p.chainId}`;
const showAmountInput = p.amountEditable && p.status === 'pending';
const displayAmount = (!p.amount || p.amount === '0') && p.amountEditable ? 'Any amount' : `${p.amount} ${p.token}`;
return `
<div class="header">
<h1 class="title">Payment Request</h1>
<div class="status-badge status-${p.status}">${p.status}</div>
</div>
<div class="amount-display">
${showAmountInput ? `
<div class="editable-amount">
<input type="number" class="amount-input" data-field="custom-amount" step="0.01" min="0.01"
placeholder="${p.amount && p.amount !== '0' ? p.amount : 'Enter amount'}"
value="${this.customAmount || ''}" />
<span class="amount-token">${this.esc(p.token)}</span>
</div>
<span class="chain-info">on ${chainName}</span>
` : `
<span class="amount">${displayAmount}</span>
${p.fiatAmount ? `<span class="fiat-amount">\u2248 $${this.esc(p.fiatAmount)} ${this.esc(p.fiatCurrency)}</span>` : ''}
<span class="chain-info">on ${chainName}</span>
`}
</div>
<div class="description">${this.esc(p.description)}</div>
${isPaid ? this.renderPaidConfirmation() :
isTerminal ? `<div class="terminal-msg">${isExpired ? 'This payment request has expired.' : 'This payment request has been cancelled.'}</div>` :
this.renderPaymentTabs()}
<div class="footer">
${this.qrDataUrl ? `
<div class="qr-section">
<img class="qr-code" src="${this.qrDataUrl}" alt="QR Code" />
<div class="share-url">
<input type="text" value="${window.location.href}" readonly class="share-input" />
<button class="btn btn-sm" data-action="copy-url">Copy</button>
</div>
</div>` : ''}
</div>`;
}
private renderPaidConfirmation(): string {
const p = this.payment;
const explorerBase: Record<number, string> = {
8453: 'https://basescan.org/tx/',
84532: 'https://sepolia.basescan.org/tx/',
1: 'https://etherscan.io/tx/',
};
const explorer = explorerBase[p.chainId] || '';
return `
<div class="confirmation">
<div class="confirm-icon">&#10003;</div>
<h2>Payment Complete</h2>
<div class="confirm-details">
${p.paymentMethod ? `<div>Method: <strong>${p.paymentMethod}</strong></div>` : ''}
${p.txHash ? `<div>Transaction: <a href="${explorer}${p.txHash}" target="_blank" rel="noopener">${p.txHash.slice(0, 10)}...${p.txHash.slice(-8)}</a></div>` : ''}
${p.paid_at ? `<div>Paid: ${new Date(p.paid_at).toLocaleString()}</div>` : ''}
</div>
</div>`;
}
private renderPaymentTabs(): string {
return `
<div class="tabs">
<button class="tab ${this.activeTab === 'card' ? 'active' : ''}" data-tab="card">Card</button>
<button class="tab ${this.activeTab === 'wallet' ? 'active' : ''}" data-tab="wallet">Wallet</button>
<button class="tab ${this.activeTab === 'encryptid' ? 'active' : ''}" data-tab="encryptid">EncryptID</button>
</div>
<div class="tab-content">
${this.activeTab === 'card' ? this.renderCardTab() :
this.activeTab === 'wallet' ? this.renderWalletTab() :
this.renderEncryptIDTab()}
</div>`;
}
private renderCardTab(): string {
if (this.transakUrl) {
return `
<div class="transak-container">
<iframe src="${this.transakUrl}" class="transak-iframe" allow="camera;microphone;payment" frameborder="0"></iframe>
</div>`;
}
return `
<div class="tab-body">
<p class="tab-desc">Pay with credit or debit card via Transak.</p>
<div class="form-row">
<input type="email" placeholder="Your email address" class="input" data-field="card-email" value="${this.esc(this.cardEmail)}" />
</div>
<button class="btn btn-primary" data-action="start-transak" ${this.cardLoading ? 'disabled' : ''}>
${this.cardLoading ? 'Loading...' : 'Pay with Card'}
</button>
${this.error ? `<div class="field-error">${this.esc(this.error)}</div>` : ''}
</div>`;
}
private renderWalletTab(): string {
if (this.walletTxHash) {
return `
<div class="tab-body">
<div class="success-msg">Transaction sent!</div>
<div class="tx-hash">Tx: ${this.walletTxHash.slice(0, 14)}...${this.walletTxHash.slice(-10)}</div>
</div>`;
}
if (this.walletConnected) {
return `
<div class="tab-body">
<p class="tab-desc">Connected: ${this.walletAccount.slice(0, 6)}...${this.walletAccount.slice(-4)}</p>
<button class="btn btn-primary" data-action="send-wallet" ${this.walletSending ? 'disabled' : ''}>
${this.walletSending ? 'Sending...' : `Send ${this.getEffectiveAmount() || '?'} ${this.payment.token}`}
</button>
${this.walletError ? `<div class="field-error">${this.esc(this.walletError)}</div>` : ''}
</div>`;
}
if (this.walletProviders.length === 0) {
return `
<div class="tab-body">
<p class="tab-desc">No wallets detected. Install MetaMask or another EIP-6963 compatible wallet.</p>
</div>`;
}
return `
<div class="tab-body">
<p class="tab-desc">Select a wallet to connect:</p>
<div class="wallet-list">
${this.walletProviders.map((p: any) => `
<button class="wallet-btn" data-wallet-uuid="${p.info.uuid}">
<img src="${p.info.icon}" alt="" class="wallet-icon" />
<span>${this.esc(p.info.name)}</span>
</button>
`).join('')}
</div>
${this.walletError ? `<div class="field-error">${this.esc(this.walletError)}</div>` : ''}
</div>`;
}
private renderEncryptIDTab(): string {
if (this.eidTxHash) {
return `
<div class="tab-body">
<div class="success-msg">Transaction sent!</div>
<div class="tx-hash">Tx: ${this.eidTxHash.slice(0, 14)}...${this.eidTxHash.slice(-10)}</div>
</div>`;
}
return `
<div class="tab-body">
<p class="tab-desc">Pay using your EncryptID passkey. Your signing key is derived locally from your passkey and never leaves your device.</p>
<button class="btn btn-primary" data-action="pay-encryptid" ${this.eidSigning ? 'disabled' : ''}>
${this.eidSigning ? 'Signing...' : 'Pay with Passkey'}
</button>
${this.eidError ? `<div class="field-error">${this.esc(this.eidError)}</div>` : ''}
</div>`;
}
private bindEvents() {
// Tab switching
this.shadow.querySelectorAll('[data-tab]').forEach((el) => {
el.addEventListener('click', () => {
this.activeTab = (el as HTMLElement).dataset.tab as any;
this.render();
});
});
// Custom amount for editable payments
const customAmtInput = this.shadow.querySelector('[data-field="custom-amount"]') as HTMLInputElement;
customAmtInput?.addEventListener('input', () => { this.customAmount = customAmtInput.value; });
// Card tab
const emailInput = this.shadow.querySelector('[data-field="card-email"]') as HTMLInputElement;
emailInput?.addEventListener('input', () => { this.cardEmail = emailInput.value; });
this.shadow.querySelector('[data-action="start-transak"]')?.addEventListener('click', () => this.startTransak());
// Wallet tab
this.shadow.querySelectorAll('[data-wallet-uuid]').forEach((el) => {
el.addEventListener('click', () => this.connectWallet((el as HTMLElement).dataset.walletUuid!));
});
this.shadow.querySelector('[data-action="send-wallet"]')?.addEventListener('click', () => this.sendWalletPayment());
// EncryptID tab
this.shadow.querySelector('[data-action="pay-encryptid"]')?.addEventListener('click', () => this.payWithEncryptID());
// Copy URL
this.shadow.querySelector('[data-action="copy-url"]')?.addEventListener('click', () => {
navigator.clipboard.writeText(window.location.href);
const btn = this.shadow.querySelector('[data-action="copy-url"]') as HTMLElement;
if (btn) { btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = 'Copy'; }, 2000); }
});
}
private getStyles(): string {
return `
:host { display: block; padding: 1.5rem; max-width: 560px; margin: 0 auto; }
* { box-sizing: border-box; }
.payment-page { }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.title { color: var(--rs-text-primary); font-size: 1.25rem; font-weight: 700; margin: 0; }
.status-badge { padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
.status-pending { background: rgba(251,191,36,0.15); color: #fbbf24; }
.status-paid, .status-confirmed { background: rgba(34,197,94,0.15); color: #4ade80; }
.status-expired { background: rgba(239,68,68,0.15); color: #f87171; }
.status-cancelled { background: rgba(156,163,175,0.15); color: #9ca3af; }
.amount-display { text-align: center; margin-bottom: 1rem; }
.amount { display: block; font-size: 2rem; font-weight: 700; color: var(--rs-text-primary); }
.fiat-amount { display: block; font-size: 0.875rem; color: var(--rs-text-secondary); margin-top: 0.25rem; }
.chain-info { display: block; font-size: 0.75rem; color: var(--rs-text-muted); margin-top: 0.25rem; }
.editable-amount { display: flex; align-items: center; justify-content: center; gap: 0.5rem; }
.amount-input { width: 160px; padding: 0.5rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-text-primary); font-size: 1.75rem; font-weight: 700; text-align: center; }
.amount-input:focus { outline: none; border-color: var(--rs-primary); }
.amount-input::placeholder { color: var(--rs-text-muted); font-weight: 400; font-size: 1rem; }
.amount-token { color: var(--rs-text-secondary); font-size: 1.25rem; font-weight: 600; }
.description { text-align: center; color: var(--rs-text-secondary); font-size: 0.9375rem; margin-bottom: 1.5rem; padding: 0.75rem; background: var(--rs-bg-surface); border-radius: 8px; border: 1px solid var(--rs-border); }
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--rs-border); margin-bottom: 1.5rem; }
.tab { flex: 1; padding: 0.75rem 1rem; border: none; background: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.875rem; font-weight: 500; border-bottom: 2px solid transparent; transition: all 0.15s; }
.tab:hover { color: var(--rs-text-primary); }
.tab.active { color: var(--rs-primary-hover); border-bottom-color: var(--rs-primary); }
.tab-content { min-height: 180px; }
.tab-body { }
.tab-desc { color: var(--rs-text-secondary); font-size: 0.875rem; margin: 0 0 1rem; line-height: 1.5; }
.form-row { margin-bottom: 1rem; }
.input { width: 100%; padding: 0.625rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; }
.input:focus { outline: none; border-color: var(--rs-primary); }
.btn { padding: 0.625rem 1.25rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 0.875rem; font-weight: 500; }
.btn:hover { border-color: var(--rs-border-strong); }
.btn-primary { background: var(--rs-primary-hover); border-color: var(--rs-primary); color: #fff; width: 100%; }
.btn-primary:hover { background: #4338ca; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
.wallet-list { display: flex; flex-direction: column; gap: 0.5rem; }
.wallet-btn { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 0.875rem; transition: border-color 0.15s; }
.wallet-btn:hover { border-color: var(--rs-primary); }
.wallet-icon { width: 28px; height: 28px; border-radius: 6px; }
.field-error { color: #f87171; font-size: 0.8125rem; margin-top: 0.75rem; }
.success-msg { color: #4ade80; font-size: 1rem; font-weight: 600; text-align: center; margin-bottom: 0.5rem; }
.tx-hash { color: var(--rs-text-secondary); font-size: 0.8125rem; text-align: center; font-family: monospace; }
.confirmation { text-align: center; padding: 2rem 1rem; }
.confirm-icon { font-size: 3rem; color: #4ade80; margin-bottom: 0.5rem; }
.confirmation h2 { color: var(--rs-text-primary); font-size: 1.25rem; margin: 0 0 1rem; }
.confirm-details { color: var(--rs-text-secondary); font-size: 0.875rem; line-height: 1.8; }
.confirm-details a { color: var(--rs-primary-hover); text-decoration: none; }
.confirm-details a:hover { text-decoration: underline; }
.terminal-msg { text-align: center; padding: 2rem; color: var(--rs-text-muted); font-size: 0.875rem; }
.transak-container { border-radius: 8px; overflow: hidden; border: 1px solid var(--rs-border); }
.transak-iframe { width: 100%; height: 600px; border: none; }
.footer { margin-top: 2rem; border-top: 1px solid var(--rs-border); padding-top: 1.5rem; }
.qr-section { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.qr-code { border-radius: 8px; background: #fff; padding: 8px; }
.share-url { display: flex; gap: 0.5rem; width: 100%; }
.share-input { flex: 1; padding: 0.5rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.75rem; font-family: monospace; }
.loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); }
.error { text-align: center; padding: 3rem; color: #f87171; }
`;
}
private esc(s: string): string {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
}
customElements.define('folk-payment-page', FolkPaymentPage);