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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test:e2e:debug": "playwright test --debug"
},
"dependencies": {
"@breeztech/breez-sdk-spark": "^0.11.0-dev3",
"@breeztech/breez-sdk-spark": "^0.11.0-dev4",
"@headlessui/react": "^1.7.17",
"@zxing/browser": "^0.1.5",
"@zxing/library": "^0.21.3",
Expand Down
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ const AppContent: React.FC = () => {
};

// Navigate to wallet after passkey connect
const handlePasskeyConnect = async (seed: Seed, walletName: string) => {
await sdk.connectWallet(seed, true, walletName);
const handlePasskeyConnect = async (seed: Seed, label: string) => {
await sdk.connectWallet(seed, true, label);
setCurrentScreen('wallet');
};

Expand Down
10 changes: 5 additions & 5 deletions src/hooks/useBreezSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export interface BreezSdkState {
}

export interface BreezSdkActions {
connectWallet: (seed: Seed, restore: boolean, passkeyWalletName?: string) => Promise<void>;
connectWallet: (seed: Seed, restore: boolean, passkeyLabel?: string) => Promise<void>;
refreshWalletData: (showLoading?: boolean) => Promise<void>;
fetchUnclaimedDeposits: () => Promise<void>;
handleLogout: () => Promise<void>;
Expand Down Expand Up @@ -212,7 +212,7 @@ export function useBreezSdk(
// Connection lifecycle
// ----------------------------------------

const connectWallet = useCallback(async (seed: Seed, restore: boolean, passkeyWalletName?: string) => {
const connectWallet = useCallback(async (seed: Seed, restore: boolean, passkeyLabel?: string) => {
let connectedSdk: BreezSdk | undefined;
try {
logger.info(LogCategory.SDK, 'Initiating wallet connection', { restore });
Expand Down Expand Up @@ -247,8 +247,8 @@ export function useBreezSdk(
logger.authSuccess(seed.type);
logger.info(LogCategory.SDK, 'Wallet connected successfully');

if (passkeyWalletName != null) {
setPasskeyMode(passkeyWalletName);
if (passkeyLabel != null) {
setPasskeyMode(passkeyLabel);
} else if (seed.type === 'mnemonic') {
saveMnemonic(seed.mnemonic);
}
Expand Down Expand Up @@ -376,7 +376,7 @@ export function useBreezSdk(
try {
setIsLoading(true);
const wallet = await getWallet();
await connectWallet(wallet.seed, false, wallet.name);
await connectWallet(wallet.seed, false, wallet.label);
} catch (e) {
logger.error(LogCategory.SDK, 'Failed to reconnect with passkey', { error: formatError(e) });
setError('Failed to authenticate with passkey. Please try again.');
Expand Down
88 changes: 44 additions & 44 deletions src/pages/PasskeyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import LoadingSpinner from '../components/LoadingSpinner';
import PageLayout from '../components/layout/PageLayout';
import { AlertCard } from '../components/AlertCard';
import { UploadIcon, CheckIcon, FingerprintIcon } from '../components/Icons';
import { getWallet, listWalletNames, storeWalletName } from '@/services/passkeyService';
import { getWallet, listLabels, storeLabel } from '@/services/passkeyService';
import { logger, LogCategory } from '@/services/logger';

interface PasskeyPageProps {
onWalletRestored: (seed: Seed, walletName: string) => void;
onWalletRestored: (seed: Seed, label: string) => void;
onBack: () => void;
}

Expand All @@ -20,25 +20,25 @@ const PasskeyPage: React.FC<PasskeyPageProps> = ({
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
const [walletNames, setWalletNames] = useState<string[]>([]);
const [selectedWalletName, setSelectedWalletName] = useState<string | null>(null);
const [labels, setLabels] = useState<string[]>([]);
const [selectedLabel, setSelectedLabel] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [manualWalletName, setManualWalletName] = useState('');
const [manualLabel, setManualLabel] = useState('');
const [showManualInput, setShowManualInput] = useState(false);

// Fetch wallet names after acknowledgment
// Fetch labels after acknowledgment
useEffect(() => {
if (!hasAcknowledged) return;
const autoCreate = async () => {
setIsConnecting(true);
try {
const w = await getWallet();
storeWalletName(w.name).catch((e) =>
logger.warn(LogCategory.AUTH, 'Failed to store wallet name', {
storeLabel(w.label).catch((e) =>
logger.warn(LogCategory.AUTH, 'Failed to store label', {
error: e instanceof Error ? e.message : String(e),
}),
);
onWalletRestored(w.seed, w.name);
onWalletRestored(w.seed, w.label);
} catch (e) {
setError('Failed to set up wallet');
logger.error(LogCategory.AUTH, 'Auto-create wallet failed', {
Expand All @@ -48,54 +48,54 @@ const PasskeyPage: React.FC<PasskeyPageProps> = ({
}
};

const fetchWalletNames = async () => {
const fetchLabels = async () => {
setIsLoading(true);
try {
const names = await listWalletNames();
setWalletNames(names);
const labels = await listLabels();
setLabels(labels);

if (names.length === 0) {
if (labels.length === 0) {
// No wallets — auto-create default
setIsLoading(false);
await autoCreate();
return;
}

// Pre-select "Default" if present, otherwise first
const defaultIdx = names.indexOf('Default');
setSelectedWalletName(defaultIdx !== -1 ? names[defaultIdx] : names[0]);
const defaultIdx = labels.indexOf('Default');
setSelectedLabel(defaultIdx !== -1 ? labels[defaultIdx] : labels[0]);
} catch (e) {
setError('Failed to discover wallets');
logger.error(LogCategory.AUTH, 'Failed to list wallet names', {
setError('Failed to discover labels');
logger.error(LogCategory.AUTH, 'Failed to list labels', {
error: e instanceof Error ? e.message : String(e),
});
} finally {
setIsLoading(false);
}
};

fetchWalletNames();
fetchLabels();
}, [hasAcknowledged, onWalletRestored]);

const handleConnect = async () => {
const manualName = manualWalletName.trim();
const nameToUse = manualName || selectedWalletName;
if (!nameToUse) return;
const label = manualLabel.trim();
const labelToUse = label || selectedLabel;
if (!labelToUse) return;

setIsConnecting(true);
setError(null);

try {
if (manualName) {
storeWalletName(nameToUse).catch((e) =>
if (label) {
storeLabel(labelToUse).catch((e) =>
logger.warn(LogCategory.AUTH, 'Failed to store wallet name', {
error: e instanceof Error ? e.message : String(e),
}),
);
}
const w = await getWallet(nameToUse);
const w = await getWallet(labelToUse);
logger.info(LogCategory.AUTH, 'Passkey wallet derived');
onWalletRestored(w.seed, w.name);
onWalletRestored(w.seed, w.label);
} catch (e) {
setError('Failed to connect');
logger.error(LogCategory.AUTH, 'Passkey wallet restore failed', {
Expand Down Expand Up @@ -148,7 +148,7 @@ const PasskeyPage: React.FC<PasskeyPageProps> = ({
return (
<PageLayout onBack={onBack} footer={<div />} title="Passkey">
<div className="flex flex-col items-center justify-center h-full">
<LoadingSpinner text="Discovering wallets..." />
<LoadingSpinner text="Discovering labels..." />
</div>
</PageLayout>
);
Expand All @@ -165,7 +165,7 @@ const PasskeyPage: React.FC<PasskeyPageProps> = ({
}

// 1+ wallets — unified selection list + create option
const canConnect = !!(manualWalletName.trim() || selectedWalletName);
const canConnect = !!(manualLabel.trim() || selectedLabel);
const footer = (
<div className="max-w-xl mx-auto space-y-3">
<PrimaryButton
Expand All @@ -192,74 +192,74 @@ const PasskeyPage: React.FC<PasskeyPageProps> = ({

<div className="text-center mb-4">
<h2 className="text-xl font-display font-bold text-spark-text-primary mb-2">
Select Wallet
Select Label
</h2>
</div>

<div className="space-y-2">
{walletNames.map((name) => (
{labels.map((label) => (
<button
key={name}
key={label}
onClick={() => {
setSelectedWalletName(name);
setManualWalletName('');
setSelectedLabel(label);
setManualLabel('');
}}
className={`
w-full p-4 rounded-2xl border text-left transition-all
${selectedWalletName === name && !manualWalletName.trim()
${selectedLabel === label && !manualLabel.trim()
? 'bg-spark-primary/10 border-spark-primary'
: 'bg-spark-dark border-spark-border hover:border-spark-border-light'
}
`}
>
<div className="flex items-center justify-between">
<span className="font-display font-medium text-spark-text-primary">
{name}
{label}
</span>
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${selectedWalletName === name && !manualWalletName.trim() ? 'bg-spark-primary' : 'bg-transparent'}`}>
{selectedWalletName === name && !manualWalletName.trim() && (
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${selectedLabel === label && !manualLabel.trim() ? 'bg-spark-primary' : 'bg-transparent'}`}>
{selectedLabel === label && !manualLabel.trim() && (
<CheckIcon size="sm" className="text-white" />
)}
</div>
</div>
</button>
))}

{/* Create new wallet */}
{/* Create new label */}
{!showManualInput ? (
<button
type="button"
onClick={() => setShowManualInput(true)}
className="w-full p-4 rounded-2xl border bg-spark-dark border-spark-border hover:border-spark-border-light text-left transition-all"
>
<span className="text-sm font-medium text-spark-text-secondary">
Create a new wallet...
Create a new label...
</span>
</button>
) : (
<div
className={`
w-full p-4 rounded-2xl border transition-all
${manualWalletName.trim()
${manualLabel.trim()
? 'bg-spark-primary/10 border-spark-primary'
: 'bg-spark-dark border-spark-border'
}
`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-spark-text-secondary">
Create a new wallet
Create a new label
</span>
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${manualWalletName.trim() ? 'bg-spark-primary' : 'bg-transparent'}`}>
{manualWalletName.trim() && (
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${manualLabel.trim() ? 'bg-spark-primary' : 'bg-transparent'}`}>
{manualLabel.trim() && (
<CheckIcon size="sm" className="text-white" />
)}
</div>
</div>
<input
type="text"
value={manualWalletName}
onChange={(e) => setManualWalletName(e.target.value)}
value={manualLabel}
onChange={(e) => setManualLabel(e.target.value)}
placeholder="Wallet name"
className="w-full bg-spark-surface border border-spark-border rounded-xl px-3 py-2 text-spark-text-primary placeholder:text-spark-text-muted focus:outline-none focus:ring-2 focus:ring-spark-primary/50 focus:border-spark-primary text-sm"
autoFocus
Expand Down
38 changes: 19 additions & 19 deletions src/services/passkeyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { passkeyPrfProvider } from './passkeyPrfProvider';
import { logger, LogCategory } from './logger';

// Storage key — presence signals passkey mode
const PASSKEY_WALLET_NAME_KEY = 'passkeyWalletName';
const PASSKEY_LABEL_KEY = 'passkeyLabel';

// Singleton Passkey instance
let passkeyInstance: Passkey | null = null;
Expand Down Expand Up @@ -60,14 +60,14 @@ export async function isPrfAvailable(): Promise<boolean> {
* Passkey mode is signalled by a stored wallet name.
*/
export function isPasskeyMode(): boolean {
return localStorage.getItem(PASSKEY_WALLET_NAME_KEY) !== null;
return localStorage.getItem(PASSKEY_LABEL_KEY) !== null;
}

/**
* Set passkey mode by storing the wallet name.
*/
export function setPasskeyMode(walletName?: string): void {
localStorage.setItem(PASSKEY_WALLET_NAME_KEY, walletName ?? 'Default');
export function setPasskeyMode(label?: string): void {
localStorage.setItem(PASSKEY_LABEL_KEY, label ?? 'Default');
}

/**
Expand All @@ -76,43 +76,43 @@ export function setPasskeyMode(walletName?: string): void {
* still exists on the device and should be reused on next login.
*/
export function clearPasskeyMode(): void {
localStorage.removeItem(PASSKEY_WALLET_NAME_KEY);
localStorage.removeItem(PASSKEY_LABEL_KEY);
}

/**
* List available wallet names from nostr relays.
* List available labels from nostr relays.
*/
export async function listWalletNames(): Promise<string[]> {
logger.info(LogCategory.AUTH, 'Listing wallet names from nostr relays');
export async function listLabels(): Promise<string[]> {
logger.info(LogCategory.AUTH, 'Listing labels from nostr relays');
const passkey = getOrCreatePasskey();
return await passkey.listWalletNames();
return await passkey.listLabels();
}

/**
* Store a wallet name to nostr relays so it can be discovered later.
* Store a label to nostr relays so it can be discovered later.
*/
export async function storeWalletName(walletName: string): Promise<void> {
logger.info(LogCategory.AUTH, 'Storing wallet name to nostr relays');
export async function storeLabel(label: string): Promise<void> {
logger.info(LogCategory.AUTH, 'Storing label to nostr relays');
const passkey = getOrCreatePasskey();
await passkey.storeWalletName(walletName);
await passkey.storeLabel(label);
}

/**
* Derive a Wallet using passkey authentication.
*
* Falls back to saved wallet name from localStorage when no name arg provided.
* Falls back to saved label from localStorage when no name arg provided.
*
* @param walletName - Optional wallet name. If omitted, uses saved name or SDK default.
* @returns The derived Wallet object containing seed and name.
* @param label - Optional label. If omitted, uses saved name or SDK default.
* @returns The derived Wallet object containing seed and label.
*/
export async function getWallet(walletName?: string): Promise<Wallet> {
const effectiveName = walletName ?? localStorage.getItem(PASSKEY_WALLET_NAME_KEY) ?? undefined;
export async function getWallet(label?: string): Promise<Wallet> {
const effectiveLabel = label ?? localStorage.getItem(PASSKEY_LABEL_KEY) ?? undefined;

logger.info(LogCategory.AUTH, 'Deriving wallet via passkey');

const passkey = getOrCreatePasskey();
try {
const wallet = await passkey.getWallet(effectiveName);
const wallet = await passkey.getWallet(effectiveLabel);
logger.info(LogCategory.AUTH, 'Passkey wallet derived successfully');
return wallet;
} catch (e) {
Expand Down
Loading