import 'dart:async'; import 'package:flutter/foundation.dart'; /// 원격 기능 플래그 — Firestore `config/features` 문서 한 장. /// /// 별첨 B v1.4 (STATION) — *1차에서는 사용자 탭과 Coming Soon 을 모두 비노출하고 /// Route/데이터 구조만 유지해 향후 **앱 재배포 없이** 활성화합니다.* /// /// 「재배포 없이」가 이 클래스가 존재하는 이유 전부다. 컴파일 상수나 /// `--dart-define` 은 값을 바꾸려면 새 빌드를 올려야 해 회신 문구를 만족하지 못한다. /// 반대로 `firebase_remote_config` 는 신규 의존성이라 pubspec 머리말대로 최소 지원 /// OS 를 다시 계산해야 하는데, `firestore.rules` 의 `config/{id}` 가 이미 /// `allow read: if true` 로 열려 있고 `cloud_firestore` 는 이미 들어와 있다. /// 그래서 이미 있는 것으로 끝낸다. /// /// **기본값은 전부 꺼짐이다.** 문서가 없거나, 목 모드거나, 읽기가 실패해도 STATION /// 은 나타나지 않는다 — 「원격 값을 못 읽었다」가 「켜졌다」가 되면 안 된다. class FeatureFlags extends ChangeNotifier { FeatureFlags({Stream>? source}) { if (source != null) _subscription = source.listen(_apply, onError: _ignore); } /// Firestore 문서 ID. 서버 설계서에 넘기는 계약 이름이다. static const String documentPath = 'config/features'; /// STATION 탭 노출 스위치. 문서의 `stationEnabled`(bool) 하나를 읽는다. static const String stationKey = 'stationEnabled'; /// 운영 중인 최신 앱 버전(문자열 `x.y.z`). 스플래시 업데이트 확인 — Figma `510:19053` /// 「앱 버전 정보 · 최신 버전 여부 확인 · 업데이트 안내 노출」. 로그인 전에 읽어야 해서 /// 같은 공개 문서에 둔다. 값이 없으면 안내를 띄우지 않는다. static const String latestVersionKey = 'latestAppVersion'; StreamSubscription>? _subscription; bool _stationTab = false; String? _latestVersion; /// 운영 최신 버전. 문서에 없거나 문자열이 아니면 null. String? get latestVersion => _latestVersion; /// 하단 탭에 STATION 을 그릴 것인가. 기본 false. bool get stationTab => _stationTab; void _apply(Map data) { // 타입까지 확인한다. 누가 문자열 "true" 를 넣어도 켜지지 않는다. final next = data[stationKey] == true; final latest = switch (data[latestVersionKey]) { final String v when v.trim().isNotEmpty => v.trim(), _ => null, }; if (next == _stationTab && latest == _latestVersion) return; _stationTab = next; _latestVersion = latest; notifyListeners(); } /// 원격 읽기 실패는 꺼짐 상태를 유지하는 것으로 끝낸다. 화면에 띄울 것이 없다. void _ignore(Object error) { debugPrint('[FeatureFlags] read failed: ${error.runtimeType}'); } @override void dispose() { unawaited(_subscription?.cancel()); _subscription = null; super.dispose(); } } /// [installed] 가 [latest] 보다 낮은가 — 점으로 나눈 숫자를 앞에서부터 비교한다(`0.5.0` < `0.10.0`). /// 빌드 번호(`+7`)와 숫자가 아닌 꼬리는 무시한다. 읽을 수 없는 값이면 false(안내하지 않음). bool isOlderVersion(String installed, String latest) { List? parse(String v) { final parts = v.split('+').first.split('.'); final nums = [for (final p in parts) int.tryParse(p.trim())]; return nums.any((n) => n == null) ? null : nums.cast(); } final a = parse(installed), b = parse(latest); if (a == null || b == null) return false; for (var i = 0; i < a.length || i < b.length; i++) { final x = i < a.length ? a[i] : 0, y = i < b.length ? b[i] : 0; if (x != y) return x < y; } return false; }