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:
parent
7b3249791e
commit
f229ff6b4b
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue