A fully functional e-commerce app — browse, cart, checkout — where every screen exercises one or more Morph SDK features on a real device. No toy examples. No stubs. Each feature has an exact test path described below.
| # | Feature | Plan | Screen |
|---|---|---|---|
| 1 | AI dark mode from AppColors |
Free | System toggle |
| 2 | Semantic color extension (ThemeColors) |
Free | All screens |
| 3 | Zone reorder (MorphReorderableColumn) |
Pro | Home |
| 4 | Navigation reorder (MorphReorderableNav) |
Pro | Bottom nav |
| 5 | Grip detection (GripAdaptiveLayout) |
Pro | Product |
| 6 | Battery-aware UI (BatteryAwareWidget) |
Pro | Catalog |
| 7 | Interruption recovery — checkout (morphSetCheckoutMultiStepContext) |
Pro | Checkout |
| 8 | Recovery card refusal → restart (onSuggestionRefused) |
Pro | Checkout |
| 9 | Fatigue detection — banner + simplified form (FatigueAdaptiveForm) |
Business | Checkout |
| 10 | Analytics consent toggle | Business | Settings |
- Flutter 3.10+, Dart 3+
- Physical device recommended for grip detection and battery signals — emulators don't expose those sensors.
- Android or iOS — both work. Samsung SM-S921B tested.
git clone https://github.com/morphuiapp/morphui-flutter-ecommerce
cd morphui-flutter-ecommerce
flutter pub get
flutter runTwo MorphConfig flags are active in this demo. Remove them before shipping to production.
MorphConfigdev flags are for demonstration only and are not documented in the public API. Remove both before shipping to production.
// lib/main.dart
config: const MorphConfig(
devMinPauseSeconds: 5, // Default is 30 s. Lets recovery card appear after 5 s.
devEnableAllFeatures: true, // Unlocks Agency features (fatigue, GPS) on a FREE key.
),The overlay timers are also shortened for demos:
MorphSuggestionOverlay(
firstCheckDelay: const Duration(seconds: 3), // Default 30 s
checkInterval: const Duration(seconds: 15), // Default 3 min
child: child,
)Morph generates the opposite theme from your AppColors — no hand-written darkTheme.
MorphProvider(
baseTheme: AppTheme.light,
colors: const MorphColors(
background: AppColors.background,
primary: AppColors.primary,
// ...
),
child: MaterialApp.router(
theme: AppTheme.light,
darkTheme: AppTheme.dark, // soft fallback — Morph overrides this
themeMode: ThemeMode.system,
),
)How to test
- Open the app in light mode.
- Toggle system dark mode.
- Every screen animates to the AI-generated palette. Text, surfaces, buttons all adapt — nothing turns pure black.
All widgets read from Theme.of(context) through a BuildContext extension.
Switching brightness is automatic — no if (isDark) anywhere in the UI code.
// lib/core/theme/theme_colors.dart
extension ThemeColors on BuildContext {
Color get colorBackground => Theme.of(this).scaffoldBackgroundColor;
Color get colorText => Theme.of(this).colorScheme.onSurface;
Color get colorSubtle => Theme.of(this).colorScheme.onSurfaceVariant;
// ...
}Note:
ThemeColorsis aBuildContextextension added in this demo — it is not part of the Morph SDK. The equivalent Morph API iscontext.morphPaletteandAppColors.backgroundOf(context). See AppColors integration.
How to test Toggle dark mode while browsing any screen. Headings, subtitles, borders, and backgrounds all update together. No widget needs a manual color override.
Morph tracks interaction frequency per zone locally (Hive — nothing leaves the
device). After minInteractions: 20 threshold, zones promoted by the scorer
float upward automatically.
// lib/features/home/home_screen.dart
MorphReorderableColumn(
zones: [
MorphZone(id: 'featured', priority: 0, child: FeaturedSection()),
MorphZone(id: 'categories', priority: 1, child: CategoriesSection()),
MorphZone(id: 'trending', priority: 2, child: TrendingSection()),
MorphZone(id: 'recent', priority: 3, child: RecentSection()),
],
)How to test
- Open the home page.
- Tap items inside Trending or Recent repeatedly (20+ taps total).
- Navigate away and come back — the heavily-tapped zone floats toward the top.
Same behavioral scorer — applied to the bottom nav. Tabs you visit most often shift toward your dominant thumb side.
How to test
- Visit Catalog and Cart repeatedly over several sessions.
- After 20+ tab switches, the most-used tab migrates toward position 1 or 2.
The accelerometer decides which hand is dominant. The primary action (Add to Cart) repositions toward the active thumb — no permission required.
// lib/features/product/product_screen.dart
GripAdaptiveLayout(
primaryAction: AddToCartButton(product: product),
child: ProductDetail(product: product),
)How to test
- Open any product page.
- Hold the phone naturally in your right hand — the Add to Cart button appears at bottom-right.
- Switch to your left hand — the button slides to bottom-left within a second.
Four image-quality tiers driven by live battery level.
Charging overrides battery level — a phone at 12% while plugged in stays in normal.
// lib/features/catalog/widgets/adaptive_product_card.dart
BatteryAwareWidget(
normal: AdaptiveProductCard(quality: ImageQuality.high, showAnimations: true),
medium: AdaptiveProductCard(quality: ImageQuality.medium, showAnimations: true),
low: AdaptiveProductCard(quality: ImageQuality.low, showAnimations: false),
critical: AdaptiveProductCard(quality: ImageQuality.minimal, minimalMode: true),
)| Battery | Tier | What changes |
|---|---|---|
| ≥ 40 % | normal |
Full images, animations on |
| 20–40 % | medium |
Compressed images |
| 10–20 % | low |
Thumbnail only, no animations |
| < 10 % | critical |
Text-only card |
How to test
- Open the Catalog screen.
- Watch the battery badge in the card — updates as battery level changes.
- Unplug the device and let it drain to < 20 % to see a tier downgrade.
The user fills the shipping form, backgrounds the app for 5 s+, and comes back. Morph surfaces a recovery card: "Pick up where you left off?"
// lib/features/checkout/checkout_screen.dart
context.morphSetCheckoutMultiStepContext(
workflowId: _workflowId,
step: _currentStep,
totalSteps: 3,
cartData: { 'total': cart.total, 'itemCount': cart.itemCount },
savedData: { for (final e in _savedStepData.entries) 'step${e.key}': e.value },
strategy: RecoveryStrategy.confirm, // explicit confirm before rehydrate
ttl: const Duration(minutes: 15),
);Each step completion is also recorded as a navigation signal:
void _completeStep(int step, Map<String, dynamic> data) {
context.morphFatigueDetector?.recordNavigationError(); // feeds fatigue scorer
setState(() { _savedStepData[step] = data; ... });
_declareStep();
}Note:
morphSetCheckoutMultiStepContextis a convenience wrapper aroundmorphSetKycContextscoped to checkout flows. See Multi-step workflows for the full API reference.
How to test
- Start checkout → fill Shipping → tap Continue to payment.
- Press Home (background the app) → wait 5 s.
- Return to the app.
- The recovery card appears: tap Continue — you resume at Payment with shipping data pre-filled.
Recovery card — "Start over" behavior
When the user taps Start over on the recovery card, onSuggestionRefused
fires and resets the checkout to step 1 via a Riverpod signal:
// lib/main.dart
MorphSuggestionOverlay(
onSuggestionRefused: (s) {
if (s.id.startsWith('recovery_/checkout')) {
ref.read(checkoutStartOverSignalProvider.notifier).state++;
}
},
child: child,
)
// lib/features/checkout/checkout_screen.dart
ref.listen<int>(checkoutStartOverSignalProvider, (_, _) {
if (mounted) _startOver(); // resets to step 1, clears saved data
});Morph scores six behavioral signals in real time. When the score crosses the banner threshold, an inline notice appears at the top of the form. Past the simplified threshold, non-essential fields hide automatically.
// lib/features/checkout/steps/payment_step.dart
FatigueAdaptiveForm(
normalFields: [
cardNumberField, cardholderField, expiryField, cvcField,
],
simplifiedFields: [
cardNumberField, cvcField, // only essentials when fatigue is high
],
submitButton: ElevatedButton(onPressed: _submit, child: Text('Review order')),
onReset: () {
_cardNumber.clear(); _holder.clear(); _expiry.clear(); _cvc.clear();
widget.onStartOver?.call(); // walks back to step 1 Shipping
},
)Fatigue score — signal weights
| Signal | How it's recorded | Max contribution |
|---|---|---|
| Missed taps (last 10) | recordTap(position, targetCenter, targetSize) |
40 pts |
| Typing slowdown | recordKeystroke() on every keystroke |
20 pts |
| Navigation errors | recordNavigationError() on each step completion |
15 pts |
| Typing errors | recordTypingError() on each backspace |
5 pts |
| Tap errors | recordTapError() on background tap |
5 pts |
| Session > 30 min | automatic | 10 pts |
Thresholds (demo — lowered for fast testing)
| Score | Effect |
|---|---|
| ≥ 15 | Banner "Interface adjusted" fades in |
| ≥ 40 | Simplified form activates, banner reads "Simplified view active" |
Production defaults: banner at 40, simplified at 70.
How to test — natural path
- Enter the checkout → fill Shipping → Continue (1 nav error, +3 pts).
- Fill Payment → Review order (2 nav errors, +6 pts).
- Tap Start over on the recovery card or go back and repeat 2–3 more times.
- After 5 step completions (15 pts), the banner appears automatically.
- Making typos (backspace) and tapping the form background accelerates it.
How to test — debug FAB (debug builds only)
A small brain icon FAB appears at bottom-right of the checkout screen in
debug builds (kDebugMode). Tap it once to instantly saturate all error
counters and see the banner without filling forms repeatedly.
// Visible in debug only — removed from release builds automatically
FloatingActionButton.small(
onPressed: () => context.morphFatigueDetector?.debugForceHighFatigue(),
child: Icon(Icons.psychology_alt_outlined),
)"Start over" on the fatigue banner
The inline banner has its own "Start over" button — separate from the recovery card. Tapping it:
- Resets the fatigue score to 0 (
detector.resetFatigue()). - Clears all form fields.
- Navigates the checkout back to step 1 Shipping.
Morph never sends data without enabled: true AND userConsent: true.
The Settings screen wires the toggle.
// lib/main.dart
analytics: analytics, // null → nothing leaves the device
// lib/shared/providers/analytics_provider.dart
final analyticsConsentProvider = StateProvider<MorphAnalyticsConfig?>((ref) => null);How to test
- Open Settings → Share usage data.
- Toggle ON → analytics starts (aggregated, never content).
- Toggle OFF → local store wiped immediately via
_db.clearAll().
main.dart
└── MorphProvider ← SDK root: colors, features, config
└── MaterialApp.router
└── builder:
└── MorphSuggestionOverlay ← recovery card surface
├── onSuggestionRefused → checkoutStartOverSignalProvider
└── navigator
home_screen.dart
└── MorphReorderableColumn ← zone reorder
└── MorphZone × 4
product_screen.dart
└── GripAdaptiveLayout ← thumb tracking
└── primaryAction: AddToCartButton
catalog_screen.dart
└── BatteryAwareWidget ← 4 image-quality tiers
checkout_screen.dart
├── context.morphSetCheckoutMultiStepContext ← step snapshot + chain
├── ref.listen(checkoutStartOverSignalProvider) ← reacts to card refusal
└── steps/
├── shipping_step.dart
│ └── FatigueAdaptiveForm ← adaptive fields + inline banner
└── payment_step.dart
└── FatigueAdaptiveForm
lib/
├── main.dart # MorphProvider · MorphSuggestionOverlay
├── core/
│ ├── constants/app_constants.dart # license key · workflow prefix
│ ├── routes/app_router.dart # GoRouter · MorphNavigatorObserver
│ └── theme/
│ ├── app_colors.dart # brand palette constants
│ ├── app_theme.dart # AppTheme.light / AppTheme.dark
│ └── theme_colors.dart # BuildContext color extension
├── features/
│ ├── home/ # MorphReorderableColumn · 4 zones
│ ├── catalog/ # BatteryAwareWidget grid
│ ├── product/ # GripAdaptiveLayout · Add to cart
│ ├── cart/ # Cart summary
│ ├── checkout/
│ │ ├── checkout_screen.dart # Recovery chain · fatigue signals
│ │ ├── steps/
│ │ │ ├── shipping_step.dart # FatigueAdaptiveForm
│ │ │ ├── payment_step.dart # FatigueAdaptiveForm
│ │ │ └── review_step.dart
│ │ └── widgets/checkout_progress.dart
│ └── settings/ # Analytics toggle · plan badge · clear data
└── shared/
├── models/ # Product · Cart · Category
├── data/ # Mock products
├── providers/
│ ├── cart_provider.dart
│ ├── analytics_provider.dart
│ └── checkout_reset_provider.dart # Riverpod signal for card refusal
└── widgets/ # MorphStatusBadge · FeatureCallout
| Layer | Choice |
|---|---|
| Framework | Flutter 3.10+ |
| Language | Dart 3+ |
| Routing | go_router |
| State | flutter_riverpod |
| SDK | morphui (path dep → pub.dev on release) |
| Storage | Hive (via SDK) — all behavioral data local |
MIT — clone, fork, customize, ship.
morphui.dev · Docs · Dashboard · pub.dev
Built with Morph — the Intelligent UI SDK for Flutter + React.