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 '../../../services/mbti_compatibility_service.dart';
import '../../../theme/app_colors.dart';
import '../../../main.dart'; // For isProVersion
import '../../../providers/license_provider.dart';
/// MBTI酒向スタンプセクション
///
@ -22,13 +22,14 @@ class SakeMbtiStampSection extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = Theme.of(context).extension<AppColors>()!;
final isPro = ref.watch(isProProvider);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
border: Border.all(
color: isProVersion
color: isPro
? appColors.brandPrimary.withValues(alpha: 0.3)
: appColors.divider,
style: BorderStyle.solid,
@ -37,7 +38,7 @@ class SakeMbtiStampSection extends ConsumerWidget {
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).cardColor.withValues(alpha: 0.5),
),
child: isProVersion
child: isPro
? _buildProContent(context, ref, 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 'sake_detail/widgets/sake_photo_edit_modal.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/widgets/sake_detail_sliver_app_bar.dart';
import '../services/mbti_compatibility_service.dart';
@ -83,6 +83,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
@override
Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColors>()!;
final isPro = ref.watch(isProProvider);
//
final allSakeAsync = ref.watch(allSakeItemsProvider);
@ -254,7 +255,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
],
// MBTI Diagnostic Stamp Section (Pro only)
if (isProVersion) ...[
if (isPro) ...[
SakeMbtiStampSection(sake: _sake),
const SizedBox(height: 24),
],

View File

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

View File

@ -149,9 +149,11 @@ class LicenseService {
if (cached == null) return LicenseStatus.free;
// freeにフォールバック
// pro revoked proは購入者を締め出さないrevokedは誤って復活させない
if (cachedAt != null) {
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;
}
}