Compare commits
No commits in common. "191274c65a40df1ae4f4fe1f1ae787eae66c3015" and "1bf59e02ccc73d7ccc2980b2f19b1d5714ce59c9" have entirely different histories.
191274c65a
...
1bf59e02cc
|
|
@ -9,8 +9,6 @@ import 'providers/theme_provider.dart';
|
|||
import 'screens/main_screen.dart';
|
||||
import 'screens/license_screen.dart';
|
||||
import 'services/migration_service.dart';
|
||||
import 'services/license_service.dart';
|
||||
import 'providers/license_provider.dart';
|
||||
|
||||
/// 店舗向けビルドかどうかを判定するビルド時フラグ
|
||||
///
|
||||
|
|
@ -76,16 +74,9 @@ void main() async {
|
|||
}
|
||||
}
|
||||
|
||||
// ちらつき防止: runApp の前にキャッシュ済みライセンス状態を取得し、
|
||||
// licenseStatusProvider が loading を経由せず即 AsyncData になるよう override する
|
||||
final cachedLicenseStatus = await LicenseService.getCachedStatusOnly();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
licenseInitialStatusProvider.overrideWithValue(cachedLicenseStatus),
|
||||
],
|
||||
child: const MyApp(),
|
||||
const ProviderScope(
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../services/license_service.dart';
|
||||
|
||||
/// 起動時にキャッシュから事前ロードした値を保持するプロバイダー
|
||||
/// ライセンス状態の非同期プロバイダー
|
||||
///
|
||||
/// main() で override して初期値を渡すことで、
|
||||
/// licenseStatusProvider が loading 状態を経由せず即座に AsyncData になる。
|
||||
final licenseInitialStatusProvider = Provider<LicenseStatus?>((ref) => null);
|
||||
|
||||
/// ライセンス状態プロバイダー
|
||||
///
|
||||
/// - build() は licenseInitialStatusProvider に値がある場合は同期で返す(ちらつきなし)
|
||||
/// - 同時にバックグラウンドでサーバー検証を実行し、差異があれば状態を更新する
|
||||
final licenseStatusProvider =
|
||||
AsyncNotifierProvider<LicenseStatusNotifier, LicenseStatus>(
|
||||
LicenseStatusNotifier.new,
|
||||
);
|
||||
|
||||
class LicenseStatusNotifier extends AsyncNotifier<LicenseStatus> {
|
||||
@override
|
||||
FutureOr<LicenseStatus> build() {
|
||||
final initial = ref.read(licenseInitialStatusProvider);
|
||||
if (initial != null) {
|
||||
// キャッシュ値を同期で返すことで loading 状態をスキップ
|
||||
_refreshFromServer();
|
||||
return initial;
|
||||
}
|
||||
// override なし(テスト等): 通常フロー
|
||||
return LicenseService.checkStatus();
|
||||
}
|
||||
|
||||
/// バックグラウンドでサーバー検証を実行し、変化があれば状態を更新する
|
||||
Future<void> _refreshFromServer() async {
|
||||
try {
|
||||
final fresh = await LicenseService.checkStatus();
|
||||
if (state.hasValue && state.value != fresh) {
|
||||
state = AsyncData(fresh);
|
||||
}
|
||||
} catch (_) {
|
||||
// ネットワークエラーはキャッシュ値を維持
|
||||
}
|
||||
}
|
||||
}
|
||||
/// アプリ起動時に一度だけVPSに問い合わせ、結果をキャッシュする。
|
||||
/// 手動更新は [licenseStatusProvider].invalidate() を呼ぶ。
|
||||
final licenseStatusProvider = FutureProvider<LicenseStatus>((ref) async {
|
||||
return LicenseService.checkStatus();
|
||||
});
|
||||
|
||||
/// Pro版かどうか(ナビゲーション・機能解放の分岐に使う)
|
||||
///
|
||||
/// licenseStatusProvider が同期で AsyncData を返すため、
|
||||
/// アプリ起動時に false をちらつかせることなく正しい値を返す。
|
||||
final isProProvider = Provider<bool>((ref) {
|
||||
final statusAsync = ref.watch(licenseStatusProvider);
|
||||
return statusAsync.maybeWhen(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'device_service.dart';
|
||||
|
|
@ -23,81 +22,43 @@ enum LicenseStatus {
|
|||
|
||||
/// ライセンス管理サービス
|
||||
///
|
||||
/// ## ストレージ方針
|
||||
/// - ライセンスキー本体: flutter_secure_storage(暗号化)
|
||||
/// - 状態キャッシュ: SharedPreferences(平文でもリスクなし)
|
||||
///
|
||||
/// ## 状態管理の優先順位
|
||||
/// 1. 起動時: SecureStorage のキャッシュを即時返却(ちらつき防止)
|
||||
/// 2. バックグラウンド: VPS で再検証し、差異があれば状態を更新
|
||||
/// 3. オフライン時: SharedPreferences のキャッシュを維持
|
||||
/// 1. オンライン時: VPSに問い合わせた結果を使用 + SharedPreferencesにキャッシュ
|
||||
/// 2. オフライン時: SharedPreferencesのキャッシュを使用 (Pro状態を維持)
|
||||
///
|
||||
/// ## ライセンスキー形式
|
||||
/// PONSHU-XXXX-XXXX-XXXX (6バイト = 12hex文字, 大文字)
|
||||
class LicenseService {
|
||||
static const _secureKeyName = 'ponshu_license_key';
|
||||
static const _prefCachedStatus = 'ponshu_license_status_cache';
|
||||
static const _prefCachedAt = 'ponshu_license_cached_at';
|
||||
static const _prefMigratedV1 = 'ponshu_license_migrated_v1';
|
||||
static const _cacheValidSeconds = 24 * 60 * 60; // 24時間
|
||||
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
// ========== Migration ==========
|
||||
|
||||
/// SharedPreferences → flutter_secure_storage の一回限りのマイグレーション
|
||||
static Future<void> _migrateIfNeeded() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool(_prefMigratedV1) == true) return;
|
||||
|
||||
// 旧ストレージにキーがあれば移行して削除
|
||||
final oldValue = prefs.getString(_secureKeyName);
|
||||
if (oldValue != null && oldValue.isNotEmpty) {
|
||||
await _storage.write(key: _secureKeyName, value: oldValue);
|
||||
await prefs.remove(_secureKeyName);
|
||||
debugPrint('[License] Migrated license key to secure storage.');
|
||||
}
|
||||
await prefs.setBool(_prefMigratedV1, true);
|
||||
}
|
||||
static const _prefLicenseKey = 'ponshu_license_key';
|
||||
static const _prefCachedStatus = 'ponshu_license_status_cache';
|
||||
static const _prefCachedAt = 'ponshu_license_cached_at';
|
||||
static const _cacheValidSeconds = 24 * 60 * 60; // 24時間キャッシュ有効
|
||||
|
||||
// ========== Public API ==========
|
||||
|
||||
/// キャッシュのみを即時返却(サーバー問い合わせなし)
|
||||
///
|
||||
/// 起動時のちらつき防止用。main() で await してから runApp() に渡す。
|
||||
static Future<LicenseStatus> getCachedStatusOnly() async {
|
||||
await _migrateIfNeeded();
|
||||
final savedKey = await _storage.read(key: _secureKeyName) ?? '';
|
||||
if (savedKey.isEmpty) return LicenseStatus.free;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _getCachedStatus(prefs);
|
||||
}
|
||||
|
||||
/// アプリ起動時(バックグラウンド): VPS でライセンス状態を検証して返す
|
||||
/// アプリ起動時に呼ぶ: ライセンス状態を確認して返す
|
||||
static Future<LicenseStatus> checkStatus() async {
|
||||
await _migrateIfNeeded();
|
||||
final savedKey = await _storage.read(key: _secureKeyName) ?? '';
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedKey = prefs.getString(_prefLicenseKey) ?? '';
|
||||
|
||||
// ライセンスキーが保存済み → サーバーで検証
|
||||
if (savedKey.isNotEmpty) {
|
||||
try {
|
||||
final status = await _validateKeyWithServer(savedKey);
|
||||
if (status == LicenseStatus.offline) {
|
||||
// ネットワーク不通: キャッシュを上書きせずに返す
|
||||
debugPrint('[License] Server unreachable, using cache');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _getCachedStatus(prefs);
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await _cacheStatus(prefs, status);
|
||||
return status;
|
||||
} catch (e) {
|
||||
debugPrint('[License] Server unreachable, using cache: $e');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _getCachedStatus(prefs);
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンスキーなし → 無料版
|
||||
return LicenseStatus.free;
|
||||
}
|
||||
|
||||
|
|
@ -115,8 +76,8 @@ class LicenseService {
|
|||
final status = await _validateKeyWithServer(key);
|
||||
|
||||
if (status == LicenseStatus.pro) {
|
||||
await _storage.write(key: _secureKeyName, value: key);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefLicenseKey, key);
|
||||
await _cacheStatus(prefs, LicenseStatus.pro);
|
||||
debugPrint('[License] Activated successfully.');
|
||||
return (success: true, message: '');
|
||||
|
|
@ -135,14 +96,14 @@ class LicenseService {
|
|||
|
||||
/// ライセンスキーがローカルに保存されているか
|
||||
static Future<bool> hasLicenseKey() async {
|
||||
await _migrateIfNeeded();
|
||||
return ((await _storage.read(key: _secureKeyName)) ?? '').isNotEmpty;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return (prefs.getString(_prefLicenseKey) ?? '').isNotEmpty;
|
||||
}
|
||||
|
||||
/// ライセンスをリセット(デバッグ用)
|
||||
static Future<void> reset() async {
|
||||
await _storage.delete(key: _secureKeyName);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_prefLicenseKey);
|
||||
await prefs.remove(_prefCachedStatus);
|
||||
await prefs.remove(_prefCachedAt);
|
||||
debugPrint('[License] Reset complete.');
|
||||
|
|
@ -167,11 +128,7 @@ class LicenseService {
|
|||
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
|
||||
|
||||
if (data['valid'] == true) return LicenseStatus.pro;
|
||||
// revoked フィールド(boolean)を優先し、error メッセージ文字列にも対応
|
||||
if (data['revoked'] == true ||
|
||||
(data['error'] as String? ?? '').contains('無効化')) {
|
||||
return LicenseStatus.revoked;
|
||||
}
|
||||
if ((data['error'] as String? ?? '').contains('無効化')) return LicenseStatus.revoked;
|
||||
return LicenseStatus.free;
|
||||
|
||||
} catch (e) {
|
||||
|
|
@ -186,18 +143,19 @@ class LicenseService {
|
|||
}
|
||||
|
||||
static LicenseStatus _getCachedStatus(SharedPreferences prefs) {
|
||||
final cached = prefs.getString(_prefCachedStatus);
|
||||
final cached = prefs.getString(_prefCachedStatus);
|
||||
final cachedAt = prefs.getString(_prefCachedAt);
|
||||
|
||||
if (cached == null) return LicenseStatus.free;
|
||||
|
||||
// オンライン時は checkStatus が常に上書きするため、
|
||||
// オンライン時は _validateKeyWithServer が常に上書きするため、
|
||||
// _getCachedStatus はオフライン時専用のフォールバックとして動作する。
|
||||
//
|
||||
// TTL 判定(_cacheValidSeconds = 24h):
|
||||
// - free / offline: 期限切れで free にフォールバック
|
||||
// - free / offline は期限切れで free にフォールバック
|
||||
// - pro : 購入者をオフライン時に締め出さないため永続扱い
|
||||
// - revoked: 不正防止を優先するため永続扱い
|
||||
// (将来 TTL を設けたい場合は isNoExpiryStatus を条件分岐ごと差し替える)
|
||||
if (cachedAt != null) {
|
||||
final age = DateTime.now().difference(DateTime.parse(cachedAt));
|
||||
final isNoExpiryStatus = cached == LicenseStatus.pro.name || cached == LicenseStatus.revoked.name;
|
||||
|
|
|
|||
52
pubspec.lock
52
pubspec.lock
|
|
@ -515,54 +515,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_speed_dial:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -849,10 +801,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.46+53
|
||||
version: 1.0.45+52
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.1
|
||||
|
|
@ -46,7 +46,7 @@ dependencies:
|
|||
device_info_plus: ^10.1.0
|
||||
http: ^1.2.0
|
||||
crypto: ^3.0.3 # SHA-256 ハッシュ計算用(キャッシュ)
|
||||
lucide_icons: 0.257.0
|
||||
lucide_icons: ^0.257.0
|
||||
reorderable_grid_view: ^2.2.5
|
||||
camera: ^0.11.3
|
||||
path_provider: ^2.1.5
|
||||
|
|
@ -61,7 +61,6 @@ dependencies:
|
|||
package_info_plus: ^8.1.2
|
||||
gal: ^2.3.0
|
||||
shared_preferences: ^2.5.4
|
||||
flutter_secure_storage: ^9.2.2
|
||||
|
||||
# Phase 9: Google Drive Backup
|
||||
googleapis: ^13.2.0
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <gal/gal_plugin_c_api.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
|
|
@ -19,8 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
GalPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
gal
|
||||
printing
|
||||
share_plus
|
||||
|
|
|
|||
Loading…
Reference in New Issue