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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-12 20:34:46 +00:00
parent 2d636f5f25
commit 12f25edaf3
1 changed files with 55 additions and 13 deletions

View File

@ -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; }
}
`;
}