This document outlines development best practices for the Flow Reference Wallet (FRW) monorepo to ensure code quality, maintainability, and adherence to our MVVM architecture.
❌ NEVER use console.log directly
// ❌ BAD - Direct console logging
console.log('User logged in:', userId);
console.error('Transaction failed:', error);✅ ALWAYS use the centralized logger
// ✅ GOOD - Structured logging
import { logger } from '@onflow/frw-utils';
logger.info('User authentication completed', { userId, timestamp: Date.now() });
logger.error('Transaction processing failed', {
error: error.message,
transactionId,
userId,
});Benefits:
- Cross-platform compatibility (Extension, React Native, Web)
- Structured logging with context
- Centralized log management
- Production-safe log levels
❌ NEVER import business logic in packages/ui
// ❌ BAD - Business logic imports in UI
import { useWalletStore } from '@onflow/frw-stores';
import { tokenService } from '@onflow/frw-services';
// ❌ BAD - Hardcoded values
const TokenCard = () => (
<YStack bg="#FF0000" p={16}> {/* Hardcoded color and size */}
<Text fontSize={14}>Balance</Text> {/* Hardcoded size */}
</YStack>
);✅ CORRECT approach for UI components
// ✅ GOOD - Pure UI with props
interface TokenCardProps {
balance: string;
tokenName: string;
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
}
const TokenCard = ({ balance, tokenName, variant = 'primary', size = 'medium' }: TokenCardProps) => (
<YStack
bg={variant === 'primary' ? '$blue500' : '$gray100'}
p={size === 'large' ? '$6' : '$4'}
>
<Text fontSize={size === 'large' ? '$6' : '$4'}>{balance}</Text>
</YStack>
);UI Package Rules:
- Zero business logic imports - no stores, services, or workflows
- No hardcoded colors/sizes - use theme tokens (
$blue500,$4, etc.) - Pure components - all data via props
- Exception handling - Only for critical cases with team approval
❌ NEVER use hardcoded strings in UI or screens packages
// ❌ BAD - Hardcoded strings
const WelcomeScreen = () => (
<YStack>
<Text>Welcome to Flow Wallet</Text>
<Button>Get Started</Button>
<Text>Create your secure wallet today</Text>
</YStack>
);✅ ALWAYS use i18n context for text content
// ✅ GOOD - Using i18n context
import { useI18n } from '@onflow/frw-context';
const WelcomeScreen = () => {
const { t } = useI18n();
return (
<YStack>
<Text>{t('welcome.title')}</Text>
<Button>{t('welcome.getStarted')}</Button>
<Text>{t('welcome.subtitle')}</Text>
</YStack>
);
};✅ Define all text content in JSON i18n files
// screens/context/i18n/en.json
{
"welcome": {
"title": "Welcome to Flow Wallet",
"getStarted": "Get Started",
"subtitle": "Create your secure wallet today"
},
"buttons": {
"cancel": "Cancel",
"confirm": "Confirm",
"next": "Next"
}
}i18n Guidelines:
- No hardcoded text - all user-facing strings must use i18n
- Structured keys - use nested objects for organization (
welcome.title) - Reusable keys - common buttons/actions should be shared (
buttons.cancel) - Context awareness - keys should indicate where they're used
- Placeholder support - use i18n interpolation for dynamic content
❌ AVOID complex logic outside screen components
// ❌ BAD - Logic scattered across multiple files
// utils/send-helpers.ts
export const prepareSendTransaction = (amount, recipient) => {
/* complex logic */
};
// SendScreen.tsx
const SendScreen = () => {
const transaction = prepareSendTransaction(amount, recipient); // Logic outside screen
// ...
};✅ KEEP screen logic self-contained
// ✅ GOOD - Logic within screen (single-use screens)
const SendTokensScreen = () => {
// Screen-specific logic here
const handleSendTransaction = useCallback(
async (amount: string, recipient: string) => {
try {
logger.info('Initiating token transfer', { amount, recipient });
const result = await sendStore.sendTokens(amount, recipient);
// Handle result...
} catch (error) {
logger.error('Token transfer failed', {
error: error.message,
amount,
recipient,
});
}
},
[]
);
// Rest of component...
};✅ PARAMETERIZE reusable screens
// ✅ GOOD - Reusable screen with parameters
interface TokenSelectorScreenProps {
onTokenSelect: (token: TokenModel) => void;
allowedTokens?: string[];
title?: string;
subtitle?: string;
}
const TokenSelectorScreen = ({
onTokenSelect,
allowedTokens = [],
title = 'Select Token',
subtitle,
}: TokenSelectorScreenProps) => {
// Implementation...
};Screen Guidelines:
- Single-use screens: Keep implementation within the screen file
- Reusable screens: Use props for customization
- Complex workflows: Use stores/services, not external utility functions
- Data fetching: Delegate to stores, not direct service calls
🚨 CRITICAL: Any changes to PlatformSpec require team notification
// packages/context/src/interfaces/PlatformSpec.ts
export interface PlatformSpec {
// Adding or modifying any method here affects ALL platforms:
// - React Native (iOS/Android)
// - Browser Extension (Chrome/Firefox)
// - Web Application
}Change Process:
- Propose changes in team discussion before implementation
- Document impact on all platforms (RN, Extension, Web)
- Coordinate implementation across platform teams
- Test thoroughly on all supported platforms
Why this matters:
- Cross-platform compatibility requirements
- Native implementation needed for RN
- Extension security model considerations
- Web browser API limitations
✅ ALL PlatformSpec data structures MUST be defined in
packages/types/src/Bridge.ts
// ✅ GOOD - Centralized bridge types
// packages/types/src/Bridge.ts
export interface WalletConnectionRequest {
dappName: string;
dappUrl: string;
permissions: string[];
chainId?: string;
}
export interface TransactionRequest {
cadence: string;
arguments: ArgumentValue[];
gasLimit?: number;
}
// packages/context/src/interfaces/PlatformSpec.ts
import { WalletConnectionRequest, TransactionRequest } from '@onflow/frw-types';
export interface PlatformSpec {
connectWallet(request: WalletConnectionRequest): Promise<boolean>;
sendTransaction(request: TransactionRequest): Promise<string>;
}❌ NEVER define bridge types inline
// ❌ BAD - Inline type definition
export interface PlatformSpec {
connectWallet(request: {
dappName: string;
dappUrl: string; // This should be in Bridge.ts!
}): Promise<boolean>;
}Benefits:
- Single source of truth for cross-platform data contracts
- Type safety across all platform implementations
- Documentation - Bridge.ts serves as API reference
- Version control - Changes are tracked in one place
✅ ALWAYS define storage keys in StorageKeyMap
// ✅ GOOD - Typed storage usage
import { StorageKeyMap } from '@onflow/frw-context';
// Define your data type
interface UserPreferences {
theme: 'light' | 'dark';
currency: string;
notifications: boolean;
}
// Add to StorageKeyMap
declare module '@onflow/frw-context' {
interface StorageKeyMap {
userPreferences: UserPreferences;
}
}
// Use with full type safety
const preferences = await storage.get('userPreferences');
await storage.set('userPreferences', {
theme: 'dark',
currency: 'USD',
notifications: true,
});❌ NEVER use untyped storage
// ❌ BAD - No type safety
await storage.set('user_prefs', { theme: 'dark' }); // Typo-prone key
const prefs = await storage.get('userPrefs'); // Different key!
// prefs is 'any' type - no IntelliSense, no type checkingStorage Best Practices:
- Strong typing - No
anytypes allowed - Consistent keys - Use camelCase, defined in StorageKeyMap
- Error handling - Always handle storage failures
- Sensitive data - Use encrypted storage for secrets
- Version migrations - Handle data structure changes gracefully
📋 Model Layer (packages/types)
- Pure data structures and interfaces
- Zero dependencies on other packages
- TypeScript definitions only
🌐 Network Layer (packages/api, packages/cadence)
- HTTP API clients and blockchain interactions
- Data fetching and external service communication
- Error handling for network operations
⚙️ Business Logic (packages/services, packages/workflow)
- Domain services and transaction orchestration
- Data transformation and validation
- Business rules implementation
🧠 ViewModel (packages/stores)
- UI state management with Zustand
- Data caching and transformation for UI consumption
- Bridge between business logic and UI
🎨 UI Layer (packages/ui)
- Pure, stateless components
- No business logic or store imports
- Theme-based styling only
📺 Screen Layer (packages/screens)
- Integration of UI components with ViewModels
- Screen-specific business logic
- Navigation and user interaction handling
// ✅ ALLOWED imports by package
// packages/types - No imports from other FRW packages
// packages/api & packages/cadence
import { UserModel } from '@onflow/frw-types'; // ✅ Types only
// packages/services & packages/workflow
import { TokenModel } from '@onflow/frw-types'; // ✅ Types
import { apiClient } from '@onflow/frw-api'; // ✅ Network layer
// packages/stores
import { WalletService } from '@onflow/frw-services'; // ✅ Business logic
import { UserModel } from '@onflow/frw-types'; // ✅ Types
// packages/ui
import { TokenModel } from '@onflow/frw-types'; // ✅ Types for props only
// ❌ NO imports from stores, services, workflow, api, cadence
// packages/screens
import { TokenCard } from '@onflow/frw-ui'; // ✅ UI components
import { useWalletStore } from '@onflow/frw-stores'; // ✅ ViewModels
import { TokenModel } from '@onflow/frw-types'; // ✅ TypesBefore submitting any PR, ensure:
- Logging: No
console.logusage, proper logger implementation - UI Purity: No business logic imports in
packages/ui - Internationalization: No hardcoded strings, proper i18n usage in UI/screens
- Screen Logic: Appropriate abstraction level for screen components
- PlatformSpec: Team notification for any interface changes
- Bridge Types: All PlatformSpec data structures in
Bridge.ts - Storage Types: Strong typing with StorageKeyMap definitions
- MVVM Boundaries: Correct import patterns between layers
- Error Handling: Proper error logging and user feedback
- Type Safety: No
anytypes without explicit justification
Run these commands before submitting:
# Type checking
pnpm typecheck
# Linting (includes import boundary checks)
pnpm lint
# Formatting
pnpm format
# Full validation
pnpm build && pnpm testWhen in doubt:
- Architecture questions: Consult team lead or architect
- PlatformSpec changes: Create RFC and notify all platform maintainers
- Storage patterns: Review existing implementations in stores
- UI components: Check Storybook for existing patterns (
pnpm storybook)
Remember: These guidelines maintain code quality, prevent bugs, and ensure our MVVM architecture scales effectively across all platforms. Following them makes the entire team more productive! 🎯