Compare commits

..

2 Commits

Author SHA1 Message Date
Ponshu Developer 191274c65a chore: bump version to 1.0.46+53 (secure_storage + flicker fix) 2026-04-23 12:07:14 +09:00
Ponshu Developer ab18b544c2 security: ライセンスキーを flutter_secure_storage へ移行、Pro UI ちらつき修正
- SharedPreferences のライセンスキーを FlutterSecureStorage(Android 暗号化)に移行
- 既存ユーザー向け一回限りのマイグレーション処理を追加(ponshu_license_migrated_v1 フラグ)
- LicenseService.getCachedStatusOnly() を追加(ネットワーク不要の即時キャッシュ返却)
- licenseStatusProvider を FutureProvider から AsyncNotifier に変換
  - main() でキャッシュを事前ロードし licenseInitialStatusProvider に渡すことで
    起動時の loading → false → pro のちらつきを根本解消
  - バックグラウンドでサーバー検証を実行し、差異があれば状態を更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 10:23:18 +09:00
7 changed files with 175 additions and 34 deletions

View File

@ -9,6 +9,8 @@ import 'providers/theme_provider.dart';
import 'screens/main_screen.dart'; import 'screens/main_screen.dart';
import 'screens/license_screen.dart'; import 'screens/license_screen.dart';
import 'services/migration_service.dart'; import 'services/migration_service.dart';
import 'services/license_service.dart';
import 'providers/license_provider.dart';
/// ///
/// ///
@ -74,9 +76,16 @@ void main() async {
} }
} }
// : runApp
// licenseStatusProvider loading AsyncData override
final cachedLicenseStatus = await LicenseService.getCachedStatusOnly();
runApp( runApp(
const ProviderScope( ProviderScope(
child: MyApp(), overrides: [
licenseInitialStatusProvider.overrideWithValue(cachedLicenseStatus),
],
child: const MyApp(),
), ),
); );
} }

View File

@ -1,15 +1,52 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/license_service.dart'; import '../services/license_service.dart';
/// ///
/// ///
/// VPSに問い合わせ /// main() override
/// [licenseStatusProvider].invalidate() /// licenseStatusProvider loading AsyncData
final licenseStatusProvider = FutureProvider<LicenseStatus>((ref) async { final licenseInitialStatusProvider = Provider<LicenseStatus?>((ref) => null);
return LicenseService.checkStatus();
}); ///
///
/// - 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 (_) {
//
}
}
}
/// Pro版かどうか使 /// Pro版かどうか使
///
/// licenseStatusProvider AsyncData
/// false
final isProProvider = Provider<bool>((ref) { final isProProvider = Provider<bool>((ref) {
final statusAsync = ref.watch(licenseStatusProvider); final statusAsync = ref.watch(licenseStatusProvider);
return statusAsync.maybeWhen( return statusAsync.maybeWhen(

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'device_service.dart'; import 'device_service.dart';
@ -22,43 +23,81 @@ enum LicenseStatus {
/// ///
/// ///
/// ##
/// - : flutter_secure_storage
/// - : SharedPreferences
///
/// ## /// ##
/// 1. : VPSに問い合わせた結果を使用 + SharedPreferencesにキャッシュ /// 1. : SecureStorage
/// 2. : SharedPreferencesのキャッシュを使用 (Pro状態を維持) /// 2. : VPS
/// 3. : SharedPreferences
/// ///
/// ## /// ##
/// PONSHU-XXXX-XXXX-XXXX (6 = 12hex文字, ) /// PONSHU-XXXX-XXXX-XXXX (6 = 12hex文字, )
class LicenseService { class LicenseService {
static const _prefLicenseKey = 'ponshu_license_key'; static const _secureKeyName = 'ponshu_license_key';
static const _prefCachedStatus = 'ponshu_license_status_cache'; static const _prefCachedStatus = 'ponshu_license_status_cache';
static const _prefCachedAt = 'ponshu_license_cached_at'; static const _prefCachedAt = 'ponshu_license_cached_at';
static const _cacheValidSeconds = 24 * 60 * 60; // 24 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);
}
// ========== Public API ========== // ========== Public API ==========
/// : ///
static Future<LicenseStatus> checkStatus() async { ///
final prefs = await SharedPreferences.getInstance(); /// main() await runApp()
final savedKey = prefs.getString(_prefLicenseKey) ?? ''; 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) ?? '';
//
if (savedKey.isNotEmpty) { if (savedKey.isNotEmpty) {
try { try {
final status = await _validateKeyWithServer(savedKey); final status = await _validateKeyWithServer(savedKey);
if (status == LicenseStatus.offline) { if (status == LicenseStatus.offline) {
// :
debugPrint('[License] Server unreachable, using cache'); debugPrint('[License] Server unreachable, using cache');
final prefs = await SharedPreferences.getInstance();
return _getCachedStatus(prefs); return _getCachedStatus(prefs);
} }
final prefs = await SharedPreferences.getInstance();
await _cacheStatus(prefs, status); await _cacheStatus(prefs, status);
return status; return status;
} catch (e) { } catch (e) {
debugPrint('[License] Server unreachable, using cache: $e'); debugPrint('[License] Server unreachable, using cache: $e');
final prefs = await SharedPreferences.getInstance();
return _getCachedStatus(prefs); return _getCachedStatus(prefs);
} }
} }
//
return LicenseStatus.free; return LicenseStatus.free;
} }
@ -76,8 +115,8 @@ class LicenseService {
final status = await _validateKeyWithServer(key); final status = await _validateKeyWithServer(key);
if (status == LicenseStatus.pro) { if (status == LicenseStatus.pro) {
await _storage.write(key: _secureKeyName, value: key);
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefLicenseKey, key);
await _cacheStatus(prefs, LicenseStatus.pro); await _cacheStatus(prefs, LicenseStatus.pro);
debugPrint('[License] Activated successfully.'); debugPrint('[License] Activated successfully.');
return (success: true, message: ''); return (success: true, message: '');
@ -96,14 +135,14 @@ class LicenseService {
/// ///
static Future<bool> hasLicenseKey() async { static Future<bool> hasLicenseKey() async {
final prefs = await SharedPreferences.getInstance(); await _migrateIfNeeded();
return (prefs.getString(_prefLicenseKey) ?? '').isNotEmpty; return ((await _storage.read(key: _secureKeyName)) ?? '').isNotEmpty;
} }
/// ///
static Future<void> reset() async { static Future<void> reset() async {
await _storage.delete(key: _secureKeyName);
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove(_prefLicenseKey);
await prefs.remove(_prefCachedStatus); await prefs.remove(_prefCachedStatus);
await prefs.remove(_prefCachedAt); await prefs.remove(_prefCachedAt);
debugPrint('[License] Reset complete.'); debugPrint('[License] Reset complete.');
@ -128,7 +167,11 @@ class LicenseService {
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>; final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
if (data['valid'] == true) return LicenseStatus.pro; if (data['valid'] == true) return LicenseStatus.pro;
if ((data['error'] as String? ?? '').contains('無効化')) return LicenseStatus.revoked; // revoked booleanerror
if (data['revoked'] == true ||
(data['error'] as String? ?? '').contains('無効化')) {
return LicenseStatus.revoked;
}
return LicenseStatus.free; return LicenseStatus.free;
} catch (e) { } catch (e) {
@ -143,19 +186,18 @@ class LicenseService {
} }
static LicenseStatus _getCachedStatus(SharedPreferences prefs) { static LicenseStatus _getCachedStatus(SharedPreferences prefs) {
final cached = prefs.getString(_prefCachedStatus); final cached = prefs.getString(_prefCachedStatus);
final cachedAt = prefs.getString(_prefCachedAt); final cachedAt = prefs.getString(_prefCachedAt);
if (cached == null) return LicenseStatus.free; if (cached == null) return LicenseStatus.free;
// _validateKeyWithServer // checkStatus
// _getCachedStatus // _getCachedStatus
// //
// TTL _cacheValidSeconds = 24h: // TTL _cacheValidSeconds = 24h:
// - free / offline free // - free / offline: free
// - pro : // - pro :
// - revoked: // - revoked:
// TTL isNoExpiryStatus
if (cachedAt != null) { if (cachedAt != null) {
final age = DateTime.now().difference(DateTime.parse(cachedAt)); final age = DateTime.now().difference(DateTime.parse(cachedAt));
final isNoExpiryStatus = cached == LicenseStatus.pro.name || cached == LicenseStatus.revoked.name; final isNoExpiryStatus = cached == LicenseStatus.pro.name || cached == LicenseStatus.revoked.name;

View File

@ -515,6 +515,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" 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: flutter_speed_dial:
dependency: "direct main" dependency: "direct main"
description: description:
@ -801,10 +849,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.6.7"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:

View File

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.45+52 version: 1.0.46+53
environment: environment:
sdk: ^3.10.1 sdk: ^3.10.1
@ -46,7 +46,7 @@ dependencies:
device_info_plus: ^10.1.0 device_info_plus: ^10.1.0
http: ^1.2.0 http: ^1.2.0
crypto: ^3.0.3 # SHA-256 ハッシュ計算用(キャッシュ) crypto: ^3.0.3 # SHA-256 ハッシュ計算用(キャッシュ)
lucide_icons: ^0.257.0 lucide_icons: 0.257.0
reorderable_grid_view: ^2.2.5 reorderable_grid_view: ^2.2.5
camera: ^0.11.3 camera: ^0.11.3
path_provider: ^2.1.5 path_provider: ^2.1.5
@ -61,6 +61,7 @@ dependencies:
package_info_plus: ^8.1.2 package_info_plus: ^8.1.2
gal: ^2.3.0 gal: ^2.3.0
shared_preferences: ^2.5.4 shared_preferences: ^2.5.4
flutter_secure_storage: ^9.2.2
# Phase 9: Google Drive Backup # Phase 9: Google Drive Backup
googleapis: ^13.2.0 googleapis: ^13.2.0

View File

@ -8,6 +8,7 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.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 <gal/gal_plugin_c_api.h>
#include <printing/printing_plugin.h> #include <printing/printing_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
GalPluginCApiRegisterWithRegistrar( GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi")); registry->GetRegistrarForPlugin("GalPluginCApi"));
PrintingPluginRegisterWithRegistrar( PrintingPluginRegisterWithRegistrar(

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
file_selector_windows file_selector_windows
flutter_secure_storage_windows
gal gal
printing printing
share_plus share_plus