/// Compares dotted numeric versions, e.g. "1.0.14" vs "1.0.2". /// /// Returns a negative number when [a] is older than [b], 0 when they are /// equivalent, and a positive number when [a] is newer. Missing segments count /// as 0, so "1.0" and "1.0.0" are equivalent. Returns null when either side /// cannot be parsed. int? compareVersions(String? a, String? b) { final left = _segments(a); final right = _segments(b); if (left == null || right == null) return null; final length = left.length > right.length ? left.length : right.length; for (var i = 0; i < length; i++) { final l = i < left.length ? left[i] : 0; final r = i < right.length ? right[i] : 0; if (l != r) return l - r; } return 0; } /// Whether [current] is strictly older than [minimum]. /// /// Deliberately fails open: a missing or unparseable value returns false, so a /// backend misconfiguration can never lock users out of the app behind the /// blocking "you must update" dialog. Equal or newer versions are fine too — a /// build ahead of the configured minimum is not out of date. bool isUpdateRequired(String? current, String? minimum) { final result = compareVersions(current, minimum); if (result == null) return false; return result < 0; } List? _segments(String? version) { if (version == null) return null; final trimmed = version.trim(); if (trimmed.isEmpty) return null; // Tolerate build suffixes such as "1.0.14+14" or "1.0.14-beta". final core = trimmed.split(RegExp(r'[+\-]')).first; final parts = core.split('.'); final segments = []; for (final part in parts) { final value = int.tryParse(part.trim()); if (value == null) return null; segments.add(value); } return segments.isEmpty ? null : segments; }