This guide documents the implementation of the "Show Friendbot Hint on Testnet" feature for the Mux Protocol frontend. The feature provides contextual guidance to developers working on Stellar's testnet by displaying helpful information about Friendbot (the testnet faucet).
-
Utilities
src/utils/friendbot.ts- Friendbot URL generation and validation utilitiessrc/utils/__tests__/friendbot.test.ts- Comprehensive unit tests for friendbot utilities
-
Components
src/components/ui/TestnetHint.tsx- Reusable TestnetHint component with two variantssrc/components/ui/__tests__/TestnetHint.test.tsx- Component unit testssrc/components/wallet/__tests__/WalletTable.integration.test.tsx- Integration tests
-
Documentation
TESTNET_HINT_FEATURE.md- Feature documentationIMPLEMENTATION_GUIDE.md- This file
- Components
src/components/wallet/WalletTable.tsx- Integrated TestnetHint component
Decision: Create a reusable TestnetHint component instead of inline logic.
Rationale:
- Promotes reusability across multiple pages
- Easier to test in isolation
- Cleaner separation of concerns
- Can be used in different contexts (wallets page, dashboard, etc.)
Decision: Provide both "default" and "compact" variants.
Rationale:
- Default variant for prominent placement (e.g., above wallet table)
- Compact variant for inline usage (e.g., in table headers or sidebars)
- Flexibility for different UI contexts
Decision: Dismissal state is local to component instance, not persisted.
Rationale:
- Keeps implementation simple and stateless
- Avoids localStorage complexity
- Ensures hint reappears on page refresh (good for development)
- Can be enhanced later with persistent storage if needed
Decision: Use useMemo to detect testnet wallets and conditionally render hint.
Rationale:
- Efficient computation (only recalculates when wallets change)
- Automatic visibility based on data
- No manual prop passing required
- Follows React best practices
Decision: Separate friendbot logic into utility functions.
Rationale:
- Reusable across components
- Easier to test
- Encapsulates Friendbot-specific logic
- Can be used in other contexts (e.g., API calls, validation)
const [isDismissed, setIsDismissed] = useState(false);- Scope: Local to component instance
- Persistence: None (resets on remount)
- Rationale: Simplicity, no side effects
const hasTestnetWallets = useMemo(
() => wallets.some((wallet) => wallet.network === "testnet"),
[wallets],
);- Scope: Computed from props
- Optimization: Memoized to prevent unnecessary recalculations
- Dependency: Only recalculates when
walletsarray changes
- ✅ URL generation with proper encoding
- ✅ Network eligibility checks
- ✅ Address validation (valid/invalid cases)
- ✅ Error handling (empty addresses, null/undefined)
- ✅ Constants validation
- ✅ Rendering in both variants
- ✅ Dismissal functionality
- ✅ External link security attributes
- ✅ Accessibility attributes (ARIA labels)
- ✅ Dark mode support
- ✅ State management (independent instances)
- ✅ Custom className application
- ✅ TestnetHint visibility based on wallet network
- ✅ Hint not shown for mainnet-only wallets
- ✅ Hint shown for testnet wallets
- ✅ Hint shown for mixed wallets
- ✅ Wallet rendering and display
- ✅ Edge cases (empty list, multiple testnet wallets)
- ✅ Dynamic updates when wallets change
Total Test Cases: 50+
Coverage Areas:
- Happy path scenarios
- Edge cases and error conditions
- Accessibility compliance
- Security attributes
- State management
- Component integration
- Responsive behavior
// Valid: Starts with 'G', 56 characters, Base32 characters
/^G[A-Z2-7]{55}$/
// Invalid cases handled:
- Empty string
- null/undefined
- Wrong length
- Invalid characters
- Non-string types// Valid: "testnet" | "mainnet"
// Invalid cases handled:
- Other network names
- null/undefined
- Non-string types// Throws error for:
- Empty address
- Whitespace-only address
// Handles:
- Special characters (URL encoded)
- Long addresses (properly encoded)// Gracefully handles:
- Empty wallet list
- Mixed network wallets
- Dismissed state
- Missing props (uses defaults)<a
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
>Security Measures:
target="_blank"- Opens in new tabrel="noopener noreferrer"- Prevents window.opener access- URL encoding - Prevents injection attacks
- Stellar address format validation before URL generation
- No innerHTML or dangerouslySetInnerHTML usage
- No user input directly interpolated into URLs
- Full TypeScript support
- Strict type checking enabled
- No
anytypes used
aria-label="Dismiss testnet hint"<button type="button">Dismiss</button>- Buttons have focus-visible styles
- Keyboard navigation supported
- Proper tab order
- Amber color scheme meets WCAG AA standards
- Dark mode variants provided
- No color-only information
const hasTestnetWallets = useMemo(
() => wallets.some((wallet) => wallet.network === "testnet"),
[wallets],
);Benefits:
- Prevents unnecessary recalculations
- Avoids re-rendering when props haven't changed
- O(n) complexity only when wallets change
- Conditional rendering (hint only shown when needed)
- No unnecessary DOM nodes
- Efficient state updates
// Store dismissal preference in localStorage
const [isDismissed, setIsDismissed] = useState(() => {
return localStorage.getItem("testnet-hint-dismissed") === "true";
});
const handleDismiss = () => {
setIsDismissed(true);
localStorage.setItem("testnet-hint-dismissed", "true");
};// Pre-fill Friendbot with address
const friendbotUrlWithAddress = getFriendbotUrl(walletAddress);// Track hint interactions
const handleDismiss = () => {
analytics.track("testnet_hint_dismissed");
setIsDismissed(true);
};interface TestnetHintProps {
title?: string;
description?: string;
friendbotUrl?: string;
docsUrl?: string;
}- Code implementation complete
- Unit tests written and passing
- Integration tests written and passing
- Documentation complete
- Accessibility verified
- Security review completed
- Dark mode tested
- Responsive design verified
- Code review completed
- Merged to main branch
- Deployed to staging
- Deployed to production
Checklist:
- Verify wallets have
network: "testnet" - Check that WalletTable is rendering with wallets
- Ensure TestnetHint component is imported correctly
- Check browser console for errors
Checklist:
- Check if all wallets are testnet
- Verify network property is correctly set
- Check for stale wallet data
- Verify useMemo dependency array
Checklist:
- Verify FRIENDBOT_URL and FRIENDBOT_DOCS_URL constants
- Check browser console for errors
- Ensure external links are not blocked by CSP
- Test in different browsers
- Testnet Hint Feature Documentation
- Explorer Link Component Documentation
- Stellar Testnet Docs
- Friendbot Faucet
- Code follows project conventions
- Tests are comprehensive and passing
- Documentation is clear and complete
- No console errors or warnings
- Accessibility standards met
- Security best practices followed
- Performance optimized
- Dark mode working correctly
- Responsive design verified
- No breaking changes to existing features
For questions about this implementation:
- Review the feature documentation
- Check the test files for usage examples
- Review the component props and interfaces
- Check the troubleshooting section