admin
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
This commit is contained in:
parent
1d9bc6c43d
commit
6d499dc584
6 changed files with 73 additions and 17 deletions
|
|
@ -150,6 +150,7 @@ class PacksApiV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates presigned URL if value is a UUID (object ID), otherwise returns null
|
/// Generates presigned URL if value is a UUID (object ID), otherwise returns null
|
||||||
|
/// Presigned URLs now have 7 days expiration (configured in MinioConfig.presignedUrlExpirySeconds)
|
||||||
Future<String?> _getPresignedUrlIfUuid(String? value, String bucket) async {
|
Future<String?> _getPresignedUrlIfUuid(String? value, String bucket) async {
|
||||||
if (value == null || value.isEmpty) return null;
|
if (value == null || value.isEmpty) return null;
|
||||||
if (!_isValidUuid(value)) return null;
|
if (!_isValidUuid(value)) return null;
|
||||||
|
|
@ -394,7 +395,8 @@ class PacksApiV2 {
|
||||||
allVoices.addAll(voices);
|
allVoices.addAll(voices);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTOs and generate presigned URLs
|
// Convert to DTOs and generate presigned URLs for images
|
||||||
|
// Presigned URLs now have 7 days expiration to avoid 403 errors on cached data
|
||||||
final cardDtos = await Future.wait(
|
final cardDtos = await Future.wait(
|
||||||
paginatedCards.map((card) async {
|
paginatedCards.map((card) async {
|
||||||
final dto = await card.toDto(
|
final dto = await card.toDto(
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ class MinioConfig {
|
||||||
static const String cardImagesBucket = 'card-images';
|
static const String cardImagesBucket = 'card-images';
|
||||||
static const String voiceAudioBucket = 'voice-audio';
|
static const String voiceAudioBucket = 'voice-audio';
|
||||||
|
|
||||||
// Presigned URL expiration (4 hours)
|
// Presigned URL expiration (7 days)
|
||||||
static const int presignedUrlExpirySeconds = 4 * 60 * 60;
|
// Increased from 4 hours to prevent 403 errors on cached card data
|
||||||
|
static const int presignedUrlExpirySeconds = 7 * 24 * 60 * 60;
|
||||||
|
|
||||||
MinioConfig({
|
MinioConfig({
|
||||||
required this.endpoint,
|
required this.endpoint,
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,16 @@ class HttpRepositoryV2 {
|
||||||
originalError: error,
|
originalError: error,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 409:
|
||||||
|
// Conflict - typically means resource already exists (e.g., pack already purchased)
|
||||||
|
apiException = ServerException(
|
||||||
|
message: message.isNotEmpty
|
||||||
|
? message
|
||||||
|
: 'Conflict: This resource already exists or is already purchased',
|
||||||
|
statusCode: 409,
|
||||||
|
originalError: error,
|
||||||
|
);
|
||||||
|
break;
|
||||||
case null:
|
case null:
|
||||||
apiException = NetworkException(
|
apiException = NetworkException(
|
||||||
message: message.isNotEmpty ? message : 'Network error',
|
message: message.isNotEmpty ? message : 'Network error',
|
||||||
|
|
@ -1059,7 +1069,10 @@ class HttpRepositoryV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get pack purchase details (includes ad reward offers)
|
/// Get pack purchase details (includes ad reward offers)
|
||||||
Future<CardPackBuyDto?> getPackBuy(String packId) async {
|
/// Returns a tuple with CardPackBuyDto and isPurchased flag
|
||||||
|
Future<({CardPackBuyDto packInfo, bool isPurchased})?> getPackBuy(
|
||||||
|
String packId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get<Map<String, dynamic>>(
|
final response = await _dio.get<Map<String, dynamic>>(
|
||||||
ApiConfigV2.packBuy(packId),
|
ApiConfigV2.packBuy(packId),
|
||||||
|
|
@ -1068,7 +1081,9 @@ class HttpRepositoryV2 {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return CardPackBuyDto.fromJson(data);
|
final packInfo = CardPackBuyDto.fromJson(data);
|
||||||
|
final isPurchased = data['isPurchased'] as bool? ?? false;
|
||||||
|
return (packInfo: packInfo, isPurchased: isPurchased);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == 404) {
|
if (e.response?.statusCode == 404) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,10 @@ class PurchasesService {
|
||||||
final HttpRepositoryV2 _httpRepository;
|
final HttpRepositoryV2 _httpRepository;
|
||||||
|
|
||||||
/// Get pack purchase information (includes preview cards, price, items)
|
/// Get pack purchase information (includes preview cards, price, items)
|
||||||
Future<CardPackBuyDto?> getPackBuy(String packId) async {
|
/// Returns a tuple with CardPackBuyDto and isPurchased flag
|
||||||
|
Future<({CardPackBuyDto packInfo, bool isPurchased})?> getPackBuy(
|
||||||
|
String packId,
|
||||||
|
) async {
|
||||||
log('Getting pack buy info for $packId', name: 'PurchasesService');
|
log('Getting pack buy info for $packId', name: 'PurchasesService');
|
||||||
try {
|
try {
|
||||||
return await _httpRepository.getPackBuy(packId);
|
return await _httpRepository.getPackBuy(packId);
|
||||||
|
|
|
||||||
|
|
@ -49,16 +49,26 @@ class PurchaseStateManager extends StateManager<PurchaseState> {
|
||||||
emit(const PurchaseState.loading());
|
emit(const PurchaseState.loading());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final packInfo = await _purchasesService.getPackBuy(packId);
|
final result = await _purchasesService.getPackBuy(packId);
|
||||||
|
|
||||||
if (packInfo == null) {
|
if (result == null) {
|
||||||
emit(const PurchaseState.error(
|
emit(const PurchaseState.error(
|
||||||
message: 'Pack not found or not available for purchase',
|
message: 'Pack not found or not available for purchase',
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(PurchaseState.loaded(packInfo: packInfo));
|
// Check if pack is already purchased
|
||||||
|
if (result.isPurchased) {
|
||||||
|
emit(PurchaseState.completed(
|
||||||
|
packInfo: result.packInfo,
|
||||||
|
message: 'This pack is already purchased. You already have access to it.',
|
||||||
|
));
|
||||||
|
log('Pack is already purchased', name: 'PurchaseStateManager');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(PurchaseState.loaded(packInfo: result.packInfo));
|
||||||
log('Pack purchase info loaded', name: 'PurchaseStateManager');
|
log('Pack purchase info loaded', name: 'PurchaseStateManager');
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log(
|
log(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:yx_state_flutter/yx_state_flutter.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../di/app_scope/app_scope_container.dart';
|
import '../../../di/app_scope/app_scope_container.dart';
|
||||||
|
import '../../../domain/exceptions/api_exception.dart';
|
||||||
import '../../../domain/state/purchase_state_manager.dart';
|
import '../../../domain/state/purchase_state_manager.dart';
|
||||||
import '../../../utils/color_extension.dart';
|
import '../../../utils/color_extension.dart';
|
||||||
import '../../widgets/error_view.dart';
|
import '../../widgets/error_view.dart';
|
||||||
|
|
@ -119,8 +120,26 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||||
name: 'PurchasePage',
|
name: 'PurchasePage',
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
String errorMessage = 'Purchase error: ${e.toString()}';
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (e is ServerException && e.statusCode == 409) {
|
||||||
|
errorMessage = 'This pack is already purchased. You already have access to it.';
|
||||||
|
} else if (e is ValidationException) {
|
||||||
|
errorMessage = e.message;
|
||||||
|
} else if (e is ServerException) {
|
||||||
|
errorMessage = e.message.isNotEmpty
|
||||||
|
? e.message
|
||||||
|
: 'Server error occurred. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Purchase error: ${e.toString()}')),
|
SnackBar(
|
||||||
|
content: Text(errorMessage),
|
||||||
|
backgroundColor: e is ServerException && e.statusCode == 409
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -233,7 +252,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final packColor = packInfo.color?.asColor ?? theme.colorScheme.primary;
|
final packColor = packInfo.color?.asColor ?? theme.colorScheme.primary;
|
||||||
|
|
||||||
return Expanded(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -243,12 +262,18 @@ class _PurchasePageState extends State<PurchasePage> {
|
||||||
// Preview cards
|
// Preview cards
|
||||||
if (packInfo.cards.isNotEmpty) _PreviewCards(cards: packInfo.cards),
|
if (packInfo.cards.isNotEmpty) _PreviewCards(cards: packInfo.cards),
|
||||||
|
|
||||||
Expanded(
|
// Items (description, features, etc.)
|
||||||
child: Column(
|
if (packInfo.items != null && packInfo.items!.isNotEmpty)
|
||||||
children: [
|
Padding(
|
||||||
...?packInfo.items?.build(),
|
padding: const EdgeInsets.all(16.0),
|
||||||
],),
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...packInfo.items!.build(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Purchase button
|
// Purchase button
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue