From 4e0b0f61b90c78b13038ea69f29673b3f1d5b612 Mon Sep 17 00:00:00 2001
From: Veenoway <77930262+Veenoway@users.noreply.github.com>
Date: Tue, 29 Jul 2025 17:22:03 +0200
Subject: [PATCH 1/4] refactor(Dialog): extract logic into custom hooks
---
packages/account-sdk/src/ui/Dialog/Dialog.tsx | 228 +++++++-----------
packages/account-sdk/src/ui/hooks/index.ts | 5 +
.../src/ui/hooks/useDragToDismiss.ts | 69 ++++++
.../account-sdk/src/ui/hooks/useMediaQuery.ts | 23 ++
.../src/ui/hooks/usePhonePortrait.ts | 7 +
.../account-sdk/src/ui/hooks/useUsername.ts | 46 ++++
6 files changed, 239 insertions(+), 139 deletions(-)
create mode 100644 packages/account-sdk/src/ui/hooks/index.ts
create mode 100644 packages/account-sdk/src/ui/hooks/useDragToDismiss.ts
create mode 100644 packages/account-sdk/src/ui/hooks/useMediaQuery.ts
create mode 100644 packages/account-sdk/src/ui/hooks/usePhonePortrait.ts
create mode 100644 packages/account-sdk/src/ui/hooks/useUsername.ts
diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.tsx
index 20df9942..70e5c3ed 100644
--- a/packages/account-sdk/src/ui/Dialog/Dialog.tsx
+++ b/packages/account-sdk/src/ui/Dialog/Dialog.tsx
@@ -2,48 +2,19 @@
import { clsx } from 'clsx';
import { FunctionComponent, render } from 'preact';
+import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
-import { getDisplayableUsername } from ':core/username/getDisplayableUsername.js';
-import { store } from ':store/store.js';
import { BaseLogo } from ':ui/assets/BaseLogo.js';
-import { useEffect, useMemo, useState } from 'preact/hooks';
+import { useDragToDismiss, usePhonePortrait, useUsername } from '../hooks/index.js';
import css from './Dialog-css.js';
const closeIcon = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzIDFMMSAxM20wLTEyTDEzIDEzIiBzdHJva2U9IiM5Q0EzQUYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+`;
-// Helper function to detect phone portrait mode
-function isPhonePortrait(): boolean {
- return window.innerWidth <= 600 && window.innerHeight > window.innerWidth;
-}
-
// Handle bar component for mobile bottom sheet
const DialogHandleBar: FunctionComponent = () => {
- const [showHandleBar, setShowHandleBar] = useState(false);
-
- useEffect(() => {
- // Only show handle bar on phone portrait mode
- const checkOrientation = () => {
- setShowHandleBar(isPhonePortrait());
- };
-
- // Initial check
- checkOrientation();
-
- // Listen for orientation/resize changes
- window.addEventListener('resize', checkOrientation);
- window.addEventListener('orientationchange', checkOrientation);
-
- return () => {
- window.removeEventListener('resize', checkOrientation);
- window.removeEventListener('orientationchange', checkOrientation);
- };
- }, []);
-
- if (!showHandleBar) {
- return null;
- }
-
- return
;
+ const isPhonePortrait = usePhonePortrait();
+
+ return isPhonePortrait ? : null;
};
export type DialogProps = {
@@ -86,7 +57,18 @@ export class Dialog {
this.render();
}
+ public dismissItem(key: number): void {
+ const item = this.items.get(key);
+ this.items.delete(key);
+ this.render();
+ item?.onClose?.();
+ }
+
public clear(): void {
+ // Call onClose for all items before clearing
+ for (const [, item] of this.items) {
+ item.onClose?.();
+ }
this.items.clear();
if (this.root) {
render(null, this.root);
@@ -94,84 +76,48 @@ export class Dialog {
}
private render(): void {
- if (this.root) {
- render(
-
-
- {Array.from(this.items.entries()).map(([key, itemProps]) => (
- {
- this.clear();
- itemProps.onClose?.();
- }}
- />
- ))}
-
-
,
- this.root
- );
- }
+ if (!this.root) return;
+
+ render(
+
+
+ {Array.from(this.items.entries()).map(([key, itemProps]) => (
+ {
+ this.dismissItem(key);
+ }}
+ />
+ ))}
+
+
,
+ this.root
+ );
}
}
-export const DialogContainer: FunctionComponent = (props) => {
- const [dragY, setDragY] = useState(0);
- const [isDragging, setIsDragging] = useState(false);
- const [startY, setStartY] = useState(0);
-
- // Touch event handlers for drag-to-dismiss (entire dialog area)
- const handleTouchStart = (e: any) => {
- // Only enable drag on mobile portrait mode
- if (!isPhonePortrait()) return;
-
- const touch = e.touches[0];
- setStartY(touch.clientY);
- setIsDragging(true);
- };
-
- const handleTouchMove = (e: any) => {
- if (!isDragging) return;
-
- const touch = e.touches[0];
- const deltaY = touch.clientY - startY;
-
- // Only allow dragging down (positive deltaY)
- if (deltaY > 0) {
- setDragY(deltaY);
- e.preventDefault(); // Prevent scrolling
+export const DialogContainer: FunctionComponent = ({ children }) => {
+ const handleDismiss = useCallback(() => {
+ // Find the dialog instance and trigger its close handler
+ const closeButton = document.querySelector(
+ '.-base-acc-sdk-dialog-instance-header-close'
+ ) as HTMLElement;
+ if (closeButton) {
+ closeButton.click();
}
- };
-
- const handleTouchEnd = () => {
- if (!isDragging) return;
-
- setIsDragging(false);
+ }, []);
- // Dismiss if dragged down more than 100px
- if (dragY > 100) {
- // Find the dialog instance and trigger its close handler
- const closeButton = document.querySelector(
- '.-base-acc-sdk-dialog-instance-header-close'
- ) as HTMLElement;
- if (closeButton) {
- closeButton.click();
- }
- } else {
- // Animate back to original position
- setDragY(0);
- }
- };
+ const { dragY, isDragging, handlers } = useDragToDismiss(handleDismiss);
return (
{
}}
>
- {props.children}
+ {children}
@@ -195,8 +141,7 @@ export const DialogInstance: FunctionComponent = ({
handleClose,
}) => {
const [hidden, setHidden] = useState(true);
- const [isLoadingUsername, setIsLoadingUsername] = useState(true);
- const [username, setUsername] = useState(null);
+ const { isLoading: isLoadingUsername, username } = useUsername();
useEffect(() => {
const timer = window.setTimeout(() => {
@@ -208,26 +153,36 @@ export const DialogInstance: FunctionComponent = ({
};
}, []);
- useEffect(() => {
- const fetchEnsName = async () => {
- const address = store.account.get().accounts?.[0];
-
- if (address) {
- const username = await getDisplayableUsername(address);
- setUsername(username);
- }
-
- setIsLoadingUsername(false);
- };
- fetchEnsName();
- }, []);
-
const headerTitle = useMemo(() => {
return username ? `Signed in as ${username}` : 'Base Account';
}, [username]);
const shouldShowHeaderTitle = !isLoadingUsername;
+ // Memoize action buttons
+ const actionButtons = useMemo(() => {
+ if (!actionItems?.length) return null;
+
+ return (
+
+ {actionItems.map((action, i) => (
+
+ ))}
+
+ );
+ }, [actionItems]);
+
return (
= ({
)}
-
+
+
- {actionItems && actionItems.length > 0 && (
-
- {actionItems.map((action, i) => (
-
- ))}
-
- )}
+
+ {actionButtons}
);
-};
+};
\ No newline at end of file
diff --git a/packages/account-sdk/src/ui/hooks/index.ts b/packages/account-sdk/src/ui/hooks/index.ts
new file mode 100644
index 00000000..e0f64e4f
--- /dev/null
+++ b/packages/account-sdk/src/ui/hooks/index.ts
@@ -0,0 +1,5 @@
+export { useDragToDismiss } from "./useDragToDismiss.js";
+export { useMediaQuery } from "./useMediaQuery.js";
+export { usePhonePortrait } from "./usePhonePortrait.js";
+export { useUsername } from "./useUsername.js";
+
diff --git a/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts
new file mode 100644
index 00000000..557fca99
--- /dev/null
+++ b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts
@@ -0,0 +1,69 @@
+import { useCallback, useState } from "preact/hooks";
+import { usePhonePortrait } from "./usePhonePortrait.js";
+
+const DRAG_DISMISS_THRESHOLD = 100;
+
+interface DragState {
+ dragY: number;
+ isDragging: boolean;
+ startY: number;
+}
+
+export function useDragToDismiss(onDismiss: () => void) {
+ const [dragState, setDragState] = useState({
+ dragY: 0,
+ isDragging: false,
+ startY: 0
+ });
+
+ const isPhonePortrait = usePhonePortrait();
+
+ const handleTouchStart = useCallback((e: TouchEvent) => {
+ if (!isPhonePortrait) return;
+
+ const touch = e.touches[0];
+ setDragState(prev => ({
+ ...prev,
+ startY: touch.clientY,
+ isDragging: true
+ }));
+ }, [isPhonePortrait]);
+
+ const handleTouchMove = useCallback((e: TouchEvent) => {
+ if (!dragState.isDragging) return;
+
+ const touch = e.touches[0];
+ const deltaY = touch.clientY - dragState.startY;
+
+ // Only allow dragging down (positive deltaY)
+ if (deltaY > 0) {
+ setDragState(prev => ({ ...prev, dragY: deltaY }));
+ e.preventDefault(); // Prevent scrolling
+ }
+ }, [dragState.isDragging, dragState.startY]);
+
+ const handleTouchEnd = useCallback(() => {
+ if (!dragState.isDragging) return;
+
+ const shouldDismiss = dragState.dragY > DRAG_DISMISS_THRESHOLD;
+
+ if (shouldDismiss) {
+ onDismiss();
+ } else {
+ // Reset to original position
+ setDragState(prev => ({ ...prev, dragY: 0 }));
+ }
+
+ setDragState(prev => ({ ...prev, isDragging: false }));
+ }, [dragState.isDragging, dragState.dragY, onDismiss]);
+
+ return {
+ dragY: dragState.dragY,
+ isDragging: dragState.isDragging,
+ handlers: {
+ onTouchStart: handleTouchStart,
+ onTouchMove: handleTouchMove,
+ onTouchEnd: handleTouchEnd
+ }
+ };
+}
diff --git a/packages/account-sdk/src/ui/hooks/useMediaQuery.ts b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts
new file mode 100644
index 00000000..33328397
--- /dev/null
+++ b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts
@@ -0,0 +1,23 @@
+import { useEffect, useState } from "preact/hooks";
+
+export function useMediaQuery(query: string): boolean {
+ const [matches, setMatches] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return window.matchMedia(query).matches;
+ }
+ return false;
+ });
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const mediaQuery = window.matchMedia(query);
+ const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
+
+ mediaQuery.addEventListener('change', handler);
+ return () => mediaQuery.removeEventListener('change', handler);
+ }, [query]);
+
+ return matches;
+}
+
diff --git a/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts
new file mode 100644
index 00000000..e5a5abca
--- /dev/null
+++ b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts
@@ -0,0 +1,7 @@
+import { useMediaQuery } from "./useMediaQuery.js";
+
+const PHONE_PORTRAIT_BREAKPOINT = 600;
+
+export function usePhonePortrait(): boolean {
+ return useMediaQuery(`(max-width: ${PHONE_PORTRAIT_BREAKPOINT}px) and (orientation: portrait)`);
+ }
\ No newline at end of file
diff --git a/packages/account-sdk/src/ui/hooks/useUsername.ts b/packages/account-sdk/src/ui/hooks/useUsername.ts
new file mode 100644
index 00000000..bf597433
--- /dev/null
+++ b/packages/account-sdk/src/ui/hooks/useUsername.ts
@@ -0,0 +1,46 @@
+import { getDisplayableUsername } from ':core/username/getDisplayableUsername.js';
+import { store } from ':store/store.js';
+import { useEffect, useRef, useState } from "preact/hooks";
+
+interface UsernameState {
+ isLoading: boolean;
+ username: string | null;
+}
+
+export function useUsername() {
+ const [state, setState] = useState({
+ isLoading: true,
+ username: null
+ });
+
+ const addressRef = useRef(null);
+
+ useEffect(() => {
+ const fetchUsername = async () => {
+ const currentAddress = store.account.get().accounts?.[0];
+
+ // Skip if address hasn't changed
+ if (currentAddress === addressRef.current) {
+ return;
+ }
+
+ addressRef.current = currentAddress ?? null;
+
+ if (currentAddress) {
+ try {
+ const username = await getDisplayableUsername(currentAddress);
+ setState({ isLoading: false, username });
+ } catch (error) {
+ console.warn('Failed to fetch username:', error);
+ setState({ isLoading: false, username: null });
+ }
+ } else {
+ setState({ isLoading: false, username: null });
+ }
+ };
+
+ fetchUsername();
+ }, []);
+
+ return state;
+}
From e067ecc190297a3419169ed4dfffcdf622c24bde Mon Sep 17 00:00:00 2001
From: Veenoway <77930262+Veenoway@users.noreply.github.com>
Date: Tue, 29 Jul 2025 17:29:26 +0200
Subject: [PATCH 2/4] feature: update dialog test
---
.../account-sdk/src/ui/Dialog/Dialog.test.tsx | 129 +++++++++++++++++-
1 file changed, 127 insertions(+), 2 deletions(-)
diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx
index a33f6de9..a3de1407 100644
--- a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx
+++ b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx
@@ -5,6 +5,37 @@ import { vi } from 'vitest';
import { DialogContainer, DialogInstance, DialogInstanceProps } from './Dialog.js';
+// Mock des hooks
+vi.mock('../hooks/index.js', () => ({
+ usePhonePortrait: vi.fn(() => false),
+ useDragToDismiss: vi.fn(() => ({
+ dragY: 0,
+ isDragging: false,
+ handlers: {
+ onTouchStart: vi.fn(),
+ onTouchMove: vi.fn(),
+ onTouchEnd: vi.fn(),
+ }
+ })),
+ useUsername: vi.fn(() => ({
+ isLoading: false,
+ username: 'testuser.eth'
+ }))
+}));
+
+// Mock du store et getDisplayableUsername
+vi.mock(':store/store.js', () => ({
+ store: {
+ account: {
+ get: vi.fn(() => ({ accounts: ['0x123'] }))
+ }
+ }
+}));
+
+vi.mock(':core/username/getDisplayableUsername.js', () => ({
+ getDisplayableUsername: vi.fn(() => Promise.resolve('testuser.eth'))
+}));
+
const renderDialogContainer = (props?: Partial) =>
render(
@@ -16,6 +47,8 @@ describe('DialogContainer', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(window, 'setTimeout');
+ // Reset mocks
+ vi.clearAllMocks();
});
afterEach(() => {
@@ -53,6 +86,7 @@ describe('DialogContainer', () => {
const button = screen.getByText('Try again');
expect(button).toBeInTheDocument();
+ expect(button.tagName).toBe('BUTTON'); // Vérifie que c'est un bouton sémantique
fireEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
@@ -72,6 +106,7 @@ describe('DialogContainer', () => {
const button = screen.getByText('Cancel');
expect(button).toBeInTheDocument();
+ expect(button.tagName).toBe('BUTTON');
fireEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
@@ -84,8 +119,11 @@ describe('DialogContainer', () => {
const closeButton = document.getElementsByClassName(
'-base-acc-sdk-dialog-instance-header-close'
)[0];
+
+ expect(closeButton.tagName).toBe('BUTTON'); // Vérifie que c'est un bouton
+ expect(closeButton).toHaveAttribute('aria-label', 'Close dialog'); // Accessibilité
+
fireEvent.click(closeButton);
-
expect(handleClose).toHaveBeenCalledTimes(1);
});
@@ -111,4 +149,91 @@ describe('DialogContainer', () => {
expect(screen.getByText('Primary')).toBeInTheDocument();
expect(screen.getByText('Secondary')).toBeInTheDocument();
});
-});
+
+ test('displays username when loaded', () => {
+ renderDialogContainer();
+
+ // Le mock retourne 'testuser.eth'
+ expect(screen.getByText('Signed in as testuser.eth')).toBeInTheDocument();
+ });
+
+ test('displays default title when no username', async () => {
+ // Mock pour retourner pas d'username
+ const { useUsername } = vi.mocked(await import('../hooks/index.js'));
+ useUsername.mockReturnValue({
+ isLoading: false,
+ username: null
+ });
+
+ renderDialogContainer();
+
+ expect(screen.getByText('Base Account')).toBeInTheDocument();
+ });
+
+ test('uses drag handlers from hook', async () => {
+ const mockHandlers = {
+ onTouchStart: vi.fn(),
+ onTouchMove: vi.fn(),
+ onTouchEnd: vi.fn(),
+ };
+
+ const { useDragToDismiss } = vi.mocked(await import('../hooks/index.js'));
+ useDragToDismiss.mockReturnValue({
+ dragY: 50,
+ isDragging: true,
+ handlers: mockHandlers
+ });
+
+ renderDialogContainer();
+
+ const backdrop = document.getElementsByClassName('-base-acc-sdk-dialog-backdrop')[0];
+
+ // Vérifie que les handlers sont attachés
+ fireEvent.touchStart(backdrop);
+ fireEvent.touchMove(backdrop);
+ fireEvent.touchEnd(backdrop);
+
+ expect(mockHandlers.onTouchStart).toHaveBeenCalled();
+ expect(mockHandlers.onTouchMove).toHaveBeenCalled();
+ expect(mockHandlers.onTouchEnd).toHaveBeenCalled();
+ });
+
+ test('applies drag transform from hook', async () => {
+ const { useDragToDismiss } = vi.mocked(await import('../hooks/index.js'));
+ useDragToDismiss.mockReturnValue({
+ dragY: 100,
+ isDragging: true,
+ handlers: {
+ onTouchStart: vi.fn(),
+ onTouchMove: vi.fn(),
+ onTouchEnd: vi.fn(),
+ }
+ });
+
+ renderDialogContainer();
+
+ const dialog = document.getElementsByClassName('-base-acc-sdk-dialog')[0];
+ expect(dialog).toHaveStyle('transform: translateY(100px)');
+ expect(dialog).toHaveStyle('transition: none');
+ });
+
+ test('shows handle bar on phone portrait', async () => {
+ const { usePhonePortrait } = vi.mocked(await import('../hooks/index.js'));
+ usePhonePortrait.mockReturnValue(true);
+
+ renderDialogContainer();
+
+ const handleBar = document.getElementsByClassName('-base-acc-sdk-dialog-handle-bar')[0];
+ expect(handleBar).toBeInTheDocument();
+ });
+
+ test('hides handle bar on desktop', async () => {
+ const { usePhonePortrait } = vi.mocked(await import('../hooks/index.js'));
+ usePhonePortrait.mockReturnValue(false);
+
+ renderDialogContainer();
+
+ const handleBar = document.getElementsByClassName('-base-acc-sdk-dialog-handle-bar')[0];
+ expect(handleBar).toBeUndefined();
+ });
+});
\ No newline at end of file
From 701fd6bf01f9b200238706c603c1947a5b97f705 Mon Sep 17 00:00:00 2001
From: Sebastien Cloiseau
Date: Tue, 29 Jul 2025 22:09:53 +0200
Subject: [PATCH 3/4] feat: gitignore
---
examples/testapp/.gitignore | 1 +
1 file changed, 1 insertion(+)
create mode 100644 examples/testapp/.gitignore
diff --git a/examples/testapp/.gitignore b/examples/testapp/.gitignore
new file mode 100644
index 00000000..6036b3a0
--- /dev/null
+++ b/examples/testapp/.gitignore
@@ -0,0 +1 @@
+src/components/test-dialog.tsx
From 71d5d23d00192a8a48e59f6383bf7960a72b52e8 Mon Sep 17 00:00:00 2001
From: Sebastien Cloiseau
Date: Mon, 11 Aug 2025 23:39:38 +0200
Subject: [PATCH 4/4] fix: format
---
biome.json | 389 +++++++++---------
.../account-sdk/src/ui/Dialog/Dialog.test.tsx | 32 +-
packages/account-sdk/src/ui/Dialog/Dialog.tsx | 12 +-
packages/account-sdk/src/ui/hooks/index.ts | 9 +-
.../src/ui/hooks/useDragToDismiss.ts | 66 +--
.../account-sdk/src/ui/hooks/useMediaQuery.ts | 9 +-
.../src/ui/hooks/usePhonePortrait.ts | 6 +-
.../account-sdk/src/ui/hooks/useUsername.ts | 12 +-
8 files changed, 260 insertions(+), 275 deletions(-)
diff --git a/biome.json b/biome.json
index 1cd1123d..1f822ebe 100644
--- a/biome.json
+++ b/biome.json
@@ -1,205 +1,186 @@
{
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
- "vcs": {
- "enabled": true,
- "clientKind": "git",
- "useIgnoreFile": true
- },
- "files": {
- "ignoreUnknown": false,
- "ignore": []
- },
- "formatter": {
- "enabled": true,
- "useEditorconfig": true,
- "formatWithErrors": false,
- "indentStyle": "space",
- "indentWidth": 2,
- "lineEnding": "lf",
- "lineWidth": 100,
- "attributePosition": "auto",
- "bracketSpacing": true,
- "ignore": [
- "**/package.json",
- "**/yarn.lock",
- "coverage/**",
- "**/coverage/**",
- "**/build",
- "**/dist",
- "**/node_modules",
- "**/vendor-js/**",
- "**/*-css.ts",
- "**/*-svg.ts"
- ]
- },
- "organizeImports": {
- "enabled": true
- },
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": false,
- "a11y": {
- "noBlankTarget": "error"
- },
- "complexity": {
- "noBannedTypes": "error",
- "noExtraBooleanCast": "error",
- "noMultipleSpacesInRegularExpressionLiterals": "error",
- "noUselessCatch": "error",
- "noUselessConstructor": "off",
- "noUselessRename": "warn",
- "noUselessStringConcat": "warn",
- "noUselessTernary": "error",
- "noUselessThisAlias": "error",
- "noUselessTypeConstraint": "error",
- "noUselessUndefinedInitialization": "error",
- "noWith": "error",
- "useArrowFunction": "warn"
- },
- "correctness": {
- "noConstAssign": "error",
- "noConstantCondition": "error",
- "noEmptyCharacterClassInRegex": "error",
- "noEmptyPattern": "off",
- "noGlobalObjectCalls": "error",
- "noInnerDeclarations": "error",
- "noInvalidConstructorSuper": "error",
- "noNewSymbol": "error",
- "noNonoctalDecimalEscape": "error",
- "noPrecisionLoss": "error",
- "noSelfAssign": "error",
- "noSetterReturn": "error",
- "noSwitchDeclarations": "error",
- "noUndeclaredVariables": "error",
- "noUnreachable": "error",
- "noUnreachableSuper": "error",
- "noUnsafeFinally": "error",
- "noUnsafeOptionalChaining": "error",
- "noUnusedImports": "error",
- "noUnusedLabels": "error",
- "noUnusedVariables": "error",
- "useArrayLiterals": "off",
- "useExhaustiveDependencies": "warn",
- "useHookAtTopLevel": "error",
- "useIsNan": "error",
- "useJsxKeyInIterable": "error",
- "useValidForDirection": "error",
- "useYield": "error"
- },
- "security": {
- "noDangerouslySetInnerHtml": "warn"
- },
- "style": {
- "noArguments": "warn",
- "noDoneCallback": "error",
- "noNamespace": "error",
- "noRestrictedGlobals": {
- "level": "error",
- "options": {
- "deniedGlobals": [
- "parseInt"
- ]
- }
- },
- "noUselessElse": "warn",
- "noVar": "warn",
- "useAsConstAssertion": "error",
- "useBlockStatements": "off",
- "useCollapsedElseIf": "error",
- "useConsistentBuiltinInstantiation": "error",
- "useTemplate": "warn"
- },
- "suspicious": {
- "noAssignInExpressions": "error",
- "noAsyncPromiseExecutor": "error",
- "noCatchAssign": "error",
- "noClassAssign": "error",
- "noCommentText": "error",
- "noCompareNegZero": "error",
- "noConsole": {
- "level": "error",
- "options": {
- "allow": [
- "warn",
- "error",
- "info"
- ]
- }
- },
- "noControlCharactersInRegex": "error",
- "noDebugger": "error",
- "noDuplicateCase": "error",
- "noDuplicateClassMembers": "error",
- "noDuplicateJsxProps": "error",
- "noDuplicateObjectKeys": "error",
- "noDuplicateParameters": "error",
- "noEmptyBlockStatements": "off",
- "noExplicitAny": "warn",
- "noExportsInTest": "error",
- "noExtraNonNullAssertion": "error",
- "noFallthroughSwitchClause": "error",
- "noFocusedTests": "error",
- "noFunctionAssign": "error",
- "noGlobalAssign": "error",
- "noImportAssign": "error",
- "noMisleadingCharacterClass": "error",
- "noMisleadingInstantiator": "error",
- "noMisplacedAssertion": "error",
- "noPrototypeBuiltins": "error",
- "noRedeclare": "error",
- "noShadowRestrictedNames": "error",
- "noSkippedTests": "warn",
- "noSparseArray": "error",
- "noUnsafeDeclarationMerging": "error",
- "noUnsafeNegation": "error",
- "useGetterReturn": "error",
- "useValidTypeof": "error"
- }
- },
- "ignore": [
- "**/*.md",
- "**/build",
- "**/dist",
- "**/node_modules",
- "**/vendor-js/**",
- "**/*.json"
- ]
- },
- "javascript": {
- "formatter": {
- "jsxQuoteStyle": "double",
- "quoteProperties": "asNeeded",
- "trailingCommas": "es5",
- "semicolons": "always",
- "arrowParentheses": "always",
- "bracketSameLine": false,
- "quoteStyle": "single",
- "attributePosition": "auto",
- "bracketSpacing": true
- },
- "jsxRuntime": "transparent",
- "globals": [
- "global",
- "browser",
- "expect"
- ]
- },
- "overrides": [
- {
- "include": [
- "**/*.test.*"
- ],
- "linter": {
- "rules": {
- "suspicious": {
- "noExplicitAny": "off"
- },
- "correctness": {
- "noUndeclaredVariables": "off"
- }
- }
- }
- }
- ]
-}
\ No newline at end of file
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "ignoreUnknown": false,
+ "ignore": []
+ },
+ "formatter": {
+ "enabled": true,
+ "useEditorconfig": true,
+ "formatWithErrors": false,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineEnding": "lf",
+ "lineWidth": 100,
+ "attributePosition": "auto",
+ "bracketSpacing": true,
+ "ignore": [
+ "**/package.json",
+ "**/yarn.lock",
+ "coverage/**",
+ "**/coverage/**",
+ "**/build",
+ "**/dist",
+ "**/node_modules",
+ "**/vendor-js/**",
+ "**/*-css.ts",
+ "**/*-svg.ts"
+ ]
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": false,
+ "a11y": {
+ "noBlankTarget": "error"
+ },
+ "complexity": {
+ "noBannedTypes": "error",
+ "noExtraBooleanCast": "error",
+ "noMultipleSpacesInRegularExpressionLiterals": "error",
+ "noUselessCatch": "error",
+ "noUselessConstructor": "off",
+ "noUselessRename": "warn",
+ "noUselessStringConcat": "warn",
+ "noUselessTernary": "error",
+ "noUselessThisAlias": "error",
+ "noUselessTypeConstraint": "error",
+ "noUselessUndefinedInitialization": "error",
+ "noWith": "error",
+ "useArrowFunction": "warn"
+ },
+ "correctness": {
+ "noConstAssign": "error",
+ "noConstantCondition": "error",
+ "noEmptyCharacterClassInRegex": "error",
+ "noEmptyPattern": "off",
+ "noGlobalObjectCalls": "error",
+ "noInnerDeclarations": "error",
+ "noInvalidConstructorSuper": "error",
+ "noNewSymbol": "error",
+ "noNonoctalDecimalEscape": "error",
+ "noPrecisionLoss": "error",
+ "noSelfAssign": "error",
+ "noSetterReturn": "error",
+ "noSwitchDeclarations": "error",
+ "noUndeclaredVariables": "error",
+ "noUnreachable": "error",
+ "noUnreachableSuper": "error",
+ "noUnsafeFinally": "error",
+ "noUnsafeOptionalChaining": "error",
+ "noUnusedImports": "error",
+ "noUnusedLabels": "error",
+ "noUnusedVariables": "error",
+ "useArrayLiterals": "off",
+ "useExhaustiveDependencies": "warn",
+ "useHookAtTopLevel": "error",
+ "useIsNan": "error",
+ "useJsxKeyInIterable": "error",
+ "useValidForDirection": "error",
+ "useYield": "error"
+ },
+ "security": {
+ "noDangerouslySetInnerHtml": "warn"
+ },
+ "style": {
+ "noArguments": "warn",
+ "noDoneCallback": "error",
+ "noNamespace": "error",
+ "noRestrictedGlobals": {
+ "level": "error",
+ "options": {
+ "deniedGlobals": ["parseInt"]
+ }
+ },
+ "noUselessElse": "warn",
+ "noVar": "warn",
+ "useAsConstAssertion": "error",
+ "useBlockStatements": "off",
+ "useCollapsedElseIf": "error",
+ "useConsistentBuiltinInstantiation": "error",
+ "useTemplate": "warn"
+ },
+ "suspicious": {
+ "noAssignInExpressions": "error",
+ "noAsyncPromiseExecutor": "error",
+ "noCatchAssign": "error",
+ "noClassAssign": "error",
+ "noCommentText": "error",
+ "noCompareNegZero": "error",
+ "noConsole": {
+ "level": "error",
+ "options": {
+ "allow": ["warn", "error", "info"]
+ }
+ },
+ "noControlCharactersInRegex": "error",
+ "noDebugger": "error",
+ "noDuplicateCase": "error",
+ "noDuplicateClassMembers": "error",
+ "noDuplicateJsxProps": "error",
+ "noDuplicateObjectKeys": "error",
+ "noDuplicateParameters": "error",
+ "noEmptyBlockStatements": "off",
+ "noExplicitAny": "warn",
+ "noExportsInTest": "error",
+ "noExtraNonNullAssertion": "error",
+ "noFallthroughSwitchClause": "error",
+ "noFocusedTests": "error",
+ "noFunctionAssign": "error",
+ "noGlobalAssign": "error",
+ "noImportAssign": "error",
+ "noMisleadingCharacterClass": "error",
+ "noMisleadingInstantiator": "error",
+ "noMisplacedAssertion": "error",
+ "noPrototypeBuiltins": "error",
+ "noRedeclare": "error",
+ "noShadowRestrictedNames": "error",
+ "noSkippedTests": "warn",
+ "noSparseArray": "error",
+ "noUnsafeDeclarationMerging": "error",
+ "noUnsafeNegation": "error",
+ "useGetterReturn": "error",
+ "useValidTypeof": "error"
+ }
+ },
+ "ignore": ["**/*.md", "**/build", "**/dist", "**/node_modules", "**/vendor-js/**", "**/*.json"]
+ },
+ "javascript": {
+ "formatter": {
+ "jsxQuoteStyle": "double",
+ "quoteProperties": "asNeeded",
+ "trailingCommas": "es5",
+ "semicolons": "always",
+ "arrowParentheses": "always",
+ "bracketSameLine": false,
+ "quoteStyle": "single",
+ "attributePosition": "auto",
+ "bracketSpacing": true
+ },
+ "jsxRuntime": "transparent",
+ "globals": ["global", "browser", "expect"]
+ },
+ "overrides": [
+ {
+ "include": ["**/*.test.*"],
+ "linter": {
+ "rules": {
+ "suspicious": {
+ "noExplicitAny": "off"
+ },
+ "correctness": {
+ "noUndeclaredVariables": "off"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx
index a3de1407..1fbfa9d4 100644
--- a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx
+++ b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx
@@ -15,25 +15,25 @@ vi.mock('../hooks/index.js', () => ({
onTouchStart: vi.fn(),
onTouchMove: vi.fn(),
onTouchEnd: vi.fn(),
- }
+ },
})),
useUsername: vi.fn(() => ({
isLoading: false,
- username: 'testuser.eth'
- }))
+ username: 'testuser.eth',
+ })),
}));
// Mock du store et getDisplayableUsername
vi.mock(':store/store.js', () => ({
store: {
account: {
- get: vi.fn(() => ({ accounts: ['0x123'] }))
- }
- }
+ get: vi.fn(() => ({ accounts: ['0x123'] })),
+ },
+ },
}));
vi.mock(':core/username/getDisplayableUsername.js', () => ({
- getDisplayableUsername: vi.fn(() => Promise.resolve('testuser.eth'))
+ getDisplayableUsername: vi.fn(() => Promise.resolve('testuser.eth')),
}));
const renderDialogContainer = (props?: Partial) =>
@@ -119,10 +119,10 @@ describe('DialogContainer', () => {
const closeButton = document.getElementsByClassName(
'-base-acc-sdk-dialog-instance-header-close'
)[0];
-
+
expect(closeButton.tagName).toBe('BUTTON'); // Vérifie que c'est un bouton
expect(closeButton).toHaveAttribute('aria-label', 'Close dialog'); // Accessibilité
-
+
fireEvent.click(closeButton);
expect(handleClose).toHaveBeenCalledTimes(1);
});
@@ -152,7 +152,7 @@ describe('DialogContainer', () => {
test('displays username when loaded', () => {
renderDialogContainer();
-
+
// Le mock retourne 'testuser.eth'
expect(screen.getByText('Signed in as testuser.eth')).toBeInTheDocument();
});
@@ -162,11 +162,11 @@ describe('DialogContainer', () => {
const { useUsername } = vi.mocked(await import('../hooks/index.js'));
useUsername.mockReturnValue({
isLoading: false,
- username: null
+ username: null,
});
renderDialogContainer();
-
+
expect(screen.getByText('Base Account')).toBeInTheDocument();
});
@@ -181,13 +181,13 @@ describe('DialogContainer', () => {
useDragToDismiss.mockReturnValue({
dragY: 50,
isDragging: true,
- handlers: mockHandlers
+ handlers: mockHandlers,
});
renderDialogContainer();
const backdrop = document.getElementsByClassName('-base-acc-sdk-dialog-backdrop')[0];
-
+
// Vérifie que les handlers sont attachés
fireEvent.touchStart(backdrop);
fireEvent.touchMove(backdrop);
@@ -207,7 +207,7 @@ describe('DialogContainer', () => {
onTouchStart: vi.fn(),
onTouchMove: vi.fn(),
onTouchEnd: vi.fn(),
- }
+ },
});
renderDialogContainer();
@@ -236,4 +236,4 @@ describe('DialogContainer', () => {
const handleBar = document.getElementsByClassName('-base-acc-sdk-dialog-handle-bar')[0];
expect(handleBar).toBeUndefined();
});
-});
\ No newline at end of file
+});
diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.tsx
index 70e5c3ed..56602683 100644
--- a/packages/account-sdk/src/ui/Dialog/Dialog.tsx
+++ b/packages/account-sdk/src/ui/Dialog/Dialog.tsx
@@ -13,7 +13,7 @@ const closeIcon = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQ
// Handle bar component for mobile bottom sheet
const DialogHandleBar: FunctionComponent = () => {
const isPhonePortrait = usePhonePortrait();
-
+
return isPhonePortrait ? : null;
};
@@ -205,20 +205,20 @@ export const DialogInstance: FunctionComponent = ({
type="button"
aria-label="Close dialog"
>
-
-
+
-
+
{actionButtons}
);
-};
\ No newline at end of file
+};
diff --git a/packages/account-sdk/src/ui/hooks/index.ts b/packages/account-sdk/src/ui/hooks/index.ts
index e0f64e4f..064451bd 100644
--- a/packages/account-sdk/src/ui/hooks/index.ts
+++ b/packages/account-sdk/src/ui/hooks/index.ts
@@ -1,5 +1,4 @@
-export { useDragToDismiss } from "./useDragToDismiss.js";
-export { useMediaQuery } from "./useMediaQuery.js";
-export { usePhonePortrait } from "./usePhonePortrait.js";
-export { useUsername } from "./useUsername.js";
-
+export { useDragToDismiss } from './useDragToDismiss.js';
+export { useMediaQuery } from './useMediaQuery.js';
+export { usePhonePortrait } from './usePhonePortrait.js';
+export { useUsername } from './useUsername.js';
diff --git a/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts
index 557fca99..9b485372 100644
--- a/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts
+++ b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts
@@ -1,5 +1,5 @@
-import { useCallback, useState } from "preact/hooks";
-import { usePhonePortrait } from "./usePhonePortrait.js";
+import { useCallback, useState } from 'preact/hooks';
+import { usePhonePortrait } from './usePhonePortrait.js';
const DRAG_DISMISS_THRESHOLD = 100;
@@ -13,48 +13,54 @@ export function useDragToDismiss(onDismiss: () => void) {
const [dragState, setDragState] = useState({
dragY: 0,
isDragging: false,
- startY: 0
+ startY: 0,
});
-
+
const isPhonePortrait = usePhonePortrait();
- const handleTouchStart = useCallback((e: TouchEvent) => {
- if (!isPhonePortrait) return;
-
- const touch = e.touches[0];
- setDragState(prev => ({
- ...prev,
- startY: touch.clientY,
- isDragging: true
- }));
- }, [isPhonePortrait]);
+ const handleTouchStart = useCallback(
+ (e: TouchEvent) => {
+ if (!isPhonePortrait) return;
- const handleTouchMove = useCallback((e: TouchEvent) => {
- if (!dragState.isDragging) return;
+ const touch = e.touches[0];
+ setDragState((prev) => ({
+ ...prev,
+ startY: touch.clientY,
+ isDragging: true,
+ }));
+ },
+ [isPhonePortrait]
+ );
- const touch = e.touches[0];
- const deltaY = touch.clientY - dragState.startY;
+ const handleTouchMove = useCallback(
+ (e: TouchEvent) => {
+ if (!dragState.isDragging) return;
- // Only allow dragging down (positive deltaY)
- if (deltaY > 0) {
- setDragState(prev => ({ ...prev, dragY: deltaY }));
- e.preventDefault(); // Prevent scrolling
- }
- }, [dragState.isDragging, dragState.startY]);
+ const touch = e.touches[0];
+ const deltaY = touch.clientY - dragState.startY;
+
+ // Only allow dragging down (positive deltaY)
+ if (deltaY > 0) {
+ setDragState((prev) => ({ ...prev, dragY: deltaY }));
+ e.preventDefault(); // Prevent scrolling
+ }
+ },
+ [dragState.isDragging, dragState.startY]
+ );
const handleTouchEnd = useCallback(() => {
if (!dragState.isDragging) return;
const shouldDismiss = dragState.dragY > DRAG_DISMISS_THRESHOLD;
-
+
if (shouldDismiss) {
onDismiss();
} else {
// Reset to original position
- setDragState(prev => ({ ...prev, dragY: 0 }));
+ setDragState((prev) => ({ ...prev, dragY: 0 }));
}
-
- setDragState(prev => ({ ...prev, isDragging: false }));
+
+ setDragState((prev) => ({ ...prev, isDragging: false }));
}, [dragState.isDragging, dragState.dragY, onDismiss]);
return {
@@ -63,7 +69,7 @@ export function useDragToDismiss(onDismiss: () => void) {
handlers: {
onTouchStart: handleTouchStart,
onTouchMove: handleTouchMove,
- onTouchEnd: handleTouchEnd
- }
+ onTouchEnd: handleTouchEnd,
+ },
};
}
diff --git a/packages/account-sdk/src/ui/hooks/useMediaQuery.ts b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts
index 33328397..c8b8aa1a 100644
--- a/packages/account-sdk/src/ui/hooks/useMediaQuery.ts
+++ b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "preact/hooks";
+import { useEffect, useState } from 'preact/hooks';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => {
@@ -9,15 +9,14 @@ export function useMediaQuery(query: string): boolean {
});
useEffect(() => {
- if (typeof window === 'undefined') return;
-
+ if (typeof window === 'undefined') return;
+
const mediaQuery = window.matchMedia(query);
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
-
+
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
-
diff --git a/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts
index e5a5abca..38a971c9 100644
--- a/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts
+++ b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts
@@ -1,7 +1,7 @@
-import { useMediaQuery } from "./useMediaQuery.js";
+import { useMediaQuery } from './useMediaQuery.js';
const PHONE_PORTRAIT_BREAKPOINT = 600;
export function usePhonePortrait(): boolean {
- return useMediaQuery(`(max-width: ${PHONE_PORTRAIT_BREAKPOINT}px) and (orientation: portrait)`);
- }
\ No newline at end of file
+ return useMediaQuery(`(max-width: ${PHONE_PORTRAIT_BREAKPOINT}px) and (orientation: portrait)`);
+}
diff --git a/packages/account-sdk/src/ui/hooks/useUsername.ts b/packages/account-sdk/src/ui/hooks/useUsername.ts
index bf597433..a1c3b4b6 100644
--- a/packages/account-sdk/src/ui/hooks/useUsername.ts
+++ b/packages/account-sdk/src/ui/hooks/useUsername.ts
@@ -1,6 +1,6 @@
import { getDisplayableUsername } from ':core/username/getDisplayableUsername.js';
import { store } from ':store/store.js';
-import { useEffect, useRef, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from 'preact/hooks';
interface UsernameState {
isLoading: boolean;
@@ -10,22 +10,22 @@ interface UsernameState {
export function useUsername() {
const [state, setState] = useState({
isLoading: true,
- username: null
+ username: null,
});
-
+
const addressRef = useRef(null);
useEffect(() => {
const fetchUsername = async () => {
const currentAddress = store.account.get().accounts?.[0];
-
+
// Skip if address hasn't changed
if (currentAddress === addressRef.current) {
return;
}
-
+
addressRef.current = currentAddress ?? null;
-
+
if (currentAddress) {
try {
const username = await getDisplayableUsername(currentAddress);