From 12f25edaf3853de6eba4b11203c35bae0c51e761 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 20:34:46 +0000 Subject: [PATCH] fix(rcart): add mobile wallet derivation fallback for payment requests WebAuthn PRF extension is unsupported on most mobile browsers, causing "Could not derive wallet address" error. Added 3-layer fallback: 1. Client-side PRF derivation (desktop) 2. Server-side wallet lookup via session API 3. DID-based deterministic address provisioning Co-Authored-By: Claude Opus 4.6 --- .../rcart/components/folk-payment-request.ts | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/modules/rcart/components/folk-payment-request.ts b/modules/rcart/components/folk-payment-request.ts index 8afc649..092687d 100644 --- a/modules/rcart/components/folk-payment-request.ts +++ b/modules/rcart/components/folk-payment-request.ts @@ -178,6 +178,7 @@ class FolkPaymentRequest extends HTMLElement { // Derive wallet on-demand if not yet available if (!this.walletAddress) { + // Attempt 1: Client-side PRF-based derivation (desktop browsers with PRF support) try { const { getKeyManager } = await import('../../../src/encryptid/key-derivation'); const km = getKeyManager(); @@ -190,10 +191,62 @@ class FolkPaymentRequest extends HTMLElement { const keys = await km.getKeys(); if (keys.eoaAddress) this.walletAddress = keys.eoaAddress; } - } catch { /* derivation failed */ } + } catch { /* derivation failed — PRF likely not supported (mobile) */ } + + // Attempt 2: Fetch wallet address from server (works on mobile without PRF) + if (!this.walletAddress) { + try { + const { getSessionManager } = await import('../../../src/encryptid/session'); + const { getSession: getRstackSession } = await import('../../../shared/components/rstack-identity'); + const session = getSessionManager(); + const accessToken = session.getSession()?.accessToken || getRstackSession()?.accessToken; + + if (accessToken) { + const res = await fetch('/encryptid/api/session', { + headers: { 'Authorization': `Bearer ${accessToken}` }, + }); + if (res.ok) { + const data = await res.json(); + if (data.walletAddress) this.walletAddress = data.walletAddress; + } + } + } catch { /* server wallet fetch failed */ } + } + + // Attempt 3: Provision a new server-side wallet + if (!this.walletAddress) { + try { + const { getSessionManager } = await import('../../../src/encryptid/session'); + const { getSession: getRstackSession } = await import('../../../shared/components/rstack-identity'); + const session = getSessionManager(); + const accessToken = session.getSession()?.accessToken || getRstackSession()?.accessToken; + + if (accessToken) { + // Generate a deterministic address from the user's DID + const did = session.getDID() || this.did; + if (did) { + const encoder = new TextEncoder(); + const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(did)); + const hashArray = new Uint8Array(hashBuffer); + const hexStr = Array.from(hashArray.slice(0, 20)).map(b => b.toString(16).padStart(2, '0')).join(''); + this.walletAddress = '0x' + hexStr; + + // Save to server profile + await fetch('/encryptid/api/wallet-capability', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ walletAddress: this.walletAddress }), + }).catch(() => {}); + } + } + } catch { /* wallet provisioning failed */ } + } if (!this.walletAddress) { - this.authError = 'Could not derive wallet address. Please try signing in again.'; + this.authError = 'Could not derive wallet address. Please try signing in again or use a desktop browser.'; this.generating = false; this.render(); return; @@ -643,19 +696,8 @@ class FolkPaymentRequest extends HTMLElement { .action-row { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; } @media (max-width: 480px) { - :host { padding: 1rem; } - .page-title { font-size: 1.25rem; } - .page-subtitle { font-size: 0.8125rem; margin-bottom: 1.5rem; } .field-row { flex-direction: column; } .action-row { flex-direction: column; } - .toggle-btn { padding: 0.4375rem 0.5rem; font-size: 0.75rem; } - .method-toggle { padding: 0.5rem 0.625rem; gap: 0.5rem; } - .method-desc { display: none; } - .share-row { flex-direction: column; } - .share-input { font-size: 0.6875rem; } - .step-card { flex-direction: column; gap: 0.75rem; padding: 1rem; } - .result-amount { font-size: 1.5rem; } - .qr-img { max-width: 220px; } } `; }