272 lines
7.9 KiB
Dart
272 lines
7.9 KiB
Dart
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: _shareRequestBody(
|
|
resourceType: 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<List<RemoteSyncedItem>> acceptShare(
|
|
String shareId,
|
|
String token,
|
|
) async {
|
|
final response = await client.postJson(
|
|
'/shares/$shareId/accept',
|
|
bearerToken: token,
|
|
);
|
|
final resources = _list(
|
|
response,
|
|
'createdResources',
|
|
).map((item) => _syncedItemFromJson(_map(item))).toList(growable: false);
|
|
if (resources.isNotEmpty) {
|
|
return resources;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
Map<String, Object?> _shareRequestBody({
|
|
required ShareResourceType resourceType,
|
|
required Map<String, Object?> payload,
|
|
required List<String> recipientEmails,
|
|
}) {
|
|
if (resourceType != ShareResourceType.pack) {
|
|
return {
|
|
'shareKind': 'single',
|
|
'resourceType': _shareResourceTypeToWire(resourceType),
|
|
'payload': payload,
|
|
'recipientEmails': recipientEmails,
|
|
};
|
|
}
|
|
final rawWorkouts = payload['workouts'];
|
|
if (rawWorkouts is! List) {
|
|
throw const RemoteAuthException(
|
|
RemoteAuthFailure.unknown,
|
|
'Pack payload must contain workouts.',
|
|
);
|
|
}
|
|
return {
|
|
'shareKind': 'pack',
|
|
'packName': _stringFromObject(payload['name'], 'Pack'),
|
|
'items': [
|
|
for (final rawWorkout in rawWorkouts)
|
|
{'resourceType': 'workoutTemplate', 'payload': _map(rawWorkout)},
|
|
],
|
|
'recipientEmails': recipientEmails,
|
|
};
|
|
}
|
|
|
|
ShareInboxItem _inboxItemFromJson(Map<String, Object?> json) {
|
|
final resourceType = _inboxResourceType(json);
|
|
return ShareInboxItem(
|
|
shareId: _requiredString(json, 'shareId'),
|
|
senderUserId: _requiredString(json, 'senderUserId'),
|
|
resourceType: resourceType,
|
|
payloadJson: _payloadJsonString(json, resourceType),
|
|
status: _shareInboxStatusFromWire(_requiredString(json, 'status')),
|
|
createdAt: _requiredDateTime(json, 'createdAt'),
|
|
respondedAt: _optionalDateTime(json, 'respondedAt'),
|
|
);
|
|
}
|
|
|
|
ShareResourceType _inboxResourceType(Map<String, Object?> json) {
|
|
final shareKind = json['shareKind'];
|
|
if (shareKind == 'pack') {
|
|
return ShareResourceType.pack;
|
|
}
|
|
return _shareResourceTypeFromWire(_requiredString(json, 'resourceType'));
|
|
}
|
|
|
|
String _payloadJsonString(
|
|
Map<String, Object?> json,
|
|
ShareResourceType resourceType,
|
|
) {
|
|
final payload = _map(json['payload']);
|
|
if (resourceType != ShareResourceType.pack) {
|
|
return jsonEncode(payload);
|
|
}
|
|
if (payload['workouts'] is List) {
|
|
return jsonEncode({
|
|
'name': json['packName'] as String? ?? payload['name'] ?? 'Pack',
|
|
...payload,
|
|
});
|
|
}
|
|
final rawItems = payload['items'];
|
|
if (rawItems is! List) {
|
|
return jsonEncode({
|
|
'name': json['packName'] as String? ?? 'Pack',
|
|
'workouts': const <Object?>[],
|
|
});
|
|
}
|
|
return jsonEncode({
|
|
'name': json['packName'] as String? ?? 'Pack',
|
|
'workouts': [
|
|
for (final rawItem in rawItems)
|
|
if (rawItem is Map &&
|
|
Map<String, Object?>.from(rawItem)['resourceType'] ==
|
|
'workoutTemplate')
|
|
_map(Map<String, Object?>.from(rawItem)['payload']),
|
|
],
|
|
});
|
|
}
|
|
|
|
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.pack => 'pack',
|
|
};
|
|
|
|
ShareResourceType _shareResourceTypeFromWire(String value) => switch (value) {
|
|
'program' => ShareResourceType.program,
|
|
'workoutTemplate' => ShareResourceType.workoutTemplate,
|
|
'pack' => ShareResourceType.pack,
|
|
_ => 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.',
|
|
);
|
|
}
|
|
|
|
String _stringFromObject(Object? value, String fallback) {
|
|
return value is String && value.trim().isNotEmpty ? value : fallback;
|
|
}
|
|
|
|
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;
|
|
}
|