updated iternary api and updated buy pass flow

This commit is contained in:
mystery012728
2026-02-10 13:58:58 +05:30
parent 0c663bdec7
commit 3a08830cce
9 changed files with 318 additions and 84 deletions

View File

@@ -27,10 +27,11 @@ class BuyPassRepository {
required int totalChild,
required int noOfAttractions,
required int noOfDays,
required double baseAmount,
}) async {
try {
final response = await _apiService.postApi(
url: ApiUrls.addToCartPasses, // add this key in ApiUrls
url: ApiUrls.addToCartPasses,
data: {
"cityXid": cityXid,
"cardTypeXid": cardTypeXid,
@@ -38,6 +39,8 @@ class BuyPassRepository {
"cardMode": cardMode,
"totalAdult": totalAdult,
"totalChild": totalChild,
"baseAmount": baseAmount,
"taxAmount": 2, // Fixed tax amount
"noOfAttractions": noOfAttractions,
"noOfDays": noOfDays,
},
@@ -48,4 +51,4 @@ class BuyPassRepository {
throw Exception('Failed to add passes to cart: $e');
}
}
}
}

View File

@@ -181,11 +181,12 @@ class PaymentCard extends StatelessWidget {
cityXid: cityXid,
cardTypeXid: cardTypeXid,
cardXid: cardXid,
cardMode: isSelectivePass ? 'flexi' : 'fixed',
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: adults,
totalChild: children,
noOfAttractions: isSelectivePass ? selectedValue : 0,
noOfDays: isUnlimitedCard ? selectedValue : 0,
baseAmount: totalPrice,
);
// ✅ Extract bookingId from response

View File

@@ -116,11 +116,11 @@ class _CheckoutContent extends StatelessWidget {
/// 🆕 Handle payment flow with client secret
/// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId) async {
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async {
final paymentSuccess = await StripePaymentScreen.showAsBottomSheet(
context: context,
clientSecret: clientSecret,
amount: checkoutData.totalPrice.toDouble(),
amount: finalTotal,
currencySymbol: '\$',
title: 'Complete Payment',
loadingMessage: 'Processing your pass payment...',
@@ -149,6 +149,7 @@ class _CheckoutContent extends StatelessWidget {
);
},
onPaymentCancelled: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Payment cancelled'),
@@ -182,22 +183,31 @@ class _CheckoutContent extends StatelessWidget {
if (state is CheckoutCouponsLoadedState) {
// Check if clientSecret is available (payment initiated)
if (state.clientSecret != null && state.clientSecret!.isNotEmpty) {
// Trigger payment flow
// ✅ Calculate finalTotal here
double discountPercentage = 0.0;
if (state.appliedCoupon != null) {
discountPercentage = state.appliedCoupon!.discountPercent.toDouble();
}
final num subtotal = checkoutData.totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100);
final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = 2;
final double finalTotal = totalBeforeTax + taxAmount;
// ✅ Trigger payment flow with finalTotal
WidgetsBinding.instance.addPostFrameCallback((_) {
_handlePaymentFlow(context, state.clientSecret!, state.bookingId ?? bookingId);
_handlePaymentFlow(
context,
state.clientSecret!,
state.bookingId ?? bookingId,
finalTotal, // ✅ Pass the calculated finalTotal
);
});
}
// 🆕 Listen for payment confirmation success
if (state.isPaymentConfirmed) {
// Show success message
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Payment confirmed successfully!'),
// backgroundColor: Colors.green,
// ),
// );
// Navigate to success page or back
Future.delayed(const Duration(seconds: 2), () {
if (context.mounted) {
@@ -255,9 +265,10 @@ class _CheckoutContent extends StatelessWidget {
final num subtotal = checkoutData.totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100);
final double taxRate = 0.05; // 5% tax
// final double taxRate = 0.05; // 5% tax
final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = totalBeforeTax * taxRate;
// final double taxAmount = totalBeforeTax * taxRate;
final double taxAmount = 2;
final double finalTotal = totalBeforeTax + taxAmount;
return Scaffold(
@@ -531,11 +542,18 @@ class _CheckoutContent extends StatelessWidget {
),
builder: (_) => AllCouponsBottomsheet(
onCouponSelected: (selectedCoupon) {
final coupon = selectedCoupon as AllCouponsModel;
// Apply the selected coupon
context.read<CheckoutBloc>().add(
ApplyCouponEvent(
coupon: selectedCoupon),
);
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: bookingId,
couponCode: coupon.couponCode,
),
);
},
),
);

View File

@@ -1,18 +1,29 @@
class MyItineraryResponse {
bool isUnlimitedPass;
List<MyItinerary> itineraries;
MyItineraryResponse({required this.itineraries});
MyItineraryResponse({
required this.isUnlimitedPass,
required this.itineraries,
});
factory MyItineraryResponse.fromJson(Map<String, dynamic>? json) {
json ??= {};
factory MyItineraryResponse.fromJson(List<dynamic>? json) {
return MyItineraryResponse(
itineraries: json == null
isUnlimitedPass: json['isUnlimitedPass'] ?? false,
itineraries: json['itineraries'] == null
? []
: json.map((e) => MyItinerary.fromJson(e)).toList(),
: List<Map<String, dynamic>>.from(json['itineraries'])
.map((e) => MyItinerary.fromJson(e))
.toList(),
);
}
List<Map<String, dynamic>> toJson() =>
itineraries.map((e) => e.toJson()).toList();
Map<String, dynamic> toJson() => {
"isUnlimitedPass": isUnlimitedPass,
"itineraries": itineraries.map((e) => e.toJson()).toList(),
};
}
class MyItinerary {
@@ -61,22 +72,21 @@ class MyItinerary {
id: (json['id'] as num?)?.toInt() ?? 0,
userXid: (json['userXid'] as num?)?.toInt() ?? 0,
cityXid: (json['cityXid'] as num?)?.toInt() ?? 0,
address: json['Address'] ?? "",
address: json['Address']?.toString() ?? "",
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
tripEnergy: json['tripEnergy'] ?? "",
tripEnergy: json['tripEnergy']?.toString() ?? "",
travelingWithKids: json['travelingWithKids'] ?? false,
dietaryPreferences: json['dietaryPreferences'] == null
? []
: List<String>.from(json['dietaryPreferences']),
preferences:
Preferences.fromJson(json['preferences'] ?? {}),
preferences: Preferences.fromJson(json['preferences']),
totalDays: (json['totalDays'] as num?)?.toInt() ?? 0,
aiModel: json['aiModel'] ?? "",
promptVersion: json['promptVersion'] ?? "",
aiModel: json['aiModel']?.toString() ?? "",
promptVersion: json['promptVersion']?.toString() ?? "",
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] ?? "",
updatedAt: json['updatedAt'] ?? "",
createdAt: json['createdAt']?.toString() ?? "",
updatedAt: json['updatedAt']?.toString() ?? "",
days: json['days'] == null
? []
: List<Map<String, dynamic>>.from(json['days'])
@@ -166,8 +176,8 @@ class ItineraryDay {
id: (json['id'] as num?)?.toInt() ?? 0,
itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0,
dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0,
title: json['title'] ?? "",
summary: json['summary'] ?? "",
title: json['title']?.toString() ?? "",
summary: json['summary']?.toString() ?? "",
items: json['items'] == null
? []
: List<Map<String, dynamic>>.from(json['items'])
@@ -216,11 +226,11 @@ class DayItem {
id: (json['id'] as num?)?.toInt() ?? 0,
itineraryDayXid:
(json['itineraryDayXid'] as num?)?.toInt() ?? 0,
timeSlot: json['timeSlot'] ?? "",
title: json['title'] ?? "",
description: json['description'] ?? "",
locationName: json['locationName'] ?? "",
imageUrl: json['imageUrl'] ?? "",
timeSlot: json['timeSlot']?.toString() ?? "",
title: json['title']?.toString() ?? "",
description: json['description']?.toString() ?? "",
locationName: json['locationName']?.toString() ?? "",
imageUrl: json['imageUrl']?.toString() ?? "",
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
);

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart';
import 'package:dio/dio.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../models/my_itinerary_model.dart';
@@ -11,8 +12,9 @@ class ItineraryRepository {
final NetworkApiService _apiService = NetworkApiService();
Future<MyItineraryResponse> fetchMyItineraries() async {
final int cityId = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: ApiUrls.myItineraries, // 👈 Make sure this endpoint exists
url: '${ApiUrls.myItineraries}/$cityId', // 👈 Make sure this endpoint exists
);
/// Because API returns LIST

View File

@@ -248,4 +248,18 @@ class PostcardCreationBloc
emit(state.copyWith(isGift: event.isGift));
});
}
// Add this getter method in PostcardCreationBloc class
String getFormattedMessage() {
if (state.message == null || state.message!.isEmpty) {
return '';
}
if (state.selectedFont == null || state.selectedFont!.isEmpty) {
// Default font (Poppins)
return '<span style="font-family: Poppins;">${state.message}</span>';
}
return '<span style="font-family: ${state.selectedFont};">${state.message}</span>';
}
}

View File

@@ -80,7 +80,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
if (creationState.imagePath != null && creationState.imagePath!.isNotEmpty) {
imageFile = File(creationState.imagePath!);
}
final postcardBloc = context.read<PostcardCreationBloc>();
context.read<PostcardCheckoutBloc>().add(
UpdateCheckoutDataEvent(
countryName: widget.countryName,
@@ -90,7 +90,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
address1: creationState.address,
address2: widget.address2,
pcTitle: widget.pcTitle,
pcContent: creationState.message ?? '',
pcContent: postcardBloc.getFormattedMessage(),
pcImageFile: imageFile,
pcNumber: widget.pcNumber,
pcDatetime: widget.pcDatetime,

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -8,6 +10,7 @@ import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
import '../widgets/postcard_preview_widget.dart';
import '../widgets/step_progressbar.dart';
import 'dart:ui' as ui;
class WriteMessageStepPageView extends StatefulWidget {
const WriteMessageStepPageView({super.key});
@@ -45,10 +48,10 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
TextPosition(offset: _controller.text.length));
final fonts = [
{"name": "Default", "font": GoogleFonts.poppins()},
{"name": "Classic", "font": GoogleFonts.playfairDisplay()},
{"name": "Handwriting", "font": GoogleFonts.dancingScript()},
{"name": "Elegant", "font": GoogleFonts.cormorantGaramond()},
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
{"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
];
return SafeArea(
@@ -91,12 +94,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
maxLines: 8,
maxLength: 400,
cursorColor: const Color(0xffF95F62),
style: TextStyle(
fontFamily: state.selectedFont ??
GoogleFonts.poppins().fontFamily,
fontSize: 14.sp,
color: Colors.black,
),
style: _getTextFieldStyle(state.selectedFont, fonts),
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Add Your Message Here",
@@ -133,43 +131,52 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
children: fonts.map((font) {
final TextStyle fontStyle = font['font'] as TextStyle;
final String fontName = font["name"] as String;
final isSelected = state.selectedFont ==
fontStyle.fontFamily ||
(state.selectedFont == null &&
fontName == "Default");
final String cleanName = font["cleanName"] as String;
final isSelected = state.selectedFont == cleanName ||
(state.selectedFont == null && fontName == "Default");
return GestureDetector(
onTap: () => bloc
.add(ChangeFontStyle(fontStyle.fontFamily ?? "")),
onTap: () => bloc.add(ChangeFontStyle(cleanName)),
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(12),
width: 90.w,
padding: const EdgeInsets.all(6),
width: 100.w,
height: 100.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
),
child: CustomPaint(
painter: DottedBorderPainter(
color: isSelected
? const Color(0xffF95F62)
: const Color(0xffE0E0E0),
width: 1.5,
strokeWidth: 1.5,
dashWidth: 4,
dashSpace: 3,
borderRadius: 12,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Aa",
style: fontStyle.copyWith(
fontSize: 24.sp,
color: const Color(0xff1A1A1A),
)),
const SizedBox(height: 4),
Text(fontName,
textAlign: TextAlign.center,
style: fontStyle.copyWith(
fontSize: 11.sp,
color: isSelected
? const Color(0xffF95F62)
: const Color(0xff2D3134),
)),
],
),
),
),
child: Column(
children: [
Text("Aa",
style: fontStyle.copyWith(
fontSize: 20.sp,
color: const Color(0xff1A1A1A),
)),
const SizedBox(height: 4),
Text(fontName,
style: TextStyle(
fontSize: 12.sp,
color: isSelected
? const Color(0xffF95F62)
: const Color(0xff2D3134),
)),
],
),
),
);
@@ -209,5 +216,117 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
},
);
}
// Helper method to get the correct font style for the text field
TextStyle _getTextFieldStyle(String? selectedFont, List<Map<String, dynamic>> fonts) {
if (selectedFont == null || selectedFont.isEmpty) {
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
// Find matching font by cleanName
for (var font in fonts) {
if (font['cleanName'] == selectedFont) {
final TextStyle fontStyle = font['font'] as TextStyle;
return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black);
}
}
// Default fallback to Poppins
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
}
// Custom Painter for Dotted Border
class DottedBorderPainter extends CustomPainter {
final Color color;
final double strokeWidth;
final double dashWidth;
final double dashSpace;
final double borderRadius;
DottedBorderPainter({
required this.color,
this.strokeWidth = 1.5,
this.dashWidth = 4,
this.dashSpace = 3,
this.borderRadius = 12,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
final path = Path()
..addRRect(RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(borderRadius),
));
// Create dashed path
final dashPath = _createDashedPath(path, dashWidth, dashSpace);
canvas.drawPath(dashPath, paint);
}
Path _createDashedPath(Path source, double dashWidth, double dashSpace) {
final Path dest = Path();
for (final PathMetric metric in source.computeMetrics()) {
double distance = 0.0;
bool draw = true;
while (distance < metric.length) {
final double length = draw ? dashWidth : dashSpace;
if (distance + length > metric.length) {
if (draw) {
dest.addPath(
metric.extractPath(distance, metric.length),
Offset.zero,
);
}
break;
} else {
if (draw) {
dest.addPath(
metric.extractPath(distance, distance + length),
Offset.zero,
);
}
distance += length;
draw = !draw;
}
}
}
return dest;
}
@override
bool shouldRepaint(DottedBorderPainter oldDelegate) {
return oldDelegate.color != color ||
oldDelegate.strokeWidth != strokeWidth ||
oldDelegate.dashWidth != dashWidth ||
oldDelegate.dashSpace != dashSpace;
}
}
// Lined Paper Painter (assuming this exists in your original code)
class LinedPaperPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFFE0E0E0)
..strokeWidth = 1;
const lineSpacing = 30.0;
for (double i = lineSpacing; i < size.height; i += lineSpacing) {
canvas.drawLine(
Offset(0, i),
Offset(size.width, i),
paint,
);
}
}
@override
bool shouldRepaint(LinedPaperPainter oldDelegate) => false;
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:html/parser.dart' as html_parser;
class BackCardWidget extends StatelessWidget {
final String message;
@@ -26,8 +27,77 @@ class BackCardWidget extends StatelessWidget {
this.scale = 1.08,
});
// Parse HTML message and extract font family and text
Map<String, String> _parseHtmlMessage(String htmlMessage) {
if (htmlMessage.isEmpty) {
return {'text': '', 'fontFamily': ''};
}
// Check if message contains HTML tags
if (!htmlMessage.contains('<span') && !htmlMessage.contains('style=')) {
// Plain text message - no font specified
return {'text': htmlMessage, 'fontFamily': ''};
}
try {
// Parse HTML
final document = html_parser.parse(htmlMessage);
final spanElement = document.querySelector('span');
if (spanElement != null) {
// Extract text content
final text = spanElement.text;
// Extract font-family from style attribute
final style = spanElement.attributes['style'] ?? '';
final fontFamilyMatch = RegExp(r'font-family:\s*([^;]+)').firstMatch(style);
final fontFamily = fontFamilyMatch?.group(1)?.trim() ?? '';
return {'text': text, 'fontFamily': fontFamily};
}
// Fallback: return plain text
return {'text': document.body?.text ?? htmlMessage, 'fontFamily': ''};
} catch (e) {
// If parsing fails, return original message
return {'text': htmlMessage, 'fontFamily': ''};
}
}
// Get TextStyle with any Google Font
TextStyle _getFontStyle(String fontFamily, double fontSize, double height) {
// If no font family specified, use default Caveat
if (fontFamily.isEmpty) {
return GoogleFonts.caveat(fontSize: fontSize, height: height);
}
try {
// Normalize font name: remove extra spaces, handle common variations
final normalizedFont = fontFamily
.trim()
.replaceAll(RegExp(r'\s+'), ' '); // Replace multiple spaces with single space
// Try to get the font from Google Fonts
// GoogleFonts.getFont() can load ANY Google Font dynamically
return GoogleFonts.getFont(
normalizedFont,
fontSize: fontSize,
height: height,
);
} catch (e) {
// If font not found in Google Fonts, fallback to default
debugPrint('⚠️ Font "$fontFamily" not found in Google Fonts. Using default Caveat font.');
return GoogleFonts.caveat(fontSize: fontSize, height: height);
}
}
@override
Widget build(BuildContext context) {
// Parse the message to extract text and font
final parsedMessage = _parseHtmlMessage(message);
final messageText = parsedMessage['text'] ?? '';
final fontFamily = parsedMessage['fontFamily'] ?? '';
return Transform.scale(
scale: scale,
child: Container(
@@ -58,11 +128,8 @@ class BackCardWidget extends StatelessWidget {
padding: EdgeInsets.fromLTRB(22.w, 22.h, 18.w, 18.h),
child: SingleChildScrollView(
child: Text(
message,
style: GoogleFonts.caveat(
fontSize: 16.sp,
height: 1.7,
),
messageText,
style: _getFontStyle(fontFamily, 16.sp, 1.7),
),
),
),