feat(online): partage client - use cases, inbox cache et import local (ticket #66)
Ajoute l'adapter API distant de partage (infrastructure/remote/share_api.dart) et les use cases associés (application/use_cases.dart), avec cache d'inbox et import local des ressources partagées acceptées. Étend le modèle Drift (migration schemaVersion 11→12) et les entités du domaine en conséquence. flutter pub get OK, build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 124/124 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
export 'auth_api.dart';
|
||||
export 'http_api_client.dart';
|
||||
export 'share_api.dart';
|
||||
export 'sync_api.dart';
|
||||
|
||||
188
lib/infrastructure/remote/share_api.dart
Normal file
188
lib/infrastructure/remote/share_api.dart
Normal file
@ -0,0 +1,188 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../application/application.dart';
|
||||
import '../../domain/domain.dart';
|
||||
import 'http_api_client.dart';
|
||||
|
||||
final class HttpRemoteShareApi implements RemoteShareApi {
|
||||
const HttpRemoteShareApi(this.client);
|
||||
|
||||
final HttpApiClient client;
|
||||
|
||||
@override
|
||||
Future<RemoteShareSendResult> sendShare({
|
||||
required ShareResourceType resourceType,
|
||||
required Map<String, Object?> payload,
|
||||
required List<String> recipientEmails,
|
||||
required String token,
|
||||
}) async {
|
||||
final response = await client.postJson(
|
||||
'/shares',
|
||||
bearerToken: token,
|
||||
body: {
|
||||
'resourceType': _shareResourceTypeToWire(resourceType),
|
||||
'payload': payload,
|
||||
'recipientEmails': recipientEmails,
|
||||
},
|
||||
expectedStatuses: const {201},
|
||||
);
|
||||
return RemoteShareSendResult(
|
||||
shareId: _requiredString(response, 'shareId'),
|
||||
recipientUserIds: _stringList(response, 'recipientUserIds'),
|
||||
unresolvedEmails: _stringList(response, 'unresolvedEmails'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ShareInboxItem>> fetchInbox(String token) async {
|
||||
final response = await client.getJson('/shares/inbox', bearerToken: token);
|
||||
return _list(
|
||||
response,
|
||||
'items',
|
||||
).map((item) => _inboxItemFromJson(_map(item))).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RemoteSyncedItem> acceptShare(String shareId, String token) async {
|
||||
final response = await client.postJson(
|
||||
'/shares/$shareId/accept',
|
||||
bearerToken: token,
|
||||
);
|
||||
return _syncedItemFromJson(_map(response['createdResource']));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> declineShare(String shareId, String token) {
|
||||
return client.postEmpty('/shares/$shareId/decline', bearerToken: token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revokeShare(String shareId, String token) {
|
||||
return client.postEmpty('/shares/$shareId/revoke', bearerToken: token);
|
||||
}
|
||||
}
|
||||
|
||||
ShareInboxItem _inboxItemFromJson(Map<String, Object?> json) {
|
||||
return ShareInboxItem(
|
||||
shareId: _requiredString(json, 'shareId'),
|
||||
senderUserId: _requiredString(json, 'senderUserId'),
|
||||
resourceType: _shareResourceTypeFromWire(
|
||||
_requiredString(json, 'resourceType'),
|
||||
),
|
||||
payloadJson: _jsonObjectString(json['payload']),
|
||||
status: _shareInboxStatusFromWire(_requiredString(json, 'status')),
|
||||
createdAt: _requiredDateTime(json, 'createdAt'),
|
||||
respondedAt: _optionalDateTime(json, 'respondedAt'),
|
||||
);
|
||||
}
|
||||
|
||||
RemoteSyncedItem _syncedItemFromJson(Map<String, Object?> json) {
|
||||
return RemoteSyncedItem(
|
||||
resourceType: _syncResourceTypeFromWire(
|
||||
_requiredString(json, 'resourceType'),
|
||||
),
|
||||
clientId: _requiredString(json, 'clientId'),
|
||||
serverId: _requiredString(json, 'serverId'),
|
||||
schemaVersion: _requiredInt(json, 'schemaVersion'),
|
||||
clientUpdatedAt: _requiredDateTime(json, 'clientUpdatedAt'),
|
||||
serverUpdatedAt: _requiredDateTime(json, 'serverUpdatedAt'),
|
||||
deletedAt: _optionalDateTime(json, 'deletedAt'),
|
||||
payload: _map(json['payload']),
|
||||
);
|
||||
}
|
||||
|
||||
String _shareResourceTypeToWire(ShareResourceType type) => switch (type) {
|
||||
ShareResourceType.program => 'program',
|
||||
ShareResourceType.workoutTemplate => 'workoutTemplate',
|
||||
};
|
||||
|
||||
ShareResourceType _shareResourceTypeFromWire(String value) => switch (value) {
|
||||
'program' => ShareResourceType.program,
|
||||
'workoutTemplate' => ShareResourceType.workoutTemplate,
|
||||
_ => throw RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'Unknown share resource type: $value',
|
||||
),
|
||||
};
|
||||
|
||||
ShareInboxStatus _shareInboxStatusFromWire(String value) => switch (value) {
|
||||
'pending' => ShareInboxStatus.pending,
|
||||
'accepted' => ShareInboxStatus.accepted,
|
||||
'declined' => ShareInboxStatus.declined,
|
||||
'revoked' => ShareInboxStatus.revoked,
|
||||
_ => throw RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'Unknown share status: $value',
|
||||
),
|
||||
};
|
||||
|
||||
SyncResourceType _syncResourceTypeFromWire(String value) => switch (value) {
|
||||
'exercise' => SyncResourceType.exercise,
|
||||
'program' => SyncResourceType.program,
|
||||
'workoutTemplate' => SyncResourceType.workoutTemplate,
|
||||
'workoutHistory' => SyncResourceType.workoutHistory,
|
||||
'mediaAsset' => SyncResourceType.mediaAsset,
|
||||
_ => throw RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'Unknown sync resource type: $value',
|
||||
),
|
||||
};
|
||||
|
||||
List<Object?> _list(Map<String, Object?> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is List) {
|
||||
return value.cast<Object?>();
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
List<String> _stringList(Map<String, Object?> json, String key) {
|
||||
return _list(json, key).whereType<String>().toList(growable: false);
|
||||
}
|
||||
|
||||
Map<String, Object?> _map(Object? value) {
|
||||
if (value is Map) {
|
||||
return Map<String, Object?>.from(value);
|
||||
}
|
||||
throw const RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'Expected JSON object.',
|
||||
);
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, Object?> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
throw RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'$key is missing from share response.',
|
||||
);
|
||||
}
|
||||
|
||||
int _requiredInt(Map<String, Object?> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
throw RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'$key is missing from share response.',
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _requiredDateTime(Map<String, Object?> json, String key) {
|
||||
return DateTime.parse(_requiredString(json, key)).toUtc();
|
||||
}
|
||||
|
||||
DateTime? _optionalDateTime(Map<String, Object?> json, String key) {
|
||||
final value = json[key];
|
||||
return value is String && value.trim().isNotEmpty
|
||||
? DateTime.parse(value).toUtc()
|
||||
: null;
|
||||
}
|
||||
|
||||
String _jsonObjectString(Object? value) {
|
||||
return jsonEncode(_map(value));
|
||||
}
|
||||
Reference in New Issue
Block a user