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.
6.1 KiB
6.1 KiB
Quickstart: Migrate No Fee Campaign to Feature Architecture
Date: 2025-01-27
Feature: 004-migrate-no-fee-campaign
Overview
This guide provides step-by-step instructions for migrating the No Fee Campaign feature to the new Feature Architecture pattern. The migration maintains 100% functional parity while enabling lazy loading and proper code-splitting.
Prerequisites
- Feature architecture infrastructure (
@/features/__core__) available createFeatureHandlehelper function working- ESLint restricted import rules configured
- Feature flag
FEATURES.NO_FEE_NOVEMBERexists - Semantic mapping
'no-fee-campaign': FEATURES.NO_FEE_NOVEMBERexists increateFeatureHandle.ts
Migration Steps
Step 1: Create Feature Contract
Create apps/web/src/features/no-fee-campaign/contract.ts:
import type NoFeeCampaignBanner from './components/NoFeeCampaignBanner'
import type NoFeeCampaignTransactionCard from './components/NoFeeCampaignTransactionCard'
import type GasTooHighBanner from './components/GasTooHighBanner'
export interface NoFeeCampaignContract {
NoFeeCampaignBanner: typeof NoFeeCampaignBanner
NoFeeCampaignTransactionCard: typeof NoFeeCampaignTransactionCard
GasTooHighBanner: typeof GasTooHighBanner
}
Step 2: Create Feature Implementation
Create apps/web/src/features/no-fee-campaign/feature.ts:
import type { NoFeeCampaignContract } from './contract'
// Direct imports - this file is already lazy-loaded
import NoFeeCampaignBanner from './components/NoFeeCampaignBanner'
import NoFeeCampaignTransactionCard from './components/NoFeeCampaignTransactionCard'
import GasTooHighBanner from './components/GasTooHighBanner'
// Flat structure - naming conventions determine stub behavior
export default {
NoFeeCampaignBanner,
NoFeeCampaignTransactionCard,
GasTooHighBanner,
} satisfies NoFeeCampaignContract
Step 3: Create Public API
Update apps/web/src/features/no-fee-campaign/index.ts:
import { createFeatureHandle } from '@/features/__core__'
import type { NoFeeCampaignContract } from './contract'
// Feature handle - uses semantic mapping (no-fee-campaign → FEATURES.NO_FEE_NOVEMBER)
export const NoFeeCampaignFeature = createFeatureHandle<NoFeeCampaignContract>('no-fee-campaign')
// Export contract type
export type { NoFeeCampaignContract } from './contract'
// Export hooks directly (always loaded, not in contract)
export { useIsNoFeeCampaignEnabled } from './hooks/useIsNoFeeCampaignEnabled'
export { useNoFeeCampaignEligibility } from './hooks/useNoFeeCampaignEligibility'
export { useGasTooHigh } from './hooks/useGasTooHigh'
Step 4: Update Consumer Code
Update all consumers to use useLoadFeature() pattern:
Before (direct import - VIOLATION)::
import NoFeeCampaignBanner from '@/features/no-fee-campaign/components/NoFeeCampaignBanner'
import { useNoFeeCampaignEligibility } from '@/features/no-fee-campaign/hooks/useNoFeeCampaignEligibility'
After:
import { NoFeeCampaignFeature, useNoFeeCampaignEligibility } from '@/features/no-fee-campaign'
import { useLoadFeature } from '@/features/__core__'
function MyComponent() {
// Prefer destructuring for cleaner component usage
const { NoFeeCampaignBanner } = useLoadFeature(NoFeeCampaignFeature)
const eligibility = useNoFeeCampaignEligibility()
return <NoFeeCampaignBanner />
}
Consumer locations to update:
apps/web/src/components/dashboard/index.tsxapps/web/src/components/tx-flow/actions/Execute/ExecuteForm.tsxapps/web/src/components/tx/ExecutionMethodSelector/index.tsxapps/web/src/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer.tsxapps/web/src/pages/balances/index.tsx
Step 5: Verify Migration
Run quality gates:
# Type check
pnpm --filter @safe-global/web type-check
# Lint (should show no restricted import warnings)
pnpm --filter @safe-global/web lint
# Format
pnpm --filter @safe-global/web prettier
# Tests (all should pass)
pnpm --filter @safe-global/web test
Step 6: Verify Bundle Splitting
Build and verify code-splitting:
pnpm --filter @safe-global/web build
# Check that No Fee Campaign code is in separate chunk
ls -la apps/web/.next/static/chunks/ | grep -i no-fee
Key Patterns
Component Usage
// Prefer destructuring for cleaner component usage
// Components render null when not ready (no check needed)
const { NoFeeCampaignBanner } = useLoadFeature(NoFeeCampaignFeature)
return <NoFeeCampaignBanner onDismiss={handleDismiss} />
Hook Usage
// Hooks imported directly, always safe to call
import { useNoFeeCampaignEligibility } from '@/features/no-fee-campaign'
const { isEligible, remaining, limit } = useNoFeeCampaignEligibility()
Explicit State Handling
const noFeeFeature = useLoadFeature(NoFeeCampaignFeature)
if (noFeeFeature.$isDisabled) return null
if (!noFeeFeature.$isReady) return <Skeleton />
// Destructure after state checks
const { NoFeeCampaignBanner } = noFeeFeature
return <NoFeeCampaignBanner />
Common Pitfalls
- Don't use lazy() in feature.ts - The entire feature is already lazy-loaded
- Don't include hooks in contract - Export them directly from index.ts
- Don't nest structure - Use flat structure in contract and feature.ts
- Don't import from internal folders - Use feature handle and direct hook imports
- Don't change business logic - Maintain 100% functional parity
Verification Checklist
contract.tscreated with flat structurefeature.tscreated with direct importsindex.tsexports feature handle and hooks- All consumers updated to use
useLoadFeature() - Type check passes
- Lint passes (no restricted import warnings)
- Tests pass
- Bundle analysis shows code-splitting
- Feature works identically to before migration
Next Steps
After migration:
- Verify all acceptance scenarios from spec
- Run bundle analysis to confirm code-splitting
- Test on chains with feature enabled and disabled
- Update Storybook stories if needed
- Document any edge cases discovered