feat(monetization-261): prepare entitlement infrastructure and quota enforcement for release
- Add EntitlementSnapshot, EntitlementRevalidationUseCase, and BillingUseCases - Add DriftEntitlementSnapshotRepository for offline entitlement caching - Add HealthConnect gateway and integration - Add quota enforcement in share acceptance (server and UI) - Add entitlement API endpoints in server - Add privacy policy v1 and release compliance checklist - Add QA gates and device runbooks for release validation - Add demo content screen and settings screen Ticket #261
This commit is contained in:
@ -56,6 +56,98 @@ final class LocalBackupException implements Exception {
|
||||
String toString() => error.name;
|
||||
}
|
||||
|
||||
enum EntitlementTier { free, pro }
|
||||
|
||||
final class EntitlementSnapshot {
|
||||
const EntitlementSnapshot._({
|
||||
required this.entitlement,
|
||||
required this.canSync,
|
||||
required this.hasUnlimitedEditableLibrary,
|
||||
required this.remainingEditableSlots,
|
||||
required this.validatedAt,
|
||||
});
|
||||
|
||||
factory EntitlementSnapshot.free({
|
||||
required int remainingEditableSlots,
|
||||
required DateTime validatedAt,
|
||||
}) {
|
||||
return EntitlementSnapshot._(
|
||||
entitlement: EntitlementTier.free,
|
||||
canSync: false,
|
||||
hasUnlimitedEditableLibrary: false,
|
||||
remainingEditableSlots: remainingEditableSlots,
|
||||
validatedAt: validatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
factory EntitlementSnapshot.pro({required DateTime validatedAt}) {
|
||||
return EntitlementSnapshot._(
|
||||
entitlement: EntitlementTier.pro,
|
||||
canSync: true,
|
||||
hasUnlimitedEditableLibrary: true,
|
||||
remainingEditableSlots: null,
|
||||
validatedAt: validatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
final EntitlementTier entitlement;
|
||||
final bool canSync;
|
||||
final bool hasUnlimitedEditableLibrary;
|
||||
final int? remainingEditableSlots;
|
||||
final DateTime validatedAt;
|
||||
}
|
||||
|
||||
abstract interface class EntitlementSnapshotRepository {
|
||||
Future<EntitlementSnapshot?> read();
|
||||
Future<void> save(EntitlementSnapshot snapshot);
|
||||
}
|
||||
|
||||
abstract interface class RemoteEntitlementSnapshotSource {
|
||||
Future<EntitlementSnapshot> read();
|
||||
}
|
||||
|
||||
final class OfflineException implements Exception {
|
||||
const OfflineException();
|
||||
}
|
||||
|
||||
enum BillingAvailability { available, unavailable }
|
||||
|
||||
final class BillingProductRef {
|
||||
const BillingProductRef({required this.id});
|
||||
|
||||
final String id;
|
||||
}
|
||||
|
||||
enum BillingAttemptStatus { pendingVerification }
|
||||
|
||||
final class BillingAttemptResult {
|
||||
const BillingAttemptResult._({required this.status, required this.product});
|
||||
|
||||
factory BillingAttemptResult.pendingVerification({
|
||||
required BillingProductRef product,
|
||||
}) {
|
||||
return BillingAttemptResult._(
|
||||
status: BillingAttemptStatus.pendingVerification,
|
||||
product: product,
|
||||
);
|
||||
}
|
||||
|
||||
final BillingAttemptStatus status;
|
||||
final BillingProductRef product;
|
||||
}
|
||||
|
||||
abstract interface class BillingPort {
|
||||
Future<BillingAvailability> availability();
|
||||
|
||||
Future<BillingAttemptResult> purchasePro({
|
||||
required BillingProductRef product,
|
||||
});
|
||||
|
||||
Future<BillingAttemptResult> restorePurchases();
|
||||
|
||||
Future<BillingAttemptResult> revalidate();
|
||||
}
|
||||
|
||||
final class LocalBackupDocument {
|
||||
const LocalBackupDocument({required this.fileName, required this.bytes});
|
||||
|
||||
@ -245,6 +337,136 @@ abstract interface class LocalBackupMediaStore {
|
||||
);
|
||||
}
|
||||
|
||||
enum HealthConnectAvailability {
|
||||
available,
|
||||
providerUpdateRequired,
|
||||
accessBlocked,
|
||||
unavailable,
|
||||
}
|
||||
|
||||
enum HealthConnectConnectionStatus {
|
||||
unavailable,
|
||||
providerUpdateRequired,
|
||||
accessBlocked,
|
||||
permissionsRequired,
|
||||
connected,
|
||||
}
|
||||
|
||||
enum HealthConnectPermissionRequestOutcome {
|
||||
none,
|
||||
granted,
|
||||
denied,
|
||||
settingsFallback,
|
||||
launchFailed,
|
||||
}
|
||||
|
||||
enum HealthConnectExportStatus {
|
||||
exported,
|
||||
unavailable,
|
||||
providerUpdateRequired,
|
||||
accessBlocked,
|
||||
permissionsRequired,
|
||||
invalidWorkout,
|
||||
failed,
|
||||
}
|
||||
|
||||
final class HealthConnectConnectionState {
|
||||
const HealthConnectConnectionState({
|
||||
required this.availability,
|
||||
required this.requiredPermissions,
|
||||
required this.grantedPermissions,
|
||||
this.permissionRequestOutcome = HealthConnectPermissionRequestOutcome.none,
|
||||
});
|
||||
|
||||
final HealthConnectAvailability availability;
|
||||
final Set<String> requiredPermissions;
|
||||
final Set<String> grantedPermissions;
|
||||
final HealthConnectPermissionRequestOutcome permissionRequestOutcome;
|
||||
|
||||
bool get hasRequiredPermissions =>
|
||||
grantedPermissions.containsAll(requiredPermissions);
|
||||
|
||||
HealthConnectConnectionStatus get status {
|
||||
switch (availability) {
|
||||
case HealthConnectAvailability.unavailable:
|
||||
return HealthConnectConnectionStatus.unavailable;
|
||||
case HealthConnectAvailability.providerUpdateRequired:
|
||||
return HealthConnectConnectionStatus.providerUpdateRequired;
|
||||
case HealthConnectAvailability.accessBlocked:
|
||||
return HealthConnectConnectionStatus.accessBlocked;
|
||||
case HealthConnectAvailability.available:
|
||||
return hasRequiredPermissions
|
||||
? HealthConnectConnectionStatus.connected
|
||||
: HealthConnectConnectionStatus.permissionsRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class HealthConnectHeartRateSample {
|
||||
const HealthConnectHeartRateSample({
|
||||
required this.time,
|
||||
required this.beatsPerMinute,
|
||||
});
|
||||
|
||||
final DateTime time;
|
||||
final int beatsPerMinute;
|
||||
}
|
||||
|
||||
final class HealthConnectWorkoutExport {
|
||||
const HealthConnectWorkoutExport({
|
||||
required this.historyId,
|
||||
required this.title,
|
||||
required this.startedAt,
|
||||
required this.endedAt,
|
||||
required this.durationMs,
|
||||
this.totalDistanceMeters,
|
||||
this.totalCaloriesKcal,
|
||||
this.minHeartRateBpm,
|
||||
this.averageHeartRateBpm,
|
||||
this.maxHeartRateBpm,
|
||||
this.heartRateSamples = const [],
|
||||
});
|
||||
|
||||
final String historyId;
|
||||
final String title;
|
||||
final DateTime startedAt;
|
||||
final DateTime endedAt;
|
||||
final int durationMs;
|
||||
final double? totalDistanceMeters;
|
||||
final double? totalCaloriesKcal;
|
||||
final int? minHeartRateBpm;
|
||||
final double? averageHeartRateBpm;
|
||||
final int? maxHeartRateBpm;
|
||||
final List<HealthConnectHeartRateSample> heartRateSamples;
|
||||
|
||||
bool get isValid =>
|
||||
historyId.trim().isNotEmpty &&
|
||||
title.trim().isNotEmpty &&
|
||||
durationMs > 0 &&
|
||||
endedAt.isAfter(startedAt);
|
||||
}
|
||||
|
||||
final class HealthConnectWorkoutExportResult {
|
||||
const HealthConnectWorkoutExportResult({
|
||||
required this.status,
|
||||
required this.workout,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final HealthConnectExportStatus status;
|
||||
final HealthConnectWorkoutExport? workout;
|
||||
final String? errorMessage;
|
||||
|
||||
bool get exported => status == HealthConnectExportStatus.exported;
|
||||
}
|
||||
|
||||
abstract interface class HealthConnectGateway {
|
||||
Future<HealthConnectConnectionState> connectionState();
|
||||
Future<HealthConnectConnectionState> requestPermissions();
|
||||
Future<bool> openSettings();
|
||||
Future<void> exportWorkout(HealthConnectWorkoutExport workout);
|
||||
}
|
||||
|
||||
enum ProgressionPeriod { fourWeeks, threeMonths, all }
|
||||
|
||||
enum ProgressionMeasure { manualScore, stopwatchScore, reps, time }
|
||||
|
||||
Reference in New Issue
Block a user