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
|
||||
/// Presigned URLs now have 7 days expiration (configured in MinioConfig.presignedUrlExpirySeconds)
|
||||
Future<String?> _getPresignedUrlIfUuid(String? value, String bucket) async {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
if (!_isValidUuid(value)) return null;
|
||||
|
|
@ -394,7 +395,8 @@ class PacksApiV2 {
|
|||
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(
|
||||
paginatedCards.map((card) async {
|
||||
final dto = await card.toDto(
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ class MinioConfig {
|
|||
static const String cardImagesBucket = 'card-images';
|
||||
static const String voiceAudioBucket = 'voice-audio';
|
||||
|
||||
// Presigned URL expiration (4 hours)
|
||||
static const int presignedUrlExpirySeconds = 4 * 60 * 60;
|
||||
// Presigned URL expiration (7 days)
|
||||
// Increased from 4 hours to prevent 403 errors on cached card data
|
||||
static const int presignedUrlExpirySeconds = 7 * 24 * 60 * 60;
|
||||
|
||||
MinioConfig({
|
||||
required this.endpoint,
|
||||
|
|
|
|||
|
|
@ -251,6 +251,16 @@ class HttpRepositoryV2 {
|
|||
originalError: error,
|
||||
);
|
||||
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:
|
||||
apiException = NetworkException(
|
||||
message: message.isNotEmpty ? message : 'Network error',
|
||||
|
|
@ -1059,7 +1069,10 @@ class HttpRepositoryV2 {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
ApiConfigV2.packBuy(packId),
|
||||
|
|
@ -1068,7 +1081,9 @@ class HttpRepositoryV2 {
|
|||
if (data == 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) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ class PurchasesService {
|
|||
final HttpRepositoryV2 _httpRepository;
|
||||
|
||||
/// 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');
|
||||
try {
|
||||
return await _httpRepository.getPackBuy(packId);
|
||||
|
|
|
|||
|
|
@ -49,16 +49,26 @@ class PurchaseStateManager extends StateManager<PurchaseState> {
|
|||
emit(const PurchaseState.loading());
|
||||
|
||||
try {
|
||||
final packInfo = await _purchasesService.getPackBuy(packId);
|
||||
final result = await _purchasesService.getPackBuy(packId);
|
||||
|
||||
if (packInfo == null) {
|
||||
if (result == null) {
|
||||
emit(const PurchaseState.error(
|
||||
message: 'Pack not found or not available for purchase',
|
||||
));
|
||||
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');
|
||||
} catch (e, s) {
|
||||
log(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:yx_state_flutter/yx_state_flutter.dart';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../di/app_scope/app_scope_container.dart';
|
||||
import '../../../domain/exceptions/api_exception.dart';
|
||||
import '../../../domain/state/purchase_state_manager.dart';
|
||||
import '../../../utils/color_extension.dart';
|
||||
import '../../widgets/error_view.dart';
|
||||
|
|
@ -119,8 +120,26 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||
name: 'PurchasePage',
|
||||
);
|
||||
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(
|
||||
SnackBar(content: Text('Purchase error: ${e.toString()}')),
|
||||
SnackBar(
|
||||
content: Text(errorMessage),
|
||||
backgroundColor: e is ServerException && e.statusCode == 409
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -233,7 +252,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||
final theme = Theme.of(context);
|
||||
final packColor = packInfo.color?.asColor ?? theme.colorScheme.primary;
|
||||
|
||||
return Expanded(
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -243,12 +262,18 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||
// Preview cards
|
||||
if (packInfo.cards.isNotEmpty) _PreviewCards(cards: packInfo.cards),
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
...?packInfo.items?.build(),
|
||||
],),
|
||||
),
|
||||
// Items (description, features, etc.)
|
||||
if (packInfo.items != null && packInfo.items!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...packInfo.items!.build(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Purchase button
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
|
|
|
|||
Loading…
Reference in a new issue