Skip to content
This repository was archived by the owner on Mar 2, 2026. It is now read-only.
Draft
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
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# FRW-Extension Development Guide

## Commands

- Build dev: `pnpm build:dev`
- Build production: `pnpm build:pro`
- Lint: `pnpm lint` or `pnpm lint:fix`
- Format: `pnpm format` or `pnpm format:fix`
- Test: `pnpm test` (watch mode) or `pnpm test:run` (single run)
- Run specific test: `pnpm test <file-pattern>` or `pnpm test --testNamePattern="<test name>"`
- E2E tests: `pnpm test:e2e` or `pnpm test:e2e:ui` (with UI)

## Code Style

- **File naming**: Use snake-case for source files
- **Components**: Use TitleCase (PascalCase) for component names
- **Imports**: Group imports (builtin → external → internal → parent → sibling → index)
- **Types**: Prefer type imports, strict typing, avoid explicit 'any'
- **React**: Functional components, avoid direct DOM manipulation
- **Error handling**: Use try/catch blocks with specific error types
- **Structure**: UI components in src/ui/FRWComponent, hooks in src/ui/hooks, background services in src/background/service

This project is a Chrome extension Web3 wallet using React.js, TypeScript, MUI, and Firebase for authentication. Always use pnpm for package management.

## Project Structure

- src/ui: React components
- src/background: Background scripts and services
- src/content: Content scripts
- src/popup: Popup UI
- src/shared: Shared types andutilities
3 changes: 3 additions & 0 deletions _raw/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,9 @@
"Things_you_should_know": {
"message": "Things you should know"
},
"Passkey_Authentication": {
"message": "Passkey Authentication"
},
"Linked_Account": {
"message": "Linked Account"
},
Expand Down
3 changes: 3 additions & 0 deletions _raw/_locales/ja/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,9 @@
"Things_you_should_know": {
"message": "知っておくべきこと"
},
"Passkey_Authentication": {
"message": "パスキー認証"
},
"Linked_Account": {
"message": "リンクされたアカウント"
},
Expand Down
3 changes: 3 additions & 0 deletions _raw/_locales/ru/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,9 @@
"Things_you_should_know": {
"message": "Вещи, которые вы должны знать"
},
"Passkey_Authentication": {
"message": "Пассфайр аутентификация"
},
"Linked_Account": {
"message": "Связанный аккаунт"
},
Expand Down
3 changes: 3 additions & 0 deletions _raw/_locales/zh_CN/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,9 @@
"Things_you_should_know": {
"message": "你应该知道的事情"
},
"Passkey_Authentication": {
"message": "Passkey 认证"
},
"Linked_Account": {
"message": "关联账户"
},
Expand Down
150 changes: 148 additions & 2 deletions src/background/controller/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
newsService,
mixpanelTrack,
evmNftService,
passkeyService,
} from 'background/service';
import i18n from 'background/service/i18n';
import { type DisplayedKeryring, KEYRING_CLASS } from 'background/service/keyring';
Expand Down Expand Up @@ -879,7 +880,7 @@ export class WalletController extends BaseController {
return preferenceService.updateIsFirstOpen();
};
// userinfo
getUserInfo = async (forceRefresh: boolean) => {
getUserInfo = async (forceRefresh: boolean): Promise<UserInfoStore> => {
const data = await userInfoService.getUserInfo();

if (forceRefresh) {
Expand All @@ -893,7 +894,7 @@ export class WalletController extends BaseController {
return await this.fetchUserInfo();
};

fetchUserInfo = async () => {
fetchUserInfo = async (): Promise<UserInfoStore> => {
const result = await openapiService.userInfo();
const info = result['data'];
const avatar = this.addTokenForFirebaseImage(info.avatar);
Expand Down Expand Up @@ -3847,6 +3848,151 @@ export class WalletController extends BaseController {
clearEvmNFTList = async () => {
await evmNftService.clearEvmNfts();
};

// === Passkey Related Methods ===

/**
* Check if passkeys are enabled for the current user
*/
isPasskeyEnabled = async (): Promise<boolean> => {
return passkeyService.store.enabled;
};

/**
* Get available passkeys for the current user
*/
getAvailablePasskeys = async () => {
return await passkeyService.getAvailablePasskeys();
};

/**
* Delete a specific passkey
*/
deletePasskey = async (passkeyId: string): Promise<boolean> => {
return await passkeyService.deletePasskey(passkeyId);
};

/**
* Register a passkey credential created in the UI
*/
registerPasskeyCredential = async (
credentialId: string,
rawId: string,
password?: string
): Promise<boolean> => {
try {
// Create a registered credential object
const registeredCredential = {
id: credentialId,
rawId,
createdAt: new Date().toISOString(),
};

// Add to the passkey store
passkeyService.store.registeredCredentials.push(registeredCredential);
passkeyService.store.enabled = true;
passkeyService.store.lastUsed = new Date().toISOString();

// If password is provided, encrypt and store it
let passwordStored = false;
if (password) {
passwordStored = await passkeyService.storeEncryptedPassword(password);
}

// Save to storage
await storage.set('passkey', passkeyService.store);

// Track successful passkey registration
mixpanelTrack.track('passkey_created', {
success: true,
with_password: !!password && passwordStored,
source: 'wallet_controller',
});

return true;
} catch (error) {
console.error('Error registering passkey credential:', error);
mixpanelTrack.track('passkey_created', {
success: false,
error: error instanceof Error ? error.message : String(error),
source: 'wallet_controller',
});
return false;
}
};

/**
* Verify a passkey credential from the UI
*/
verifyPasskeyCredential = async (credentialId: string): Promise<boolean> => {
try {
// Check if the credential exists in our store
const matchedCredential = passkeyService.store.registeredCredentials.find(
(cred) => cred.id === credentialId
);

if (matchedCredential) {
// Update last used timestamp
passkeyService.store.lastUsed = new Date().toISOString();
await storage.set('passkey', passkeyService.store);

// Get the decrypted password if available
const password = await passkeyService.getDecryptedPassword();
let passwordDecrypted = false;

if (password) {
passwordDecrypted = true;

// Follow the same flow as the unlock method
await keyringService.submitPassword(password);

// Store the password temporarily in the password service
await passwordService.setPassword(password);

// Get the public key and switch login
const pubKey = await this.getPubKey();
await userWalletService.switchLogin(pubKey);

// Broadcast the unlock event
sessionService.broadcastEvent('unlock');
} else {
// If no password is available, fall back to just unlocking the keyring
keyringService.setUnlocked();

try {
const pubKey = await this.getPubKey();
await userWalletService.switchLogin(pubKey);
} catch (error) {
console.error('Error switching login after passkey authentication:', error);
// Even if switching login fails, the wallet is still unlocked
}
}

// Track successful passkey sign-in
mixpanelTrack.track('passkey_signin', {
success: true,
password_decrypted: passwordDecrypted,
});

return true;
}

mixpanelTrack.track('passkey_signin', {
success: false,
reason: 'credential_not_found',
});

return false;
} catch (error) {
console.error('Error verifying passkey credential:', error);
mixpanelTrack.track('passkey_signin', {
success: false,
error: error instanceof Error ? error.message : String(error),
});

return false;
}
};
}

export default new WalletController();
2 changes: 2 additions & 0 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
evmNftService,
googleSafeHostService,
passwordService,
passkeyService,
mixpanelTrack,
} from './service';
import { getFirbaseConfig } from './utils/firebaseConfig';
Expand Down Expand Up @@ -95,6 +96,7 @@ async function restoreAppState() {
keyringService.loadStore(keyringState);
keyringService.store.subscribe((value) => storage.set('keyringState', value));
await openapiService.init();
await passkeyService.init();

// clear premnemonic in storage
storage.remove('premnemonic');
Expand Down
1 change: 1 addition & 0 deletions src/background/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as userWalletService } from './userWallet';
export { default as proxyService } from './proxy';
export { default as transactionService } from './transaction';
export { default as passwordService } from './password';
export { default as passkeyService } from './passkey';
export { default as nftService } from './nft';
export { default as evmNftService } from './evmNfts';
export { default as googleDriveService } from './googleDrive';
Expand Down
Loading