fix: 外部コードレビュー指摘の修正4件

device_service.dart:
- iOS device_idをSharedPreferencesにUUID永続化(identifierForVendor廃止)
  → 全ベンダーアプリ削除後の再インストール後もライセンスが継続する
- fallback device_idもSharedPreferencesに永続化
  → アプリ再起動のたびにIDが変わるバグを修正

license_service.dart:
- revokedキャッシュが24h後にfreeに降格するバグを修正
  → proとrevokedはキャッシュ有効期限の対象外にする

sake_mbti_stamp_section.dart / sake_detail_screen.dart:
- isProVersion(コンパイル時) → isProProvider(実行時ライセンス)に移行
  → ライセンス購入後にアプリ再起動なしでMBTIスタンプが有効になる

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ponshu Developer 2026-04-11 08:14:37 +09:00
parent 7b3249791e
commit f229ff6b4b
4 changed files with 55 additions and 34 deletions

View File

@ -5,7 +5,7 @@ import '../../../models/sake_item.dart';
import '../../../providers/theme_provider.dart'; import '../../../providers/theme_provider.dart';
import '../../../services/mbti_compatibility_service.dart'; import '../../../services/mbti_compatibility_service.dart';
import '../../../theme/app_colors.dart'; import '../../../theme/app_colors.dart';
import '../../../main.dart'; // For isProVersion import '../../../providers/license_provider.dart';
/// MBTI酒向スタンプセクション /// MBTI酒向スタンプセクション
/// ///
@ -22,13 +22,14 @@ class SakeMbtiStampSection extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final appColors = Theme.of(context).extension<AppColors>()!; final appColors = Theme.of(context).extension<AppColors>()!;
final isPro = ref.watch(isProProvider);
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: isProVersion color: isPro
? appColors.brandPrimary.withValues(alpha: 0.3) ? appColors.brandPrimary.withValues(alpha: 0.3)
: appColors.divider, : appColors.divider,
style: BorderStyle.solid, style: BorderStyle.solid,
@ -37,7 +38,7 @@ class SakeMbtiStampSection extends ConsumerWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: Theme.of(context).cardColor.withValues(alpha: 0.5), color: Theme.of(context).cardColor.withValues(alpha: 0.5),
), ),
child: isProVersion child: isPro
? _buildProContent(context, ref, appColors) ? _buildProContent(context, ref, appColors)
: _buildLiteContent(context, appColors), : _buildLiteContent(context, appColors),
); );

View File

@ -18,7 +18,7 @@ import '../widgets/sake_detail/sake_detail_memo.dart';
import '../widgets/sake_detail/sake_detail_specs.dart'; import '../widgets/sake_detail/sake_detail_specs.dart';
import 'sake_detail/widgets/sake_photo_edit_modal.dart'; import 'sake_detail/widgets/sake_photo_edit_modal.dart';
import 'sake_detail/sections/sake_mbti_stamp_section.dart'; import 'sake_detail/sections/sake_mbti_stamp_section.dart';
import '../main.dart' show isProVersion; import '../providers/license_provider.dart';
import 'sake_detail/sections/sake_basic_info_section.dart'; import 'sake_detail/sections/sake_basic_info_section.dart';
import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart'; import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart';
import '../services/mbti_compatibility_service.dart'; import '../services/mbti_compatibility_service.dart';
@ -83,6 +83,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColors>()!; final appColors = Theme.of(context).extension<AppColors>()!;
final isPro = ref.watch(isProProvider);
// //
final allSakeAsync = ref.watch(allSakeItemsProvider); final allSakeAsync = ref.watch(allSakeItemsProvider);
@ -254,7 +255,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
], ],
// MBTI Diagnostic Stamp Section (Pro only) // MBTI Diagnostic Stamp Section (Pro only)
if (isProVersion) ...[ if (isPro) ...[
SakeMbtiStampSection(sake: _sake), SakeMbtiStampSection(sake: _sake),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],

View File

@ -2,54 +2,71 @@ import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
/// ID取得サービス /// ID取得サービス
/// 使 /// 使
///
/// ##
/// - Android: ANDROID_ID 使Factory Reset
/// - iOS: SharedPreferences UUID identifierForVendor
/// 使
/// - /: SharedPreferences UUID
class DeviceService { class DeviceService {
static final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin(); static final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
static String? _cachedDeviceId; static String? _cachedDeviceId;
static const _prefKey = 'ponshu_device_id';
/// IDを取得SHA256ハッシュ化 /// IDを取得SHA256ハッシュ化
static Future<String> getDeviceId() async { static Future<String> getDeviceId() async {
// if (_cachedDeviceId != null) return _cachedDeviceId!;
if (_cachedDeviceId != null) {
final prefs = await SharedPreferences.getInstance();
// SharedPreferencesに永続化済みならそれを使うIDを返す
final stored = prefs.getString(_prefKey);
if (stored != null && stored.isNotEmpty) {
_cachedDeviceId = stored;
debugPrint('[Device] Using persisted device ID: ${stored.substring(0, 8)}...');
return _cachedDeviceId!; return _cachedDeviceId!;
} }
// : IDから生成してSharedPreferencesに保存
final id = await _generateAndPersist(prefs);
_cachedDeviceId = id;
return id;
}
static Future<String> _generateAndPersist(SharedPreferences prefs) async {
String deviceIdentifier;
try { try {
String deviceIdentifier;
if (defaultTargetPlatform == TargetPlatform.android) { if (defaultTargetPlatform == TargetPlatform.android) {
final androidInfo = await _deviceInfo.androidInfo; final info = await _deviceInfo.androidInfo;
// Android IDを使用IDを維持 // ANDROID_ID: Factory Resetで変わる
deviceIdentifier = androidInfo.id; deviceIdentifier = 'android-${info.id}';
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
final iosInfo = await _deviceInfo.iosInfo;
// identifierForVendor
deviceIdentifier = iosInfo.identifierForVendor ?? 'unknown-ios';
} else { } else {
// // iOS / : UUIDを生成して永続化
deviceIdentifier = 'unknown-platform'; // identifierForVendor 使
deviceIdentifier = 'uuid-${const Uuid().v4()}';
} }
// SHA256ハッシュ化64
final bytes = utf8.encode(deviceIdentifier);
final digest = sha256.convert(bytes);
_cachedDeviceId = digest.toString();
debugPrint('Device ID (hashed): ${_cachedDeviceId!.substring(0, 8)}...');
return _cachedDeviceId!;
} catch (e) { } catch (e) {
debugPrint('Error getting device ID: $e'); debugPrint('[Device] Failed to get hardware ID, using UUID fallback: $e');
// IDを生成IDを使用 deviceIdentifier = 'fallback-${const Uuid().v4()}';
_cachedDeviceId = sha256.convert(utf8.encode('fallback-${DateTime.now().millisecondsSinceEpoch}')).toString();
return _cachedDeviceId!;
} }
final id = sha256.convert(utf8.encode(deviceIdentifier)).toString();
await prefs.setString(_prefKey, id);
debugPrint('[Device] Generated and persisted device ID: ${id.substring(0, 8)}...');
return id;
} }
/// ///
static void reset() { static Future<void> reset() async {
_cachedDeviceId = null; _cachedDeviceId = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_prefKey);
} }
} }

View File

@ -149,9 +149,11 @@ class LicenseService {
if (cached == null) return LicenseStatus.free; if (cached == null) return LicenseStatus.free;
// freeにフォールバック // freeにフォールバック
// pro revoked proは購入者を締め出さないrevokedは誤って復活させない
if (cachedAt != null) { if (cachedAt != null) {
final age = DateTime.now().difference(DateTime.parse(cachedAt)); final age = DateTime.now().difference(DateTime.parse(cachedAt));
if (age.inSeconds > _cacheValidSeconds && cached != LicenseStatus.pro.name) { final isPermanentStatus = cached == LicenseStatus.pro.name || cached == LicenseStatus.revoked.name;
if (age.inSeconds > _cacheValidSeconds && !isPermanentStatus) {
return LicenseStatus.free; return LicenseStatus.free;
} }
} }