mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-10-30 15:19:21 +00:00
Upstream merge brings 52 commits from safe-global/safe-wallet-monorepo into the luxfi/safe-wallet@dev fork. Conflicts resolved: - yarn.lock: deleted (we are on pnpm) - apps/web/package.json: kept pnpm version + brand build scripts (build:safe / build:lux / build:hanzo) plus upstream's new generate-version.mjs step - apps/mobile/.../Signer/appKit.ts: took upstream's import pattern for shared/projectId, shared/metadata, shared/mmkvStorageAdapter Yarn sweep applied to 42 files (docs, github-actions, workflows, AGENTS, specs, codemods, scripts, READMEs). Net result: zero yarn-* command references anywhere outside node_modules. ❯ pnpm is the law in this fork. No yarn.
10 KiB
10 KiB
Feature Architecture Quickstart
Date: 2026-01-08 Feature: 001-feature-architecture
This guide explains how to create a new feature or migrate an existing one to the standard feature architecture.
Table of Contents
- Creating a New Feature
- Migrating an Existing Feature
- Feature Checklist
- Code Examples
- Common Mistakes
Creating a New Feature
Step 1: Create Directory Structure
mkdir -p apps/web/src/features/{feature-name}/{components,hooks,services,store}
Step 2: Create Required Files
touch apps/web/src/features/{feature-name}/index.ts
touch apps/web/src/features/{feature-name}/types.ts
touch apps/web/src/features/{feature-name}/constants.ts
touch apps/web/src/features/{feature-name}/components/index.ts
touch apps/web/src/features/{feature-name}/hooks/index.ts
touch apps/web/src/features/{feature-name}/hooks/useIs{FeatureName}Enabled.ts
Step 3: Add Feature Flag (if new)
-
Add to
FEATURESenum inpackages/utils/src/utils/chains.ts:export enum FEATURES { // ... existing features MY_NEW_FEATURE = 'MY_NEW_FEATURE', } -
Configure in CGW API chain configs (coordinate with backend team)
Step 4: Implement Feature Flag Hook
// hooks/useIsMyFeatureEnabled.ts
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@safe-global/utils/utils/chains'
export function useIsMyFeatureEnabled(): boolean | undefined {
return useHasFeature(FEATURES.MY_NEW_FEATURE)
}
Step 5: Create Main Component
// components/MyFeatureWidget/index.tsx
import type { ReactElement } from 'react'
import { useIsMyFeatureEnabled } from '../../hooks'
export function MyFeatureWidget(): ReactElement | null {
const isEnabled = useIsMyFeatureEnabled()
// Don't render anything if disabled or loading
if (!isEnabled) return null
return (
<div>
{/* Feature content */}
</div>
)
}
Step 6: Create Public API (index.ts)
// index.ts
import dynamic from 'next/dynamic'
// Lazy-loaded main component
const MyFeatureWidget = dynamic(
() => import('./components/MyFeatureWidget').then((mod) => ({ default: mod.MyFeatureWidget })),
{ ssr: false },
)
// Re-export types
export type { MyFeatureConfig, MyFeatureState } from './types'
// Re-export feature flag hook
export { useIsMyFeatureEnabled } from './hooks'
// Default export
export default MyFeatureWidget
Migrating an Existing Feature
Step 1: Assess Current State
Check what's missing against the standard structure:
| Required | Check |
|---|---|
index.ts |
Feature root barrel file |
types.ts |
All TypeScript interfaces |
constants.ts |
Feature constants |
components/index.ts |
Component barrel file |
hooks/index.ts |
Hook barrel file |
hooks/useIs{Feature}Enabled.ts |
Feature flag hook |
services/index.ts |
Service barrel (if services exist) |
store/index.ts |
Store barrel (if store exists) |
Step 2: Create Missing Barrel Files
Add index.ts files that re-export public APIs:
// components/index.ts
export { MyComponent } from './MyComponent'
export { AnotherComponent } from './AnotherComponent'
// hooks/index.ts
export { useIsFeatureEnabled } from './useIsFeatureEnabled'
export { useFeatureHook } from './useFeatureHook'
// services/index.ts
export { FeatureService } from './FeatureService'
Step 3: Create types.ts
Extract all interfaces/types from component files:
// types.ts
export interface FeatureConfig {
// ...
}
export interface FeatureState {
// ...
}
export type FeatureEventType = 'connect' | 'disconnect' | 'error'
Step 4: Create Feature Flag Hook
// hooks/useIs{FeatureName}Enabled.ts
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@safe-global/utils/utils/chains'
export function useIs{FeatureName}Enabled(): boolean | undefined {
return useHasFeature(FEATURES.{FEATURE_FLAG})
}
Step 5: Create Root index.ts
// index.ts
import dynamic from 'next/dynamic'
const FeatureWidget = dynamic(() => import('./components/FeatureWidget'), {
ssr: false,
})
export type { FeatureConfig, FeatureState } from './types'
export { useIsFeatureEnabled } from './hooks'
export default FeatureWidget
Step 6: Update External Imports
Find all imports to feature internals and update to use the public API:
// Before (invalid)
import { WcInput } from '@/features/walletconnect/components/WcInput'
// After (valid)
import WalletConnect, { useIsWalletConnectEnabled } from '@/features/walletconnect'
Step 7: Run ESLint to Verify
pnpm --filter @safe-global/web lint
ESLint will warn on any remaining internal imports.
Feature Checklist
Use this checklist when creating or reviewing features:
Structure
- Feature directory uses kebab-case
index.tsexists at feature roottypes.tsexists with all interfacesconstants.tsexistscomponents/index.tsexistshooks/index.tsexistsservices/index.tsexists (if services present)store/index.tsexists (if store present)
Feature Flag
- Feature has entry in
FEATURESenum useIs{FeatureName}Enabledhook exists- Main component checks feature flag
- Component renders
nullwhen disabled
Lazy Loading
- Main component uses
dynamic()import { ssr: false }set for browser-only features- No static imports from outside the feature
Isolation
- No external imports to feature internals
- Shared code extracted to
src/utils/orsrc/hooks/ - Cross-feature communication via Redux or services
Code Examples
Complete Feature Index File
// apps/web/src/features/my-feature/index.ts
import dynamic from 'next/dynamic'
// Types (tree-shakeable)
export type { MyFeatureConfig, MyFeatureState, MyFeatureEvent } from './types'
// Feature flag hook (required)
export { useIsMyFeatureEnabled } from './hooks'
// Store selectors (if feature has Redux state)
export { selectMyFeatureState, selectMyFeatureStatus } from './store'
// Constants (if needed externally)
export { MY_FEATURE_EVENTS } from './constants'
// Lazy-loaded component (default export)
const MyFeatureWidget = dynamic(() => import('./components/MyFeatureWidget'), { ssr: false })
export default MyFeatureWidget
Feature Flag Hook
// apps/web/src/features/my-feature/hooks/useIsMyFeatureEnabled.ts
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@safe-global/utils/utils/chains'
export function useIsMyFeatureEnabled(): boolean | undefined {
return useHasFeature(FEATURES.MY_FEATURE)
}
Component with Feature Flag Check
// apps/web/src/features/my-feature/components/MyFeatureWidget/index.tsx
import type { ReactElement } from 'react'
import { useIsMyFeatureEnabled } from '../../hooks'
export function MyFeatureWidget(): ReactElement | null {
const isEnabled = useIsMyFeatureEnabled()
// Return nothing while loading or disabled
if (isEnabled !== true) return null
return (
<div data-testid="my-feature-widget">
{/* Feature UI */}
</div>
)
}
Using a Feature from Outside
// apps/web/src/pages/my-page.tsx
import dynamic from 'next/dynamic'
import { useIsMyFeatureEnabled } from '@/features/my-feature'
const MyFeature = dynamic(() => import('@/features/my-feature'), { ssr: false })
export default function MyPage() {
const isEnabled = useIsMyFeatureEnabled()
return (
<main>
<h1>My Page</h1>
{isEnabled && <MyFeature />}
</main>
)
}
Common Mistakes
❌ Importing Internal Files
// WRONG - imports feature internals
import { WcInput } from '@/features/walletconnect/components/WcInput'
import { useWcUri } from '@/features/walletconnect/hooks/useWcUri'
// CORRECT - imports from feature index only
import WalletConnect, { useIsWalletConnectEnabled } from '@/features/walletconnect'
❌ Missing Feature Flag Check
// WRONG - no feature flag check
export function MyFeature() {
return <div>Always renders</div>
}
// CORRECT - checks feature flag
export function MyFeature() {
const isEnabled = useIsMyFeatureEnabled()
if (!isEnabled) return null
return <div>Conditionally renders</div>
}
❌ Static Import of Feature
// WRONG - static import bundles feature in main chunk
import MyFeature from '@/features/my-feature/components/MyFeatureWidget'
// CORRECT - dynamic import enables code splitting
const MyFeature = dynamic(() => import('@/features/my-feature'), { ssr: false })
❌ Side Effects When Disabled
// WRONG - API call happens even when disabled
export function MyFeature() {
const isEnabled = useIsMyFeatureEnabled()
const { data } = useQuery('my-feature-data') // Always fetches!
if (!isEnabled) return null
return <div>{data}</div>
}
// CORRECT - no side effects when disabled
export function MyFeature() {
const isEnabled = useIsMyFeatureEnabled()
if (!isEnabled) return null
// Data fetching only happens when enabled
return <MyFeatureContent />
}
function MyFeatureContent() {
const { data } = useQuery('my-feature-data')
return <div>{data}</div>
}
Next Steps
After creating/migrating a feature:
- Run
pnpm --filter @safe-global/web lintto check for violations - Run
pnpm --filter @safe-global/web type-checkto verify types - Run
pnpm --filter @safe-global/web testto ensure tests pass - Run
pnpm --filter @safe-global/web buildto verify bundle splitting