diff --git a/.gitignore b/.gitignore index 892a386..2f94414 100644 --- a/.gitignore +++ b/.gitignore @@ -3,46 +3,28 @@ *.log *.pyc *.swp +*.swo +*~ .DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id +# Dart .dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ +packages/ +pubspec.lock -# Symbolication related -app.*.symbols +# Build outputs +build/ +ios/iPhone*.xcodeproj/ +ios/Runner.xcworkspace/ +ios/Pods/ +libappbundle/ +*.apk +*.aab -# Obfuscation related -app.*.map.json +# Flutter tool +.flutter-plugins +.flutter-version -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# Local SQLite snapshots -app.db +# Environment variables +.env +.env.local \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..acc41ca --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,16 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + - prefer_const_constructors + - prefer_const_declarations + - avoid_unnecessary_containers + - prefer_single_quotes + - sort_child_properties_last + - prefer_final_locals + - avoid_print + +analyzer: + exclude: + - '**/*.g.dart' + - '**/*.freezed.dart' \ No newline at end of file diff --git a/data/mothership/pubspec.yaml b/data/mothership/pubspec.yaml new file mode 100644 index 0000000..f369aae --- /dev/null +++ b/data/mothership/pubspec.yaml @@ -0,0 +1,18 @@ +// Version: 1.0.0 +name: mothership_server +description: 母艦「お局様」用 HTTP サーバーアプリケーション +publish_to: 'none' + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + http: ^1.1.0 + uuid: ^4.3.3 + json_annotation: ^4.9.0 + path_provider: ^2.1.1 + +dev_dependencies: + build_runner: ^2.4.0 + json_serializable: ^6.8.0 + http_mock_adapter: ^0.7.0 \ No newline at end of file diff --git a/lib/utils/build_expiry_info.dart b/lib/utils/build_expiry_info.dart new file mode 100644 index 0000000..6161b82 --- /dev/null +++ b/lib/utils/build_expiry_info.dart @@ -0,0 +1,126 @@ +// 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('設定画面へ戻る'), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..2e32637 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,32 @@ +name: sales_assist_1 +description: オフライン単体で見積・納品・請求・レジ業務まで完結できる販売アシスタント +publish_to: 'none' + +version: 1.0.0+4 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + + # SQLite データ永続化 + sqflite: ^2.3.3 + path_provider: ^2.1.1 + + # Google エコシステム連携 + google_sign_in: ^6.1.0 + http: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/ \ No newline at end of file diff --git a/scripts/build_with_expiry.sh b/scripts/build_with_expiry.sh new file mode 100644 index 0000000..3b3a8e5 --- /dev/null +++ b/scripts/build_with_expiry.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Version: 1.0.0 +# +# 販売アシスト 1 号用の APK ビルドスクリプト +# 自動的な BUILD_TIMESTAMP(UTC タイムスタンプ)を付与し、90 日寿命チェックされた APK を生成 + +set -e + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FLUTTER="${FLUTTER:-flutter}" +BUILD_TYPE="${1:-release}" # debug|profile|release(デフォルト:release) + +echo "=== 販売アシスト 1 号 APK ビルドスクリプト ===" +echo "ビルドモード: ${BUILD_TYPE}" +echo "" + +# プロジェクトディレクトリへ cd +cd "$PROJECT_DIR" + +# パッケージ名を取得 +PACKAGE_NAME=$(grep '^name:' pubspec.yaml | cut -d' ' -f2) + +# 環境チェック +if [ ! -f "pubspec.lock" ]; then + echo "[エラー] pubspec.lock を発見できません。flutter pub get を実行してください。" + exit 1 +fi + +echo "[情報] パッケージ名: ${PACKAGE_NAME}" + +# flutter analyze を実行(オプション:--no-fatal-warnings は必要に応じて) +echo "[実装] flutter analyze..." +$FLUTTER analyze --no-fatal-warnings || true # エラーを警告として扱う + +# ビルドタインプスタンプの自動付与 +BUILD_TIMESTAMP=$(date -u +%s) +DART_DEFINE="--dart-define=APP_BUILD_TIMESTAMP=${BUILD_TIMESTAMP}" + +echo "[情報] BUILD_TIMESTAMP: ${BUILD_TIMESTAMP}" +echo " 有効期限(UTC): $(date -u -d "@${BUILD_TIMESTAMP}" "+%Y-%m-%d %H:%M:%S")" +echo " 90 日後の期限:$(date -u -d "@${BUILD_TIMESTAMP} + 90 days" "+%Y-%m-%d %H:%M:%S")" + +# APK ビルド実行(リリースビルドモードが推奨) +echo "" +echo "[実装] flutter build apk ${DART_DEFINE} --release..." +$FLUTTER build apk $DART_DEFINE \ + --dart-define=APP_BUILD_TIMESTAMP=$BUILD_TIMESTAMP \ + --release \ + --build-number=$((date +%s)) # ビルド番号を秒数(ビルド毎に異なる) + +# バイナリ出力先を確認 +APK_PATH="" +if [ -f "$PROJECT_DIR/build/app/outputs/flutter-apk/app-release.apk" ]; then + APK_PATH="$PROJECT_DIR/build/app/outputs/flutter-apk/app-release.apk" +elif [ -f "$PROJECT_DIR/build/app/outputs/bundle/release/app-release.aab" ]; then + # 必要に応じて AAB(Google Play)も出力可 + APK_PATH="$PROJECT_DIR/build/app/outputs/bundle/release/app-release.aab" +fi + +if [ ! -z "$APK_PATH" ]; then + echo "" + echo "[成功] APK 生成完了:" + echo " ファイル: ${APK_PATH}" + echo " サイズ:$(ls -lh "$APK_PATH" | awk '{print $5}')" +else + echo "[警告] バイナリ出力先が見つかりませんでした。" +fi + +echo "" +echo "=== ビルド完了 ===" \ No newline at end of file