Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
39 changes: 39 additions & 0 deletions src/components/StellarReceive.wallet.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* StellarReceive.wallet.integration.ts (feat/stellar-multi-wallet)
*
* Three-point merge guide: how to wire useStellarWallet into the
* existing StellarReceive.tsx to replace direct Freighter calls.
*
* Search for "── WALLET PATCH N ──" in your editor.
*/

// ── WALLET PATCH 1 ── Replace these existing imports:
//
// import { requestAccess, signTransaction, getPublicKey }
// from '@stellar/freighter-api';
//
// With:
import { useStellarWallet } from '@/hooks/useStellarWallet';
import { StellarWalletPicker } from '@/components/StellarWalletPicker';
import { StellarWalletButton } from '@/components/StellarWalletButton';

// ── WALLET PATCH 2 ── Inside the StellarReceive component function,
// replace the existing Freighter state + useEffect with:
//
// const walletState = useStellarWallet();
// const { publicKey, status, signTransaction, openPicker } = walletState;
//
// Replace every call to the Freighter API:
// requestAccess() → walletState.connect(walletState.walletId!)
// getPublicKey() → walletState.publicKey
// freighterSignTx(xdr, opts) → walletState.signTransaction(xdr, NETWORK_PASSPHRASE)

// ── WALLET PATCH 3 ── In the JSX, replace the old "Connect Freighter" button:
//
// <StellarWalletButton state={walletState} />
// <StellarWalletPicker state={walletState} />
//
// StellarWalletPicker renders null when pickerOpen=false, so it can sit
// anywhere in the component tree safely.

export {};
96 changes: 96 additions & 0 deletions src/components/StellarWalletButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* src/components/StellarWalletButton.tsx
*
* Compact wallet status button rendered in the Stellar chain header.
*
* States:
* - Disconnected → "Connect wallet" button → opens picker
* - Connecting → spinner
* - Connected → icon + truncated pubkey + disconnect option on click
*/

import { useState } from 'react';
import { WALLET_META } from '@/wallets/stellar';
import type { StellarWalletState } from '@/hooks/useStellarWallet';

interface Props {
state: StellarWalletState;
}

function truncate(key: string): string {
if (key.length <= 12) return key;
return `${key.slice(0, 6)}…${key.slice(-4)}`;
}

export function StellarWalletButton({ state }: Props) {
const { status, walletId, publicKey, openPicker, disconnect } = state;
const [showMenu, setShowMenu] = useState(false);

if (status === 'connecting') {
return (
<div className="flex items-center gap-2 px-3 py-1.5 border border-[#2a2a2a] text-xs text-[#767575]">
<svg className="animate-spin" width="12" height="12" viewBox="0 0 12 12" fill="none">
<circle cx="6" cy="6" r="4.5" stroke="currentColor" strokeWidth="1.5"
strokeDasharray="20" strokeDashoffset="8" strokeLinecap="round"/>
</svg>
Connecting…
</div>
);
}

if (status === 'connected' && walletId && publicKey) {
const meta = WALLET_META[walletId];
return (
<div className="relative">
<button
onClick={() => setShowMenu((v) => !v)}
className={[
'flex items-center gap-2 px-3 py-1.5 border text-xs transition-colors',
'border-[#2a2a2a] text-[#e6e1e5] hover:border-[#444444]',
].join(' ')}
aria-label="Wallet menu"
data-testid="wallet-connected-button"
>
<img src={meta.icon} alt={meta.name} width={14} height={14} className="rounded-sm" />
<span className="font-mono">{truncate(publicKey)}</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<polyline points="1,2 4,5 7,2"/>
</svg>
</button>

{showMenu && (
<div className="absolute right-0 top-full mt-1 z-20 bg-[#141414] border border-[#2a2a2a] w-44">
<div className="px-3 py-2 border-b border-[#1e1e1e]">
<p className="text-[10px] text-[#555555] uppercase tracking-wide">Connected via</p>
<p className="text-xs text-[#c4c7c5] mt-0.5">{meta.name}</p>
</div>
<button
onClick={async () => {
setShowMenu(false);
await disconnect();
}}
className="w-full text-left px-3 py-2 text-xs text-[#ee7d77] hover:bg-[#1a1a1a] transition-colors"
data-testid="wallet-disconnect-button"
>
Disconnect
</button>
</div>
)}
</div>
);
}

return (
<button
onClick={openPicker}
className={[
'px-3 py-1.5 border border-[#2a2a2a] text-xs text-[#c4c7c5]',
'hover:border-[#444444] hover:text-[#e6e1e5] transition-colors',
].join(' ')}
data-testid="wallet-connect-button"
>
Connect wallet
</button>
);
}
159 changes: 159 additions & 0 deletions src/components/StellarWalletPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* src/components/StellarWalletPicker.tsx
*
* Wallet selection modal shown when the user switches to the Stellar
* chain and no wallet is connected.
*
* Displays all supported wallets with:
* - Icon + name
* - "Installed" badge (green) or "Not detected" + install link (muted)
* - Loading spinner while detection is running
* - Error message if connect fails
*
* Albedo is always shown as available (web-based, no extension needed).
*/

import { useState } from 'react';
import { WALLET_IDS, WALLET_META, type WalletId } from '@/wallets/stellar';
import type { StellarWalletState } from '@/hooks/useStellarWallet';

interface Props {
state: StellarWalletState;
}

export function StellarWalletPicker({ state }: Props) {
const { pickerOpen, closePicker, connect, status, error, detecting, available } = state;

const [pending, setPending] = useState<WalletId | null>(null);

if (!pickerOpen) return null;

async function handleSelect(id: WalletId) {
if (pending) return;
setPending(id);
try {
await connect(id);
} finally {
setPending(null);
}
}

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
role="dialog"
aria-modal="true"
aria-label="Connect Stellar wallet"
>
<div className="bg-[#141414] border border-[#2a2a2a] w-full max-w-sm p-5 space-y-4">

{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold text-[#e6e1e5]">Connect Stellar wallet</h2>
<button
onClick={closePicker}
className="text-[#555555] hover:text-[#c4c7c5] transition-colors"
aria-label="Close"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<line x1="1" y1="1" x2="13" y2="13"/>
<line x1="13" y1="1" x2="1" y2="13"/>
</svg>
</button>
</div>

{/* Wallet list */}
<div className="space-y-2">
{WALLET_IDS.map((id) => {
const meta = WALLET_META[id];
const isAvail = available[id] ?? (id === 'albedo'); // albedo always available
const isLoading = pending === id;
const isDisabled = !!pending && pending !== id;

return (
<button
key={id}
data-testid={`wallet-option-${id}`}
onClick={() => handleSelect(id)}
disabled={isDisabled || isLoading}
className={[
'w-full flex items-center gap-3 px-3 py-2.5 border transition-colors',
isLoading
? 'border-[#c6c6c7] bg-[#1e1e1e] opacity-100'
: isDisabled
? 'border-[#1e1e1e] opacity-40 cursor-not-allowed'
: 'border-[#2a2a2a] hover:border-[#444444] hover:bg-[#1a1a1a] cursor-pointer',
].join(' ')}
>
{/* Icon */}
<img
src={meta.icon}
alt=""
width={28}
height={28}
className="shrink-0 rounded-sm"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>

{/* Name + status */}
<div className="flex-1 text-left min-w-0">
<p className="text-sm text-[#e6e1e5] font-medium">{meta.name}</p>
{detecting ? (
<p className="text-[11px] text-[#444444]">Detecting…</p>
) : isAvail ? (
<p className="text-[11px] text-[#22c55e]">Installed</p>
) : (
<a
href={meta.installUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[11px] text-[#555555] hover:text-[#767575] transition-colors"
onClick={(e) => e.stopPropagation()}
>
Not detected — install ↗
</a>
)}
</div>

{/* Right side: spinner or connect cue */}
<div className="shrink-0">
{isLoading ? (
<svg
className="animate-spin text-[#c6c6c7]"
width="14" height="14" viewBox="0 0 14 14" fill="none"
>
<circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5"
strokeDasharray="25" strokeDashoffset="10" strokeLinecap="round"/>
</svg>
) : (
<svg
className="text-[#333333]"
width="12" height="12" viewBox="0 0 12 12" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
>
<polyline points="4,2 8,6 4,10"/>
</svg>
)}
</div>
</button>
);
})}
</div>

{/* Error message */}
{error && status === 'error' && (
<p className="text-xs text-[#ee7d77] leading-relaxed">{error}</p>
)}

{/* Footer note */}
<p className="text-[10px] text-[#333333] leading-relaxed pt-1">
Albedo works in any browser — no extension needed. Other wallets require
their browser extension to be installed.
</p>
</div>
</div>
);
}
Loading