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
217 lines
5.6 KiB
Dart
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();
|
|
}
|
|
}
|