95 lines
2.8 KiB
Dart
95 lines
2.8 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'entitlement_state.dart';
|
|
|
|
/// Contract defining persistence operations for cached entitlement state.
|
|
/// This abstraction isolates the billing coordinator and domain from physical storage.
|
|
abstract class EntitlementStorage {
|
|
/// Reads cached entitlement state, returning [EntitlementState.free] if none exists or invalid.
|
|
Future<EntitlementState> read();
|
|
|
|
/// Persists [state] to cache.
|
|
Future<void> write(EntitlementState state);
|
|
|
|
/// Clears cached entitlement, reverting cache to default.
|
|
Future<void> clear();
|
|
}
|
|
|
|
/// In-memory implementation of [EntitlementStorage] for unit testing.
|
|
class InMemoryEntitlementStorage implements EntitlementStorage {
|
|
EntitlementState _state;
|
|
|
|
InMemoryEntitlementStorage([this._state = const EntitlementState.free()]);
|
|
|
|
@override
|
|
Future<EntitlementState> read() async => _state;
|
|
|
|
@override
|
|
Future<void> write(EntitlementState state) async {
|
|
_state = state;
|
|
}
|
|
|
|
@override
|
|
Future<void> clear() async {
|
|
_state = const EntitlementState.free();
|
|
}
|
|
}
|
|
|
|
/// Production implementation backed by [FlutterSecureStorage].
|
|
/// Stores normalized JSON with versioning. Never stores raw Google Play purchase tokens.
|
|
class SecureEntitlementStorage implements EntitlementStorage {
|
|
static const String storageKey = 'motorsignal_entitlement_v1';
|
|
|
|
final FlutterSecureStorage _storage;
|
|
|
|
const SecureEntitlementStorage({FlutterSecureStorage? storage})
|
|
: _storage = storage ?? const FlutterSecureStorage();
|
|
|
|
@override
|
|
Future<EntitlementState> read() async {
|
|
try {
|
|
final raw = await _storage.read(key: storageKey);
|
|
if (raw == null || raw.trim().isEmpty) {
|
|
return const EntitlementState.free();
|
|
}
|
|
|
|
final decoded = jsonDecode(raw);
|
|
if (decoded is! Map<String, dynamic>) {
|
|
debugPrint('[EntitlementStorage] Cache payload not a valid JSON map');
|
|
return const EntitlementState.free();
|
|
}
|
|
|
|
return EntitlementState.fromJson(decoded);
|
|
} catch (e, st) {
|
|
debugPrint(
|
|
'[EntitlementStorage] Error reading cached entitlement: $e\n$st',
|
|
);
|
|
return const EntitlementState.free();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> write(EntitlementState state) async {
|
|
try {
|
|
final payload = jsonEncode(state.toJson());
|
|
await _storage.write(key: storageKey, value: payload);
|
|
} catch (e, st) {
|
|
debugPrint(
|
|
'[EntitlementStorage] Error writing cached entitlement: $e\n$st',
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> clear() async {
|
|
try {
|
|
await _storage.delete(key: storageKey);
|
|
} catch (e, st) {
|
|
debugPrint(
|
|
'[EntitlementStorage] Error clearing cached entitlement: $e\n$st',
|
|
);
|
|
}
|
|
}
|
|
}
|