126 lines
No EOL
4.1 KiB
Dart
126 lines
No EOL
4.1 KiB
Dart
// 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<void> 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<ExpiredApp>(
|
||
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('設定画面へ戻る'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |