Files
Eventify-frontend/lib/features/booking/providers/checkout_provider.dart
Sicherhaven e9752c3d61 feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K  Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
        Notionists avatar via TierAvatarRing, district picker (14 pills)
        with 183-day cooldown lock, multipart photo upload to server
P3-B    Home: Top Events converted to PageView scroll-snap
        (viewportFraction 0.9 + PageScrollPhysics)
P3-C    Event detail: contributor widget (tier ring + name + navigation),
        related events horizontal row; added getEventsByCategory() to
        EventsService; added contributorId/Name/Tier fields to EventModel
P3-D    Kerala pincodes: 463-entry JSON (all 14 districts), registered as
        asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E    Checkout: promo code field + Apply/Remove button in Step 2,
        discountAmount subtracted from total, applyPromo()/resetPromo()
        methods in CheckoutProvider
P3-F/G  Gamification: reward cycle countdown + EP→RP progress bar (blue→
        amber) in contribute + profile screens; TierAvatarRing in podium
        and all leaderboard rows; GlassCard current-user stats card at
        top of leaderboard tab
P3-H    New ContributorProfileScreen: tier ring, stats, submission grid
        with status chips; getDashboardForUser() in GamificationService;
        wired from leaderboard row taps
P3-I    Achievements: 11 default badges (up from 6), 6 new icon map
        entries; progress % labels already confirmed present
P3-J    Reviews: CustomPainter circular arc rating ring (amber, 84px)
        replaces large rating number in ReviewSummary
P3-L    Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
        share button wired in profile header and leaderboard tab
P3-M    SafeArea audit: home bottom nav, contribute/achievements scroll
        padding, profile CustomScrollView top inset

New files: tier_avatar_ring.dart, glass_card.dart,
  eventify_bottom_sheet.dart, contributor_profile_screen.dart,
  share_rank_card.dart, assets/data/kerala_pincodes.json
New dep:   path_provider ^2.1.0
2026-04-04 17:17:36 +05:30

217 lines
5.6 KiB
Dart

// lib/features/booking/providers/checkout_provider.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/api/api_endpoints.dart';
import '../../../core/utils/error_utils.dart';
import '../models/booking_models.dart';
import '../services/booking_service.dart';
enum CheckoutStep { tickets, details, payment, confirmation }
class CheckoutProvider extends ChangeNotifier {
final BookingService _service = BookingService();
// Event being booked
int? eventId;
String eventName = '';
// Step tracking
CheckoutStep currentStep = CheckoutStep.tickets;
// Ticket selection
List<TicketMetaModel> availableTickets = [];
List<CartItemModel> cart = [];
// Shipping
ShippingDetails? shippingDetails;
// Coupon / promo
String? couponCode;
double discountAmount = 0.0;
String? promoMessage;
bool promoApplied = false;
// Status
bool loading = false;
String? error;
String? paymentId;
/// Initialize checkout for an event.
Future<void> initForEvent(int eventId, String eventName) async {
this.eventId = eventId;
this.eventName = eventName;
currentStep = CheckoutStep.tickets;
cart = [];
shippingDetails = null;
couponCode = null;
discountAmount = 0.0;
promoMessage = null;
promoApplied = false;
paymentId = null;
error = null;
loading = true;
notifyListeners();
try {
availableTickets = await _service.getTicketMeta(eventId);
} catch (e) {
error = userFriendlyError(e);
} finally {
loading = false;
notifyListeners();
}
}
/// Add or update cart item.
void setTicketQuantity(TicketMetaModel ticket, int qty) {
cart.removeWhere((c) => c.ticket.id == ticket.id);
if (qty > 0) {
cart.add(CartItemModel(ticket: ticket, quantity: qty));
}
notifyListeners();
}
double get subtotal => cart.fold(0, (sum, item) => sum + item.subtotal);
double get total => subtotal - discountAmount;
bool get hasItems => cart.isNotEmpty;
/// Move to next step.
void nextStep() {
if (currentStep == CheckoutStep.tickets && hasItems) {
currentStep = CheckoutStep.details;
} else if (currentStep == CheckoutStep.details && shippingDetails != null) {
currentStep = CheckoutStep.payment;
}
notifyListeners();
}
/// Move to previous step.
void previousStep() {
if (currentStep == CheckoutStep.payment) {
currentStep = CheckoutStep.details;
} else if (currentStep == CheckoutStep.details) {
currentStep = CheckoutStep.tickets;
}
notifyListeners();
}
/// Set shipping details from form.
void setShipping(ShippingDetails details) {
shippingDetails = details;
notifyListeners();
}
/// Apply a promo code against the backend.
Future<bool> applyPromo(String code) async {
if (code.trim().isEmpty) return false;
loading = true;
error = null;
notifyListeners();
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('access_token') ?? '';
final response = await http.post(
Uri.parse('${ApiEndpoints.baseUrl}/bookings/apply-promo/'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({'code': code.trim(), 'event_id': eventId}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['valid'] == true) {
discountAmount = (data['discount_amount'] as num?)?.toDouble() ?? 0.0;
couponCode = code.trim();
promoMessage = data['message'] as String? ?? 'Promo applied!';
promoApplied = true;
notifyListeners();
return true;
} else {
promoMessage = data['message'] as String? ?? 'Invalid promo code';
promoApplied = false;
discountAmount = 0.0;
couponCode = null;
notifyListeners();
return false;
}
} else {
promoMessage = 'Could not apply promo code';
return false;
}
} catch (e) {
promoMessage = 'Could not apply promo code';
return false;
} finally {
loading = false;
notifyListeners();
}
}
/// Remove applied promo code.
void resetPromo() {
discountAmount = 0.0;
couponCode = null;
promoMessage = null;
promoApplied = false;
notifyListeners();
}
/// Process checkout on backend.
Future<Map<String, dynamic>> processCheckout() async {
loading = true;
error = null;
notifyListeners();
try {
final tickets = cart.map((c) => {
'ticket_meta_id': c.ticket.id,
'quantity': c.quantity,
}).toList();
final res = await _service.processCheckout(
eventId: eventId!,
tickets: tickets,
shippingDetails: shippingDetails?.toJson() ?? {},
couponCode: couponCode,
);
return res;
} catch (e) {
error = userFriendlyError(e);
rethrow;
} finally {
loading = false;
notifyListeners();
}
}
/// Mark payment as complete.
void markPaymentSuccess(String id) {
paymentId = id;
currentStep = CheckoutStep.confirmation;
notifyListeners();
}
/// Reset checkout state.
void reset() {
eventId = null;
eventName = '';
currentStep = CheckoutStep.tickets;
availableTickets = [];
cart = [];
shippingDetails = null;
couponCode = null;
discountAmount = 0.0;
promoMessage = null;
promoApplied = false;
paymentId = null;
error = null;
loading = false;
notifyListeners();
}
}