diff --git a/packages/stac/lib/src/framework/stac.dart b/packages/stac/lib/src/framework/stac.dart index a761594b4..59371ff6e 100644 --- a/packages/stac/lib/src/framework/stac.dart +++ b/packages/stac/lib/src/framework/stac.dart @@ -188,6 +188,7 @@ class Stac extends StatelessWidget { bool logStackTraces = true, StacErrorWidgetBuilder? errorWidgetBuilder, StacCacheConfig? cacheConfig, + int? buildNumber, }) async { return StacService.initialize( options: options, @@ -199,6 +200,7 @@ class Stac extends StatelessWidget { logStackTraces: logStackTraces, errorWidgetBuilder: errorWidgetBuilder, cacheConfig: cacheConfig, + buildNumber: buildNumber, ); } diff --git a/packages/stac/lib/src/framework/stac_registry.dart b/packages/stac/lib/src/framework/stac_registry.dart index 0ca935166..600fff330 100644 --- a/packages/stac/lib/src/framework/stac_registry.dart +++ b/packages/stac/lib/src/framework/stac_registry.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:stac_logger/stac_logger.dart'; import 'package:stac_framework/stac_framework.dart'; @@ -14,6 +15,9 @@ class StacRegistry { static final _stacActionParsers = {}; + Color? Function(String?)? parseCustomColor; + int? _buildNumber; + int? get buildNumber => _buildNumber; static final Map _variables = {}; bool register(StacParser parser, [bool override = false]) { @@ -68,6 +72,12 @@ class StacRegistry { }); } + void registerBuildNumber(int? buildNumber) { + if (buildNumber != null) { + _buildNumber = buildNumber; + } + } + StacParser? getParser(String type) { return _stacParsers[type]; } diff --git a/packages/stac/lib/src/framework/stac_service.dart b/packages/stac/lib/src/framework/stac_service.dart index 4da4147f8..55a270187 100644 --- a/packages/stac/lib/src/framework/stac_service.dart +++ b/packages/stac/lib/src/framework/stac_service.dart @@ -5,24 +5,18 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide ErrorWidgetBuilder; import 'package:flutter/services.dart'; -import 'package:stac/src/framework/stac.dart'; -import 'package:stac/src/models/stac_cache_config.dart'; import 'package:stac/src/framework/stac_error.dart'; -import 'package:stac/src/framework/stac_registry.dart'; import 'package:stac/src/parsers/actions/stac_form_validate/stac_form_validate_parser.dart'; import 'package:stac/src/parsers/actions/stac_get_form_value/stac_get_form_value_parser.dart'; import 'package:stac/src/parsers/actions/stac_network_request/stac_network_request_parser.dart'; -import 'package:stac/src/parsers/parsers.dart'; import 'package:stac/src/parsers/widgets/stac_app_bar/stac_app_bar_parser.dart'; import 'package:stac/src/parsers/widgets/stac_inkwell/stac_inkwell_parser.dart'; import 'package:stac/src/parsers/widgets/stac_row/stac_row_parser.dart'; -import 'package:stac/src/parsers/widgets/stac_text/stac_text_parser.dart'; import 'package:stac/src/parsers/widgets/stac_tool_tip/stac_tool_tip_parser.dart'; -import 'package:stac/src/services/stac_network_service.dart'; import 'package:stac/src/utils/variable_resolver.dart'; +import 'package:stac/stac.dart'; import 'package:stac_core/core/stac_options.dart'; import 'package:stac_core/stac_core.dart'; -import 'package:stac_framework/stac_framework.dart'; import 'package:stac_logger/stac_logger.dart'; /// Internal service that manages Stac parsers, actions, and rendering. @@ -181,6 +175,7 @@ class StacService { bool logStackTraces = true, StacErrorWidgetBuilder? errorWidgetBuilder, StacCacheConfig? cacheConfig, + int? buildNumber, }) async { _options = options; if (cacheConfig != null) { @@ -190,18 +185,70 @@ class StacService { _actionParsers.addAll(actionParsers); StacRegistry.instance.registerAll(_parsers, override); StacRegistry.instance.registerAllActions(_actionParsers, override); + StacRegistry.instance.registerBuildNumber(buildNumber); StacNetworkService.initialize(dio ?? Dio()); _showErrorWidgets = showErrorWidgets; _logStackTraces = logStackTraces; _errorWidgetBuilder = errorWidgetBuilder; } + static void setParseCustomColor(Color? Function(String?)? parseCustomColor) => + StacRegistry.instance.parseCustomColor = parseCustomColor; + static Widget? fromJson(Map? json, BuildContext context) { try { if (json == null) { return null; } + Map? jsonVersion = json['version']; + final platform = json['platform']; + + /// Check if has version and buildNumber is not null + if (jsonVersion != null && StacRegistry.instance.buildNumber != null) { + final stacVersion = StacVersion.fromJson(jsonVersion); + final isSatisfied = stacVersion.isSatisfied( + StacRegistry.instance.buildNumber!, + ); + // If version is not satisfied, return null + if (!isSatisfied) { + Log.w( + 'Stac buildNumber ${stacVersion.buildNumber} is not satisfied; current build is: ${StacRegistry.instance.buildNumber}', + ); + return null; + } + } + + /// Check if platform is specified and validate + if (platform != null) { + final currentPlatform = currentPlatformString(); + final rawList = platform is List + ? platform.map((e) => e.toString().toLowerCase()).toList() + : [platform.toString().toLowerCase()]; + final validatedPlatforms = rawList + .where((s) => supportedPlatformStrings.contains(s)) + .toList(); + final invalid = rawList + .where((s) => !supportedPlatformStrings.contains(s)) + .toList(); + if (invalid.isNotEmpty) { + Log.w( + 'Unknown platform identifier(s) in "platform": $invalid. Supported: $supportedPlatformStrings', + ); + } + + final isPlatformSupported = validatedPlatforms.contains( + currentPlatform, + ); + + if (!isPlatformSupported && validatedPlatforms.isNotEmpty) { + Log.w( + 'Widget not supported on platform [$currentPlatform]. Only available for: ${validatedPlatforms.join(', ')}', + ); + return null; + } + } + // Safely extract widget type with validation final widgetType = json['type']; if (widgetType == null) { diff --git a/packages/stac/lib/src/parsers/foundation/foundation.dart b/packages/stac/lib/src/parsers/foundation/foundation.dart index db690b1eb..7fe236077 100644 --- a/packages/stac/lib/src/parsers/foundation/foundation.dart +++ b/packages/stac/lib/src/parsers/foundation/foundation.dart @@ -69,6 +69,7 @@ export 'layout/stac_stack_fit_parser.dart'; export 'layout/stac_vertical_direction_parser.dart'; export 'layout/stac_wrap_alignment_parser.dart'; export 'layout/stac_wrap_cross_alignment_parser.dart'; +export 'layout/parsers.dart'; // Navigation parsers export 'navigation/stac_bottom_navigation_bar_landscape_layout_parser.dart'; export 'navigation/stac_bottom_navigation_bar_type_parser.dart'; diff --git a/packages/stac/lib/src/parsers/foundation/layout/parsers.dart b/packages/stac/lib/src/parsers/foundation/layout/parsers.dart new file mode 100644 index 000000000..b3590a8d3 --- /dev/null +++ b/packages/stac/lib/src/parsers/foundation/layout/parsers.dart @@ -0,0 +1,10 @@ +export 'stac_axis_parser.dart'; +export 'stac_box_fit_parser.dart'; +export 'stac_box_shape_parser.dart'; +export 'stac_clip_parser.dart'; +export 'stac_flex_fit_parser.dart'; +export 'stac_material_tap_target_size_parser.dart'; +export 'stac_stack_fit_parser.dart'; +export 'stac_vertical_direction_parser.dart'; +export 'stac_wrap_alignment_parser.dart'; +export 'stac_wrap_cross_alignment_parser.dart'; diff --git a/packages/stac/lib/src/parsers/widgets/widgets.dart b/packages/stac/lib/src/parsers/widgets/widgets.dart index ab44d9fc6..a67be923c 100644 --- a/packages/stac/lib/src/parsers/widgets/widgets.dart +++ b/packages/stac/lib/src/parsers/widgets/widgets.dart @@ -20,6 +20,7 @@ export 'package:stac/src/parsers/widgets/stac_column/stac_column_parser.dart'; export 'package:stac/src/parsers/widgets/stac_conditional/stac_conditional_parser.dart'; export 'package:stac/src/parsers/widgets/stac_container/stac_container_parser.dart'; export 'package:stac/src/parsers/widgets/stac_custom_scroll_view/stac_custom_scroll_view_parser.dart'; +export 'package:stac/src/parsers/widgets/stac_double/stac_double.dart'; export 'package:stac/src/parsers/widgets/stac_default_bottom_navigation_controller/stac_default_bottom_navigation_controller_parser.dart'; export 'package:stac/src/parsers/widgets/stac_default_tab_controller/stac_default_tab_controller_parser.dart'; export 'package:stac/src/parsers/widgets/stac_divider/stac_divider_parser.dart'; @@ -58,6 +59,7 @@ export 'package:stac/src/parsers/widgets/stac_refresh_indicator/stac_refresh_ind export 'package:stac/src/parsers/widgets/stac_safe_area/stac_safe_area_parser.dart'; export 'package:stac/src/parsers/widgets/stac_scaffold/stac_scaffold_parser.dart'; export 'package:stac/src/parsers/widgets/stac_selectable_text/stac_selectable_text_parser.dart'; +export 'package:stac/src/parsers/widgets/stac_text/stac_text_parser.dart'; export 'package:stac/src/parsers/widgets/stac_set_value/stac_set_value_parser.dart'; export 'package:stac/src/parsers/widgets/stac_single_child_scroll_view/stac_single_child_scroll_view_parser.dart'; export 'package:stac/src/parsers/widgets/stac_sized_box/stac_sized_box_parser.dart'; diff --git a/packages/stac/lib/src/utils/color_utils.dart b/packages/stac/lib/src/utils/color_utils.dart index c9d2f3ea2..6b13d9175 100644 --- a/packages/stac/lib/src/utils/color_utils.dart +++ b/packages/stac/lib/src/utils/color_utils.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stac/src/utils/color_type.dart'; +import 'package:stac/stac.dart'; const String _hashtag = "#"; const String _empty = ""; @@ -145,6 +146,10 @@ Color? _parseThemeColor(String color, BuildContext context) { case StacColorType.surfaceTint: return Theme.of(context).colorScheme.surfaceTint; case StacColorType.none: + final customColor = StacRegistry.instance.parseCustomColor?.call(color); + if (customColor != null) { + return customColor; + } return null; } } diff --git a/packages/stac/lib/src/utils/stac_platforms.dart b/packages/stac/lib/src/utils/stac_platforms.dart new file mode 100644 index 000000000..aee548d1d --- /dev/null +++ b/packages/stac/lib/src/utils/stac_platforms.dart @@ -0,0 +1,28 @@ +import 'package:flutter/foundation.dart'; + +/// Canonical platform identifiers supported in JSON "platform" field. +/// Incoming values are normalized (e.g. lowercased) and validated against this list. +const List supportedPlatformStrings = [ + 'android', + 'ios', + 'linux', + 'macos', + 'windows', + 'web', +]; + +/// Returns the current platform as a canonical string (one of [supportedPlatformStrings]). +String currentPlatformString() { + if (kIsWeb) { + return 'web'; + } + + return switch (defaultTargetPlatform) { + TargetPlatform.android => 'android', + TargetPlatform.iOS => 'ios', + TargetPlatform.linux => 'linux', + TargetPlatform.macOS => 'macos', + TargetPlatform.windows => 'windows', + _ => 'unknown', + }; +} \ No newline at end of file diff --git a/packages/stac/lib/src/utils/utils.dart b/packages/stac/lib/src/utils/utils.dart index 6f7dd263f..2b2845558 100644 --- a/packages/stac/lib/src/utils/utils.dart +++ b/packages/stac/lib/src/utils/utils.dart @@ -1 +1,3 @@ export 'package:stac/src/utils/color_utils.dart'; +export 'package:stac/src/utils/version/stac_version.dart'; +export 'package:stac/src/utils/stac_platforms.dart'; \ No newline at end of file diff --git a/packages/stac/lib/src/utils/version/stac_version.dart b/packages/stac/lib/src/utils/version/stac_version.dart new file mode 100644 index 000000000..0ae407161 --- /dev/null +++ b/packages/stac/lib/src/utils/version/stac_version.dart @@ -0,0 +1,277 @@ +import 'package:stac/stac.dart'; + +/// # STAC Version Management System +/// +/// This module provides a comprehensive version management system for STAC applications, +/// allowing for flexible version-based feature control and compatibility checking. +/// +/// ## Overview +/// +/// The STAC Version system enables developers to control feature availability and +/// application behavior based on build numbers and version conditions. This is +/// particularly useful for: +/// +/// - **Feature Flags**: Enable/disable features based on app version +/// - **Backward Compatibility**: Ensure compatibility across different app versions +/// - **Gradual Rollouts**: Control feature deployment to specific version ranges +/// - **Platform-Specific Versioning**: Handle different versioning schemes per platform +/// +/// ## Key Components +/// +/// - `StacVersion`: Main class representing a version constraint +/// - `StacConditionVersion`: Enum defining comparison operators +/// - Platform-aware JSON parsing for cross-platform compatibility +/// +/// ## Usage Examples +/// +/// ### Basic Version Checking +/// ```dart +/// // Check if current app version satisfies a minimum requirement +/// final minVersion = StacVersion( +/// buildNumber: 100, +/// condition: StacConditionVersion.greaterThanOrEqual, +/// ); +/// +/// final currentBuild = 120; +/// if (minVersion.isSatisfied(currentBuild)) { +/// // Enable new feature +/// showNewFeature(); +/// } +/// ``` +/// +/// ### JSON Configuration +/// ```dart +/// // Load version constraints from server/config +/// final json = { +/// 'buildNumber': 150, +/// 'condition': '>=', +/// // Platform-specific overrides +/// 'buildNumber_ios': 155, +/// 'buildNumber_android': 145, +/// }; +/// +/// final version = StacVersion.fromJson(json); +/// ``` +/// +/// ### Feature Gate Example +/// ```dart +/// class FeatureManager { +/// static bool isNewUIEnabled(int appBuildNumber) { +/// final requirement = StacVersion( +/// buildNumber: 200, +/// condition: StacConditionVersion.greaterThanOrEqual, +/// ); +/// return requirement.isSatisfied(appBuildNumber); +/// } +/// } +/// ``` +/// +/// ## Use Cases +/// +/// ### 1. **Progressive Feature Rollout** +/// Deploy features gradually by setting minimum version requirements: +/// ```dart +/// final premiumFeatureGate = StacVersion( +/// buildNumber: 300, +/// condition: StacConditionVersion.greaterThan, +/// ); +/// ``` +/// +/// ### 2. **Deprecation Management** +/// Handle deprecated features with maximum version limits: +/// ```dart +/// final legacyAPIGate = StacVersion( +/// buildNumber: 250, +/// condition: StacConditionVersion.lessThanOrEqual, +/// ); +/// ``` +/// +/// ### 3. **A/B Testing** +/// Control experiment participation based on version: +/// ```dart +/// final experimentGroup = StacVersion( +/// buildNumber: 180, +/// condition: StacConditionVersion.equal, +/// ); +/// ``` +/// +/// ### 4. **Emergency Fixes** +/// Quickly disable problematic features: +/// ```dart +/// final emergencyDisable = StacVersion( +/// buildNumber: 195, +/// condition: StacConditionVersion.notEqual, +/// ); +/// ``` + +/// Represents a version constraint with a build number and comparison condition. +/// +/// This class encapsulates version requirements that can be evaluated against +/// an application's current build number to determine feature availability +/// or compatibility. +/// +/// Example: +/// ```dart +/// final versionGate = StacVersion( +/// buildNumber: 150, +/// condition: StacConditionVersion.greaterThanOrEqual, +/// ); +/// +/// if (versionGate.isSatisfied(currentAppBuild)) { +/// // Feature is available +/// } +/// ``` +class StacVersion { + /// Creates a new version constraint. + /// + /// [buildNumber] is the reference build number for comparison. + /// [condition] defines how the comparison should be performed. + const StacVersion({required this.buildNumber, required this.condition}); + + /// The reference build number used for version comparisons. + final int buildNumber; + + /// The condition that defines how version comparison is performed. + final StacConditionVersion condition; + + /// Creates a [StacVersion] instance from JSON data. + /// + /// Supports platform-specific overrides by checking for keys with + /// platform suffixes (e.g., 'buildNumber_ios', 'condition_android'). + /// + /// Example JSON: + /// ```json + /// { + /// "buildNumber": 100, + /// "condition": ">=", + /// "buildNumber_ios": 110, + /// "buildNumber_android": 95 + /// } + /// ``` + factory StacVersion.fromJson(Map json) { + return StacVersion( + buildNumber: toPlatformJson(json, 'buildNumber').toInt(), + condition: (toPlatformJson(json, 'condition') as String?) + .toStacConditionVersion(), + ); + } + + /// Extracts platform-specific values from JSON, falling back to generic keys. + /// + /// First attempts to find a platform-specific key (e.g., 'key_ios'), + /// then falls back to the generic key if not found. + /// + /// This enables different version requirements per platform while + /// maintaining a single configuration source. + static dynamic toPlatformJson(Map json, String key) { + return json['${key}_${currentPlatformString()}'] ?? json[key]; + } +} + +/// Defines the available comparison operators for version conditions. +/// +/// These operators determine how a [StacVersion] compares an application's +/// build number against the reference build number. +enum StacConditionVersion { + /// Greater than (>) + /// App build must be strictly greater than the reference build. + greaterThan, + + /// Greater than or equal (>=) + /// App build must be greater than or equal to the reference build. + greaterThanOrEqual, + + /// Less than (<) + /// App build must be strictly less than the reference build. + lessThan, + + /// Less than or equal (<=) + /// App build must be less than or equal to the reference build. + lessThanOrEqual, + + /// Equal (==) + /// App build must exactly match the reference build. + equal, + + /// Not equal (!=) + /// App build must not match the reference build. + notEqual, +} + +/// Extension to convert string representations to [StacConditionVersion] enum values. +/// +/// Supports standard comparison operators: +/// - '>' -> greaterThan +/// - '>=' -> greaterThanOrEqual +/// - '<' -> lessThan +/// - '<=' -> lessThanOrEqual +/// - '==' -> equal +/// - '!=' -> notEqual +/// +/// Defaults to [StacConditionVersion.notEqual] for invalid input. +extension StacConditionVersionX on String? { + /// Converts a string operator to its corresponding [StacConditionVersion]. + StacConditionVersion toStacConditionVersion() => switch (this) { + '>' => StacConditionVersion.greaterThan, + '>=' => StacConditionVersion.greaterThanOrEqual, + '<' => StacConditionVersion.lessThan, + '<=' => StacConditionVersion.lessThanOrEqual, + '==' => StacConditionVersion.equal, + '!=' => StacConditionVersion.notEqual, + _ => StacConditionVersion.notEqual, + }; +} + +/// Extension to convert [StacConditionVersion] enum values back to string operators. +/// +/// Useful for serialization, logging, or displaying version conditions in UI. +extension StacConditionVersionToStringX on StacConditionVersion { + /// Returns the string representation of the condition operator. + String toTypeString() => switch (this) { + StacConditionVersion.greaterThan => '>', + StacConditionVersion.greaterThanOrEqual => '>=', + StacConditionVersion.lessThan => '<', + StacConditionVersion.lessThanOrEqual => '<=', + StacConditionVersion.equal => '==', + StacConditionVersion.notEqual => '!=', + }; +} + +/// Extension providing version satisfaction checking functionality. +/// +/// This is the core functionality that determines whether a given +/// application build number satisfies the version constraint. +extension StacVersionX on StacVersion { + /// Determines if the given app build number satisfies this version constraint. + /// + /// Returns `true` if [appBuildNumber] is null (indicating no version check required) + /// or if the build number satisfies the condition relative to the reference build number. + /// + /// Examples: + /// ```dart + /// final minVersion = StacVersion(buildNumber: 100, condition: StacConditionVersion.greaterThanOrEqual); + /// + /// minVersion.isSatisfied(120); // true - 120 >= 100 + /// minVersion.isSatisfied(90); // false - 90 < 100 + /// minVersion.isSatisfied(null); // true - no version constraint + /// ``` + /// + /// [appBuildNumber] The current application's build number to check. + /// Returns whether the version constraint is satisfied. + bool isSatisfied(int? appBuildNumber) { + // If no app build number is provided, assume constraint is satisfied + if (appBuildNumber == null) { + return true; + } + + // Evaluate the condition against the reference build number + return switch (condition) { + StacConditionVersion.greaterThan => appBuildNumber > buildNumber, + StacConditionVersion.greaterThanOrEqual => appBuildNumber >= buildNumber, + StacConditionVersion.lessThan => appBuildNumber < buildNumber, + StacConditionVersion.lessThanOrEqual => appBuildNumber <= buildNumber, + StacConditionVersion.equal => appBuildNumber == buildNumber, + StacConditionVersion.notEqual => appBuildNumber != buildNumber, + }; + } +} diff --git a/packages/stac/lib/stac.dart b/packages/stac/lib/stac.dart index 749fd39aa..9e3d5334f 100644 --- a/packages/stac/lib/stac.dart +++ b/packages/stac/lib/stac.dart @@ -6,4 +6,5 @@ export 'package:stac/src/services/services.dart'; export 'package:stac/src/utils/utils.dart'; // Theme exports export 'package:stac_core/stac_core.dart' show StacTheme; + export 'package:stac_framework/stac_framework.dart'; diff --git a/packages/stac/pubspec.yaml b/packages/stac/pubspec.yaml index 08ae28f77..68d79859b 100644 --- a/packages/stac/pubspec.yaml +++ b/packages/stac/pubspec.yaml @@ -23,7 +23,12 @@ dependencies: cached_network_image: ^3.4.1 flutter_svg: ^2.2.3 stac_logger: ^1.1.0 - stac_core: ^1.3.0 + # when approved, use stac_core: ^1.3.x + stac_core: + git: + url: https://github.com/SuaMusica/stac.git + path: packages/stac_core + ref: devall shared_preferences: ^2.5.4 dev_dependencies: diff --git a/packages/stac/test/src/utils/version/stac_version_test.dart b/packages/stac/test/src/utils/version/stac_version_test.dart new file mode 100644 index 000000000..59c77261a --- /dev/null +++ b/packages/stac/test/src/utils/version/stac_version_test.dart @@ -0,0 +1,279 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/foundation.dart'; +import 'package:stac/src/framework/stac_registry.dart'; +import 'package:stac/src/utils/version/stac_version.dart'; +import 'package:stac/src/utils/stac_platforms.dart'; + +final String platform = currentPlatformString(); + +void main() { + group('StacVersion', () { + setUp(() { + // Reset the app version before each test + StacRegistry.instance.registerBuildNumber(null); + }); + + test('creates StacVersion instance correctly', () { + final version = StacVersion( + buildNumber: 1000, + condition: StacConditionVersion.equal, + ); + + expect(version.buildNumber, 1000); + expect(version.condition, StacConditionVersion.equal); + }); + + test('creates StacVersion from JSON correctly', () { + final json = { + 'buildNumber': 1000, + 'condition': '>', + }; + + final version = StacVersion.fromJson(json); + + expect(version.buildNumber, 1000); + expect(version.condition, StacConditionVersion.greaterThan); + }); + + group('condition parsing', () { + test('parses all condition types correctly', () { + final conditions = { + '>': StacConditionVersion.greaterThan, + '>=': StacConditionVersion.greaterThanOrEqual, + '<': StacConditionVersion.lessThan, + '<=': StacConditionVersion.lessThanOrEqual, + '==': StacConditionVersion.equal, + 'invalid': StacConditionVersion.notEqual, + }; + + conditions.forEach((input, expected) { + expect(input.toStacConditionVersion(), expected); + }); + }); + + test('handles null input by returning notEqual', () { + String? nullString; + expect(nullString.toStacConditionVersion(), + StacConditionVersion.notEqual); + }); + }); + + group('isSatisfied', () { + setUp(() { + StacRegistry.instance.registerBuildNumber(2000); + }); + + test('returns true when app version is null', () { + final version = StacVersion( + buildNumber: 1000, + condition: StacConditionVersion.equal, + ); + // When there is no app build number (e.g. StacRegistry.instance.registerBuildNumber(null)), + // StacVersion.isSatisfied should treat the constraint as satisfied. + expect(version.isSatisfied(null), true); + }); + + test('handles equal condition correctly', () { + final version = StacVersion( + buildNumber: 2000, + condition: StacConditionVersion.equal, + ); + expect(version.isSatisfied(StacRegistry.instance.buildNumber), true); + + final differentVersion = StacVersion( + buildNumber: 1000, + condition: StacConditionVersion.equal, + ); + expect(differentVersion.isSatisfied(StacRegistry.instance.buildNumber), + false); + }); + + test('handles greater than condition correctly', () { + final version = StacVersion( + buildNumber: 1000, + condition: StacConditionVersion.greaterThan, + ); + expect(version.isSatisfied(StacRegistry.instance.buildNumber), true); + + final higherVersion = StacVersion( + buildNumber: 3000, + condition: StacConditionVersion.greaterThan, + ); + expect(higherVersion.isSatisfied(StacRegistry.instance.buildNumber), + false); + }); + + test('handles less than condition correctly', () { + final version = StacVersion( + buildNumber: 3000, + condition: StacConditionVersion.lessThan, + ); + expect(version.isSatisfied(StacRegistry.instance.buildNumber), true); + + final lowerVersion = StacVersion( + buildNumber: 1000, + condition: StacConditionVersion.lessThan, + ); + expect( + lowerVersion.isSatisfied(StacRegistry.instance.buildNumber), false); + }); + + test('handles version components of different lengths', () { + StacRegistry.instance.registerBuildNumber(20001); + + final version = StacVersion( + buildNumber: 2000, + condition: StacConditionVersion.equal, + ); + expect(version.isSatisfied(StacRegistry.instance.buildNumber), false); + + final versionWithExtra = StacVersion( + buildNumber: 2000, + condition: StacConditionVersion.equal, + ); + expect(versionWithExtra.isSatisfied(StacRegistry.instance.buildNumber), + false); + + final exactVersion = StacVersion( + buildNumber: 20001, + condition: StacConditionVersion.equal, + ); + expect( + exactVersion.isSatisfied(StacRegistry.instance.buildNumber), true); + }); + + test('handles build number equality', () { + StacRegistry.instance.registerBuildNumber(2000); + + final version = StacVersion( + buildNumber: 2000, + condition: StacConditionVersion.equal, + ); + expect(version.isSatisfied(StacRegistry.instance.buildNumber), true); + }); + }); + + test('converts condition to string correctly', () { + final conditions = { + StacConditionVersion.greaterThan: '>', + StacConditionVersion.greaterThanOrEqual: '>=', + StacConditionVersion.lessThan: '<', + StacConditionVersion.lessThanOrEqual: '<=', + StacConditionVersion.equal: '==', + StacConditionVersion.notEqual: '!=', + }; + + conditions.forEach((condition, expected) { + expect(condition.toTypeString(), expected); + }); + }); + }); + + /// platform tests + group('platform tests', () { + test('uses platform-specific buildNumber when available', () { + final json = { + 'buildNumber': 1000, + 'buildNumber_$platform': 2000, + 'condition': '>', + }; + + final version = StacVersion.fromJson(json); + + // Should use platform-specific buildNumber (`platform` in this case) + expect(version.buildNumber, 2000); + expect(version.condition, StacConditionVersion.greaterThan); + }); + + test( + 'falls back to generic buildNumber when platform-specific not available', + () { + final json = { + 'buildNumber': 1500, + // Use a platform key that will not match the current platform, + // so the generic buildNumber is always used. + 'buildNumber_fuchsia': 2500, + 'condition': '>=', + }; + + final version = StacVersion.fromJson(json); + + // Should use generic buildNumber since `platform`-specific is not available + expect(version.buildNumber, 1500); + expect(version.condition, StacConditionVersion.greaterThanOrEqual); + }); + + test('uses platform-specific condition when available', () { + final json = { + 'buildNumber': 1000, + 'condition': '>', + 'condition_$platform': '<=', + }; + + final version = StacVersion.fromJson(json); + + expect(version.buildNumber, 1000); + // Should use platform-specific condition + expect(version.condition, StacConditionVersion.lessThanOrEqual); + }); + + test('falls back to generic condition when platform-specific not available', + () { + final json = { + 'buildNumber': 1000, + 'condition': '<', + 'condition_windows': '!=', // Different platform + }; + + final version = StacVersion.fromJson(json); + + expect(version.buildNumber, 1000); + // Should use generic condition since `platform`-specific is not available + expect(version.condition, StacConditionVersion.lessThan); + }); + + test('uses both platform-specific values when available', () { + final json = { + 'buildNumber': 1000, + 'buildNumber_$platform': 3000, + 'condition': '>', + 'condition_$platform': '==', + }; + + final version = StacVersion.fromJson(json); + + // Should use both platform-specific values + expect(version.buildNumber, 3000); + expect(version.condition, StacConditionVersion.equal); + }); + + test('handles missing generic fallback gracefully', () { + final json = { + 'buildNumber_ios': + 2000, // Only platform-specific for different platform + 'condition_android': + '>=', // Only platform-specific for different platform + }; + + // This should throw or handle gracefully since there's no fallback + expect(() => StacVersion.fromJson(json), throwsA(isA())); + }); + + test('toPlatformJson method works correctly', () { + final json = { + 'key': 'generic_value', + 'key_$platform': 'platform_value', + 'other_key': 'other_generic', + }; + + // Should return platform-specific value when available + expect(StacVersion.toPlatformJson(json, 'key'), 'platform_value'); + + // Should return generic value when platform-specific not available + expect(StacVersion.toPlatformJson(json, 'other_key'), 'other_generic'); + + // Should return null when neither exists + expect(StacVersion.toPlatformJson(json, 'missing_key'), isNull); + }); + }); +} diff --git a/packages/stac_core/lib/foundation/text/stac_text_style/stac_text_style.dart b/packages/stac_core/lib/foundation/text/stac_text_style/stac_text_style.dart index 6c7c990ee..95577c4c5 100644 --- a/packages/stac_core/lib/foundation/text/stac_text_style/stac_text_style.dart +++ b/packages/stac_core/lib/foundation/text/stac_text_style/stac_text_style.dart @@ -705,3 +705,79 @@ class StacThemeTextStyle extends StacTextStyle { ); } } + +/// Extension so [StacTextStyle] can use [copyWith] without changing the plugin. +/// Delegates to [StacCustomTextStyle.copyWith] or [StacThemeTextStyle.copyWith]. +extension StacTextStyleCopyWith on StacTextStyle { + StacTextStyle copyWith({ + bool? inherit, + StacColor? color, + StacColor? backgroundColor, + double? fontSize, + StacFontWeight? fontWeight, + StacFontStyle? fontStyle, + double? letterSpacing, + double? wordSpacing, + StacTextBaseline? textBaseline, + double? height, + StacTextLeadingDistribution? leadingDistribution, + StacColor? decorationColor, + StacTextDecorationStyle? decorationStyle, + double? decorationThickness, + String? debugLabel, + String? fontFamily, + List? fontFamilyFallback, + String? package, + StacTextOverflow? overflow, + }) { + if (this is StacCustomTextStyle) { + return (this as StacCustomTextStyle).copyWith( + inherit: inherit, + color: color, + backgroundColor: backgroundColor, + fontSize: fontSize, + fontWeight: fontWeight, + fontStyle: fontStyle, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + textBaseline: textBaseline, + height: height, + leadingDistribution: leadingDistribution, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + debugLabel: debugLabel, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + package: package, + overflow: overflow, + ); + } + if (this is StacThemeTextStyle) { + return (this as StacThemeTextStyle).copyWith( + inherit: inherit, + color: color, + backgroundColor: backgroundColor, + fontSize: fontSize, + fontWeight: fontWeight, + fontStyle: fontStyle, + letterSpacing: letterSpacing, + wordSpacing: wordSpacing, + textBaseline: textBaseline, + height: height, + leadingDistribution: leadingDistribution, + decorationColor: decorationColor, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + debugLabel: debugLabel, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + package: package, + overflow: overflow, + ); + } + throw StateError( + 'copyWith only supported on StacCustomTextStyle and StacThemeTextStyle', + ); + } +} diff --git a/packages/stac_webview/pubspec.yaml b/packages/stac_webview/pubspec.yaml index 5d3867fd6..6253f50a3 100644 --- a/packages/stac_webview/pubspec.yaml +++ b/packages/stac_webview/pubspec.yaml @@ -19,7 +19,12 @@ dependencies: sdk: flutter webview_flutter: ^4.13.1 json_annotation: ^4.10.0 - stac_core: ^1.3.0 + # when approved, use stac_core: ^1.3.x + stac_core: + git: + url: https://github.com/SuaMusica/stac.git + path: packages/stac_core + ref: devall stac_framework: ^1.0.0 dev_dependencies: diff --git a/pubspec.yaml b/pubspec.yaml index 580d8c098..7b57af382 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,4 +5,3 @@ environment: dev_dependencies: melos: ^6.3.2 - \ No newline at end of file