Compare commits
No commits in common. "0a0abc2c3d9d66976169f536d33bbc2367fa4c4b" and "6f10efc88d489c7e79b24fa13cc866b9a34562ff" have entirely different histories.
0a0abc2c3d
...
6f10efc88d
|
|
@ -93,7 +93,6 @@ Rules:
|
|||
- ✅ Multi-select groundwork landed (Stage 3A complete — merged `feat/multi-select-groundwork` → `main`, 2026-05-22).
|
||||
- ✅ List efficiency improvements landed (Stage 3B complete — merged `feat/list-efficiency-improvements` → `main`, 2026-05-22). Stage 3 complete.
|
||||
- ✅ Design system expansion and shared widget migration landed (Stage 4A complete — merged `feat/design-system-shared-widgets` → `main`, 2026-05-22).
|
||||
- ✅ Cross-platform shell composition strategy landed (Stage 4B complete — merged `feat/shared-composition-pattern` → `main`, 2026-05-22).
|
||||
|
||||
### Current narrow edit capabilities on `main`
|
||||
|
||||
|
|
@ -106,20 +105,18 @@ Rules:
|
|||
### Latest known validation state on `main`
|
||||
|
||||
- `dart analyze` clean
|
||||
- `core` tests passing
|
||||
- `design_system` tests passing
|
||||
- `feature_wordpress` tests passing
|
||||
- `kell_web` tests passing
|
||||
- latest reported count for `core`: `20/20 passed`
|
||||
- latest reported count for `design_system`: `41/41 passed`
|
||||
- latest reported count for `feature_wordpress`: `294/294 passed`
|
||||
- latest reported count for `kell_web`: `24/24 passed`
|
||||
- baseline commit: merge of `feat/shared-composition-pattern` (2026-05-22)
|
||||
- baseline commit: merge of `feat/design-system-shared-widgets` (2026-05-22)
|
||||
|
||||
### Next recommended branch
|
||||
|
||||
**`feat/flutter-cicd`** — Stage 4C: Flutter CI/CD pipeline.
|
||||
Branch from latest `main`. Stage 4B (cross-platform shell composition) is complete.
|
||||
**`feat/flutter-cicd`** — Stage 4B: Cross-platform shell composition strategy.
|
||||
Branch from latest `main`. Stage 4A (design system expansion) is complete.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -594,4 +591,4 @@ Working rules:
|
|||
| `data` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** |
|
||||
| `integrations` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** |
|
||||
| `design_system` | N/A | N/A | N/A | N/A | ✅ Expanded (theme, typography, layout, 7 widgets) | 41 | **Foundation ready** |
|
||||
| `core` | ✅ Partial | N/A | N/A | N/A | N/A | 20 | **Foundation ready** |
|
||||
| `core` | ✅ Partial | N/A | N/A | N/A | N/A | Minimal | **Foundation only** |
|
||||
|
|
|
|||
|
|
@ -1,18 +1,85 @@
|
|||
/// Re-exports [KcAppConfig] and [KcAppEnvironment] from the shared `core` package.
|
||||
/// Runtime configuration read from `--dart-define` values.
|
||||
///
|
||||
/// This file preserves backward compatibility for existing imports within
|
||||
/// `kell_web`. New code should import directly from `package:core/core.dart`.
|
||||
/// The app reads the following compile-time constants:
|
||||
///
|
||||
/// The original class names are preserved as typedefs so that existing
|
||||
/// references continue to compile without changes.
|
||||
library;
|
||||
/// | Key | Description | Default |
|
||||
/// |------------------------|----------------------------------------------|----------|
|
||||
/// | `KC_ENV` | `fake` or `wordpress` | `fake` |
|
||||
/// | `KC_WC_SITE_URL` | WordPress site URL | (empty) |
|
||||
/// | `KC_WC_CONSUMER_KEY` | WooCommerce REST API consumer key | (empty) |
|
||||
/// | `KC_WC_CONSUMER_SECRET`| WooCommerce REST API consumer secret | (empty) |
|
||||
///
|
||||
/// Usage:
|
||||
/// ```sh
|
||||
/// flutter run -d chrome \
|
||||
/// --dart-define=KC_ENV=wordpress \
|
||||
/// --dart-define=KC_WC_SITE_URL=https://store.kellcreations.com \
|
||||
/// --dart-define=KC_WC_CONSUMER_KEY=ck_xxx \
|
||||
/// --dart-define=KC_WC_CONSUMER_SECRET=cs_xxx
|
||||
/// ```
|
||||
class AppConfig {
|
||||
/// The environment mode: `fake` or `wordpress`.
|
||||
final AppEnvironment environment;
|
||||
|
||||
export 'package:core/core.dart' show KcAppConfig, KcAppEnvironment;
|
||||
/// WordPress / WooCommerce site URL (only used in [AppEnvironment.wordpress]).
|
||||
final String wcSiteUrl;
|
||||
|
||||
import 'package:core/core.dart';
|
||||
/// WooCommerce REST API consumer key.
|
||||
final String wcConsumerKey;
|
||||
|
||||
/// @Deprecated('Use KcAppConfig from core instead.')
|
||||
typedef AppConfig = KcAppConfig;
|
||||
/// WooCommerce REST API consumer secret.
|
||||
final String wcConsumerSecret;
|
||||
|
||||
/// @Deprecated('Use KcAppEnvironment from core instead.')
|
||||
typedef AppEnvironment = KcAppEnvironment;
|
||||
const AppConfig({
|
||||
required this.environment,
|
||||
required this.wcSiteUrl,
|
||||
required this.wcConsumerKey,
|
||||
required this.wcConsumerSecret,
|
||||
});
|
||||
|
||||
/// Reads configuration from compile-time `--dart-define` constants.
|
||||
factory AppConfig.fromEnvironment() {
|
||||
const envString = String.fromEnvironment('KC_ENV', defaultValue: 'fake');
|
||||
const siteUrl = String.fromEnvironment('KC_WC_SITE_URL');
|
||||
const consumerKey = String.fromEnvironment('KC_WC_CONSUMER_KEY');
|
||||
const consumerSecret = String.fromEnvironment('KC_WC_CONSUMER_SECRET');
|
||||
|
||||
final environment = AppEnvironment.fromString(envString);
|
||||
|
||||
return AppConfig(
|
||||
environment: environment,
|
||||
wcSiteUrl: siteUrl,
|
||||
wcConsumerKey: consumerKey,
|
||||
wcConsumerSecret: consumerSecret,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the WordPress configuration values are all present and non-empty.
|
||||
bool get hasWordPressConfig =>
|
||||
wcSiteUrl.isNotEmpty && wcConsumerKey.isNotEmpty && wcConsumerSecret.isNotEmpty;
|
||||
|
||||
/// A human-readable label for the current environment (e.g. for badges).
|
||||
String get environmentLabel => environment.label;
|
||||
}
|
||||
|
||||
/// The supported runtime environments.
|
||||
enum AppEnvironment {
|
||||
/// In-memory fakes – no network calls.
|
||||
fake('fake', 'FAKE'),
|
||||
|
||||
/// Real WooCommerce backend.
|
||||
wordpress('wordpress', 'WP');
|
||||
|
||||
final String key;
|
||||
final String label;
|
||||
|
||||
const AppEnvironment(this.key, this.label);
|
||||
|
||||
/// Parses a string into an [AppEnvironment], defaulting to [fake].
|
||||
static AppEnvironment fromString(String value) {
|
||||
for (final env in values) {
|
||||
if (env.key == value.toLowerCase()) return env;
|
||||
}
|
||||
return fake;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,18 @@
|
|||
/// Re-exports [KcAppScope] from the shared `core` package and provides
|
||||
/// convenience accessors typed to [AppServices].
|
||||
///
|
||||
/// This file preserves backward compatibility for existing imports within
|
||||
/// `kell_web`. New code should use [KcAppScope] from `package:core/core.dart`
|
||||
/// directly.
|
||||
library;
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'app_config.dart';
|
||||
import 'app_services.dart';
|
||||
|
||||
export 'package:core/core.dart' show KcAppScope;
|
||||
/// An [InheritedWidget] that exposes [AppServices] and [AppConfig] to the
|
||||
/// widget tree.
|
||||
///
|
||||
/// Wrap the app (or a subtree) with [AppScope] and retrieve the services
|
||||
/// anywhere below via [AppScope.of(context)].
|
||||
class AppScope extends InheritedWidget {
|
||||
final AppServices services;
|
||||
final AppConfig config;
|
||||
|
||||
/// App-specific convenience wrapper around [KcAppScope] for `kell_web`.
|
||||
///
|
||||
/// Preserves the original `AppScope.of(context)` and `AppScope.configOf(context)`
|
||||
/// call sites so existing code continues to work without modification.
|
||||
///
|
||||
/// New code should prefer:
|
||||
/// ```dart
|
||||
/// KcAppScope.of<AppServices>(context)
|
||||
/// KcAppScope.configOf<AppServices>(context)
|
||||
/// ```
|
||||
class AppScope extends KcAppScope<AppServices> {
|
||||
const AppScope({super.key, required super.services, required super.config, required super.child});
|
||||
const AppScope({super.key, required this.services, required this.config, required super.child});
|
||||
|
||||
/// Returns the nearest [AppServices] from the widget tree.
|
||||
///
|
||||
|
|
@ -35,12 +23,16 @@ class AppScope extends KcAppScope<AppServices> {
|
|||
return scope!.services;
|
||||
}
|
||||
|
||||
/// Returns the nearest [KcAppConfig] from the widget tree.
|
||||
/// Returns the nearest [AppConfig] from the widget tree.
|
||||
///
|
||||
/// Throws if no [AppScope] ancestor is found.
|
||||
static KcAppConfig configOf(BuildContext context) {
|
||||
static AppConfig configOf(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<AppScope>();
|
||||
assert(scope != null, 'No AppScope found in the widget tree');
|
||||
return scope!.config;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AppScope oldWidget) =>
|
||||
services != oldWidget.services || config != oldWidget.config;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
import 'package:core/core.dart';
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:feature_policy/feature_policy.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
|
||||
/// Holds the concrete service implementations used by `kell_web`.
|
||||
///
|
||||
/// Extends [KcAppServices] from the shared `core` package so that the
|
||||
/// generic [KcBootstrap] and [KcAppScope] infrastructure can work with
|
||||
/// this app's specific service set.
|
||||
/// Holds the concrete service implementations used by the app.
|
||||
///
|
||||
/// The [AppServices.fake] factory wires up the in-memory fakes that live
|
||||
/// inside each feature package. The [AppServices.wordpress] factory wires up
|
||||
/// a real WooCommerce-backed product repository while keeping other services
|
||||
/// fake until their backends are ready.
|
||||
class AppServices extends KcAppServices {
|
||||
class AppServices {
|
||||
final InventoryRepository inventoryRepository;
|
||||
final OrdersRepository ordersRepository;
|
||||
final PolicyRepository policyRepository;
|
||||
|
|
@ -62,16 +57,4 @@ class AppServices extends KcAppServices {
|
|||
productPublishingRepository: WordPressProductPublishingRepository(apiClient: apiClient),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a [KcServiceFactory] for use with [KcBootstrap.run].
|
||||
static KcServiceFactory<AppServices> get serviceFactory {
|
||||
return KcServiceFactory<AppServices>(
|
||||
createFake: () => AppServices.fake(),
|
||||
createWordPress: (config) => AppServices.wordpress(
|
||||
siteUrl: config.wcSiteUrl,
|
||||
consumerKey: config.wcConsumerKey,
|
||||
consumerSecret: config.wcConsumerSecret,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,15 @@
|
|||
/// Re-exports [KcBootstrap] from the shared `core` package and provides
|
||||
/// a backward-compatible [Bootstrap] wrapper for `kell_web`.
|
||||
///
|
||||
/// This file preserves the existing `Bootstrap.run(config)` call site.
|
||||
/// New code should use [KcBootstrap.run] from `package:core/core.dart`
|
||||
/// with a [KcServiceFactory] directly.
|
||||
library;
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'app_config.dart';
|
||||
import 'app_services.dart';
|
||||
|
||||
export 'package:core/core.dart' show KcBootstrap;
|
||||
|
||||
/// Backward-compatible bootstrap for `kell_web`.
|
||||
/// Bootstraps [AppServices] from the runtime [AppConfig].
|
||||
///
|
||||
/// Delegates to [KcBootstrap.run] using the [AppServices.serviceFactory].
|
||||
/// In **fake** mode the in-memory fakes are used unconditionally.
|
||||
///
|
||||
/// Existing call sites:
|
||||
/// ```dart
|
||||
/// final (:services, config: effectiveConfig) = Bootstrap.run(config);
|
||||
/// ```
|
||||
///
|
||||
/// New code should prefer:
|
||||
/// ```dart
|
||||
/// final (:services, config: effectiveConfig) = KcBootstrap.run(
|
||||
/// config,
|
||||
/// AppServices.serviceFactory,
|
||||
/// );
|
||||
/// ```
|
||||
/// In **wordpress** mode the WooCommerce credentials are validated first.
|
||||
/// If any credential is missing the app falls back to fake mode and logs a
|
||||
/// warning so the developer knows what went wrong.
|
||||
class Bootstrap {
|
||||
const Bootstrap._();
|
||||
|
||||
|
|
@ -36,7 +18,39 @@ class Bootstrap {
|
|||
/// Returns a record containing the resolved services and the effective
|
||||
/// config (which may differ from the input when WordPress credentials are
|
||||
/// missing and a fallback to fake mode occurs).
|
||||
static ({AppServices services, KcAppConfig config}) run(KcAppConfig config) {
|
||||
return KcBootstrap.run<AppServices>(config, AppServices.serviceFactory);
|
||||
static ({AppServices services, AppConfig config}) run(AppConfig config) {
|
||||
switch (config.environment) {
|
||||
case AppEnvironment.fake:
|
||||
return (services: AppServices.fake(), config: config);
|
||||
|
||||
case AppEnvironment.wordpress:
|
||||
if (!config.hasWordPressConfig) {
|
||||
if (kDebugMode) {
|
||||
// ignore: avoid_print
|
||||
debugPrint(
|
||||
'⚠️ KC_ENV=wordpress but WooCommerce credentials are missing.\n'
|
||||
' Falling back to fake mode.\n'
|
||||
' Set KC_WC_SITE_URL, KC_WC_CONSUMER_KEY, and '
|
||||
'KC_WC_CONSUMER_SECRET via --dart-define.',
|
||||
);
|
||||
}
|
||||
final fallbackConfig = AppConfig(
|
||||
environment: AppEnvironment.fake,
|
||||
wcSiteUrl: config.wcSiteUrl,
|
||||
wcConsumerKey: config.wcConsumerKey,
|
||||
wcConsumerSecret: config.wcConsumerSecret,
|
||||
);
|
||||
return (services: AppServices.fake(), config: fallbackConfig);
|
||||
}
|
||||
|
||||
return (
|
||||
services: AppServices.wordpress(
|
||||
siteUrl: config.wcSiteUrl,
|
||||
consumerKey: config.wcConsumerKey,
|
||||
consumerSecret: config.wcConsumerSecret,
|
||||
),
|
||||
config: config,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,204 +0,0 @@
|
|||
# Kell Creations — Cross-Platform Shell Composition Strategy
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the shared composition pattern that all Kell Creations
|
||||
app targets (`kell_web`, `kell_mobile`, future targets) must follow. The pattern
|
||||
lives in the `core` package and provides a consistent way to:
|
||||
|
||||
1. Read runtime configuration from `--dart-define` values.
|
||||
2. Construct environment-appropriate services (fake vs. real backends).
|
||||
3. Expose services and configuration to the widget tree.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ core (shared package) │
|
||||
│ │
|
||||
│ KcAppConfig / KcAppEnvironment │
|
||||
│ └─ Runtime config from --dart-define │
|
||||
│ │
|
||||
│ KcAppServices (abstract) │
|
||||
│ └─ Base class for app service containers │
|
||||
│ │
|
||||
│ KcServiceFactory<T extends KcAppServices> │
|
||||
│ └─ Generic factory: createFake() / createWordPress(cfg) │
|
||||
│ │
|
||||
│ KcBootstrap │
|
||||
│ └─ Shared bootstrap: env switch + WP credential fallback │
|
||||
│ │
|
||||
│ KcAppScope<T extends KcAppServices> │
|
||||
│ └─ InheritedWidget exposing T + KcAppConfig to tree │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
▲ ▲
|
||||
│ │
|
||||
┌────────┴───────────┐ ┌────────────┴──────────────┐
|
||||
│ kell_web (app) │ │ kell_mobile (app) │
|
||||
│ │ │ │
|
||||
│ AppServices │ │ MobileAppServices │
|
||||
│ extends │ │ extends │
|
||||
│ KcAppServices │ │ KcAppServices │
|
||||
│ │ │ │
|
||||
│ AppScope │ │ (uses KcAppScope │
|
||||
│ extends │ │ <MobileAppServices>) │
|
||||
│ KcAppScope │ │ │
|
||||
│ <AppServices> │ │ │
|
||||
│ │ │ │
|
||||
│ Bootstrap │ │ (uses KcBootstrap.run │
|
||||
│ delegates to │ │ with own factory) │
|
||||
│ KcBootstrap.run │ │ │
|
||||
└─────────────────────┘ └────────────────────────────┘
|
||||
```
|
||||
|
||||
## Why abstract services?
|
||||
|
||||
Concrete service containers hold references to feature-package repository types
|
||||
(e.g., `InventoryRepository`, `ProductPublishingRepository`). Moving those
|
||||
references into `core` would create **circular dependencies**:
|
||||
|
||||
```
|
||||
core → feature_wordpress → core ← NOT ALLOWED
|
||||
```
|
||||
|
||||
By keeping `KcAppServices` abstract in `core`, the package owns the composition
|
||||
**contract** without knowing concrete types. Each app provides its own concrete
|
||||
subclass.
|
||||
|
||||
## Contract for new app targets
|
||||
|
||||
When creating a new app target (e.g., `kell_mobile`), follow this pattern:
|
||||
|
||||
### 1. Create a concrete services class
|
||||
|
||||
```dart
|
||||
// apps/kell_mobile/lib/composition/mobile_app_services.dart
|
||||
import 'package:core/core.dart';
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
// ... other feature imports
|
||||
|
||||
class MobileAppServices extends KcAppServices {
|
||||
final InventoryRepository inventoryRepository;
|
||||
final ProductPublishingRepository productPublishingRepository;
|
||||
// ... other repositories as needed
|
||||
|
||||
const MobileAppServices({
|
||||
required this.inventoryRepository,
|
||||
required this.productPublishingRepository,
|
||||
});
|
||||
|
||||
factory MobileAppServices.fake() {
|
||||
return MobileAppServices(
|
||||
inventoryRepository: FakeInventoryRepository(),
|
||||
productPublishingRepository: FakeProductPublishingRepository(),
|
||||
);
|
||||
}
|
||||
|
||||
factory MobileAppServices.wordpress({
|
||||
required String siteUrl,
|
||||
required String consumerKey,
|
||||
required String consumerSecret,
|
||||
}) {
|
||||
final apiClient = WooCommerceApiClient(
|
||||
siteUrl: siteUrl,
|
||||
consumerKey: consumerKey,
|
||||
consumerSecret: consumerSecret,
|
||||
);
|
||||
return MobileAppServices(
|
||||
inventoryRepository: FakeInventoryRepository(),
|
||||
productPublishingRepository:
|
||||
WordPressProductPublishingRepository(apiClient: apiClient),
|
||||
);
|
||||
}
|
||||
|
||||
static KcServiceFactory<MobileAppServices> get serviceFactory {
|
||||
return KcServiceFactory<MobileAppServices>(
|
||||
createFake: () => MobileAppServices.fake(),
|
||||
createWordPress: (config) => MobileAppServices.wordpress(
|
||||
siteUrl: config.wcSiteUrl,
|
||||
consumerKey: config.wcConsumerKey,
|
||||
consumerSecret: config.wcConsumerSecret,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Wire up main.dart
|
||||
|
||||
```dart
|
||||
// apps/kell_mobile/lib/main.dart
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'composition/mobile_app_services.dart';
|
||||
|
||||
void main() {
|
||||
final config = KcAppConfig.fromEnvironment();
|
||||
final (:services, config: effectiveConfig) = KcBootstrap.run(
|
||||
config,
|
||||
MobileAppServices.serviceFactory,
|
||||
);
|
||||
|
||||
runApp(
|
||||
KcAppScope<MobileAppServices>(
|
||||
services: services,
|
||||
config: effectiveConfig,
|
||||
child: const KellMobileApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Access services in widgets
|
||||
|
||||
```dart
|
||||
// Anywhere in the widget tree:
|
||||
final services = KcAppScope.of<MobileAppServices>(context);
|
||||
final config = KcAppScope.configOf<MobileAppServices>(context);
|
||||
```
|
||||
|
||||
## Backward compatibility (kell_web)
|
||||
|
||||
The `kell_web` app retains its original class names (`AppConfig`, `AppScope`,
|
||||
`Bootstrap`, `AppServices`) as thin wrappers/typedefs around the shared `core`
|
||||
types. Existing code continues to work without modification:
|
||||
|
||||
| Original (kell_web) | Shared (core) | Relationship |
|
||||
| ------------------- | ------------------ | --------------- |
|
||||
| `AppConfig` | `KcAppConfig` | typedef |
|
||||
| `AppEnvironment` | `KcAppEnvironment` | typedef |
|
||||
| `AppServices` | `KcAppServices` | extends |
|
||||
| `AppScope` | `KcAppScope` | extends (typed) |
|
||||
| `Bootstrap` | `KcBootstrap` | delegates |
|
||||
|
||||
New code in `kell_web` may use either the original names or the shared `Kc`-prefixed
|
||||
names. The shared names are preferred for clarity and consistency with `kell_mobile`.
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
All app targets use the same `--dart-define` keys:
|
||||
|
||||
| Key | Description | Default |
|
||||
| ----------------------- | ------------------------------------ | ------- |
|
||||
| `KC_ENV` | `fake` or `wordpress` | `fake` |
|
||||
| `KC_WC_SITE_URL` | WordPress site URL | (empty) |
|
||||
| `KC_WC_CONSUMER_KEY` | WooCommerce REST API consumer key | (empty) |
|
||||
| `KC_WC_CONSUMER_SECRET` | WooCommerce REST API consumer secret | (empty) |
|
||||
|
||||
When `KC_ENV=wordpress` but credentials are missing, `KcBootstrap` automatically
|
||||
falls back to fake mode with a debug-mode warning.
|
||||
|
||||
## What remains app-specific
|
||||
|
||||
The following concerns are **not** shared and remain in each app target:
|
||||
|
||||
- **App shell / navigation** — Web uses `NavigationRail`, mobile will use
|
||||
`BottomNavigationBar` or similar.
|
||||
- **Routing** — Route definitions and page builders are platform-specific.
|
||||
- **Platform-specific presentation** — Layout, responsive breakpoints, etc.
|
||||
- **Cross-feature navigation handoffs** — Wired in the app's routing layer.
|
||||
|
||||
The `design_system` package provides shared visual components (widgets, theme,
|
||||
typography, breakpoints) that both web and mobile can use for consistent styling.
|
||||
|
|
@ -1,17 +1,5 @@
|
|||
/// Core shared abstractions for Kell Creations applications.
|
||||
///
|
||||
/// This package provides the platform-agnostic composition pattern used by
|
||||
/// all app targets (`kell_web`, `kell_mobile`, etc.):
|
||||
///
|
||||
/// - [KcAppConfig] / [KcAppEnvironment] — runtime configuration from `--dart-define`
|
||||
/// - [KcAppServices] — abstract service container base class
|
||||
/// - [KcServiceFactory] — generic factory for environment-aware service construction
|
||||
/// - [KcBootstrap] — shared bootstrap logic with WordPress credential validation
|
||||
/// - [KcAppScope] — [InheritedWidget] exposing services and config to the widget tree
|
||||
library;
|
||||
|
||||
// Composition
|
||||
export 'src/composition/kc_app_config.dart';
|
||||
export 'src/composition/kc_app_scope.dart';
|
||||
export 'src/composition/kc_app_services.dart';
|
||||
export 'src/composition/kc_bootstrap.dart';
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
/// Runtime configuration read from `--dart-define` values.
|
||||
///
|
||||
/// This is the shared, platform-agnostic configuration model used by all
|
||||
/// Kell Creations app targets (`kell_web`, `kell_mobile`, etc.).
|
||||
///
|
||||
/// The app reads the following compile-time constants:
|
||||
///
|
||||
/// | Key | Description | Default |
|
||||
/// |------------------------|----------------------------------------------|----------|
|
||||
/// | `KC_ENV` | `fake` or `wordpress` | `fake` |
|
||||
/// | `KC_WC_SITE_URL` | WordPress site URL | (empty) |
|
||||
/// | `KC_WC_CONSUMER_KEY` | WooCommerce REST API consumer key | (empty) |
|
||||
/// | `KC_WC_CONSUMER_SECRET`| WooCommerce REST API consumer secret | (empty) |
|
||||
///
|
||||
/// Usage:
|
||||
/// ```sh
|
||||
/// flutter run -d chrome \
|
||||
/// --dart-define=KC_ENV=wordpress \
|
||||
/// --dart-define=KC_WC_SITE_URL=https://store.kellcreations.com \
|
||||
/// --dart-define=KC_WC_CONSUMER_KEY=ck_xxx \
|
||||
/// --dart-define=KC_WC_CONSUMER_SECRET=cs_xxx
|
||||
/// ```
|
||||
class KcAppConfig {
|
||||
/// The environment mode: `fake` or `wordpress`.
|
||||
final KcAppEnvironment environment;
|
||||
|
||||
/// WordPress / WooCommerce site URL (only used in [KcAppEnvironment.wordpress]).
|
||||
final String wcSiteUrl;
|
||||
|
||||
/// WooCommerce REST API consumer key.
|
||||
final String wcConsumerKey;
|
||||
|
||||
/// WooCommerce REST API consumer secret.
|
||||
final String wcConsumerSecret;
|
||||
|
||||
const KcAppConfig({
|
||||
required this.environment,
|
||||
required this.wcSiteUrl,
|
||||
required this.wcConsumerKey,
|
||||
required this.wcConsumerSecret,
|
||||
});
|
||||
|
||||
/// Reads configuration from compile-time `--dart-define` constants.
|
||||
factory KcAppConfig.fromEnvironment() {
|
||||
const envString = String.fromEnvironment('KC_ENV', defaultValue: 'fake');
|
||||
const siteUrl = String.fromEnvironment('KC_WC_SITE_URL');
|
||||
const consumerKey = String.fromEnvironment('KC_WC_CONSUMER_KEY');
|
||||
const consumerSecret = String.fromEnvironment('KC_WC_CONSUMER_SECRET');
|
||||
|
||||
final environment = KcAppEnvironment.fromString(envString);
|
||||
|
||||
return KcAppConfig(
|
||||
environment: environment,
|
||||
wcSiteUrl: siteUrl,
|
||||
wcConsumerKey: consumerKey,
|
||||
wcConsumerSecret: consumerSecret,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the WordPress configuration values are all present and non-empty.
|
||||
bool get hasWordPressConfig =>
|
||||
wcSiteUrl.isNotEmpty && wcConsumerKey.isNotEmpty && wcConsumerSecret.isNotEmpty;
|
||||
|
||||
/// A human-readable label for the current environment (e.g. for badges).
|
||||
String get environmentLabel => environment.label;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is KcAppConfig &&
|
||||
environment == other.environment &&
|
||||
wcSiteUrl == other.wcSiteUrl &&
|
||||
wcConsumerKey == other.wcConsumerKey &&
|
||||
wcConsumerSecret == other.wcConsumerSecret;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(environment, wcSiteUrl, wcConsumerKey, wcConsumerSecret);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'KcAppConfig(environment: ${environment.key}, '
|
||||
'hasWordPressConfig: $hasWordPressConfig)';
|
||||
}
|
||||
|
||||
/// The supported runtime environments.
|
||||
enum KcAppEnvironment {
|
||||
/// In-memory fakes – no network calls.
|
||||
fake('fake', 'FAKE'),
|
||||
|
||||
/// Real WooCommerce backend.
|
||||
wordpress('wordpress', 'WP');
|
||||
|
||||
final String key;
|
||||
final String label;
|
||||
|
||||
const KcAppEnvironment(this.key, this.label);
|
||||
|
||||
/// Parses a string into a [KcAppEnvironment], defaulting to [fake].
|
||||
static KcAppEnvironment fromString(String value) {
|
||||
for (final env in values) {
|
||||
if (env.key == value.toLowerCase()) return env;
|
||||
}
|
||||
return fake;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
/// Shared [InheritedWidget] that exposes [KcAppServices] and [KcAppConfig]
|
||||
/// to the widget tree.
|
||||
///
|
||||
/// Both `kell_web` and `kell_mobile` use this widget at the root of their
|
||||
/// widget tree so that any descendant can retrieve services and configuration.
|
||||
library;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'kc_app_config.dart';
|
||||
import 'kc_app_services.dart';
|
||||
|
||||
/// An [InheritedWidget] that exposes [KcAppServices] and [KcAppConfig] to the
|
||||
/// widget tree.
|
||||
///
|
||||
/// Wrap the app (or a subtree) with [KcAppScope] and retrieve the services
|
||||
/// anywhere below via [KcAppScope.of<T>(context)].
|
||||
///
|
||||
/// The type parameter [T] allows each app target to retrieve its concrete
|
||||
/// services subclass without casting:
|
||||
///
|
||||
/// ```dart
|
||||
/// // In kell_web:
|
||||
/// final services = KcAppScope.of<WebAppServices>(context);
|
||||
/// services.inventoryRepository; // ← strongly typed
|
||||
/// ```
|
||||
class KcAppScope<T extends KcAppServices> extends InheritedWidget {
|
||||
/// The concrete services for this app target.
|
||||
final T services;
|
||||
|
||||
/// The effective runtime configuration.
|
||||
final KcAppConfig config;
|
||||
|
||||
const KcAppScope({super.key, required this.services, required this.config, required super.child});
|
||||
|
||||
/// Returns the nearest [T] (concrete services) from the widget tree.
|
||||
///
|
||||
/// Throws if no [KcAppScope] ancestor is found.
|
||||
static T of<T extends KcAppServices>(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<KcAppScope<T>>();
|
||||
assert(scope != null, 'No KcAppScope<$T> found in the widget tree');
|
||||
return scope!.services;
|
||||
}
|
||||
|
||||
/// Returns the nearest [KcAppConfig] from the widget tree.
|
||||
///
|
||||
/// This is a convenience accessor that works regardless of the concrete
|
||||
/// services type — it looks for any [KcAppScope] with any type parameter.
|
||||
///
|
||||
/// For apps that know their concrete type, prefer using the typed overload.
|
||||
static KcAppConfig configOf<T extends KcAppServices>(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<KcAppScope<T>>();
|
||||
assert(scope != null, 'No KcAppScope<$T> found in the widget tree');
|
||||
return scope!.config;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(KcAppScope<T> oldWidget) =>
|
||||
services != oldWidget.services || config != oldWidget.config;
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/// Shared composition abstractions for app service containers.
|
||||
///
|
||||
/// Provides [KcAppServices] as an abstract base and [KcServiceFactory] as
|
||||
/// the generic factory pattern used by [KcBootstrap] to construct services.
|
||||
library;
|
||||
|
||||
import 'kc_app_config.dart';
|
||||
|
||||
/// Abstract base class for application service containers.
|
||||
///
|
||||
/// Each app target (`kell_web`, `kell_mobile`) creates a concrete subclass
|
||||
/// that holds the specific repository and service implementations needed by
|
||||
/// that target. The base class defines the factory contract that
|
||||
/// [KcBootstrap] uses to construct services from a [KcAppConfig].
|
||||
///
|
||||
/// ## Pattern
|
||||
///
|
||||
/// ```dart
|
||||
/// class WebAppServices extends KcAppServices {
|
||||
/// final InventoryRepository inventoryRepository;
|
||||
/// final ProductPublishingRepository productPublishingRepository;
|
||||
/// // ...
|
||||
///
|
||||
/// const WebAppServices({
|
||||
/// required this.inventoryRepository,
|
||||
/// required this.productPublishingRepository,
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Why abstract?
|
||||
///
|
||||
/// Concrete service containers reference feature-package repository types
|
||||
/// directly. Moving those references into `core` would create circular
|
||||
/// dependencies (`core` → feature → `core`). By keeping the base abstract,
|
||||
/// `core` owns the composition *contract* without knowing concrete types.
|
||||
abstract class KcAppServices {
|
||||
const KcAppServices();
|
||||
}
|
||||
|
||||
/// A factory that constructs the appropriate [KcAppServices] subclass
|
||||
/// from a [KcAppConfig].
|
||||
///
|
||||
/// Each app target provides two factories — one for fake mode and one for
|
||||
/// the real backend — via [KcServiceFactory].
|
||||
///
|
||||
/// ```dart
|
||||
/// final factory = KcServiceFactory<WebAppServices>(
|
||||
/// createFake: () => WebAppServices.fake(),
|
||||
/// createWordPress: (config) => WebAppServices.wordpress(
|
||||
/// siteUrl: config.wcSiteUrl,
|
||||
/// consumerKey: config.wcConsumerKey,
|
||||
/// consumerSecret: config.wcConsumerSecret,
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
class KcServiceFactory<T extends KcAppServices> {
|
||||
/// Creates services backed by fake, in-memory repositories.
|
||||
final T Function() createFake;
|
||||
|
||||
/// Creates services backed by the real WordPress/WooCommerce backend.
|
||||
///
|
||||
/// Receives the full [KcAppConfig] so the factory can extract credentials.
|
||||
final T Function(KcAppConfig config) createWordPress;
|
||||
|
||||
const KcServiceFactory({required this.createFake, required this.createWordPress});
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/// Shared bootstrap logic for all Kell Creations app targets.
|
||||
///
|
||||
/// Uses a [KcServiceFactory] to construct the appropriate services based
|
||||
/// on the runtime [KcAppConfig].
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'kc_app_config.dart';
|
||||
import 'kc_app_services.dart';
|
||||
|
||||
/// Bootstraps app services from the runtime [KcAppConfig].
|
||||
///
|
||||
/// In **fake** mode the in-memory fakes are used unconditionally.
|
||||
///
|
||||
/// In **wordpress** mode the WooCommerce credentials are validated first.
|
||||
/// If any credential is missing the app falls back to fake mode and logs a
|
||||
/// warning so the developer knows what went wrong.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// ```dart
|
||||
/// final factory = KcServiceFactory<WebAppServices>(
|
||||
/// createFake: () => WebAppServices.fake(),
|
||||
/// createWordPress: (config) => WebAppServices.wordpress(...),
|
||||
/// );
|
||||
///
|
||||
/// final (:services, config: effectiveConfig) = KcBootstrap.run(config, factory);
|
||||
/// ```
|
||||
class KcBootstrap {
|
||||
const KcBootstrap._();
|
||||
|
||||
/// Creates the appropriate services for the given [config] using [factory].
|
||||
///
|
||||
/// Returns a record containing the resolved services and the effective
|
||||
/// config (which may differ from the input when WordPress credentials are
|
||||
/// missing and a fallback to fake mode occurs).
|
||||
static ({T services, KcAppConfig config}) run<T extends KcAppServices>(
|
||||
KcAppConfig config,
|
||||
KcServiceFactory<T> factory,
|
||||
) {
|
||||
switch (config.environment) {
|
||||
case KcAppEnvironment.fake:
|
||||
return (services: factory.createFake(), config: config);
|
||||
|
||||
case KcAppEnvironment.wordpress:
|
||||
if (!config.hasWordPressConfig) {
|
||||
if (kDebugMode) {
|
||||
// ignore: avoid_print
|
||||
debugPrint(
|
||||
'⚠️ KC_ENV=wordpress but WooCommerce credentials are missing.\n'
|
||||
' Falling back to fake mode.\n'
|
||||
' Set KC_WC_SITE_URL, KC_WC_CONSUMER_KEY, and '
|
||||
'KC_WC_CONSUMER_SECRET via --dart-define.',
|
||||
);
|
||||
}
|
||||
final fallbackConfig = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: config.wcSiteUrl,
|
||||
wcConsumerKey: config.wcConsumerKey,
|
||||
wcConsumerSecret: config.wcConsumerSecret,
|
||||
);
|
||||
return (services: factory.createFake(), config: fallbackConfig);
|
||||
}
|
||||
|
||||
return (services: factory.createWordPress(config), config: config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,297 +1,12 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:core/core.dart';
|
||||
|
||||
void main() {
|
||||
group('KcAppConfig', () {
|
||||
test('creates config with required fields', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
expect(config.environment, KcAppEnvironment.fake);
|
||||
expect(config.environmentLabel, 'FAKE');
|
||||
expect(config.hasWordPressConfig, isFalse);
|
||||
});
|
||||
|
||||
test('hasWordPressConfig returns true when all credentials present', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: 'https://example.com',
|
||||
wcConsumerKey: 'ck_test',
|
||||
wcConsumerSecret: 'cs_test',
|
||||
);
|
||||
|
||||
expect(config.hasWordPressConfig, isTrue);
|
||||
expect(config.environmentLabel, 'WP');
|
||||
});
|
||||
|
||||
test('hasWordPressConfig returns false when any credential is missing', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: 'https://example.com',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: 'cs_test',
|
||||
);
|
||||
|
||||
expect(config.hasWordPressConfig, isFalse);
|
||||
});
|
||||
|
||||
test('equality compares all fields', () {
|
||||
const a = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
const b = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
const c = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
expect(a, equals(b));
|
||||
expect(a, isNot(equals(c)));
|
||||
expect(a.hashCode, equals(b.hashCode));
|
||||
});
|
||||
|
||||
test('toString includes environment and hasWordPressConfig', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
expect(config.toString(), contains('fake'));
|
||||
expect(config.toString(), contains('hasWordPressConfig: false'));
|
||||
});
|
||||
});
|
||||
|
||||
group('KcAppEnvironment', () {
|
||||
test('fromString parses known values', () {
|
||||
expect(KcAppEnvironment.fromString('fake'), KcAppEnvironment.fake);
|
||||
expect(KcAppEnvironment.fromString('wordpress'), KcAppEnvironment.wordpress);
|
||||
});
|
||||
|
||||
test('fromString is case-insensitive', () {
|
||||
expect(KcAppEnvironment.fromString('FAKE'), KcAppEnvironment.fake);
|
||||
expect(KcAppEnvironment.fromString('WordPress'), KcAppEnvironment.wordpress);
|
||||
});
|
||||
|
||||
test('fromString defaults to fake for unknown values', () {
|
||||
expect(KcAppEnvironment.fromString('unknown'), KcAppEnvironment.fake);
|
||||
expect(KcAppEnvironment.fromString(''), KcAppEnvironment.fake);
|
||||
});
|
||||
|
||||
test('label returns expected display string', () {
|
||||
expect(KcAppEnvironment.fake.label, 'FAKE');
|
||||
expect(KcAppEnvironment.wordpress.label, 'WP');
|
||||
});
|
||||
|
||||
test('key returns expected raw string', () {
|
||||
expect(KcAppEnvironment.fake.key, 'fake');
|
||||
expect(KcAppEnvironment.wordpress.key, 'wordpress');
|
||||
});
|
||||
});
|
||||
|
||||
group('KcAppServices', () {
|
||||
test('abstract base can be extended', () {
|
||||
final services = _TestAppServices();
|
||||
expect(services, isA<KcAppServices>());
|
||||
});
|
||||
});
|
||||
|
||||
group('KcServiceFactory', () {
|
||||
test('createFake returns fake services', () {
|
||||
final factory = KcServiceFactory<_TestAppServices>(
|
||||
createFake: () => _TestAppServices(mode: 'fake'),
|
||||
createWordPress: (config) => _TestAppServices(mode: 'wp'),
|
||||
);
|
||||
|
||||
final services = factory.createFake();
|
||||
expect(services.mode, 'fake');
|
||||
});
|
||||
|
||||
test('createWordPress returns wordpress services', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: 'https://example.com',
|
||||
wcConsumerKey: 'ck_test',
|
||||
wcConsumerSecret: 'cs_test',
|
||||
);
|
||||
final factory = KcServiceFactory<_TestAppServices>(
|
||||
createFake: () => _TestAppServices(mode: 'fake'),
|
||||
createWordPress: (cfg) => _TestAppServices(mode: 'wp-${cfg.wcSiteUrl}'),
|
||||
);
|
||||
|
||||
final services = factory.createWordPress(config);
|
||||
expect(services.mode, 'wp-https://example.com');
|
||||
});
|
||||
});
|
||||
|
||||
group('KcBootstrap', () {
|
||||
late KcServiceFactory<_TestAppServices> factory;
|
||||
|
||||
setUp(() {
|
||||
factory = KcServiceFactory<_TestAppServices>(
|
||||
createFake: () => _TestAppServices(mode: 'fake'),
|
||||
createWordPress: (config) => _TestAppServices(mode: 'wp'),
|
||||
);
|
||||
});
|
||||
|
||||
test('fake environment returns fake services', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
final result = KcBootstrap.run(config, factory);
|
||||
|
||||
expect(result.services.mode, 'fake');
|
||||
expect(result.config.environment, KcAppEnvironment.fake);
|
||||
});
|
||||
|
||||
test('wordpress environment with valid credentials returns wp services', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: 'https://example.com',
|
||||
wcConsumerKey: 'ck_test',
|
||||
wcConsumerSecret: 'cs_test',
|
||||
);
|
||||
|
||||
final result = KcBootstrap.run(config, factory);
|
||||
|
||||
expect(result.services.mode, 'wp');
|
||||
expect(result.config.environment, KcAppEnvironment.wordpress);
|
||||
});
|
||||
|
||||
test('wordpress environment without credentials falls back to fake', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
final result = KcBootstrap.run(config, factory);
|
||||
|
||||
expect(result.services.mode, 'fake');
|
||||
expect(result.config.environment, KcAppEnvironment.fake);
|
||||
});
|
||||
|
||||
test('wordpress fallback preserves partial credentials in config', () {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.wordpress,
|
||||
wcSiteUrl: 'https://example.com',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
final result = KcBootstrap.run(config, factory);
|
||||
|
||||
expect(result.services.mode, 'fake');
|
||||
expect(result.config.environment, KcAppEnvironment.fake);
|
||||
expect(result.config.wcSiteUrl, 'https://example.com');
|
||||
});
|
||||
});
|
||||
|
||||
group('KcAppScope', () {
|
||||
testWidgets('of<T> retrieves typed services from widget tree', (tester) async {
|
||||
final services = _TestAppServices(mode: 'test');
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
late _TestAppServices retrievedServices;
|
||||
late KcAppConfig retrievedConfig;
|
||||
|
||||
await tester.pumpWidget(
|
||||
KcAppScope<_TestAppServices>(
|
||||
services: services,
|
||||
config: config,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
retrievedServices = KcAppScope.of<_TestAppServices>(context);
|
||||
retrievedConfig = KcAppScope.configOf<_TestAppServices>(context);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(retrievedServices.mode, 'test');
|
||||
expect(retrievedConfig.environment, KcAppEnvironment.fake);
|
||||
});
|
||||
|
||||
testWidgets('updateShouldNotify returns true when services change', (tester) async {
|
||||
final scope1 = KcAppScope<_TestAppServices>(
|
||||
services: _TestAppServices(mode: 'a'),
|
||||
config: const KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
),
|
||||
child: const SizedBox.shrink(),
|
||||
);
|
||||
final scope2 = KcAppScope<_TestAppServices>(
|
||||
services: _TestAppServices(mode: 'b'),
|
||||
config: const KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
),
|
||||
child: const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
expect(scope1.updateShouldNotify(scope2), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('updateShouldNotify returns false when identical', (tester) async {
|
||||
final services = _TestAppServices(mode: 'same');
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
|
||||
final scope1 = KcAppScope<_TestAppServices>(
|
||||
services: services,
|
||||
config: config,
|
||||
child: const SizedBox.shrink(),
|
||||
);
|
||||
final scope2 = KcAppScope<_TestAppServices>(
|
||||
services: services,
|
||||
config: config,
|
||||
child: const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
expect(scope1.updateShouldNotify(scope2), isFalse);
|
||||
});
|
||||
test('adds one to input values', () {
|
||||
final calculator = Calculator();
|
||||
expect(calculator.addOne(2), 3);
|
||||
expect(calculator.addOne(-7), -6);
|
||||
expect(calculator.addOne(0), 1);
|
||||
});
|
||||
}
|
||||
|
||||
/// A minimal concrete [KcAppServices] subclass for testing.
|
||||
class _TestAppServices extends KcAppServices {
|
||||
final String mode;
|
||||
const _TestAppServices({this.mode = 'default'});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue