feat(online): partage sortant et boîte de réception (ticket #69)

Ajoute l'écran de partage sortant (presentation/share_screen.dart) et
la boîte de réception des partages (presentation/share_inbox_screen.dart),
avec les points d'entrée sur profile_screen.dart, home_screen.dart,
program_screen.dart et workout_template_screen.dart. flutter analyze
propre (mêmes infos préexistantes), 132/132 tests verts, build APK
debug validé. Dernier ticket de développement du chantier "Ajouter
les features serveur au client" (#63) — il ne reste que la QA finale
(#70).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 07:23:14 +02:00
parent eee4348b52
commit d16a017743
11 changed files with 1598 additions and 22 deletions

View File

@ -15,6 +15,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -37,6 +38,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -69,6 +71,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -111,6 +114,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -138,6 +142,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -164,6 +169,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -190,6 +196,7 @@ void main() {
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
@ -204,6 +211,26 @@ void main() {
expect(find.textContaining('Dernière synchro :'), findsOneWidget);
});
testWidgets('partages reçus ouvre la boîte de réception', (tester) async {
final harness = _AuthHarness.connected();
await tester.pumpWidget(
MaterialApp(
home: ProfileScreen(
authUseCases: harness.useCases,
syncUseCases: harness.syncUseCases,
shareUseCases: harness.shareUseCases,
),
),
);
await tester.pump();
await tester.tap(find.text('Partages reçus'));
await tester.pumpAndSettle();
expect(find.text('Aucun partage reçu'), findsOneWidget);
});
}
final class _AuthHarness {
@ -214,7 +241,12 @@ final class _AuthHarness {
syncRemote = _FakeRemoteSyncApi(),
syncMetadata = _FakeSyncMetadataRepository(),
syncMappings = _FakeRemoteResourceMappingRepository(),
syncChanges = _FakeLocalSyncChangeRepository() {
syncChanges = _FakeLocalSyncChangeRepository(),
shareRemote = _FakeRemoteShareApi(),
shareInbox = _FakeShareInboxRepository(),
pendingShares = _FakePendingShareActionRepository(),
programRepository = _FakeProgramRepository(),
templateRepository = _FakeWorkoutTemplateRepository() {
useCases = AuthUseCases(
tokenStore: tokenStore,
accountRepository: accountRepository,
@ -231,6 +263,17 @@ final class _AuthHarness {
clock: const _FakeClock(),
deviceId: 'device-1',
);
shareUseCases = ShareUseCases(
tokenStore: tokenStore,
remoteShareApi: shareRemote,
inboxRepository: shareInbox,
pendingActionRepository: pendingShares,
localChanges: syncChanges,
programRepository: programRepository,
templateRepository: templateRepository,
clock: const _FakeClock(),
ids: _FakeIds(),
);
}
factory _AuthHarness.connected() {
@ -255,8 +298,14 @@ final class _AuthHarness {
final _FakeSyncMetadataRepository syncMetadata;
final _FakeRemoteResourceMappingRepository syncMappings;
final _FakeLocalSyncChangeRepository syncChanges;
final _FakeRemoteShareApi shareRemote;
final _FakeShareInboxRepository shareInbox;
final _FakePendingShareActionRepository pendingShares;
final _FakeProgramRepository programRepository;
final _FakeWorkoutTemplateRepository templateRepository;
late final AuthUseCases useCases;
late final SyncUseCases syncUseCases;
late final ShareUseCases shareUseCases;
}
final class _FakeClock implements Clock {
@ -429,3 +478,144 @@ final class _FakeLocalSyncChangeRepository
DateTime syncedAt,
) async {}
}
final class _FakeRemoteShareApi implements RemoteShareApi {
@override
Future<RemoteSyncedItem> acceptShare(String shareId, String token) async {
return RemoteSyncedItem(
resourceType: SyncResourceType.program,
clientId: 'program-remote',
serverId: 'program-server',
schemaVersion: 1,
clientUpdatedAt: DateTime.utc(2026, 7, 17),
serverUpdatedAt: DateTime.utc(2026, 7, 17),
deletedAt: null,
payload: const {'name': 'Programme partagé'},
);
}
@override
Future<void> declineShare(String shareId, String token) async {}
@override
Future<List<ShareInboxItem>> fetchInbox(String token) async => const [];
@override
Future<void> revokeShare(String shareId, String token) async {}
@override
Future<RemoteShareSendResult> sendShare({
required ShareResourceType resourceType,
required Map<String, Object?> payload,
required List<String> recipientEmails,
required String token,
}) async {
return const RemoteShareSendResult(
shareId: 'share-1',
recipientUserIds: ['recipient-1'],
unresolvedEmails: [],
);
}
}
final class _FakeShareInboxRepository implements ShareInboxRepository {
final items = <ShareInboxItem>[];
@override
Future<ShareInboxItem?> findByShareId(String shareId) async {
return items.cast<ShareInboxItem?>().firstWhere(
(item) => item?.shareId == shareId,
orElse: () => null,
);
}
@override
Future<List<ShareInboxItem>> listAll() async => List.of(items);
@override
Future<void> markStatus(
String shareId,
ShareInboxStatus status,
DateTime respondedAt,
) async {
final index = items.indexWhere((item) => item.shareId == shareId);
if (index == -1) return;
items[index] = items[index].copyWith(
status: status,
respondedAt: respondedAt,
);
}
@override
Future<void> upsert(ShareInboxItem item) async {
items.removeWhere((existing) => existing.shareId == item.shareId);
items.add(item);
}
@override
Future<void> upsertAll(List<ShareInboxItem> items) async {
for (final item in items) {
await upsert(item);
}
}
}
final class _FakePendingShareActionRepository
implements PendingShareActionRepository {
final actions = <PendingShareAction>[];
@override
Future<void> add(PendingShareAction action) async {
actions.add(action);
}
@override
Future<List<PendingShareAction>> listPending() async => actions;
@override
Future<void> markFailed(String id, DateTime attemptedAt) async {}
@override
Future<void> markSucceeded(String id, DateTime attemptedAt) async {}
}
final class _FakeProgramRepository implements ProgramRepository {
@override
Future<Program?> findById(String id) async => null;
@override
Future<List<Program>> listActive() async => const [];
@override
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
@override
Future<void> save(Program program) async {}
@override
Future<void> saveExercise(ProgramExercise exercise) async {}
}
final class _FakeWorkoutTemplateRepository
implements WorkoutTemplateRepository {
@override
Future<WorkoutTemplate?> findById(String id) async => null;
@override
Future<List<WorkoutTemplate>> listActive() async => const [];
@override
Future<void> replaceComposition(
WorkoutTemplate template,
DateTime deletedAt,
) async {}
@override
Future<void> save(WorkoutTemplate template) async {}
@override
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
@override
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
}