// Version: 1.0.0 import 'package:flutter/foundation.dart'; /// アプリのビルド lifetime を管理するクラス class BuildExpiryInfo { /// バリッドな状態(90 日以内) static const String statusValid = 'valid'; /// 期限切れ状態 static const String statusExpired = 'expired'; /// ビルド時間を UTC 秒数として受け取り、残存寿命を判定する /// [timestamp] は UTC タイムスタンプ(秒) String getStatus(int timestamp) { final now = DateTime.now().toUtc(); final buildTime = DateTime.fromMillisecondsSinceEpoch( timestamp * 1000, isUtc: true, ); final expiryTime = _expiryTimestamp.buildDateTime; if (now.isAfter(expiryTime)) { return statusExpired; } return statusValid; } /// 残り寿命を DateTime オブジェクトとして返す(期限切れの場合 null) DateTime? getRemainingExpiry(int timestamp) { final now = DateTime.now().toUtc(); final expiryTime = _expiryTimestamp.buildDateTime; if (now.isAfter(expiryTime)) { return null; } // 残り寿命を計算(例:90 日 + 15 日間延命用 buffer) final daysRemaining = ((expiryTime.millisecondsSinceEpoch - now.millisecondsSinceEpoch) ~/ Duration.millisecondsPerDay); return now.add(Duration(days: daysRemaining)); } static BuildExpiryInfo get instance => _instance; factory BuildExpiryInfo() => _instance; late final DateTime _expiryTimestamp; String _appBuildTimestamp = ''; /// アプリのパッケージ情報からビルド時間を取得する void initializeFromPackageInfo(PackageInfo packageInfo) async { _appBuildTimestamp = (await packageInfo).version; } /// 寿命切れ画面を表示すべきかどうかを判定 bool get isExpired => getStatus(_parseTimestamp(_appBuildTimestamp)); /// 寿命切れメッセージの取得 String get expiredMessage { return '本アプリの有効期限が切れています。\n母艦(お局様)からの同期が必要となります。'; } } /// アプリ初期化時に呼ぶヘルパー class ExpiryInitHelper { static Future initialize(BuildContext context, PackageInfo packageInfo) async { final expiry = BuildExpiryInfo(); // パッケージ情報からビルド時間を含むバージョンをパース(例: "1.0.0+1234567890") final timestamp = int.tryParse(_extractTimestamp(packageInfo.version)); if (timestamp != null) { expiry._expiryTimestamp = DateTime.fromMillisecondsSinceEpoch( timestamp * 1000, isUtc: true, ).add(const Duration(days: 90)); // トランザクションで寿命チェックを実行 await context.mount( key: ExpiryInitHelper._routeName, condition: () => expiry.isExpired, builder: (context) => ExpiredApp(expiry: expiry), ); } } static String _extractTimestamp(String version) { // "1.0.0+1234567890" の形式の場合、+以降をパース final parts = version.split('+'); if (parts.length > 1) { return parts[1]; } return '0'; } static const String _routeName = '/expired_app_route'; } class ExpiredApp extends StatelessWidget { final BuildExpiryInfo expiry; const ExpiredApp({super.key, required this.expiry}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('有効期限切れ')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 80, color: Colors.red), const SizedBox(height: 24), Text( expiry.expiredMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 32), ElevatedButton( onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false), child: const Text('設定画面へ戻る'), ), ], ), ), ); } }