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.
17 KiB
Implementation Plan: Refactor Earn Feature
Branch: 002-refactor-earn-feature | Date: 2026-01-15 | Spec: spec.md
Input: Feature specification from /specs/002-refactor-earn-feature/spec.md
Summary
Refactor the earn feature to follow the standard feature architecture pattern established in 001-feature-architecture. This involves reorganizing the folder structure, creating proper public API boundaries through barrel exports, ensuring lazy loading compliance, centralizing TypeScript types, and preserving 100% of existing functionality. The refactoring follows the proven walletconnect reference implementation pattern.
Technical Context
Language/Version: TypeScript 5.x (Next.js web application)
Primary Dependencies: React 18, Next.js 14, MUI, ethers.js, @safe-global/utils
Storage: Browser LocalStorage (for consent state), CGW API chain configs (for feature flags)
Testing: Jest, React Testing Library
Target Platform: Web browsers (Chrome, Firefox, Safari, Edge)
Project Type: Web (Next.js monorepo workspace)
Performance Goals: Code splitting to prevent earn code from loading when feature disabled; lazy loading on-demand
Constraints:
- Zero functional changes (pure refactoring)
- All existing tests must pass without modification
- External imports must be updated to use new public API
- Bundle must not include earn code when EARN feature flag is disabled
Scale/Scope: - 1 feature with ~10 components
- 2 external import locations to update (Assets dashboard widget, AssetsTable PromoButtons)
- 2 hooks, 1 utility service file
- Kiln widget integration (third-party iframe)
Constitution Check (Post-Phase 1 Re-evaluation)
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
✅ Monorepo Unity
Status: PASS - No violations
- This is a web-only feature (no shared packages affected)
- Refactoring does not touch mobile or shared code
- All changes are isolated to
apps/web/src/features/earn/
Post-Phase 1 Confirmation: No new violations introduced. Research and design confirm all work is web-only.
✅ Type Safety
Status: PASS - No violations
- All code uses TypeScript with explicit types
- No
anytype usage detected in current earn feature code - Refactoring will centralize types in
types.ts(improvement) - Type-check will be run before committing
Post-Phase 1 Confirmation: Types extracted to types.ts follow strict typing. EarnButtonProps interface defined without any types. Feature module contract documents all public types.
✅ Test-First Development
Status: PASS - No violations
- Existing tests will be preserved without modification (per requirement FR-021)
- No new business logic is being added (pure refactoring)
- All existing test coverage remains intact
Post-Phase 1 Confirmation: Research revealed zero existing automated tests. Manual testing checklist created to validate functionality preservation. This is acceptable for a pure refactoring task.
✅ Design System Compliance
Status: PASS - No violations
- Earn feature currently uses MUI components and theme variables
- No changes to UI rendering or styling in this refactoring
- Storybook stories exist for VaultDepositConfirmation and VaultRedeemConfirmation
- Stories will be preserved during folder reorganization
Post-Phase 1 Confirmation: Design system usage remains unchanged. No new components require Storybook stories. Existing stories will remain in their component directories.
✅ Safe-Specific Security
Status: PASS - No violations
- Earn feature uses asset addresses and chain IDs correctly
- No transaction building or signature flows in earn feature (delegates to Kiln widget)
- Geoblocking and blocked address checks are preserved (per FR-023)
- No security-sensitive patterns are being modified
Post-Phase 1 Confirmation: Data model confirms proper handling of geoblocking (GeoblockingContext), blocked addresses (useBlockedAddress hook), and asset validation (isEligibleEarnToken utility). No security-sensitive changes in refactoring.
Final Conclusion: All constitutional principles satisfied after Phase 1 design. No violations requiring justification. Ready to proceed to Phase 2 (Task Breakdown).
Project Structure
Documentation (this feature)
specs/002-refactor-earn-feature/
├── spec.md # Feature specification (COMPLETED ✅)
├── checklists/
│ └── requirements.md # Spec quality checklist (COMPLETED ✅)
├── plan.md # This file (COMPLETED ✅)
├── research.md # Phase 0 output (COMPLETED ✅)
├── data-model.md # Phase 1 output (COMPLETED ✅)
├── quickstart.md # Phase 1 output (COMPLETED ✅)
├── contracts/ # Phase 1 output (COMPLETED ✅)
│ └── feature-module.ts # TypeScript interface contract for earn feature
└── tasks.md # Phase 2 output (/speckit.tasks command - PENDING)
Source Code (repository root)
Current Structure (to be refactored):
apps/web/src/features/earn/
├── index.tsx # Main component (currently .tsx, needs to become barrel file)
├── constants.ts # ✅ Already exists
├── utils.ts # ⚠️ Should be moved to services/
├── components/
│ ├── EarnButton/ # ⚠️ No barrel export
│ ├── EarnInfo/
│ ├── EarnView/
│ ├── EarnWidget/
│ ├── VaultDepositConfirmation/
│ ├── VaultDepositTxDetails/
│ ├── VaultDepositTxInfo/
│ ├── VaultRedeemConfirmation/
│ ├── VaultRedeemTxDetails/
│ └── VaultRedeemTxInfo/
└── hooks/
├── useGetWidgetUrl.ts
└── useIsEarnFeatureEnabled.ts # ✅ Already exists
Missing:
- hooks/index.ts (barrel export)
- components/index.ts (barrel export)
- services/ directory
- services/index.ts (barrel export)
- types.ts (centralized types)
- Public API in index.ts
Target Structure (standard pattern):
apps/web/src/features/earn/
├── index.ts # 🔄 Public API barrel file (rename from index.tsx)
├── types.ts # ➕ NEW - Centralized TypeScript interfaces
├── constants.ts # ✅ Already exists
├── components/
│ ├── index.ts # ➕ NEW - Component barrel export
│ ├── EarnPage/ # 🔄 RENAMED from index.tsx (main component)
│ ├── EarnButton/ # Public component (exported)
│ ├── EarnInfo/ # Internal component (not exported)
│ ├── EarnView/ # Internal component (not exported)
│ ├── EarnWidget/ # Internal component (not exported)
│ ├── VaultDepositConfirmation/
│ ├── VaultDepositTxDetails/
│ ├── VaultDepositTxInfo/
│ ├── VaultRedeemConfirmation/
│ ├── VaultRedeemTxDetails/
│ └── VaultRedeemTxInfo/
├── hooks/
│ ├── index.ts # ➕ NEW - Hook barrel export
│ ├── useIsEarnFeatureEnabled.ts # ✅ Public hook
│ └── useGetWidgetUrl.ts # Internal hook
└── services/
├── index.ts # ➕ NEW - Service barrel export
└── utils.ts # 🔄 MOVED from root
Structure Decision: Web application structure selected because this is a Next.js feature within the web monorepo workspace (apps/web/). The feature follows the standard pattern documented in apps/web/docs/feature-architecture.md with components, hooks, services, and types organized into dedicated subdirectories with barrel exports. The main index.ts at the feature root exposes only the public API to prevent tight coupling with external code.
Complexity Tracking
Fill ONLY if Constitution Check has violations that must be justified
No violations detected - this section intentionally left empty.
Phase 0: Research & Decisions ✅
Status: COMPLETED
Research Completed
- ✅ External dependencies mapped: 2 component imports for
EarnButton, 1 hook import foruseIsEarnFeatureEnabled - ✅ Public API surface defined: Default export (EarnPage), EarnButton, useIsEarnFeatureEnabled, EarnButtonProps type
- ✅ Type extraction strategy determined: Extract
EarnButtonPropstotypes.ts; keep simple inline types - ✅ Analytics tracking pattern confirmed: Keep in global service (
@/services/analytics/events/earn) - ✅ Feature flag pattern validated: Add
undefinedreturn type to preserve loading state - ✅ Test strategy defined: Manual testing checklist (no automated tests exist currently)
- ✅ Barrel export structure determined: Follow walletconnect reference pattern exactly
Key Decisions Documented
| Decision Area | Choice | Rationale |
|---|---|---|
| Public API Components | EarnButton only |
Only component used outside feature |
| Public API Hooks | useIsEarnFeatureEnabled only |
Only hook used outside feature |
| Public API Types | EarnButtonProps only |
Only type needed by external consumers |
| Analytics | Keep in global service | Already properly separated, used outside feature |
| Feature Flag Hook | Add undefined return type |
Preserve loading state per standard pattern |
| utils.ts Location | Move to services/utils.ts |
Align with standard structure |
| Type Extraction | Extract EarnButtonProps, keep simple types inline |
Balance between DRY and readability |
| Barrel Export Pattern | Follow walletconnect pattern exactly | Proven, compliant reference implementation |
| Main Component Location | Rename index.tsx to components/EarnPage/index.tsx |
Separate concerns (component vs. barrel) |
| Testing Strategy | Manual testing checklist | No automated tests exist currently |
Output: research.md
Phase 1: Design & Contracts ✅
Status: COMPLETED
Data Model Designed
The earn feature has a simple data model with 7 key entities:
- Earn Consent State: Boolean in LocalStorage (
lendDisclaimerAcceptedV1) - Feature Flag State: Boolean | undefined from CGW API (
FEATURES.EARN) - Geoblocking State: Boolean from global context
- Blocked Address State: string | null from backend check
- Asset Selection State: string | undefined from URL query parameter (
asset_id) - Info Panel Display State: Boolean in LocalStorage (
hideEarnInfoV2) - Widget Configuration: Computed dynamically (URL, theme, asset_id)
No Redux store needed - state managed through React hooks and browser APIs.
Output: data-model.md
API Contracts Defined
TypeScript interface contract created defining the public API:
- Default export:
EarnPagecomponent (lazy-loadable) - Named exports:
EarnButton,useIsEarnFeatureEnabled,EarnButtonProps - Internal components/hooks/services documented but not exported
Contract includes usage examples and migration guide for updating deep imports.
Output: contracts/feature-module.ts
Quickstart Guide Created
Comprehensive developer guide covering:
- Quick reference for imports and folder structure
- Usage examples for
EarnButtonanduseIsEarnFeatureEnabled - Development workflow (type-check, lint, test)
- Manual testing checklist (consent, geoblocking, asset selection, analytics)
- Common tasks (adding components, hooks, constants, types)
- Troubleshooting guide (type errors, bundle issues, widget problems)
Output: quickstart.md
Agent Context Updated
✅ Claude Code context file updated with:
- Language: TypeScript 5.x (Next.js web application)
- Framework: React 18, Next.js 14, MUI, ethers.js, @safe-global/utils
- Database: Browser LocalStorage, CGW API chain configs
Phase 2: Task Breakdown
Not generated by this command. Use /speckit.tasks after Phase 1 completion to generate detailed implementation tasks.
Gates & Validation
Pre-Phase 0 Gates
- ✅ Specification complete and validated (
spec.md,checklists/requirements.md) - ✅ Constitution Check passed (no violations)
- ✅ Branch created (
002-refactor-earn-feature) - ✅ External dependencies identified (2 import locations for
EarnButton)
Post-Phase 0 Gates
- ✅ All NEEDS CLARIFICATION items resolved in research.md
- ✅ Public API surface documented with rationale
- ✅ Type extraction strategy defined
- ✅ All research decisions documented
Post-Phase 1 Gates
- ✅
data-model.mdcomplete with entity definitions - ✅
contracts/feature-module.tscomplete with TypeScript interfaces - ✅
quickstart.mdcomplete with developer guide - ✅ Constitution Check re-verified (remains PASS)
- ✅ Agent context updated with new patterns
Pre-Implementation Gates (Phase 2)
- ✅ All Phase 0 and Phase 1 gates passed
- ⏳
tasks.mdgenerated via/speckit.tasks - ⏳ Task priorities and sequencing validated
Notes
Reference Implementation
This refactoring follows the walletconnect feature pattern documented in:
apps/web/docs/feature-architecture.md(architecture documentation)apps/web/src/features/walletconnect/(reference implementation)
Key patterns from walletconnect to replicate:
- Main
index.tsusesdynamic()for default export component - Types exported both individually and via barrel file
- Hooks barrel exports all public hooks
- Constants exported selectively (only public constants)
- Services barrel exports utilities and service functions
External Dependencies
Confirmed external imports (must be updated):
apps/web/src/components/dashboard/Assets/index.tsx- importsEarnButtonapps/web/src/components/balances/AssetsTable/PromoButtons.tsx- importsEarnButtonapps/web/src/components/dashboard/index.tsx- importsuseIsEarnFeatureEnabled
These imports currently use deep paths:
import EarnButton from '@/features/earn/components/EarnButton'
import { useIsEarnFeatureEnabled as useIsEarnPromoEnabled } from '@/features/earn/hooks/useIsEarnFeatureEnabled'
After refactoring, they must use:
import { EarnButton } from '@/features/earn'
import { useIsEarnFeatureEnabled as useIsEarnPromoEnabled } from '@/features/earn'
Lazy Loading Current Status
The earn page (apps/web/src/pages/earn.tsx) already uses dynamic() import:
const LazyEarnPage = dynamic(() => import('@/features/earn'), { ssr: false })
This is correct and compliant. However, the current @/features/earn exports the main component as default from index.tsx. After refactoring to index.ts, the pattern will be preserved but the file extension changes.
Testing Strategy
- All existing tests will remain in their current locations (colocated with components/hooks)
- No test modifications should be necessary as the refactoring preserves all functionality
- Tests will be run before committing to validate no regressions
- Bundle analysis will be performed to verify code splitting works correctly
- Manual testing checklist provided in
quickstart.md(no automated tests exist currently)
Risk Areas
- External imports: The three external import locations must be updated correctly or the app will fail to build
- Type extraction: Inline types must be extracted carefully to avoid breaking type inference
- Barrel export ordering: Circular dependencies could arise if barrel exports are not structured correctly
- Analytics events: Analytics remain in global service (no extraction needed)
Success Validation
After implementation, validate success by:
- ✅ Running type-check:
pnpm --filter @safe-global/web type-check - ✅ Running tests:
pnpm --filter @safe-global/web test - ✅ Running lint:
pnpm --filter @safe-global/web lint - ✅ Bundle analysis to confirm code splitting
- ✅ Manual testing of earn flows (consent, widget, asset selection)
- ✅ Verify external imports use
@/features/earn(no deep paths)
Summary
Phase 0 (Research) ✅: Completed - All unknowns resolved, public API defined, decisions documented
Phase 1 (Design) ✅: Completed - Data model, contracts, and quickstart guide created
Phase 2 (Tasks) ⏳: Pending - Run /speckit.tasks to generate implementation tasks
Next Command: /speckit.tasks to break down the implementation into actionable tasks