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:
2026-08-28 23:12:25 +02:00
parent d4df3822b7
commit b12a04ba30
10 changed files with 1096 additions and 26 deletions

View File

@ -1,3 +1,4 @@
import '../infrastructure/health_connect/health_connect.dart';
import '../infrastructure/local/local.dart';
import '../infrastructure/remote/remote.dart';
import '../infrastructure/security/security.dart';
@ -14,7 +15,10 @@ abstract interface class AppDependencies {
ActiveWorkoutSessionUseCases get activeWorkoutSessionUseCases;
ActiveExerciseStepUseCases get activeExerciseStepUseCases;
ActiveWorkoutSensorUseCases get activeWorkoutSensorUseCases;
ExecutionDebugUseCase get executionDebugUseCase;
SeedQaFunctionalContentUseCase get seedQaFunctionalContentUseCase;
WorkoutTelemetryUseCases get workoutTelemetryUseCases;
WatchCompanionProjectionUseCases get watchCompanionProjectionUseCases;
CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase;
WorkoutHistoryUseCases get workoutHistoryUseCases;
ProgressionStatsUseCase get progressionStatsUseCase;
@ -22,6 +26,8 @@ abstract interface class AppDependencies {
WatchAlertPublisher get watchAlertPublisher;
DataExportUseCase get dataExportUseCase;
DataImportUseCase get dataImportUseCase;
HealthConnectUseCases get healthConnectUseCases;
EntitlementRevalidationUseCase get entitlementRevalidationUseCase;
SyncUseCases get syncUseCases;
ShareUseCases get shareUseCases;
}
@ -37,6 +43,8 @@ final class AppBootstrap implements AppDependencies {
required this.activeWorkoutSessionUseCases,
required this.activeExerciseStepUseCases,
required this.activeWorkoutSensorUseCases,
required this.executionDebugUseCase,
required this.seedQaFunctionalContentUseCase,
required this.workoutTelemetryUseCases,
required this.watchCompanionProjectionUseCases,
required this.watchCompanionCommandHandler,
@ -48,6 +56,8 @@ final class AppBootstrap implements AppDependencies {
required this.exercisePerformanceReferenceUseCase,
required this.dataExportUseCase,
required this.dataImportUseCase,
required this.healthConnectUseCases,
required this.entitlementRevalidationUseCase,
required this.syncUseCases,
required this.shareUseCases,
required this.syncGateway,
@ -71,7 +81,12 @@ final class AppBootstrap implements AppDependencies {
@override
final ActiveWorkoutSensorUseCases activeWorkoutSensorUseCases;
@override
final ExecutionDebugUseCase executionDebugUseCase;
@override
final SeedQaFunctionalContentUseCase seedQaFunctionalContentUseCase;
@override
final WorkoutTelemetryUseCases workoutTelemetryUseCases;
@override
final WatchCompanionProjectionUseCases watchCompanionProjectionUseCases;
final WatchCompanionCommandHandler watchCompanionCommandHandler;
final WatchWearDataLayerAdapter watchWearDataLayerAdapter;
@ -91,6 +106,10 @@ final class AppBootstrap implements AppDependencies {
@override
final DataImportUseCase dataImportUseCase;
@override
final HealthConnectUseCases healthConnectUseCases;
@override
final EntitlementRevalidationUseCase entitlementRevalidationUseCase;
@override
final SyncUseCases syncUseCases;
@override
final ShareUseCases shareUseCases;
@ -110,6 +129,9 @@ final class AppBootstrap implements AppDependencies {
final pendingShareActionRepository = DriftPendingShareActionRepository(
database,
);
final entitlementSnapshotRepository = DriftEntitlementSnapshotRepository(
database,
);
final programRepository = DriftProgramRepository(database);
final templateRepository = DriftWorkoutTemplateRepository(database);
final activeSessionRepository = DriftActiveSessionRepository(database);
@ -124,6 +146,7 @@ final class AppBootstrap implements AppDependencies {
const localBackupMediaStore = PathProviderLocalMediaStorage();
final ids = LocalIdGenerator();
const clock = SystemClock();
const authTokenStore = SecureStorageAuthTokenStore();
const originDeviceId = 'local-device';
final activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases(
sessionRepository: activeSessionRepository,
@ -142,6 +165,16 @@ final class AppBootstrap implements AppDependencies {
final activeWorkoutSensorUseCases = ActiveWorkoutSensorUseCases(
clock: clock,
);
final executionDebugUseCase = ExecutionDebugUseCase(
sessionRepository: activeSessionRepository,
clock: clock,
);
final seedQaFunctionalContentUseCase = SeedQaFunctionalContentUseCase(
seedStateRepository: starterSeedRepository,
contentRepository: starterSeedRepository,
clock: clock,
originDeviceId: originDeviceId,
);
final workoutTelemetryUseCases = WorkoutTelemetryUseCases(
repository: telemetryRepository,
sessionRepository: activeSessionRepository,
@ -153,6 +186,7 @@ final class AppBootstrap implements AppDependencies {
clock: clock,
ids: ids,
originDeviceId: originDeviceId,
activeWorkoutSensorUseCases: activeWorkoutSensorUseCases,
);
final watchCompanionCommandHandler = WatchCompanionCommandHandler(
sessionRepository: activeSessionRepository,
@ -164,6 +198,11 @@ final class AppBootstrap implements AppDependencies {
repository: historyRepository,
clock: clock,
);
final healthConnectUseCases = HealthConnectUseCases(
gateway: const MethodChannelHealthConnectGateway(),
historyRepository: historyRepository,
telemetryUseCases: workoutTelemetryUseCases,
);
final watchWearDataLayerAdapter = WatchWearDataLayerAdapter(
nativeChannel: const MethodChannelWatchBridgeNativeChannel(),
commandIngress: watchCompanionCommandHandler,
@ -193,11 +232,15 @@ final class AppBootstrap implements AppDependencies {
final remoteShareApi = HttpRemoteShareApi(
HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)),
);
final remoteEntitlementSnapshotSource = HttpRemoteEntitlementSnapshotSource(
client: HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)),
tokenStore: authTokenStore,
);
return AppBootstrap._(
database: database,
authUseCases: AuthUseCases(
tokenStore: const SecureStorageAuthTokenStore(),
tokenStore: authTokenStore,
accountRepository: onlineAccountRepository,
remoteAuthApi: remoteAuthApi,
clock: clock,
@ -236,6 +279,8 @@ final class AppBootstrap implements AppDependencies {
activeWorkoutSessionUseCases: activeWorkoutSessionUseCases,
activeExerciseStepUseCases: activeExerciseStepUseCases,
activeWorkoutSensorUseCases: activeWorkoutSensorUseCases,
executionDebugUseCase: executionDebugUseCase,
seedQaFunctionalContentUseCase: seedQaFunctionalContentUseCase,
workoutTelemetryUseCases: workoutTelemetryUseCases,
watchCompanionProjectionUseCases: watchCompanionProjectionUseCases,
watchCompanionCommandHandler: watchCompanionCommandHandler,
@ -245,6 +290,8 @@ final class AppBootstrap implements AppDependencies {
sessionRepository: activeSessionRepository,
historyRepository: historyRepository,
telemetryRepository: telemetryRepository,
healthConnectUseCases: healthConnectUseCases,
activeWorkoutSensorUseCases: activeWorkoutSensorUseCases,
clock: clock,
ids: ids,
originDeviceId: originDeviceId,
@ -267,8 +314,13 @@ final class AppBootstrap implements AppDependencies {
mediaStore: localBackupMediaStore,
clock: clock,
),
healthConnectUseCases: healthConnectUseCases,
entitlementRevalidationUseCase: EntitlementRevalidationUseCase(
repository: entitlementSnapshotRepository,
remote: remoteEntitlementSnapshotSource,
),
syncUseCases: SyncUseCases(
tokenStore: const SecureStorageAuthTokenStore(),
tokenStore: authTokenStore,
remoteSyncApi: remoteSyncApi,
metadataRepository: syncMetadataRepository,
mappingRepository: mappingRepository,
@ -277,7 +329,7 @@ final class AppBootstrap implements AppDependencies {
deviceId: originDeviceId,
),
shareUseCases: ShareUseCases(
tokenStore: const SecureStorageAuthTokenStore(),
tokenStore: authTokenStore,
remoteShareApi: remoteShareApi,
inboxRepository: shareInboxRepository,
pendingActionRepository: pendingShareActionRepository,