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).
|
- ✅ 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.
|
- ✅ 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).
|
- ✅ 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`
|
### Current narrow edit capabilities on `main`
|
||||||
|
|
||||||
|
|
@ -106,20 +105,18 @@ Rules:
|
||||||
### Latest known validation state on `main`
|
### Latest known validation state on `main`
|
||||||
|
|
||||||
- `dart analyze` clean
|
- `dart analyze` clean
|
||||||
- `core` tests passing
|
|
||||||
- `design_system` tests passing
|
- `design_system` tests passing
|
||||||
- `feature_wordpress` tests passing
|
- `feature_wordpress` tests passing
|
||||||
- `kell_web` 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 `design_system`: `41/41 passed`
|
||||||
- latest reported count for `feature_wordpress`: `294/294 passed`
|
- latest reported count for `feature_wordpress`: `294/294 passed`
|
||||||
- latest reported count for `kell_web`: `24/24 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
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/flutter-cicd`** — Stage 4C: Flutter CI/CD pipeline.
|
**`feat/flutter-cicd`** — Stage 4B: Cross-platform shell composition strategy.
|
||||||
Branch from latest `main`. Stage 4B (cross-platform shell composition) is complete.
|
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** |
|
| `data` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** |
|
||||||
| `integrations` | ❌ 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** |
|
| `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
|
/// The app reads the following compile-time constants:
|
||||||
/// `kell_web`. New code should import directly from `package:core/core.dart`.
|
|
||||||
///
|
///
|
||||||
/// The original class names are preserved as typedefs so that existing
|
/// | Key | Description | Default |
|
||||||
/// references continue to compile without changes.
|
/// |------------------------|----------------------------------------------|----------|
|
||||||
library;
|
/// | `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.')
|
/// WooCommerce REST API consumer secret.
|
||||||
typedef AppConfig = KcAppConfig;
|
final String wcConsumerSecret;
|
||||||
|
|
||||||
/// @Deprecated('Use KcAppEnvironment from core instead.')
|
const AppConfig({
|
||||||
typedef AppEnvironment = KcAppEnvironment;
|
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 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'app_config.dart';
|
||||||
import 'app_services.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`.
|
const AppScope({super.key, required this.services, required this.config, required super.child});
|
||||||
///
|
|
||||||
/// 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});
|
|
||||||
|
|
||||||
/// Returns the nearest [AppServices] from the widget tree.
|
/// Returns the nearest [AppServices] from the widget tree.
|
||||||
///
|
///
|
||||||
|
|
@ -35,12 +23,16 @@ class AppScope extends KcAppScope<AppServices> {
|
||||||
return scope!.services;
|
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.
|
/// Throws if no [AppScope] ancestor is found.
|
||||||
static KcAppConfig configOf(BuildContext context) {
|
static AppConfig configOf(BuildContext context) {
|
||||||
final scope = context.dependOnInheritedWidgetOfExactType<AppScope>();
|
final scope = context.dependOnInheritedWidgetOfExactType<AppScope>();
|
||||||
assert(scope != null, 'No AppScope found in the widget tree');
|
assert(scope != null, 'No AppScope found in the widget tree');
|
||||||
return scope!.config;
|
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_inventory/feature_inventory.dart';
|
||||||
import 'package:feature_orders/feature_orders.dart';
|
import 'package:feature_orders/feature_orders.dart';
|
||||||
import 'package:feature_policy/feature_policy.dart';
|
import 'package:feature_policy/feature_policy.dart';
|
||||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||||
|
|
||||||
/// Holds the concrete service implementations used by `kell_web`.
|
/// Holds the concrete service implementations used by the app.
|
||||||
///
|
|
||||||
/// Extends [KcAppServices] from the shared `core` package so that the
|
|
||||||
/// generic [KcBootstrap] and [KcAppScope] infrastructure can work with
|
|
||||||
/// this app's specific service set.
|
|
||||||
///
|
///
|
||||||
/// The [AppServices.fake] factory wires up the in-memory fakes that live
|
/// The [AppServices.fake] factory wires up the in-memory fakes that live
|
||||||
/// inside each feature package. The [AppServices.wordpress] factory wires up
|
/// inside each feature package. The [AppServices.wordpress] factory wires up
|
||||||
/// a real WooCommerce-backed product repository while keeping other services
|
/// a real WooCommerce-backed product repository while keeping other services
|
||||||
/// fake until their backends are ready.
|
/// fake until their backends are ready.
|
||||||
class AppServices extends KcAppServices {
|
class AppServices {
|
||||||
final InventoryRepository inventoryRepository;
|
final InventoryRepository inventoryRepository;
|
||||||
final OrdersRepository ordersRepository;
|
final OrdersRepository ordersRepository;
|
||||||
final PolicyRepository policyRepository;
|
final PolicyRepository policyRepository;
|
||||||
|
|
@ -62,16 +57,4 @@ class AppServices extends KcAppServices {
|
||||||
productPublishingRepository: WordPressProductPublishingRepository(apiClient: apiClient),
|
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
|
import 'package:flutter/foundation.dart';
|
||||||
/// 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 'app_config.dart';
|
||||||
import 'app_services.dart';
|
import 'app_services.dart';
|
||||||
|
|
||||||
export 'package:core/core.dart' show KcBootstrap;
|
/// Bootstraps [AppServices] from the runtime [AppConfig].
|
||||||
|
|
||||||
/// Backward-compatible bootstrap for `kell_web`.
|
|
||||||
///
|
///
|
||||||
/// Delegates to [KcBootstrap.run] using the [AppServices.serviceFactory].
|
/// In **fake** mode the in-memory fakes are used unconditionally.
|
||||||
///
|
///
|
||||||
/// Existing call sites:
|
/// In **wordpress** mode the WooCommerce credentials are validated first.
|
||||||
/// ```dart
|
/// If any credential is missing the app falls back to fake mode and logs a
|
||||||
/// final (:services, config: effectiveConfig) = Bootstrap.run(config);
|
/// warning so the developer knows what went wrong.
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// New code should prefer:
|
|
||||||
/// ```dart
|
|
||||||
/// final (:services, config: effectiveConfig) = KcBootstrap.run(
|
|
||||||
/// config,
|
|
||||||
/// AppServices.serviceFactory,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class Bootstrap {
|
class Bootstrap {
|
||||||
const Bootstrap._();
|
const Bootstrap._();
|
||||||
|
|
||||||
|
|
@ -36,7 +18,39 @@ class Bootstrap {
|
||||||
/// Returns a record containing the resolved services and the effective
|
/// Returns a record containing the resolved services and the effective
|
||||||
/// config (which may differ from the input when WordPress credentials are
|
/// config (which may differ from the input when WordPress credentials are
|
||||||
/// missing and a fallback to fake mode occurs).
|
/// missing and a fallback to fake mode occurs).
|
||||||
static ({AppServices services, KcAppConfig config}) run(KcAppConfig config) {
|
static ({AppServices services, AppConfig config}) run(AppConfig config) {
|
||||||
return KcBootstrap.run<AppServices>(config, AppServices.serviceFactory);
|
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.
|
/// A Calculator.
|
||||||
///
|
class Calculator {
|
||||||
/// This package provides the platform-agnostic composition pattern used by
|
/// Returns [value] plus 1.
|
||||||
/// all app targets (`kell_web`, `kell_mobile`, etc.):
|
int addOne(int value) => value + 1;
|
||||||
///
|
}
|
||||||
/// - [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';
|
|
||||||
|
|
|
||||||
|
|
@ -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:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:core/core.dart';
|
import 'package:core/core.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('KcAppConfig', () {
|
test('adds one to input values', () {
|
||||||
test('creates config with required fields', () {
|
final calculator = Calculator();
|
||||||
const config = KcAppConfig(
|
expect(calculator.addOne(2), 3);
|
||||||
environment: KcAppEnvironment.fake,
|
expect(calculator.addOne(-7), -6);
|
||||||
wcSiteUrl: '',
|
expect(calculator.addOne(0), 1);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A minimal concrete [KcAppServices] subclass for testing.
|
|
||||||
class _TestAppServices extends KcAppServices {
|
|
||||||
final String mode;
|
|
||||||
const _TestAppServices({this.mode = 'default'});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue