20 Commits

Author SHA1 Message Date
9d962b3bde first commit 2026-03-27 11:29:45 +05:30
c12464e89a Merge remote-tracking branch 'origin/raj' into Anuj
# Conflicts:
#	ios/Podfile.lock
#	ios/Runner/Info.plist
2026-03-24 17:03:07 +05:30
aeeb1c27e0 first commit 2026-03-24 16:56:07 +05:30
c06c844210 bug fixes done 2026-03-24 13:21:37 +05:30
9d27389bf2 bug fixes done 2026-03-23 19:25:32 +05:30
d1038e846e bug fixes done 2026-03-20 18:08:52 +05:30
177f891a31 added check in and Qr scaner and bookng with api and more fixes and changes 2026-03-17 17:07:14 +05:30
adc737a6af magic itinerary api intigration and more fixes 2026-03-13 11:11:53 +05:30
265bddc784 added create magic itinerary with api and more and bug fixes 2026-03-05 19:02:22 +05:30
60486e737a bug fixes 2026-02-26 15:54:57 +05:30
77aba2f1a0 added fixes of city catds word 2026-02-26 11:29:06 +05:30
06e60cfd57 my cart with postcads added and more fixes. 2026-02-26 10:20:34 +05:30
f59b14bec7 bug fixes and ui updates and my passses cart updated. 2026-02-20 18:50:28 +05:30
cbe03f21b4 pull taken from shreeyash 2026-02-17 15:24:10 +05:30
a80a0ac790 bug fixes and more 2026-02-17 15:18:45 +05:30
80b724d6d4 bug fixes and more 2026-02-16 19:13:08 +05:30
46906b04f4 Merge remote-tracking branch 'origin/raj' into Anuj 2026-02-13 20:06:48 +05:30
8f7a68edbc first commit 2026-02-13 20:06:04 +05:30
eb9ca9299e Merge remote-tracking branch 'origin/raj' into Anuj 2026-02-06 19:11:29 +05:30
e15a979c0c first commit 2026-02-06 19:11:09 +05:30
220 changed files with 15077 additions and 7924 deletions

View File

@@ -1,7 +1,7 @@
# citycards_customer # citycards_customer
A new Flutter project. A new Flutter project.
## Getting Started ## Getting Started
This project is a starting point for a Flutter application. This project is a starting point for a Flutter application.

View File

@@ -7,6 +7,8 @@
<application <application
android:label="CityCard Customer" android:label="CityCard Customer"
android:name="${applicationName}" android:name="${applicationName}"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/downlaod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/icons/love_them.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/maybe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
assets/icons/no_kids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/icons/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/card_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -0,0 +1,141 @@
[{
"version": "1.0",
"image": {
"name": "frames/frame002.png",
"baseName": "frame002.png",
"permissions": 664,
"format": "PNG",
"formatDescription": "Portable Network Graphics",
"mimeType": "image/png",
"class": "DirectClass",
"geometry": {
"width": 1868,
"height": 3840,
"x": 0,
"y": 0
},
"resolution": {
"x": 370753,
"y": 370798
},
"printSize": {
"x": 0.00503839,
"y": 0.010356
},
"units": "Undefined",
"type": "TrueColor",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 8,
"baseDepth": 8,
"channelDepth": {
"red": 8,
"green": 8,
"blue": 1
},
"pixels": 7173120,
"imageStatistics": {
"Overall": {
"min": 67,
"max": 255,
"mean": 142.829,
"median": 140,
"standardDeviation": 17.1849,
"kurtosis": 37.2771,
"skewness": 4.24387,
"entropy": 0.291301
}
},
"channelStatistics": {
"red": {
"min": 174,
"max": 255,
"mean": 237.888,
"median": 238,
"standardDeviation": 2.65253,
"kurtosis": 41.5763,
"skewness": 0.61346,
"entropy": 0.338084
},
"green": {
"min": 73,
"max": 255,
"mean": 94.2729,
"median": 90,
"standardDeviation": 24.5069,
"kurtosis": 35.19,
"skewness": 6.06676,
"entropy": 0.237928
},
"blue": {
"min": 67,
"max": 255,
"mean": 96.325,
"median": 92,
"standardDeviation": 24.3954,
"kurtosis": 35.0649,
"skewness": 6.05138,
"entropy": 0.297891
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1868,
"height": 3840,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"scene": 1,
"scenes": 2,
"compression": "Zip",
"orientation": "Undefined",
"properties": {
"date:create": "2026-02-18T13:36:29+00:00",
"date:modify": "2026-02-18T13:36:29+00:00",
"date:timestamp": "2026-02-18T13:36:29+00:00",
"png:IHDR.bit-depth-orig": "8",
"png:IHDR.bit_depth": "8",
"png:IHDR.color-type-orig": "2",
"png:IHDR.color_type": "2 (Truecolor)",
"png:IHDR.interlace_method": "0 (Not interlaced)",
"png:IHDR.width,height": "1868, 3840",
"png:pHYs": "x_res=370753, y_res=370798, units=0",
"signature": "7fb181e6439aa51f6eb134a4991711167b5850e80e40ae5cb0c67cf29c118dfe"
},
"tainted": false,
"filesize": "3422B",
"numberPixels": "7.17312M",
"pixelsPerSecond": "2.74974MB",
"userTime": "2.880u",
"elapsedTime": "0:03.608",
"version": "ImageMagick 7.1.1-41 Q16-HDRI x86_64 22504 https://imagemagick.org"
}
}]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,6 @@
PODS: PODS:
- app_links (7.0.0):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_angle (0.3.8): - flutter_angle (0.3.8):
- Flutter - Flutter
@@ -7,6 +9,8 @@ PODS:
- flutter_native_splash (2.4.3): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- FlutterAngle (0.0.8) - FlutterAngle (0.0.8)
- geocoding_ios (1.0.5):
- Flutter
- geolocator_apple (1.2.0): - geolocator_apple (1.2.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -16,83 +20,287 @@ PODS:
- Flutter - Flutter
- Google-Maps-iOS-Utils (< 7.0, >= 5.0) - Google-Maps-iOS-Utils (< 7.0, >= 5.0)
- GoogleMaps (< 10.0, >= 8.4) - GoogleMaps (< 10.0, >= 8.4)
- google_mlkit_commons (0.11.1):
- Flutter
- MLKitVision (~> 10.0.0)
- google_mlkit_translation (0.13.1):
- Flutter
- google_mlkit_commons
- GoogleMLKit/Translate (~> 9.0.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMaps (9.4.0): - GoogleMaps (9.4.0):
- GoogleMaps/Maps (= 9.4.0) - GoogleMaps/Maps (= 9.4.0)
- GoogleMaps/Maps (9.4.0) - GoogleMaps/Maps (9.4.0)
- GoogleMLKit/MLKitCore (9.0.0):
- MLKitCommon (~> 14.0.0)
- GoogleMLKit/Translate (9.0.0):
- GoogleMLKit/MLKitCore
- MLKitTranslate (~> 8.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleToolboxForMac/StringEncoding (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- MLImage (1.0.0-beta8)
- MLKitCommon (14.0.0):
- GoogleDataTransport (~> 10.0)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitNaturalLanguage (10.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleToolboxForMac/StringEncoding (< 5.0, >= 4.2.1)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitCommon (~> 14.0)
- MLKitTranslate (8.0.0):
- MLKitNaturalLanguage (~> 10.0)
- SSZipArchive (< 3.0, >= 2.5.5)
- MLKitVision (10.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta8)
- MLKitCommon (~> 14.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- PromisesObjC (2.4.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SSZipArchive (2.6.0)
- Stripe (25.0.1):
- StripeApplePay (= 25.0.1)
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripePaymentsUI (= 25.0.1)
- StripeUICore (= 25.0.1)
- stripe_ios (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- stripe_ios/stripe_ios (= 0.0.1)
- stripe_ios/stripe_objc (= 0.0.1)
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- stripe_ios/stripe_ios (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- stripe_ios/stripe_objc
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- stripe_ios/stripe_objc (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- StripeApplePay (25.0.1):
- StripeCore (= 25.0.1)
- StripeCore (25.0.1)
- StripeFinancialConnections (25.0.1):
- StripeCore (= 25.0.1)
- StripeUICore (= 25.0.1)
- StripePayments (25.0.1):
- StripeCore (= 25.0.1)
- StripePayments/Stripe3DS2 (= 25.0.1)
- StripePayments/Stripe3DS2 (25.0.1):
- StripeCore (= 25.0.1)
- StripePaymentSheet (25.0.1):
- StripeApplePay (= 25.0.1)
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripePaymentsUI (= 25.0.1)
- StripePaymentsUI (25.0.1):
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripeUICore (= 25.0.1)
- StripeUICore (25.0.1):
- StripeCore (= 25.0.1)
- three_js_sensors (0.1.2): - three_js_sensors (0.1.2):
- Flutter - Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`) - flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- geocoding_ios (from `.symlinks/plugins/geocoding_ios/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
- google_mlkit_translation (from `.symlinks/plugins/google_mlkit_translation/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)
- three_js_sensors (from `.symlinks/plugins/three_js_sensors/ios`) - three_js_sensors (from `.symlinks/plugins/three_js_sensors/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FlutterAngle - FlutterAngle
- Google-Maps-iOS-Utils - Google-Maps-iOS-Utils
- GoogleDataTransport
- GoogleMaps - GoogleMaps
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitCommon
- MLKitNaturalLanguage
- MLKitTranslate
- MLKitVision
- nanopb
- PromisesObjC
- SSZipArchive
- Stripe
- StripeApplePay
- StripeCore
- StripeFinancialConnections
- StripePayments
- StripePaymentSheet
- StripePaymentsUI
- StripeUICore
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_angle: flutter_angle:
:path: ".symlinks/plugins/flutter_angle/darwin" :path: ".symlinks/plugins/flutter_angle/darwin"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
geocoding_ios:
:path: ".symlinks/plugins/geocoding_ios/ios"
geolocator_apple: geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin" :path: ".symlinks/plugins/geolocator_apple/darwin"
google_maps_flutter_ios: google_maps_flutter_ios:
:path: ".symlinks/plugins/google_maps_flutter_ios/ios" :path: ".symlinks/plugins/google_maps_flutter_ios/ios"
google_mlkit_commons:
:path: ".symlinks/plugins/google_mlkit_commons/ios"
google_mlkit_translation:
:path: ".symlinks/plugins/google_mlkit_translation/ios"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
stripe_ios:
:path: ".symlinks/plugins/stripe_ios/ios"
three_js_sensors: three_js_sensors:
:path: ".symlinks/plugins/three_js_sensors/ios" :path: ".symlinks/plugins/three_js_sensors/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_angle: 7b1a2b3e733221bf2e0325e42fc3edf95b5d44c4 flutter_angle: fc44e198cea1f07e1a5919bad1484049fab65c96
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18 FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e geocoding_ios: eafacae6ad11a1eb56681f7d11df602a5fd49416
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96 Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264 google_maps_flutter_ios: e31555a04d1986ab130f2b9f24b6cdc861acc6d3
google_mlkit_commons: 1e6ef6605d7281f35baf2b355e6049b9984fd624
google_mlkit_translation: 8a0e84c632121250843e9bab526cb926a2a2a7b7
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
three_js_sensors: f516b092803411e05b1e3dc7625efa36acd8f455 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a MLImage: 0de5c6c2bf9e93b80ef752e2797f0836f03b58c0
MLKitCommon: 47d47b50a031d00db62f1b0efe5a1d8b09a3b2e6
MLKitNaturalLanguage: 498154f2461f97abb00eb161eb773670ffc46250
MLKitTranslate: fcb283a2cbaaa595e1cf1d3fedffcea2e97a1168
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea
Stripe: 4728e3e0dd8df134e4a420ab504e929a93a815f0
stripe_ios: c552a249333c2e810e02539140dba366c7f0683f
StripeApplePay: 43997281ace138a1c75a8f2d7be11925ea28644c
StripeCore: 457c30e2fd3a7c4b274a5ad53d1ff03661eef2a0
StripeFinancialConnections: 8c2e326f767fb014b53174b3a5f8592c0a45fa56
StripePayments: 6955de4298a5265e66f02cffcc7954475ac7f6c8
StripePaymentSheet: 3f93ce6ea84afde770d3c7e18a9b8f99aed63896
StripePaymentsUI: 626726a01255a6458c35436f7f6431dacee82684
StripeUICore: 30f8352fd7a5cf1541b7777a57b3ad1133bf6763
three_js_sensors: ab5f24fbeb97ab5c5ce2978c3e63a25d67a076f5
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86
PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506 PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506

View File

@@ -7,15 +7,15 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
60A4FC1A895BADD3C1597C0B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C9496C6E1D2E0AEA2083C14C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D485254B2B32B84B17D20864 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -44,13 +44,13 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; 6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -61,10 +61,10 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; 9EEFD1F245CF2AAD027ADE1E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; D485254B2B32B84B17D20864 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; D719B3676BADD267F44F4A59 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; D8B9C9F2F3A8FEF639B5A528 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -72,7 +72,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */, C9496C6E1D2E0AEA2083C14C /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -80,13 +80,22 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */, 60A4FC1A895BADD3C1597C0B /* Pods_RunnerTests.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
227A5CBF4270AAC4960A7CAF /* Frameworks */ = {
isa = PBXGroup;
children = (
D485254B2B32B84B17D20864 /* Pods_Runner.framework */,
6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = { 331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -95,24 +104,15 @@
path = RunnerTests; path = RunnerTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5D45FB84C63476582408C414 /* Frameworks */ = {
isa = PBXGroup;
children = (
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */,
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
6D4A73F1E55857ADBD000C6A /* Pods */ = { 6D4A73F1E55857ADBD000C6A /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */, 9EEFD1F245CF2AAD027ADE1E /* Pods-Runner.debug.xcconfig */,
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */, D8B9C9F2F3A8FEF639B5A528 /* Pods-Runner.release.xcconfig */,
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */, D719B3676BADD267F44F4A59 /* Pods-Runner.profile.xcconfig */,
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */, 6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */,
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */, 18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */,
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */, 5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -136,7 +136,7 @@
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
6D4A73F1E55857ADBD000C6A /* Pods */, 6D4A73F1E55857ADBD000C6A /* Pods */,
5D45FB84C63476582408C414 /* Frameworks */, 227A5CBF4270AAC4960A7CAF /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -171,7 +171,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */, FCD91A1B9B088634E06F7C98 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
CF8A29BE993C0C902CB143AF /* Frameworks */, CF8A29BE993C0C902CB143AF /* Frameworks */,
@@ -190,15 +190,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */, 8B4387D9368AF1A601B17194 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */, CA082AA85EC21FFAA42CE12F /* [CP] Embed Pods Frameworks */,
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */, 3F2F0E04CC81D63DDD8C37A9 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -270,7 +270,40 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
3F2F0E04CC81D63DDD8C37A9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
8B4387D9368AF1A601B17194 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -292,23 +325,22 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Run Script";
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
}; };
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */ = { CA082AA85EC21FFAA42CE12F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -325,22 +357,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { FCD91A1B9B088634E06F7C98 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -362,23 +379,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -515,7 +515,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -533,7 +533,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = 18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -549,7 +549,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = 5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -1,59 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>NSCameraUsageDescription</key>
<string>Citycards Customer</string> <string>Scan your card to add it automatically</string>
<key>CFBundleExecutable</key> <key>NSCameraUsageDescription</key>
<string>$(EXECUTABLE_NAME)</string> <string>To scan cards</string>
<key>CFBundleIdentifier</key> <key>CFBundleDisplayName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>Citycards Customer</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleExecutable</key>
<string>6.0</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleName</key> <key>CFBundleIdentifier</key>
<string>citycards_customer</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundlePackageType</key> <key>CFBundleInfoDictionaryVersion</key>
<string>APPL</string> <string>6.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleName</key>
<string>1.0.0</string> <string>citycards_customer</string>
<key>CFBundleSignature</key> <key>CFBundlePackageType</key>
<string>????</string> <string>APPL</string>
<key>CFBundleVersion</key> <key>CFBundleShortVersionString</key>
<string>3</string> <string>1.0.0</string>
<key>LSRequiresIPhoneOS</key> <key>CFBundleSignature</key>
<true/> <string>????</string>
<key>NSCameraUsageDescription</key> <key>CFBundleVersion</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string> <string>3</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>LSApplicationCategoryType</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string> <string></string>
<key>NSLocationWhenInUseUsageDescription</key> <key>LSRequiresIPhoneOS</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string> <true/>
<key>NSPhotoLibraryUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string> <string>To scan cards</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<true/> <string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>UILaunchStoryboardName</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>LaunchScreen</string> <string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>UIMainStoryboardFile</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Main</string> <string>We need access to your camera for taking photos for profile and to build a postcard.</string>
<key>UIStatusBarHidden</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<false/> <true/>
<key>UISupportedInterfaceOrientations</key> <key>UILaunchStoryboardName</key>
<array> <string>LaunchScreen</string>
<string>UIInterfaceOrientationPortrait</string> <key>UIMainStoryboardFile</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>Main</string>
<string>UIInterfaceOrientationLandscapeRight</string> <key>UIStatusBarHidden</key>
</array> <false/>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeRight</string> </array>
</array> <key>UISupportedInterfaceOrientations~ipad</key>
</dict> <array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist> </plist>

View File

@@ -58,9 +58,10 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
paymentIntentClientSecret: clientSecret, paymentIntentClientSecret: clientSecret,
merchantDisplayName: "CityCards", merchantDisplayName: "CityCards",
style: ThemeMode.light, style: ThemeMode.light,
allowsDelayedPaymentMethods: true,
), ),
); );
await Stripe.instance.presentPaymentSheet();
emit(const StripePaymentSheetReady()); emit(const StripePaymentSheetReady());
emit(const StripePaymentLoading( emit(const StripePaymentLoading(
@@ -105,6 +106,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
paymentIntentClientSecret: event.clientSecret, paymentIntentClientSecret: event.clientSecret,
merchantDisplayName: "CityCards", merchantDisplayName: "CityCards",
style: ThemeMode.light, style: ThemeMode.light,
allowsDelayedPaymentMethods: true,
), ),
); );

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../bloc/stripe_payment_bloc.dart'; import '../bloc/stripe_payment_bloc.dart';
import '../bloc/stripe_payment_event.dart'; import '../bloc/stripe_payment_event.dart';
import '../bloc/stripe_payment_state.dart'; import '../bloc/stripe_payment_state.dart';
@@ -346,6 +345,7 @@ class StripePaymentScreen extends StatelessWidget {
return Column( return Column(
children: [ children: [
CircularProgressIndicator( CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 3, strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor), valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
), ),

View File

@@ -2,9 +2,11 @@ import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_textfield.dart'; import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:country_code_picker/country_code_picker.dart'; // ✅ NEW IMPORT
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:phone_numbers_parser/phone_numbers_parser.dart'; // ✅ NEW IMPORT
import '../checkout/bloc/pass_purchase_details_bloc.dart'; import '../checkout/bloc/pass_purchase_details_bloc.dart';
import '../checkout/bloc/pass_purchase_details_event.dart'; import '../checkout/bloc/pass_purchase_details_event.dart';
@@ -25,29 +27,46 @@ class _AddDetailsViewState extends State<AddDetailsView> {
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final TextEditingController cityController = TextEditingController(); final TextEditingController cityController = TextEditingController();
String? selectedCountry; final TextEditingController countryController = TextEditingController();
String _selectedIsdCode = '+61'; // ✅ NEW: tracks selected country dial code
@override @override
void dispose() { void dispose() {
firstNameController.dispose(); firstNameController.dispose();
lastNameController.dispose(); lastNameController.dispose();
emailController.dispose(); emailController.dispose();
countryController.dispose();
phoneController.dispose(); phoneController.dispose();
cityController.dispose(); cityController.dispose();
super.dispose(); super.dispose();
} }
bool _isValidEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return emailRegex.hasMatch(email);
}
// ✅ UPDATED: now validates phone using phone_numbers_parser against the selected ISD code
bool _isValidPhone(String phone) {
try {
final fullNumber = '$_selectedIsdCode$phone';
final parsed = PhoneNumber.parse(fullNumber);
return parsed.isValid();
} catch (_) {
return false;
}
}
void _handleSubmit(BuildContext context, bool isSubmitting) { void _handleSubmit(BuildContext context, bool isSubmitting) {
// If already submitting, do nothing
if (isSubmitting) return; if (isSubmitting) return;
// Validate inputs
if (firstNameController.text.isEmpty || if (firstNameController.text.isEmpty ||
lastNameController.text.isEmpty || lastNameController.text.isEmpty ||
emailController.text.isEmpty || emailController.text.isEmpty ||
phoneController.text.isEmpty || phoneController.text.isEmpty ||
cityController.text.isEmpty || cityController.text.isEmpty ||
selectedCountry == null) { countryController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Please fill all fields'), content: Text('Please fill all fields'),
@@ -57,17 +76,38 @@ class _AddDetailsViewState extends State<AddDetailsView> {
return; return;
} }
// Submit gift details if (!_isValidEmail(emailController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter a valid email address'),
backgroundColor: Colors.red,
),
);
return;
}
// ✅ UPDATED: error message now shows the selected ISD code
if (!_isValidPhone(phoneController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Enter a valid phone number for $_selectedIsdCode'),
backgroundColor: Colors.red,
),
);
return;
}
context.read<PurchaseDetailsBloc>().add( context.read<PurchaseDetailsBloc>().add(
SubmitUserDetailsEvent( SubmitUserDetailsEvent(
bookingId: widget.bookingId, bookingId: widget.bookingId,
isForSelf: false, isForSelf: false,
recipientFirstName: firstNameController.text, recipientFirstName: firstNameController.text,
recipientLastName: lastNameController.text, recipientLastName: lastNameController.text,
isdCode: _selectedIsdCode,
recipientEmail: emailController.text, recipientEmail: emailController.text,
recipientPhone: phoneController.text, recipientPhone: phoneController.text,
city: cityController.text, city: cityController.text,
country: selectedCountry!, country: countryController.text,
), ),
); );
} }
@@ -78,21 +118,10 @@ class _AddDetailsViewState extends State<AddDetailsView> {
create: (_) => PurchaseDetailsBloc(), create: (_) => PurchaseDetailsBloc(),
child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>( child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>(
listener: (context, state) { listener: (context, state) {
// Handle API submission success
if (state is PurchaseDetailsSubmitted) { if (state is PurchaseDetailsSubmitted) {
// Show success message
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Gift details submitted successfully!'),
// backgroundColor: Color(0xffF95F62),
// ),
// );
// Navigate back
Navigator.of(context).pop('success'); Navigator.of(context).pop('success');
} }
// Handle API submission error
if (state is PurchaseDetailsError) { if (state is PurchaseDetailsError) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -151,106 +180,100 @@ class _AddDetailsViewState extends State<AddDetailsView> {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "First Name", label: "First Name *",
hint: "Enter recipient's first name", hint: "Enter recipient's first name",
controller: firstNameController, controller: firstNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
), ),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Last Name", label: "Last Name *",
hint: "Enter recipient's last name", hint: "Enter recipient's last name",
controller: lastNameController, controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
), ),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Email", label: "Email *",
hint: "Enter recipient's email address", hint: "Enter recipient's email address",
controller: emailController, controller: emailController,
keyboardType: TextInputType.emailAddress,
), ),
), ),
// ✅ NEW: Phone field with CountryCodePicker (replaces plain CustomTextField)
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Phone Number", label: "Phone Number *",
hint: "Enter recipient's phone number", hint: "Enter phone number",
controller: phoneController, controller: phoneController,
keyboardType: TextInputType.phone,
maxLength: 12,
numbersOnly: true,
prefixWidget: CountryCodePicker(
onChanged: (country) {
setState(() => _selectedIsdCode = country.dialCode!);
},
initialSelection: 'AU',
favorite: const ['+61', '+1', '+44', '+91'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
flagWidth: 24.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
textStyle: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF2D3134),
),
dialogTextStyle: TextStyle(fontSize: 14.sp),
searchDecoration: const InputDecoration(
hintText: 'Search country...',
prefixIcon: Icon(Icons.search),
),
),
), ),
), ),
// ✅ END of new phone field
SizedBox(height: 8.h),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "City", label: "City *",
hint: "Enter the name of the city", hint: "Enter the name of the city",
controller: cityController, controller: cityController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
), ),
), ),
Padding( Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column( child: CustomTextField(
crossAxisAlignment: CrossAxisAlignment.start, label: "Country *",
children: [ hint: "Enter country name",
CustomText(text: "Country", size: 14.sp), controller: countryController,
SizedBox(height: 6.h), maxLength: 50,
Container( onlyLetters: true,
height: 42.h, isFirstLetterCapital: true,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select country",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedCountry = value;
});
},
items: ["Australia"]
.map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
), ),
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
// Option 1: Pass empty function when disabled (doesn't change button appearance)
CustomFilledButton( CustomFilledButton(
onTap: () => _handleSubmit(context, isSubmitting), onTap: () => _handleSubmit(context, isSubmitting),
label: isSubmitting ? "Submitting..." : "Continue", label: isSubmitting ? "Submitting..." : "Continue",

View File

@@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import '../../core/route_constants.dart'; import '../../core/route_constants.dart';
import '../bloc/attraction_details_bloc.dart'; import '../bloc/attraction_details_bloc.dart';
@@ -33,7 +34,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Center( body: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(color: Color(0xffF95F62)),
), ),
); );
} }
@@ -150,12 +151,9 @@ class AttractionDetailsView extends StatelessWidget {
right: 17.w, right: 17.w,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
showModalBottomSheet( Share.share(
context: context, 'www.google.com',
isScrollControlled: true, subject: 'Check this out',
backgroundColor: Colors.transparent,
builder: (context) =>
const ShareBottomSheet(),
); );
}, },
child: Container( child: Container(
@@ -174,7 +172,7 @@ class AttractionDetailsView extends StatelessWidget {
), ),
), ),
), ),
), )
], ],
), ),

View File

@@ -7,9 +7,9 @@ import 'attractions_state.dart';
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> { class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
final AttractionsRepository repository; final AttractionsRepository repository;
AttractionsBloc({required this.repository}) AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
: super(AttractionsInitial()) {
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory); on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
on<SearchAttractions>(_onSearchAttractions);
} }
Future<void> _onFetchAttractionsByCategory( Future<void> _onFetchAttractionsByCategory(
@@ -21,22 +21,50 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
try { try {
final AttractionsResponse response = final AttractionsResponse response =
await repository.fetchAttractionsByCategory( await repository.fetchAttractionsByCategory(
categoryXid: event.categoryXid, // Can be null now categoryXid: event.categoryXid,
); );
final allAttractions = response.attractions ?? [];
emit( emit(
AttractionsLoaded( AttractionsLoaded(
attractions: response.attractions ?? [], attractions: allAttractions,
allAttractions: allAttractions,
categories: response.categories ?? [], categories: response.categories ?? [],
selectedCategoryId: event.categoryXid, // Can be null selectedCategoryId: event.categoryXid,
searchQuery: '',
), ),
); );
} catch (e) { } catch (e) {
emit( emit(AttractionsError(e.toString()));
AttractionsError(
e.toString(),
),
);
} }
} }
void _onSearchAttractions(
SearchAttractions event,
Emitter<AttractionsState> emit,
) {
final currentState = state;
if (currentState is! AttractionsLoaded) return;
final query = event.query.trim().toLowerCase();
final filtered = query.isEmpty
? currentState.allAttractions
: currentState.allAttractions.where((attraction) {
final name = (attraction.title ?? '').toLowerCase();
final description = (attraction.description ?? '').toLowerCase();
return name.contains(query) || description.contains(query);
}).toList();
emit(
AttractionsLoaded(
attractions: filtered,
allAttractions: currentState.allAttractions,
categories: currentState.categories,
selectedCategoryId: currentState.selectedCategoryId,
searchQuery: event.query,
),
);
}
} }

View File

@@ -8,10 +8,19 @@ abstract class AttractionsEvent extends Equatable {
} }
class FetchAttractionsByCategory extends AttractionsEvent { class FetchAttractionsByCategory extends AttractionsEvent {
final int? categoryXid; // Make it nullable final int? categoryXid;
const FetchAttractionsByCategory({this.categoryXid}); // Remove required const FetchAttractionsByCategory({this.categoryXid});
@override @override
List<Object?> get props => [categoryXid]; List<Object?> get props => [categoryXid];
}
class SearchAttractions extends AttractionsEvent {
final String query;
const SearchAttractions(this.query);
@override
List<Object?> get props => [query];
} }

View File

@@ -14,17 +14,27 @@ class AttractionsLoading extends AttractionsState {}
class AttractionsLoaded extends AttractionsState { class AttractionsLoaded extends AttractionsState {
final List<Attraction> attractions; final List<Attraction> attractions;
final List<Attraction> allAttractions; // Keep full list for local filtering
final List<Category> categories; final List<Category> categories;
final int? selectedCategoryId; // Make it nullable final int? selectedCategoryId;
final String searchQuery;
const AttractionsLoaded({ const AttractionsLoaded({
required this.attractions, required this.attractions,
required this.allAttractions,
required this.categories, required this.categories,
this.selectedCategoryId, // Remove required this.selectedCategoryId,
this.searchQuery = '',
}); });
@override @override
List<Object?> get props => [attractions, categories, selectedCategoryId]; List<Object?> get props => [
attractions,
allAttractions,
categories,
selectedCategoryId,
searchQuery,
];
} }
class AttractionsError extends AttractionsState { class AttractionsError extends AttractionsState {

View File

@@ -56,8 +56,7 @@ class AttractionsPage extends StatelessWidget {
hint: "Search attractions...", hint: "Search attractions...",
hintColor: Colors.grey.shade500, hintColor: Colors.grey.shade500,
onChanged: (value) { onChanged: (value) {
// ❌ Search logic intentionally disabled bloc.add(SearchAttractions(value));
// UI only, no API call
}, },
), ),
@@ -106,7 +105,7 @@ class AttractionsPage extends StatelessWidget {
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 60), padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(), child: CircularProgressIndicator(color: Color(0xffF95F62)),
), ),
) )
else if (state is AttractionsLoaded) else if (state is AttractionsLoaded)

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@@ -42,12 +43,13 @@ class AttractionCard extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
child: imageUrl.isNotEmpty child: imageUrl.isNotEmpty
? Image.network( ? CachedNetworkImage(
imageUrl, imageUrl: imageUrl,
height: 94.h, height: 94.h,
width: 94.w, width: 94.w,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageFallback(), placeholder: (context, url) => _imageFallback(),
errorWidget: (_, __, ___) => _imageFallback(),
) )
: _imageFallback(), : _imageFallback(),
), ),
@@ -88,7 +90,7 @@ class AttractionCard extends StatelessWidget {
TextSpan( TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: "from \$${attraction.ticketPriceAdult}", text: "\$${attraction.ticketPriceAdult}",
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@@ -20,7 +20,18 @@ class BuyPassBloc extends Bloc<BuyPassEvent, BuyPassState> {
on<UpdateChildCount>(_onUpdateChildCount); on<UpdateChildCount>(_onUpdateChildCount);
/// Handle update validity duration event /// Handle update validity duration event
on<UpdateValidityDuration>(_onUpdateValidityDuration); // ✅ Added on<UpdateValidityDuration>(_onUpdateValidityDuration);
on<AddToCartLoading>((event, emit) {
if (state is BuyPassLoaded) {
emit((state as BuyPassLoaded).copyWith(isAddingToCart: true));
}
});
on<AddToCartDone>((event, emit) {
if (state is BuyPassLoaded) {
emit((state as BuyPassLoaded).copyWith(isAddingToCart: false));
}
});// ✅ Added
} }
/// Fetch buy pass data from repository /// Fetch buy pass data from repository

View File

@@ -29,4 +29,6 @@ class UpdateValidityDuration extends BuyPassEvent {
final int duration; final int duration;
UpdateValidityDuration(this.duration); UpdateValidityDuration(this.duration);
} }
class AddToCartLoading extends BuyPassEvent {}
class AddToCartDone extends BuyPassEvent {}

View File

@@ -14,15 +14,17 @@ class BuyPassLoaded extends BuyPassState {
final int selectedCardIndex; final int selectedCardIndex;
final int adultCount; final int adultCount;
final int childCount; final int childCount;
final int validityDuration; // ✅ Added final int validityDuration;
final bool isAddingToCart;
BuyPassLoaded({ BuyPassLoaded({
required this.data, required this.data,
this.selectedCardIndex = 0, this.selectedCardIndex = 0,
this.adultCount = 1, this.adultCount = 1,
this.childCount = 1, this.childCount = 1,
int? validityDuration, // ✅ Added as optional parameter int? validityDuration,
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; // ✅ Initialize with minNumber this.isAddingToCart = false, // ✅ default false, NOT required
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber;
/// Method to copy state with updated values /// Method to copy state with updated values
BuyPassLoaded copyWith({ BuyPassLoaded copyWith({
@@ -30,14 +32,16 @@ class BuyPassLoaded extends BuyPassState {
int? selectedCardIndex, int? selectedCardIndex,
int? adultCount, int? adultCount,
int? childCount, int? childCount,
int? validityDuration, // ✅ Added int? validityDuration,
bool? isAddingToCart,
}) { }) {
return BuyPassLoaded( return BuyPassLoaded(
data: data ?? this.data, data: data ?? this.data,
selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex, selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex,
adultCount: adultCount ?? this.adultCount, adultCount: adultCount ?? this.adultCount,
childCount: childCount ?? this.childCount, childCount: childCount ?? this.childCount,
validityDuration: validityDuration ?? this.validityDuration, // ✅ Added validityDuration: validityDuration ?? this.validityDuration,
isAddingToCart: isAddingToCart ?? this.isAddingToCart,
); );
} }
@@ -47,7 +51,8 @@ class BuyPassLoaded extends BuyPassState {
/// Calculate total price /// Calculate total price
double get totalPrice { double get totalPrice {
final card = selectedCard; final card = selectedCard;
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) * validityDuration.toDouble(); // ✅ Multiply by validityDuration return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) *
validityDuration.toDouble();
} }
} }

View File

@@ -26,9 +26,25 @@ class BuyPassView extends StatelessWidget {
} }
} }
class BuyPassContent extends StatelessWidget { class BuyPassContent extends StatefulWidget {
const BuyPassContent({super.key}); const BuyPassContent({super.key});
@override
State<BuyPassContent> createState() => _BuyPassContentState();
}
class _BuyPassContentState extends State<BuyPassContent> {
late PageController _pageController;@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 0.85);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -92,58 +108,49 @@ class BuyPassContent extends StatelessWidget {
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0.w), padding: EdgeInsets.symmetric(horizontal: 20.0.w),
child: Row( child: GestureDetector(
children: [ onTap: () {
GestureDetector( Navigator.pop(context);
onTap: () { },
Navigator.pop(context); child: Row(
}, children: [
child: const Icon(Icons.arrow_back), const Icon(Icons.arrow_back),
), SizedBox(width: 8.w),
SizedBox(width: 8.w), CustomText(text: "Buy a Card", size: 12.sp),
CustomText(text: "Buy a Pass", size: 12.sp), ],
], ),
), ),
), ),
SizedBox(height: 22.h), SizedBox(height: 22.h),
// Pass Cards Horizontal List // Pass Cards Horizontal List
Padding( // Pass Cards Horizontal List
padding: EdgeInsets.only(left: 20.0.w), SizedBox(
child: SingleChildScrollView( height: 140.h,
scrollDirection: Axis.horizontal, child: PageView.builder(
child: Row( controller: PageController(viewportFraction: 0.92),
children: List.generate( itemCount: data.cards.length,
data.cards.length, onPageChanged: (index) {
(index) { context.read<BuyPassBloc>().add(ChangeSelectedCard(index));
final card = data.cards[index]; },
final isSelected = index == state.selectedCardIndex; itemBuilder: (context, index) {
final card = data.cards[index];
return GestureDetector( return Padding(
onTap: () { padding: EdgeInsets.symmetric(horizontal: 8.w),
context.read<BuyPassBloc>().add( child: PassCardView(
ChangeSelectedCard(index), themeColor: card.cardType.name == "selective_pass"
); ? const Color(0xFFF95FAF)
}, : const Color(0xFFF95F62),
child: Padding( city: data.city.name,
padding: EdgeInsets.only(right: 12.w), heroImage: data.city.heroBanner.image,
child: PassCardView( adultPrice: card.adultPrice,
themeColor: isSelected childPrice: card.childPrice,
? Color(0xFFF97316) cardType: card.cardType.displayName,
: Color(0xFF1E8AF6), description: card.description,
city: data.city.name, isSelected: false,
heroImage: data.city.heroBanner.image, ),
adultPrice: card.adultPrice, );
childPrice: card.childPrice, },
cardType: card.cardType.displayName,
description: card.description,
isSelected: isSelected,
),
),
);
},
),
),
), ),
), ),
@@ -159,9 +166,9 @@ class BuyPassContent extends StatelessWidget {
heroImage: data.city.heroBanner.image, heroImage: data.city.heroBanner.image,
cardType: selectedCard.cardType.name, cardType: selectedCard.cardType.name,
cardDisplayName: selectedCard.cardType.displayName, cardDisplayName: selectedCard.cardType.displayName,
themeColor: state.selectedCardIndex == 0 themeColor: selectedCard.cardType.name == "selective_pass"
? Color(0xFFF97316) ? Color(0xFFF95FAF) // pink for flexi/selective pass
: Color(0xFF1E8AF6), : Color(0xFFF95F62),
adultPrice: selectedCard.adultPrice.toDouble(), adultPrice: selectedCard.adultPrice.toDouble(),
childPrice: selectedCard.childPrice.toDouble(), childPrice: selectedCard.childPrice.toDouble(),
adults: state.adultCount, adults: state.adultCount,
@@ -209,7 +216,7 @@ class BuyPassContent extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
CustomText(text: "Card Offers", size: 18.sp), CustomText(text: "Member Privileges", size: 18.sp),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.pushNamed( Navigator.pushNamed(
@@ -246,12 +253,12 @@ class BuyPassContent extends StatelessWidget {
final offer = selectedCard.offers[index]; final offer = selectedCard.offers[index];
return GestureDetector( return GestureDetector(
onTap: () { // onTap: () {
Navigator.of(context).pushNamed( // Navigator.of(context).pushNamed(
RouteConstants.offerPassDetail, // RouteConstants.offerPassDetail,
arguments: offer.id, // ✅ pass offerId // arguments: offer.id, // ✅ pass offerId
); // );
}, // },
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 6.w, horizontal: 6.w,
@@ -344,7 +351,7 @@ class BuyPassContent extends StatelessWidget {
text: offer.description??"N/A", text: offer.description??"N/A",
color: Colors.black.withOpacity(.6), color: Colors.black.withOpacity(.6),
size: 12.sp, size: 12.sp,
maxLines: 2, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
@@ -426,7 +433,7 @@ class BuyPassContent extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: 20.w, width: 20.w,
height: 20.w, height: 20.w,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(color: Color(0xffF95F62),strokeWidth: 2),
), ),
); );
}, },

View File

@@ -5,7 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
class PassCardView extends StatelessWidget { class PassCardView extends StatelessWidget {
final Color? themeColor; final Color? themeColor;
final String? city; final String? city;
final String? heroImage; // ✅ heroBanner.image from API final String? heroImage;
final num? adultPrice; final num? adultPrice;
final num? childPrice; final num? childPrice;
final String? cardType; final String? cardType;
@@ -31,140 +31,143 @@ class PassCardView extends StatelessWidget {
color: Colors.white, color: Colors.white,
border: Border.all( border: Border.all(
color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24), color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24),
width: isSelected ? 2 : 1, width: 1,
), ),
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( /// -------- LEFT: IMAGE + DETAILS --------
children: [ Expanded(
/// -------- HERO BANNER IMAGE -------- child: Row(
ClipRRect( children: [
borderRadius: BorderRadius.only( /// HERO BANNER IMAGE
topLeft: Radius.circular(8.r), ClipRRect(
bottomLeft: Radius.circular(8.r), borderRadius: BorderRadius.only(
), topLeft: Radius.circular(8.r),
child: Container( bottomLeft: Radius.circular(8.r),
width: 103.w, ),
height: 140.h, child: Container(
color: Colors.grey[200], width: 103.w,
child: heroImage != null && heroImage!.isNotEmpty height: 140.h,
? Image.network( color: Colors.grey[200],
heroImage!, child: heroImage != null && heroImage!.isNotEmpty
fit: BoxFit.cover, ? Image.network(
errorBuilder: (context, error, stackTrace) { heroImage!,
return _fallbackIcon(); fit: BoxFit.cover,
}, errorBuilder: (context, error, stackTrace) {
loadingBuilder: (context, child, loadingProgress) { return _fallbackIcon();
if (loadingProgress == null) return child; },
return Center( loadingBuilder: (context, child, loadingProgress) {
child: SizedBox( if (loadingProgress == null) return child;
width: 24.w, return Center(
height: 24.w, child: SizedBox(
child: const CircularProgressIndicator( width: 24.w,
strokeWidth: 2, height: 24.w,
child: const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
), ),
), );
); },
}, )
) : _fallbackIcon(),
: _fallbackIcon(), ),
), ),
),
SizedBox(width: 6.66.w), SizedBox(width: 6.66.w),
/// -------- CARD DETAILS -------- /// CARD DETAILS
Column( Flexible(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisAlignment: MainAxisAlignment.center,
CustomText(
text: city ?? "City",
weight: FontWeight.w500,
size: 16.sp,
),
/// Adult Price
Row(
children: [ children: [
Text( CustomText(
"From ", text: city ?? "City",
style: TextStyle( weight: FontWeight.w500,
color: Colors.black.withOpacity(0.6), size: 16.sp,
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
), ),
Text(
"\$${adultPrice ?? 0}", /// Adult Price
style: TextStyle( Row(
color: themeColor, children: [
fontWeight: FontWeight.w500, Text(
fontSize: 24.sp, "From ",
), style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${adultPrice ?? 0}",
style: TextStyle(
color:Color(0xFFF95F62),
fontWeight: FontWeight.w800,
fontSize: 24.sp,
),
),
Text(
" /Adult",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
), ),
Text(
" /Adult", /// Child Price
style: TextStyle( Row(
color: Colors.black.withOpacity(0.8), children: [
fontSize: 11.sp, Text(
fontWeight: FontWeight.w400, "and ",
), style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${childPrice ?? 0}",
style: TextStyle(
color:Color(0xFFF95F62),
fontWeight: FontWeight.w800,
fontSize: 24.sp,
),
),
Text(
" /child",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
/// Description
CustomText(
text: description ??
"Dive into an extensive selection of thrilling destinations!",
color: const Color(0xFF000000).withOpacity(0.6),
size: 11.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
),
/// Child Price ],
Row( ),
children: [
Text(
"and ",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
Text(
"\$${childPrice ?? 0}",
style: TextStyle(
color: themeColor,
fontWeight: FontWeight.w500,
fontSize: 24.sp,
),
),
Text(
" /child",
style: TextStyle(
color: Colors.black.withOpacity(0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w400,
),
),
],
),
/// Description
SizedBox(
width: 193.w,
child: CustomText(
text: description ??
"Dive into an extensive selection of thrilling destinations!",
color: const Color(0xFF000000).withOpacity(0.6),
size: 11.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
), ),
/// -------- CARD TYPE LABEL -------- /// -------- RIGHT: CARD TYPE LABEL --------
Container( Container(
width: 35.w, width: 35.w,
height: 140.h, height: 140.h,
@@ -194,7 +197,7 @@ class PassCardView extends StatelessWidget {
); );
} }
/// -------- FALLBACK ICON -------- /// FALLBACK ICON
Widget _fallbackIcon() { Widget _fallbackIcon() {
return Icon( return Icon(
Icons.card_travel, Icons.card_travel,
@@ -202,4 +205,4 @@ class PassCardView extends StatelessWidget {
color: Colors.grey[400], color: Colors.grey[400],
); );
} }
} }

View File

@@ -1,14 +1,17 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../localPreference/local_preference.dart'; import '../../localPreference/local_preference.dart';
import '../bloc/buy_pass_bloc.dart';
import '../bloc/buy_pass_event.dart';
import '../bloc/buy_pass_state.dart';
import '../models/checkout_model.dart'; import '../models/checkout_model.dart';
import '../../checkout/view/checkout_view.dart'; import '../../checkout/view/checkout_view.dart';
import '../repository/buy_pass_repository.dart'; // ✅ Import repository import '../repository/buy_pass_repository.dart'; // ✅ Import repository
class PaymentCard extends StatelessWidget { class PaymentCard extends StatefulWidget {
final String city; final String city;
final String heroImage; final String heroImage;
final String cardType; final String cardType;
@@ -56,10 +59,16 @@ class PaymentCard extends StatelessWidget {
required this.cardXid, // ✅ NEW required this.cardXid, // ✅ NEW
}); });
@override
State<PaymentCard> createState() => _PaymentCardState();
}
class _PaymentCardState extends State<PaymentCard> {
bool _isLoading = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isUnlimitedCard = cardType == "unlimited_card"; final bool isUnlimitedCard = widget.cardType == "unlimited_card";
final bool isSelectivePass = cardType == "selective_pass"; final bool isSelectivePass = widget.cardType == "selective_pass";
return Padding( return Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
@@ -83,7 +92,7 @@ class PaymentCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
CustomText( CustomText(
text: city, text: widget.city,
size: 20.sp, size: 20.sp,
weight: FontWeight.bold, weight: FontWeight.bold,
), ),
@@ -91,32 +100,32 @@ class PaymentCard extends StatelessWidget {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFF95FAF), color: widget.themeColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20.r), borderRadius: BorderRadius.circular(20.r),
), ),
child: CustomText( child: CustomText(
text: cardDisplayName, text: widget.cardDisplayName,
size: 12.sp, size: 12.sp,
color: Colors.white, color: widget.themeColor,
weight: FontWeight.w500, weight: FontWeight.w500,
), ),
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildCounterRow("No. of Adults", adults, onAdultChanged), _buildCounterRow("No. of Adults", widget.adults, widget.onAdultChanged, context, minValue: 1),
SizedBox(height: 10.h), SizedBox(height: 10.h),
_buildCounterRow("No. of Children", children, onChildChanged), _buildCounterRow("No. of Children", widget.children, widget.onChildChanged, context),
SizedBox(height: 10.h), SizedBox(height: 10.h),
if (isUnlimitedCard) if (isUnlimitedCard)
_buildDropdownRow( _buildDropdownRow(
label: "No. of Days", label: "No. of Days",
value: selectedValue, value: widget.selectedValue,
onChanged: onValidityChanged, onChanged: widget.onValidityChanged,
) )
else if (isSelectivePass) else if (isSelectivePass)
_buildDropdownRow( _buildDropdownRow(
label: "No. of Attractions", label: "No. of Attractions",
value: selectedValue, value: widget.selectedValue,
onChanged: onValidityChanged, onChanged: widget.onValidityChanged,
), ),
Divider(height: 30.h, thickness: 1), Divider(height: 30.h, thickness: 1),
Row( Row(
@@ -128,7 +137,7 @@ class PaymentCard extends StatelessWidget {
weight: FontWeight.w500, weight: FontWeight.w500,
), ),
CustomText( CustomText(
text: "\$${totalPrice.toStringAsFixed(0)}", text: "\$${widget.totalPrice.toStringAsFixed(0)}",
size: 18.sp, size: 18.sp,
color: Color(0xFFF95F62), color: Color(0xFFF95F62),
weight: FontWeight.bold, weight: FontWeight.bold,
@@ -136,101 +145,111 @@ class PaymentCard extends StatelessWidget {
], ],
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
CustomFilledButton( BlocBuilder<BuyPassBloc, BuyPassState>(
onTap: () async { builder: (context, state) {
try { final isLoading = state is BuyPassLoaded && state.isAddingToCart;
// ✅ Check login status first
final bool isLoggedIn = await LocalPreference.getLogin();
// ✅ Create checkout data (needed for both cases) return CustomFilledButton(
final checkoutData = CheckoutData( onTap: isLoading
cityName: city, ? null
heroImage: heroImage, : () async {
cardTypeName: cardType, final bloc = context.read<BuyPassBloc>();
cardDisplayName: cardDisplayName, bloc.add(AddToCartLoading());
themeColor: themeColor, try {
adultCount: adults, // ✅ Check login status first
childCount: children, final bool isLoggedIn = await LocalPreference.getLogin();
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
// ✅ Save to local preference (for both logged in and guest users) // ✅ Create checkout data (needed for both cases)
await LocalPreference.setPassCart( final checkoutData = CheckoutData(
cityName: city, cityName: widget.city,
heroImage: heroImage, heroImage: widget.heroImage,
cardTypeName: cardType, cardTypeName: widget.cardType,
cardDisplayName: cardDisplayName, cardDisplayName: widget.cardDisplayName,
themeColor: themeColor.value, themeColor: widget.themeColor,
adultCount: adults, adultCount: widget.adults,
childCount: children, childCount: widget.children,
adultPrice: adultPrice, adultPrice: widget.adultPrice,
childPrice: childPrice, childPrice: widget.childPrice,
validityDuration: selectedValue, validityDuration: widget.selectedValue,
totalPrice: totalPrice, totalPrice: widget.totalPrice,
description: description, description: widget.description,
);
if (isLoggedIn) {
// ✅ User is logged in - hit API
final repository = BuyPassRepository();
final response = await repository.addToCartPasses(
cityXid: cityXid,
cardTypeXid: cardTypeXid,
cardXid: cardXid,
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: adults,
totalChild: children,
noOfAttractions: isSelectivePass ? selectedValue : 0,
noOfDays: isUnlimitedCard ? selectedValue : 0,
baseAmount: totalPrice,
);
// ✅ Extract bookingId from response
final int bookingId = response['id'];
// ✅ Navigate to checkout with bookingId
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: bookingId),
settings: RouteSettings(
arguments: checkoutData,
),
),
); );
}
} else { if (isLoggedIn) {
// ✅ User is NOT logged in - skip API, navigate directly // ✅ User is logged in - hit API
if (context.mounted) { final repository = BuyPassRepository();
Navigator.of(context).push( final response = await repository.addToCartPasses(
MaterialPageRoute( cityXid: widget.cityXid,
builder: (context) => CheckoutView(bookingId: 0), // or 0, depending on your CheckoutView implementation cardTypeXid: widget.cardTypeXid,
settings: RouteSettings( cardXid: widget.cardXid,
arguments: checkoutData, cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: widget.adults,
totalChild: widget.children,
noOfAttractions: isSelectivePass ? widget.selectedValue : 0,
noOfDays: isUnlimitedCard ? widget.selectedValue : 0,
baseAmount: widget.totalPrice,
);
// ✅ Extract bookingId from response
final int bookingId = response['id'];
// ✅ Navigate to checkout with bookingId
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: bookingId),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
}
} else {
// ✅ User is NOT logged in - skip API, navigate directly
await LocalPreference.setPassCart(
cityName: widget.city,
heroImage: widget.heroImage,
cardTypeName: widget.cardType,
cardDisplayName: widget.cardDisplayName,
themeColor: widget.themeColor.value,
adultCount: widget.adults,
childCount: widget.children,
adultPrice: widget.adultPrice,
childPrice: widget.childPrice,
validityDuration: widget.selectedValue,
totalPrice: widget.totalPrice,
description: widget.description,
);
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: 0),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
}
}
} catch (e) {
// ✅ Show error message
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to proceed: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
), ),
), );
); }
} finally {
bloc.add(AddToCartDone()); // ✅ stop loading
} }
} },
} catch (e) { label: isLoading ? "Please wait..." : "Proceed to Pay",
// ✅ Show error message );
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to proceed: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
),
);
}
}
}, },
label: "Proceed to Pay",
), ),
], ],
), ),
@@ -244,8 +263,8 @@ class PaymentCard extends StatelessWidget {
required Function(int) onChanged, required Function(int) onChanged,
}) { }) {
List<int> numbersList = List.generate( List<int> numbersList = List.generate(
maxNumber - minNumber + 1, widget.maxNumber - widget.minNumber + 1,
(index) => minNumber + index, (index) => widget.minNumber + index,
); );
return Row( return Row(
@@ -305,7 +324,9 @@ class PaymentCard extends StatelessWidget {
String label, String label,
int value, int value,
Function(int) onChanged, Function(int) onChanged,
) { BuildContext context, {
int minValue = 0,
}) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -313,7 +334,22 @@ class PaymentCard extends StatelessWidget {
Row( Row(
children: [ children: [
_circleButton(Icons.remove, () { _circleButton(Icons.remove, () {
if (value > 0) onChanged(value - 1); if (value > minValue) {
onChanged(value - 1);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
minValue == 1
? "At least 1 adult is required"
: "Cannot go below 0",
),
backgroundColor: const Color(0xFFF95F62),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
}), }),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w), padding: EdgeInsets.symmetric(horizontal: 10.w),

View File

@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import '../../model/my_passes_cart_mode.dart'; import '../../model/my_passes_cart_model.dart';
abstract class MyPassCartState extends Equatable { abstract class MyPassCartState extends Equatable {
const MyPassCartState(); const MyPassCartState();

View File

@@ -0,0 +1,57 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/foundation.dart';
import '../../../localPreference/local_preference.dart';
import '../../repository/my_postcards_cart_repository.dart';
import 'my_postcards_cart_state.dart';
part 'my_postcards_cart_event.dart';
class MyPostCardsCartBloc
extends Bloc<MyPostCardsCartEvent, MyPostCardsCartState> {
final MyPostCardCartRepository _repository;
MyPostCardsCartBloc({MyPostCardCartRepository? repository})
: _repository = repository ?? MyPostCardCartRepository(),
super(MyPostCardsCartInitial()) {
on<CheckLoginAndFetchPostcardsCart>(_onCheckLoginAndFetch);
}
Future<void> _onCheckLoginAndFetch(
CheckLoginAndFetchPostcardsCart event,
Emitter<MyPostCardsCartState> emit,
) async {
emit(MyPostCardsCartLoading());
try {
// 1. Check login status
final isLoggedIn = await LocalPreference.getLogin();
if (kDebugMode) {
print('🔐 [CART-BLOC] isLoggedIn: $isLoggedIn');
}
if (!isLoggedIn) {
// User not logged in → show not-logged-in screen
emit(MyPostCardsCartNotLoggedIn());
return;
}
// 2. Fetch cart from API
final cartData = await _repository.fetchMyPostCardsCart();
if (kDebugMode) {
print('🛒 [CART-BLOC] Cart items: ${cartData.totalItems}');
}
if (cartData.cartItems.isEmpty) {
emit(MyPostCardsCartEmpty());
} else {
emit(MyPostCardsCartLoaded(cartData: cartData));
}
} catch (e) {
if (kDebugMode) {
print('❌ [CART-BLOC] Error: $e');
}
emit(MyPostCardsCartError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,6 @@
part of 'my_postcards_cart_bloc.dart';
abstract class MyPostCardsCartEvent {}
/// Checks login status then fetches cart if logged in
class CheckLoginAndFetchPostcardsCart extends MyPostCardsCartEvent {}

View File

@@ -0,0 +1,27 @@
import '../../model/my_postcards_cart_model.dart';
abstract class MyPostCardsCartState {}
/// Initial / idle state
class MyPostCardsCartInitial extends MyPostCardsCartState {}
/// Checking login or fetching data
class MyPostCardsCartLoading extends MyPostCardsCartState {}
/// User is NOT logged in
class MyPostCardsCartNotLoggedIn extends MyPostCardsCartState {}
/// Logged in but cart is empty
class MyPostCardsCartEmpty extends MyPostCardsCartState {}
/// Logged in and data loaded
class MyPostCardsCartLoaded extends MyPostCardsCartState {
final MyPostCardsCartModel cartData;
MyPostCardsCartLoaded({required this.cartData});
}
/// Error state
class MyPostCardsCartError extends MyPostCardsCartState {
final String message;
MyPostCardsCartError({required this.message});
}

View File

@@ -35,14 +35,16 @@ class MyPassesCartModel {
}; };
} }
/// ---------- CITY ---------- /// ---------- TOP LEVEL CITY ----------
class CartCity { class CartCity {
int id; int id;
String name; String name;
String bannerImage;
CartCity({ CartCity({
required this.id, required this.id,
required this.name, required this.name,
required this.bannerImage,
}); });
factory CartCity.fromJson(Map<String, dynamic>? json) { factory CartCity.fromJson(Map<String, dynamic>? json) {
@@ -51,12 +53,14 @@ class CartCity {
return CartCity( return CartCity(
id: (json['id'] as num?)?.toInt() ?? 0, id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name']?.toString() ?? "", name: json['name']?.toString() ?? "",
bannerImage: json['bannerImage']?.toString() ?? "",
); );
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"name": name, "name": name,
"bannerImage": bannerImage,
}; };
} }
@@ -65,6 +69,7 @@ class CartItem {
int id; int id;
String bookingNumber; String bookingNumber;
String cardMode; String cardMode;
String displayCardMode;
int noOfDays; int noOfDays;
int noOfAttractions; int noOfAttractions;
int totalAdult; int totalAdult;
@@ -74,6 +79,7 @@ class CartItem {
num totalAmount; num totalAmount;
String bookingStatus; String bookingStatus;
bool isForSelf; bool isForSelf;
String recipientFirstName; String recipientFirstName;
String recipientLastName; String recipientLastName;
String recipientEmail; String recipientEmail;
@@ -81,18 +87,22 @@ class CartItem {
String recipientCity; String recipientCity;
String recipientCountry; String recipientCountry;
String giftMessage; String giftMessage;
bool isPaymentRequired; bool isPaymentRequired;
int couponXid; int couponXid;
num couponDiscountAmount; num couponDiscountAmount;
num couponDiscountPercent; num couponDiscountPercent;
String paymentStatus; String paymentStatus;
String createdAt; String createdAt;
Coupon? coupon;
ItemCity city; ItemCity city;
CartItem({ CartItem({
required this.id, required this.id,
required this.bookingNumber, required this.bookingNumber,
required this.cardMode, required this.cardMode,
required this.displayCardMode,
required this.noOfDays, required this.noOfDays,
required this.noOfAttractions, required this.noOfAttractions,
required this.totalAdult, required this.totalAdult,
@@ -115,6 +125,7 @@ class CartItem {
required this.couponDiscountPercent, required this.couponDiscountPercent,
required this.paymentStatus, required this.paymentStatus,
required this.createdAt, required this.createdAt,
required this.coupon,
required this.city, required this.city,
}); });
@@ -125,6 +136,7 @@ class CartItem {
id: (json['id'] as num?)?.toInt() ?? 0, id: (json['id'] as num?)?.toInt() ?? 0,
bookingNumber: json['bookingNumber']?.toString() ?? "", bookingNumber: json['bookingNumber']?.toString() ?? "",
cardMode: json['cardMode']?.toString() ?? "", cardMode: json['cardMode']?.toString() ?? "",
displayCardMode: json['displayCardMode']?.toString() ?? "",
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0, noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0, noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0, totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
@@ -147,6 +159,8 @@ class CartItem {
couponDiscountPercent: json['couponDiscountPercent'] ?? 0, couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
paymentStatus: json['paymentStatus']?.toString() ?? "", paymentStatus: json['paymentStatus']?.toString() ?? "",
createdAt: json['createdAt']?.toString() ?? "", createdAt: json['createdAt']?.toString() ?? "",
coupon:
json['coupon'] == null ? null : Coupon.fromJson(json['coupon']),
city: ItemCity.fromJson(json['city']), city: ItemCity.fromJson(json['city']),
); );
} }
@@ -155,6 +169,7 @@ class CartItem {
"id": id, "id": id,
"bookingNumber": bookingNumber, "bookingNumber": bookingNumber,
"cardMode": cardMode, "cardMode": cardMode,
"displayCardMode": displayCardMode,
"noOfDays": noOfDays, "noOfDays": noOfDays,
"noOfAttractions": noOfAttractions, "noOfAttractions": noOfAttractions,
"totalAdult": totalAdult, "totalAdult": totalAdult,
@@ -177,18 +192,49 @@ class CartItem {
"couponDiscountPercent": couponDiscountPercent, "couponDiscountPercent": couponDiscountPercent,
"paymentStatus": paymentStatus, "paymentStatus": paymentStatus,
"createdAt": createdAt, "createdAt": createdAt,
"coupon": coupon?.toJson(),
"city": city.toJson(), "city": city.toJson(),
}; };
} }
/// ---------- COUPON ----------
class Coupon {
int id;
String couponCode;
String title;
Coupon({
required this.id,
required this.couponCode,
required this.title,
});
factory Coupon.fromJson(Map<String, dynamic>? json) {
json ??= {};
return Coupon(
id: (json['id'] as num?)?.toInt() ?? 0,
couponCode: json['couponCode']?.toString() ?? "",
title: json['title']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"couponCode": couponCode,
"title": title,
};
}
/// ---------- ITEM CITY ---------- /// ---------- ITEM CITY ----------
class ItemCity { class ItemCity {
int id; int id;
String cityName; String cityName;
List<CityBanner> cityBanners;
ItemCity({ ItemCity({
required this.id, required this.id,
required this.cityName, required this.cityName,
required this.cityBanners,
}); });
factory ItemCity.fromJson(Map<String, dynamic>? json) { factory ItemCity.fromJson(Map<String, dynamic>? json) {
@@ -197,11 +243,35 @@ class ItemCity {
return ItemCity( return ItemCity(
id: (json['id'] as num?)?.toInt() ?? 0, id: (json['id'] as num?)?.toInt() ?? 0,
cityName: json['cityName']?.toString() ?? "", cityName: json['cityName']?.toString() ?? "",
cityBanners: json['cityBanners'] == null
? []
: List<Map<String, dynamic>>.from(json['cityBanners'])
.map((e) => CityBanner.fromJson(e))
.toList(),
); );
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"cityName": cityName, "cityName": cityName,
"cityBanners": cityBanners.map((e) => e.toJson()).toList(),
};
}
/// ---------- CITY BANNER ----------
class CityBanner {
String imageFilePath;
CityBanner({required this.imageFilePath});
factory CityBanner.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CityBanner(
imageFilePath: json['imageFilePath']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"imageFilePath": imageFilePath,
}; };
} }

View File

@@ -0,0 +1,163 @@
class MyPostCardsCartModel {
final int totalItems;
final List<CartItem> cartItems;
MyPostCardsCartModel({
required this.totalItems,
required this.cartItems,
});
factory MyPostCardsCartModel.fromJson(Map<String, dynamic> json) {
return MyPostCardsCartModel(
totalItems: json['totalItems'] ?? 0,
cartItems: (json['cartItems'] as List<dynamic>? ?? [])
.map((e) => CartItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'totalItems': totalItems,
'cartItems': cartItems.map((e) => e.toJson()).toList(),
};
}
}
class CartItem {
final int id;
final String pcTitle;
final String pcNumber;
final String cityName;
final DateTime? pcDatetime;
final String pcContent;
final String pcImagePath;
final bool isForSelf;
final String? senderFullName;
final String? senderCityName;
final String? senderCountryName;
final String fullname;
final String emailAddress;
final String isdCode;
final String mobileNumber;
final String address1;
final String? address2;
final String zipCode;
final String stateName;
final String countryName;
final num baseAmount;
final num totalTaxAmount;
final num totalAmount;
final String paymentStatus;
final String orderStatus;
final bool isDraft;
final bool isAddedToCart;
final DateTime? createdAt;
CartItem({
required this.id,
required this.pcTitle,
required this.pcNumber,
required this.cityName,
required this.pcDatetime,
required this.pcContent,
required this.pcImagePath,
required this.isForSelf,
required this.senderFullName,
required this.senderCityName,
required this.senderCountryName,
required this.fullname,
required this.emailAddress,
required this.isdCode,
required this.mobileNumber,
required this.address1,
required this.address2,
required this.zipCode,
required this.stateName,
required this.countryName,
required this.baseAmount,
required this.totalTaxAmount,
required this.totalAmount,
required this.paymentStatus,
required this.orderStatus,
required this.isDraft,
required this.isAddedToCart,
required this.createdAt,
});
factory CartItem.fromJson(Map<String, dynamic> json) {
return CartItem(
id: json['id'] ?? 0,
pcTitle: json['pcTitle'] ?? '',
pcNumber: json['pcNumber'] ?? '',
cityName: json['cityName'] ?? '',
pcDatetime: json['pcDatetime'] != null
? DateTime.tryParse(json['pcDatetime'])
: null,
pcContent: json['pcContent'] ?? '',
pcImagePath: json['pcImagePath'] ?? '',
isForSelf: json['isForSelf'] ?? false,
senderFullName: json['senderFullName'],
senderCityName: json['senderCityName'],
senderCountryName: json['senderCountryName'],
fullname: json['fullname'] ?? '',
emailAddress: json['emailAddress'] ?? '',
isdCode: json['isdCode'] ?? '',
mobileNumber: json['mobileNumber'] ?? '',
address1: json['address1'] ?? '',
address2: json['address2'],
zipCode: json['zipCode'] ?? '',
stateName: json['stateName'] ?? '',
countryName: json['countryName'] ?? '',
baseAmount: json['baseAmount'] ?? 0,
totalTaxAmount: json['totalTaxAmount'] ?? 0,
totalAmount: json['totalAmount'] ?? 0,
paymentStatus: json['paymentStatus'] ?? '',
orderStatus: json['orderStatus'] ?? '',
isDraft: json['isDraft'] ?? false,
isAddedToCart: json['isAddedToCart'] ?? false,
createdAt: json['createdAt'] != null
? DateTime.tryParse(json['createdAt'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'pcTitle': pcTitle,
'pcNumber': pcNumber,
'cityName': cityName,
'pcDatetime': pcDatetime?.toIso8601String(),
'pcContent': pcContent,
'pcImagePath': pcImagePath,
'isForSelf': isForSelf,
'senderFullName': senderFullName,
'senderCityName': senderCityName,
'senderCountryName': senderCountryName,
'fullname': fullname,
'emailAddress': emailAddress,
'isdCode': isdCode,
'mobileNumber': mobileNumber,
'address1': address1,
'address2': address2,
'zipCode': zipCode,
'stateName': stateName,
'countryName': countryName,
'baseAmount': baseAmount,
'totalTaxAmount': totalTaxAmount,
'totalAmount': totalAmount,
'paymentStatus': paymentStatus,
'orderStatus': orderStatus,
'isDraft': isDraft,
'isAddedToCart': isAddedToCart,
'createdAt': createdAt?.toIso8601String(),
};
}
}

View File

@@ -1,21 +1,21 @@
class PassModel { // class PassModel {
final String title; // final String title;
final String imageUrl; // final String imageUrl;
final String duration; // final String duration;
final int adults; // final int adults;
final int kids; // final int kids;
final int quantity; // final int quantity;
final double price; // final double price;
final double discount; // final double discount;
//
PassModel({ // PassModel({
required this.title, // required this.title,
required this.imageUrl, // required this.imageUrl,
required this.duration, // required this.duration,
required this.adults, // required this.adults,
required this.kids, // required this.kids,
required this.quantity, // required this.quantity,
required this.price, // required this.price,
required this.discount, // required this.discount,
}); // });
} // }

View File

@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart'; import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart'; import '../../networkApiServices/network_api_services.dart';
import '../model/my_passes_cart_mode.dart'; import '../model/my_passes_cart_model.dart';
class MyPassCartRepository { class MyPassCartRepository {
final NetworkApiService _apiService = NetworkApiService(); final NetworkApiService _apiService = NetworkApiService();

View File

@@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/my_postcards_cart_model.dart';
class MyPostCardCartRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch postcards cart data from API
Future<MyPostCardsCartModel> fetchMyPostCardsCart() async {
try {
if (kDebugMode) {
print('🌐 [POSTCARD-REPO] Fetching postcards cart from API...');
}
final cityID = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: '${ApiUrls.myPostCardsCart}?cityXid=$cityID',
);
if (kDebugMode) {
print('✅ [POSTCARD-REPO] Postcards cart API response received');
}
return MyPostCardsCartModel.fromJson(response.data);
} catch (e) {
if (kDebugMode) {
print('❌ [POSTCARD-REPO] Error fetching postcards cart from API: $e');
}
rethrow;
}
}
}

View File

@@ -5,10 +5,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/back_widget.dart'; import '../../common_packages/back_widget.dart';
import '../blocs/myPassCart/my_pass_cart_bloc.dart'; import '../blocs/myPassCart/my_pass_cart_bloc.dart';
import '../blocs/myPassCart/my_pass_cart_event.dart'; import '../blocs/myPassCart/my_pass_cart_event.dart';
import '../blocs/postcard_bloc.dart'; import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../repository/my_pass_cart_repository.dart';
import 'my_pass_cart_page_view.dart'; import 'my_pass_cart_page_view.dart';
import 'my_postcard_page_view.dart'; import 'my_postcard_cart_page_view.dart';
class MyCartPage extends StatefulWidget { class MyCartPage extends StatefulWidget {
const MyCartPage({super.key}); const MyCartPage({super.key});
@@ -20,62 +19,61 @@ class MyCartPage extends StatefulWidget {
class _MyCartPageState extends State<MyCartPage> { class _MyCartPageState extends State<MyCartPage> {
int selectedTab = 0; int selectedTab = 0;
@override
void initState() {
super.initState();
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return Scaffold(
providers: [ backgroundColor: Colors.white,
BlocProvider( body: SafeArea(
create: (_) => PostCardBloc()..add(LoadPostCards()), child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
BlocProvider( children: [
create: (_) => MyPassCartBloc( Padding(
repository: MyPassCartRepository(), padding: EdgeInsets.symmetric(horizontal: 16.w),
)..add(const FetchPassCartEvent()), child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
], children: [
child: Scaffold( CommonAppBar(
backgroundColor: Colors.white, isWhiteLogo: false,
body: SafeArea( isProfilePage: false,
child: SingleChildScrollView( showCart: false,
padding: EdgeInsets.all(16), showDivider: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
backWidget(context, "Your Cart", Colors.black),
SizedBox(height: 24.h),
Container(
padding: EdgeInsets.all(4.0),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(30),
), ),
child: Row( backWidget(context, "Your Cart", Colors.black),
children: [ SizedBox(height: 24.h),
_tabButton("My Passes", 0), Container(
_tabButton("My Post Cards", 1), padding: EdgeInsets.all(4.w),
], decoration: BoxDecoration(
), color: const Color(0xffFEE7E7),
), borderRadius: BorderRadius.circular(30.r),
Row(
children: [
Expanded(
child: selectedTab == 0
? const MyPassesPage()
: const MyPostCardsPage(),
), ),
], child: Row(
), children: [
], _tabButton("My Passes", 0),
_tabButton("My Post Cards", 1),
],
),
),
SizedBox(height: 8.h),
],
),
), ),
), Expanded(
child: IndexedStack(
index: selectedTab,
children: const [
MyPassesCartPage(),
MyPostCardsCartPage(),
],
),
),
],
), ),
), ),
); );
@@ -87,17 +85,27 @@ class _MyCartPageState extends State<MyCartPage> {
child: GestureDetector( child: GestureDetector(
onTap: () => setState(() => selectedTab = index), onTap: () => setState(() => selectedTab = index),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 12), padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent, color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30.r),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 6,
offset: const Offset(0, 2),
)
]
: [],
), ),
child: Center( child: Center(
child: Text( child: Text(
title, title,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w400, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: Color(0xff2A2A2A), fontSize: 13.sp,
color: const Color(0xff2A2A2A),
), ),
), ),
), ),
@@ -105,4 +113,4 @@ class _MyCartPageState extends State<MyCartPage> {
), ),
); );
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,501 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../common_packages/custom_text.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../../postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../postcard/blocs/pick_images/pick_images_bloc.dart';
import '../../postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../../postcard/models/my_postcard_model.dart';
import '../../postcard/repository/postcard_checkout_repository.dart';
import '../../postcard/views/edit_postcard_view.dart';
import '../../postcard/views/postcard_checkout_page_view.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../blocs/myPostcardsCart/my_postcards_cart_state.dart';
import '../model/my_postcards_cart_model.dart';
import '../widget/ticket_card_view.dart';
class MyPostCardsCartPage extends StatelessWidget {
const MyPostCardsCartPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPostCardsCartBloc, MyPostCardsCartState>(
builder: (context, state) {
if (state is MyPostCardsCartLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
if (state is MyPostCardsCartNotLoggedIn) {
return _NotLoggedInScreen(onLoginTap: () {});
}
if (state is MyPostCardsCartEmpty) {
return _EmptyCartScreen(
onRefresh: () =>
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart()),
);
}
if (state is MyPostCardsCartError) {
return _ErrorScreen(
message: state.message,
onRetry: () =>
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart()),
);
}
if (state is MyPostCardsCartLoaded) {
return _CartLoadedScreen(cartData: state.cartData);
}
return const SizedBox.shrink();
},
);
}
}
// ─────────────────────────────────────────────────────────
// CART LOADED
// ─────────────────────────────────────────────────────────
class _CartLoadedScreen extends StatefulWidget {
final MyPostCardsCartModel cartData;
const _CartLoadedScreen({required this.cartData});
@override
State<_CartLoadedScreen> createState() => _CartLoadedScreenState();
}
class _CartLoadedScreenState extends State<_CartLoadedScreen> {
final ScrollController _scrollController = ScrollController();
int _selectedIndex = 0;
// Height of one card slot (card height + bottom padding).
// 330h card + 20h gap = 350. Adjust if your device renders differently.
static const double _cardItemHeight = 350.0;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
final offset = _scrollController.offset;
final newIndex = (offset / _cardItemHeight).round();
final clamped = newIndex.clamp(0, widget.cartData.cartItems.length - 1);
if (clamped != _selectedIndex) {
setState(() => _selectedIndex = clamped);
}
}
void _navigateToCheckout(CartItem item) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (_) =>
PostcardCheckoutBloc(repository: CreatePostCardRepository()),
child: PostcardCheckoutPageView(
countryName: item.countryName,
cityName: item.cityName,
stateName: item.stateName,
zipCode: item.zipCode,
address1: item.address1,
address2: item.address2 ?? '',
pcTitle: item.pcTitle,
pcNumber: item.pcNumber,
fullname: item.fullname,
emailAddress: item.emailAddress,
mobileNumber: item.mobileNumber,
isdCode: item.isdCode.isNotEmpty ? item.isdCode : '+91',
isForSelf: true,
baseAmount: item.baseAmount.toDouble(),
totalTaxAmount: item.totalTaxAmount.toDouble(),
totalAmount: item.totalAmount.toDouble(),
postcardId: item.id,
pcImage: item.pcImagePath,
pcContent: item.pcContent,
isEditMode: true,
senderName: item.senderFullName,
senderCity: item.senderCityName,
senderCountry: item.senderCountryName,
isCartMode: true,
),
),
),
);
}
@override
Widget build(BuildContext context) {
final items = widget.cartData.cartItems;
return Column(
children: [
// ── Info Banner ──────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withValues(alpha: 0.1),
border: Border.all(color: const Color(0xffF95F62), width: 1),
borderRadius: BorderRadius.circular(15.r),
),
child: Row(
children: [
Container(
width: 28.w,
height: 28.w,
decoration: const BoxDecoration(
color: Color(0xffF95F62),
shape: BoxShape.circle,
),
child: Icon(
Icons.info_outline_rounded,
color: Colors.white,
size: 16.sp,
),
),
SizedBox(width: 10.w),
Expanded(
child: Text(
'You can purchase one postcard at a time',
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: const Color(0xFF212121),
),
),
),
],
),
),
),
// ── Scrollable list ──────────────────────────────────
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
// Actual pixel height of the visible list area
final listViewHeight = constraints.maxHeight;
// KEY FIX: Add trailing bottom padding equal to
// (listHeight - one card slot) so the last card can scroll
// all the way to the top and become "selected".
final trailingPadding = (listViewHeight - _cardItemHeight).clamp(
0.0,
double.infinity,
);
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, trailingPadding),
itemCount: items.length,
itemBuilder: (context, index) {
final isSelected = index == _selectedIndex;
return Padding(
padding: EdgeInsets.only(bottom: 20.h),
child: AnimatedOpacity(
opacity: isSelected ? 1.0 : 0.4,
duration: const Duration(milliseconds: 300),
child: AnimatedScale(
scale: isSelected ? 1.0 : 0.95,
duration: const Duration(milliseconds: 300),
child: Stack(
children: [
TicketCard(
cartItem: items[index],
onEditDraft: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
EditPostcardBloc(),
),
BlocProvider(
create: (context) => PickImagesBloc(),
),
],
child: EditPostcardView(
myPostCard: MyPostCard(
id: items[index].id,
pcTitle: items[index].pcTitle,
pcNumber: items[index].pcNumber,
pcImagePath: items[index].pcImagePath,
pcContent: items[index].pcContent,
fullname: items[index].fullname,
emailAddress: items[index].emailAddress,
mobileNumber: items[index].mobileNumber,
isdCode: items[index].isdCode.isNotEmpty ? items[index].isdCode : '+91',
address1: items[index].address1,
address2: items[index].address2 ?? '',
cityName: items[index].cityName,
stateName: items[index].stateName,
countryName: items[index].countryName,
zipCode: items[index].zipCode,
baseAmount: items[index].baseAmount.toDouble(),
totalTaxAmount: items[index].totalTaxAmount.toDouble(),
totalAmount: items[index].totalAmount.toDouble(),
isForSelf: items[index].isForSelf,
senderCityName: items[index].senderCityName,
senderCountryName: items[index].senderCountryName,
senderFullName: items[index].senderFullName,
userXid: 0,
pcDatetime: DateTime.now(),
orderStatus: '',
isPaid: false,
paymentMode: '',
paymentStatus: '',
isDraft: false,
isAddedToCart: true,
isActive: true,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
), isCartMode: true,
),
),
),
);
if (result == true) {
// ignore: use_build_context_synchronously
context.read<MyPostCardBloc>().add(
const RefreshDraftPostCards(),
);
}
},
),
// ── Selected badge ──
// if (isSelected)
// Positioned(
// top: 12.h,
// right: 20.w,
// child: Container(
// padding: EdgeInsets.symmetric(
// horizontal: 10.w, vertical: 4.h),
// decoration: BoxDecoration(
// color: const Color(0xffF95F62),
// borderRadius: BorderRadius.circular(20.r),
// ),
// child: Text(
// 'Selected',
// style: GoogleFonts.poppins(
// color: Colors.white,
// fontSize: 10.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ),
],
),
),
),
);
},
);
},
),
),
SizedBox(height: 14.h),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CustomFilledButton(
width: double.infinity,
onTap: () {
// Navigator.pop(context);
_navigateToCheckout(items[_selectedIndex]);
},
label: "Proceed to Checkout",
),
),
SizedBox(height: 14.h),
],
);
}
}
// ─────────────────────────────────────────────────────────
// NOT LOGGED IN
// ─────────────────────────────────────────────────────────
class _NotLoggedInScreen extends StatelessWidget {
final VoidCallback onLoginTap;
const _NotLoggedInScreen({required this.onLoginTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/gif/empty_cart.gif", width: 250.w),
CustomText(
text: "You are not logged in yet!",
size: 22.sp,
color: const Color(0xFFF95F62),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"To access my postcards cart please login",
style: TextStyle(
color: const Color(0xFF656565),
fontSize: 14.sp,
),
textAlign: TextAlign.center,
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
builder: (_) => const LoginEmailBottomsheet(),
);
},
label: "Login to Checkout",
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// EMPTY CART
// ─────────────────────────────────────────────────────────
class _EmptyCartScreen extends StatelessWidget {
final VoidCallback onRefresh;
const _EmptyCartScreen({required this.onRefresh});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/gif/empty_post_card.gif', width: 200.w),
SizedBox(height: 16.h),
Text(
'You do not have any postcards',
style: GoogleFonts.poppins(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 8.h),
Text(
"You do not possess any postcards yet nor have you sent to anyone",
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
Navigator.pop(context);
},
label: "Design my postcard",
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// ERROR
// ─────────────────────────────────────────────────────────
class _ErrorScreen extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorScreen({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline_rounded,
size: 64.sp,
color: const Color(0xffF95F62),
),
SizedBox(height: 16.h),
Text(
'Something went wrong',
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF212121),
),
),
SizedBox(height: 8.h),
Text(
message,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 13.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 24.h),
OutlinedButton(
onPressed: onRetry,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
),
child: Text(
'Retry',
style: TextStyle(
color: const Color(0xffF95F62),
fontSize: 14.sp,
),
),
),
],
),
),
);
}
}

View File

@@ -1,204 +0,0 @@
import 'package:citycards_customer/cart/widget/ticket_card_view.dart';
import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../blocs/postcard_bloc.dart';
class MyPostCardsPage extends StatelessWidget {
const MyPostCardsPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<PostCardBloc, PostCardState>(
builder: (context, state) {
if (state is PostCardLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is PostCardLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child:
Column(
children: [
TicketCard(),
SizedBox(height: 40.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
decoration: BoxDecoration(
color: Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: Color(0xFFBB474A).withOpacity(0.4),
width: 0.8,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Get 10% off on your first trip",
color: Color(0xFF262626),
size: 14.sp,
),
SizedBox(height: 7.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => AllCouponsBottomsheet(),
);
},
child: CustomText(
text: "View all coupons",
color: Color(0xFFF95F62),
size: 12,
),
),
SizedBox(width: 3.w),
Icon(Icons.arrow_right, color: Color(0xFFF95F62)),
],
),
],
),
const Spacer(),
Container(
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 10.h,
),
decoration: BoxDecoration(
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(8.r),
),
child: CustomText(
text: "Apply",
color: Color(0xFFF95F62),
size: 14.sp,
),
),
],
),
),
SizedBox(height: 15.h),
Divider(color: Color(0xFFACACAC), thickness: 1.h),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Subtotal", size: 14.sp),
CustomText(
text: "\$49.50",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Discount", size: 14.sp),
CustomText(
text: "-7.20%",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 10.h),
Divider(color: Color(0xFFACACAC), thickness: 1.h),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: 'Total', size: 14.sp),
SizedBox(height: 4.h),
CustomText(
text: "Including \$2.24 in taxes",
size: 12.sp,
color: Colors.black.withOpacity(0.6),
),
],
),
),
CustomText(
text: "\$42.60",
size: 24.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 60.h),
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
},
width: double.infinity,
label: "Proceed to Checkout",
),
],
),
);
}
return Center(
child: Column(
children: [
Image.asset("assets/gif/empty_post_card.gif", width: 250.w),
Text(
"You do not have any postcards",
style: TextStyle(
fontSize: 24.sp,
color: Color(0xFFF95F62)
),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
"You do not possess any postcards yet nor have you sent to anyone",
style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp),
textAlign: TextAlign.center,
),
],
),
);
},
);
}
}

View File

@@ -1,9 +1,18 @@
import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../model/my_postcards_cart_model.dart';
class TicketCard extends StatelessWidget { class TicketCard extends StatelessWidget {
const TicketCard({super.key}); final CartItem cartItem;
final VoidCallback onEditDraft;
const TicketCard({
super.key,
required this.cartItem,
required this.onEditDraft,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -13,43 +22,101 @@ class TicketCard extends StatelessWidget {
child: ClipPath( child: ClipPath(
clipper: TicketClipper(), clipper: TicketClipper(),
child: Container( child: Container(
width: 270.w, width: 240.w,
height: 400.h, height: 340.h,
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(14.w),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(24.r), // ← was 12.r
), ),
child: Column( child: Column(
children: [ children: [
// ── Postcard Image ──
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(16.r),
child: Image.asset( child: cartItem.pcImagePath.isNotEmpty
'assets/images/card_banner.png', ? CachedNetworkImage(
width: 237.w, imageUrl:
height: 198.h, '${ApiUrls.baseUrl}${cartItem.pcImagePath}',
width: 210.w,
height: 170.h,
fit: BoxFit.cover, fit: BoxFit.cover,
progressIndicatorBuilder:
(context, url, progress) {
return Container(
width: 210.w,
height: 170.h,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16.r),
),
alignment: Alignment.center,
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
value: progress.progress,
),
);
},
errorWidget: (_, __, ___) => _placeholderImage(),
)
: _placeholderImage(),
),
SizedBox(height: 25.h),
// ── Dashed Divider ──
// Transform.translate shifts left by container padding (14.w)
// so dashes start/end at the notch centers.
Transform.translate(
offset: Offset(-14.w, 0),
child: SizedBox(
width: 240.w,
height: 14.h,
child: CustomPaint(
painter: _NotchDashPainter(),
),
), ),
), ),
SizedBox(height: 20.h),
SizedBox( SizedBox(height: 8.h),
width: 200.w,
child: DashedDivider( // ── Title ──
color: const Color(0xFFBEBEBE),
thickness: 2.h,
),
),
SizedBox(height: 6.h),
Text( Text(
"Melbourne", cartItem.pcTitle.isNotEmpty ? cartItem.pcTitle : 'No Title',
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 13.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: const Color(0xFF212121),
),
),
SizedBox(height: 22.h),
// ── Edit Draft Button ──
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: onEditDraft,
style: OutlinedButton.styleFrom(
backgroundColor: Color(0xffF95F62).withValues(alpha: 0.15),
side: const BorderSide(color: Color(0xffF95F62), width: 1.5),
padding: EdgeInsets.symmetric(vertical: 8.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
),
child: Text(
'Edit Draft',
style: TextStyle(
color: const Color(0xffF95F62),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
), ),
), ),
SizedBox(height: 6.h),
_infoRow("Postcards :", "5"),
_infoRow("Date :", "22/04/2025"),
_infoRow("Time :", "12:00PM - 2:00PM"),
], ],
), ),
), ),
@@ -58,104 +125,123 @@ class TicketCard extends StatelessWidget {
); );
} }
Widget _infoRow(String title, String value) { Widget _placeholderImage() {
return Padding( return Container(
padding: EdgeInsets.symmetric(vertical: 6.h), width: 210.w,
child: Row( height: 170.h,
mainAxisAlignment: MainAxisAlignment.spaceBetween, decoration: BoxDecoration(
children: [ color: Colors.grey.shade200,
Text( borderRadius: BorderRadius.circular(16.r),
title,
style: TextStyle(color: const Color(0xFF808080), fontSize: 12.sp),
),
Text(
value,
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 12.sp),
),
],
), ),
child: Icon(Icons.image_outlined, size: 42.sp, color: Colors.grey),
); );
} }
} }
class TicketPainter extends CustomPainter { // ─────────────────────────────────────────────
// Notch Dash Painter
// Draws dashes from center of left notch to center of right notch.
// notchRadius = 28.r, so startX = 28.w, endX = (240 - 28).w
// ─────────────────────────────────────────────
class _NotchDashPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final notchRadius = 23.r; final paint = Paint()
final dividerY = 240.h; ..color = const Color(0xffF95F62)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final ticketPath = Path() // Dashes from left notch center to right notch center.
..moveTo(12.w, 0) // Card is 240.w wide, notchRadius = 28.w. We hardcode these because
..lineTo(size.width - 12.w, 0) // size.width here is the inner column width (240-2*14=212.w), not the card width.
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r)) final double startX = 30.w; // 2.w gap from notch edge
..lineTo(size.width, dividerY - notchRadius) final double endX = 240.w - 30.w; // 2.w gap from notch edge
..arcToPoint( final double dashH = 6.h;
Offset(size.width, dividerY + notchRadius), final double dashW = 12.w;
radius: Radius.circular(notchRadius), final double gap = 5.w;
clockwise: false, final double top = (size.height - dashH) / 2;
) final double span = endX - startX;
..lineTo(size.width, size.height - 12.h)
..arcToPoint(
Offset(size.width - 12.w, size.height),
radius: Radius.circular(12.r),
)
..lineTo(12.w, size.height)
..arcToPoint(
Offset(0, size.height - 12.h),
radius: Radius.circular(12.r),
)
..lineTo(0, dividerY + notchRadius)
..arcToPoint(
Offset(0, dividerY - notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(0, 12.h)
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r))
..close();
final shadowPaint = Paint() // Fit exact number of dashes: n*dashW + (n-1)*gap <= span
..color = Colors.black.withOpacity(0.3) final int count = ((span + gap) / (dashW + gap)).floor();
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8);
canvas.drawPath(ticketPath, shadowPaint); // Recalculate actual gap to distribute evenly
final double actualGap = count > 1 ? (span - count * dashW) / (count - 1) : 0;
final cardPaint = Paint() double x = startX;
..color = const Color(0xFFFAC9CA).withOpacity(0.12) for (int i = 0; i < count; i++) {
..style = PaintingStyle.fill; canvas.drawRRect(
RRect.fromRectAndRadius(
canvas.drawPath(ticketPath, cardPaint); Rect.fromLTWH(x, top, dashW, dashH),
Radius.circular(dashH / 2),
),
paint,
);
x += dashW + actualGap;
}
} }
@override @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
class TicketClipper extends CustomClipper<Path> { // ─────────────────────────────────────────────
// Ticket Painter (shadow + fill)
// ─────────────────────────────────────────────
class TicketPainter extends CustomPainter {
@override @override
Path getClip(Size size) { void paint(Canvas canvas, Size size) {
final notchRadius = 23.r; final notchRadius = 28.r;
final dividerY = 240.h; final dividerY = 218.h;
final path = Path() final ticketPath = _buildPath(size, notchRadius, dividerY);
..moveTo(12.w, 0)
..lineTo(size.width - 12.w, 0) // Shadow
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r)) canvas.drawPath(
ticketPath,
Paint()
..color = Colors.black.withOpacity(0.15)
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8),
);
// Fill
canvas.drawPath(
ticketPath,
Paint()
..color = Colors.white
..style = PaintingStyle.fill,
);
// Border stroke
canvas.drawPath(
ticketPath,
Paint()
..color = const Color(0xffF95F62).withOpacity(0.5)
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
Path _buildPath(Size size, double notchRadius, double dividerY) {
return Path()
..moveTo(24.w, 0) // ← was 12.w
..lineTo(size.width - 24.w, 0) // ← was 12.w
..arcToPoint(Offset(size.width, 24.h), radius: Radius.circular(24.r)) // ← was 12
..lineTo(size.width, dividerY - notchRadius) ..lineTo(size.width, dividerY - notchRadius)
..arcToPoint( ..arcToPoint(
Offset(size.width, dividerY + notchRadius), Offset(size.width, dividerY + notchRadius),
radius: Radius.circular(notchRadius), radius: Radius.circular(notchRadius),
clockwise: false, clockwise: false,
) )
..lineTo(size.width, size.height - 12.h) ..lineTo(size.width, size.height - 24.h) // ← was 12.h
..arcToPoint( ..arcToPoint(
Offset(size.width - 12.w, size.height), Offset(size.width - 24.w, size.height), // ← was 12.w
radius: Radius.circular(12.r), radius: Radius.circular(24.r), // ← was 12.r
) )
..lineTo(12.w, size.height) ..lineTo(24.w, size.height) // ← was 12.w
..arcToPoint( ..arcToPoint(
Offset(0, size.height - 12.h), Offset(0, size.height - 24.h), // ← was 12.h
radius: Radius.circular(12.r), radius: Radius.circular(24.r), // ← was 12.r
) )
..lineTo(0, dividerY + notchRadius) ..lineTo(0, dividerY + notchRadius)
..arcToPoint( ..arcToPoint(
@@ -163,13 +249,55 @@ class TicketClipper extends CustomClipper<Path> {
radius: Radius.circular(notchRadius), radius: Radius.circular(notchRadius),
clockwise: false, clockwise: false,
) )
..lineTo(0, 12.h) ..lineTo(0, 24.h) // ← was 12.h
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r)) ..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // ← was 12
..close(); ..close();
}
return path; @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// ─────────────────────────────────────────────
// Ticket Clipper
// ─────────────────────────────────────────────
class TicketClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final notchRadius = 28.r;
final dividerY = 218.h;
return Path()
..moveTo(24.w, 0) // ← was 12.w
..lineTo(size.width - 24.w, 0) // ← was 12.w
..arcToPoint(Offset(size.width, 24.h), radius: Radius.circular(24.r)) // ← was 12
..lineTo(size.width, dividerY - notchRadius)
..arcToPoint(
Offset(size.width, dividerY + notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(size.width, size.height - 24.h) // ← was 12.h
..arcToPoint(
Offset(size.width - 24.w, size.height), // ← was 12.w
radius: Radius.circular(24.r), // ← was 12.r
)
..lineTo(24.w, size.height) // ← was 12.w
..arcToPoint(
Offset(0, size.height - 24.h), // ← was 12.h
radius: Radius.circular(24.r), // ← was 12.r
)
..lineTo(0, dividerY + notchRadius)
..arcToPoint(
Offset(0, dividerY - notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(0, 24.h) // ← was 12.h
..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // ← was 12
..close();
} }
@override @override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false; bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
} }

View File

@@ -80,6 +80,7 @@ class PurchaseDetailsBloc
isForSelf: event.isForSelf, isForSelf: event.isForSelf,
recipientFirstName: event.recipientFirstName, recipientFirstName: event.recipientFirstName,
recipientLastName: event.recipientLastName, recipientLastName: event.recipientLastName,
isdCode: event.isdCode,
recipientEmail: event.recipientEmail, recipientEmail: event.recipientEmail,
recipientPhone: event.recipientPhone, recipientPhone: event.recipientPhone,
city: event.city, city: event.city,

View File

@@ -19,6 +19,7 @@ class SubmitUserDetailsEvent extends PassPurchaseDetailsEvent {
final bool isForSelf; final bool isForSelf;
final String? recipientFirstName; final String? recipientFirstName;
final String? recipientLastName; final String? recipientLastName;
final String? isdCode;
final String? recipientEmail; final String? recipientEmail;
final String? recipientPhone; final String? recipientPhone;
final String? city; final String? city;
@@ -29,6 +30,7 @@ class SubmitUserDetailsEvent extends PassPurchaseDetailsEvent {
required this.isForSelf, required this.isForSelf,
this.recipientFirstName, this.recipientFirstName,
this.recipientLastName, this.recipientLastName,
this.isdCode,
this.recipientEmail, this.recipientEmail,
this.recipientPhone, this.recipientPhone,
this.city, this.city,

View File

@@ -1,6 +1,3 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart'; import '../../networkApiServices/network_api_services.dart';
@@ -8,63 +5,34 @@ class PassPurchaseDetailsRepository {
final NetworkApiService _apiServices = NetworkApiService(); final NetworkApiService _apiServices = NetworkApiService();
/// Submit user details for pass purchase /// Submit user details for pass purchase
/// POST https://devapi.citycards.betadelivery.com/mobile/passes/{bookingId}/user-details
Future<Map<String, dynamic>> submitUserDetails({ Future<Map<String, dynamic>> submitUserDetails({
required int bookingId, required int bookingId,
required bool isForSelf, required bool isForSelf,
String? recipientFirstName, String? recipientFirstName,
String? recipientLastName, String? recipientLastName,
String? isdCode,
String? recipientEmail, String? recipientEmail,
String? recipientPhone, String? recipientPhone,
String? city, String? city,
String? country, String? country,
}) async { }) async {
try { try {
log('🟢 submitUserDetails() called');
log('📤 [SUBMIT USER DETAILS] Booking ID: $bookingId');
log('📤 [SUBMIT USER DETAILS] Is For Self: $isForSelf');
// Construct URL with bookingId
final url = '${ApiUrls.baseUrl}/mobile/passes/$bookingId/user-details';
if (kDebugMode) {
print('📤 [SUBMIT USER DETAILS] API URL: $url');
}
// Request body
final requestBody = {
'isForSelf': isForSelf,
'recipientFirstName': recipientFirstName ?? '',
'recipientLastName': recipientLastName ?? '',
'recipientEmail': recipientEmail ?? '',
'recipientPhone': recipientPhone ?? '',
'recipientCity': city ?? '',
'recipientCountry': country ?? '',
};
log('📦 Request Body: $requestBody');
// Send POST request
final response = await _apiServices.putApi( final response = await _apiServices.putApi(
url: url, url: '${ApiUrls.baseUrl}/mobile/passes/$bookingId/user-details',
data: requestBody, data: {
'isForSelf': isForSelf,
'recipientFirstName': recipientFirstName ?? '',
'recipientLastName': recipientLastName ?? '',
'isdCode': isdCode ?? '',
'recipientEmail': recipientEmail ?? '',
'recipientPhone': recipientPhone ?? '',
'recipientCity': city ?? '',
'recipientCountry': country ?? '',
},
); );
log('✅ [SUBMIT USER DETAILS] Response Status: ${response.statusCode}');
log('📥 [SUBMIT USER DETAILS] Response Data: ${response.data}');
if (kDebugMode) {
print('📤 [SUBMIT USER DETAILS] ✅ User details submission successful');
print('📤 [SUBMIT USER DETAILS] Full Response: ${response.data}');
}
return response.data as Map<String, dynamic>; return response.data as Map<String, dynamic>;
} catch (e, stackTrace) { } catch (e) {
log(
'❌ submitUserDetails FAILED',
error: e,
stackTrace: stackTrace,
);
throw Exception('Failed to submit user details: $e'); throw Exception('Failed to submit user details: $e');
} }
} }

View File

@@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../StripePayment/view/stripe_payment.dart'; import '../../StripePayment/view/stripe_payment.dart';
import '../../add_details/add_details_view.dart'; import '../../add_details/add_details_view.dart';
import '../../buy_a_pass/models/checkout_model.dart'; import '../../buy_a_pass/models/checkout_model.dart';
import '../../common_packages/custom_snackbar.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart'; import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart'; import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
@@ -24,7 +25,8 @@ import '../models/all_coupons_model.dart';
class CheckoutView extends StatefulWidget { class CheckoutView extends StatefulWidget {
final int bookingId; final int bookingId;
const CheckoutView({super.key, required this.bookingId}); final int? couponId;
const CheckoutView({super.key, required this.bookingId, this.couponId});
@override @override
State<CheckoutView> createState() => _CheckoutViewState(); State<CheckoutView> createState() => _CheckoutViewState();
@@ -93,6 +95,7 @@ class _CheckoutViewState extends State<CheckoutView> {
child: _CheckoutContent( child: _CheckoutContent(
checkoutData: checkoutData, checkoutData: checkoutData,
bookingId: widget.bookingId, bookingId: widget.bookingId,
couponId: widget.couponId,
isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed, isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed,
onPurchaseDetailsChanged: (value) { onPurchaseDetailsChanged: (value) {
setState(() { setState(() {
@@ -107,12 +110,14 @@ class _CheckoutViewState extends State<CheckoutView> {
class _CheckoutContent extends StatefulWidget { class _CheckoutContent extends StatefulWidget {
final CheckoutData checkoutData; final CheckoutData checkoutData;
final int bookingId; final int bookingId;
final int? couponId;
final bool isPurchaseDetailsConfirmed; final bool isPurchaseDetailsConfirmed;
final Function(bool) onPurchaseDetailsChanged; final Function(bool) onPurchaseDetailsChanged;
const _CheckoutContent({ const _CheckoutContent({
required this.checkoutData, required this.checkoutData,
required this.bookingId, required this.bookingId,
this.couponId,
required this.isPurchaseDetailsConfirmed, required this.isPurchaseDetailsConfirmed,
required this.onPurchaseDetailsChanged, required this.onPurchaseDetailsChanged,
}); });
@@ -123,9 +128,16 @@ class _CheckoutContent extends StatefulWidget {
class _CheckoutContentState extends State<_CheckoutContent> { class _CheckoutContentState extends State<_CheckoutContent> {
bool _hasHandledPaymentResult = false; bool _hasHandledPaymentResult = false;
bool _hasAutoAppliedCoupon = false;
/// 🆕 Handle payment flow with client secret /// 🆕 Handle payment flow with client secret
/// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION /// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async { Future<void> _handlePaymentFlow(
BuildContext context,
String clientSecret,
int bookingId,
double finalTotal,
) async {
final paymentSuccess = await StripePaymentScreen.showAsBottomSheet( final paymentSuccess = await StripePaymentScreen.showAsBottomSheet(
context: context, context: context,
clientSecret: clientSecret, clientSecret: clientSecret,
@@ -179,11 +191,11 @@ class _CheckoutContentState extends State<_CheckoutContent> {
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary()); context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses()); context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Payment confirmed successfully!'), content: Text('Payment confirmed successfully!'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
} }
} }
@@ -196,37 +208,60 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// 🔒 CHECK: Prevent duplicate payment flow initiation // 🔒 CHECK: Prevent duplicate payment flow initiation
if (state.clientSecret != null && if (state.clientSecret != null &&
state.clientSecret!.isNotEmpty && state.clientSecret!.isNotEmpty &&
!_hasHandledPaymentResult) { // 🔒 Only proceed if not already handled !_hasHandledPaymentResult) {
// 🔒 MARK: Set flag immediately to prevent re-entry
_hasHandledPaymentResult = true; _hasHandledPaymentResult = true;
// ✅ Calculate finalTotal here
double discountPercentage = 0.0; double discountPercentage = 0.0;
if (state.appliedCoupon != null) { if (state.appliedCoupon != null) {
discountPercentage = state.appliedCoupon!.discountPercent.toDouble(); discountPercentage = state.appliedCoupon!.discountPercent
.toDouble();
} }
final num subtotal = widget.checkoutData.totalPrice; // Changed to widget. final num subtotal = widget.checkoutData.totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100); final double discountAmount = subtotal * (discountPercentage / 100);
final double totalBeforeTax = subtotal - discountAmount; final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = 2; final double taxAmount = 2;
final double finalTotal = totalBeforeTax + taxAmount; final double finalTotal = totalBeforeTax + taxAmount;
// ✅ Trigger payment flow with finalTotal
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_handlePaymentFlow( _handlePaymentFlow(
context, context,
state.clientSecret!, state.clientSecret!,
state.bookingId ?? widget.bookingId, state.bookingId ?? widget.bookingId,
finalTotal, // ✅ Pass the calculated finalTotal finalTotal,
); );
}); });
} }
// 🆕 AUTO-APPLY COUPON FROM PARAMETER
if (!_hasAutoAppliedCoupon &&
widget.couponId != null &&
state.appliedCoupon == null &&
state.coupons.isNotEmpty) {
final matchedCoupon = state.coupons
.cast<AllCouponsModel?>()
.firstWhere(
(c) => c?.id == widget.couponId,
orElse: () => null,
);
if (matchedCoupon != null) {
_hasAutoAppliedCoupon = true; // ✅ Set flag before async call
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<CheckoutBloc>().add(
ApplyCouponEvent(coupon: matchedCoupon),
);
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: widget.bookingId,
couponCode: matchedCoupon.couponCode,
),
);
});
}
}
// 🆕 Listen for payment confirmation success // 🆕 Listen for payment confirmation success
if (state.isPaymentConfirmed) { if (state.isPaymentConfirmed) {
// Navigate to success page or back
Future.delayed(const Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 2), () {
if (context.mounted) { if (context.mounted) {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
@@ -248,20 +283,14 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// 🆕 Handle payment initiation error // 🆕 Handle payment initiation error
if (state is CheckoutPaymentInitiationErrorState) { if (state is CheckoutPaymentInitiationErrorState) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text(state.error), backgroundColor: Colors.red),
content: Text(state.error),
backgroundColor: Colors.red,
),
); );
} }
// 🆕 Handle payment confirmation error // 🆕 Handle payment confirmation error
if (state is CheckoutPaymentConfirmationErrorState) { if (state is CheckoutPaymentConfirmationErrorState) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text(state.error), backgroundColor: Colors.red),
content: Text(state.error),
backgroundColor: Colors.red,
),
); );
} }
}, },
@@ -332,131 +361,135 @@ class _CheckoutContentState extends State<_CheckoutContent> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( // ✅ Expanded forces left side to only take remaining space after the 35.w label
children: [ Expanded(
// ✅ Hero Image child: Row(
ClipRRect( children: [
borderRadius: BorderRadius.only( // Hero Image
topLeft: Radius.circular(8.r), ClipRRect(
bottomLeft: Radius.circular(8.r), borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: widget.checkoutData.heroImage.isNotEmpty
? Image.network(
widget.checkoutData.heroImage,
width: 105.w,
height: 140.h,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
_fallbackImage(),
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null)
return child;
return Container(
width: 105.w,
height: 140.h,
color: Colors.grey[200],
child: Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child:
CircularProgressIndicator(
color: const Color(
0xffF95F62,
),
strokeWidth: 2,
),
),
),
);
},
)
: _fallbackImage(),
), ),
child: widget.checkoutData.heroImage.isNotEmpty
? Image.network( SizedBox(width: 6.66.w),
widget.checkoutData.heroImage,
width: 105.w, // ✅ Expanded so text column doesn't overflow
height: 140.h, Expanded(
fit: BoxFit.cover, child: Column(
errorBuilder: (context, error, stackTrace) { crossAxisAlignment: CrossAxisAlignment.start,
return _fallbackImage(); mainAxisAlignment: MainAxisAlignment.center,
}, children: [
loadingBuilder: CustomText(
(context, child, loadingProgress) { text: widget.checkoutData.cityName,
if (loadingProgress == null) return child; weight: FontWeight.w500,
return Container( size: 16.sp,
width: 105.w, ),
height: 140.h, SizedBox(height: 5.h),
color: Colors.grey[200], CustomText(
child: Center( text: widget.checkoutData.validityLabel,
child: SizedBox( color: const Color(0xFF8E8E8E),
width: 24.w, size: 12.sp,
height: 24.w, ),
child: CircularProgressIndicator( SizedBox(height: 5.h),
strokeWidth: 2,
// Adults row
if (widget.checkoutData.adultCount > 0)
Row(
children: [
Image.asset(
'assets/icons/adult.png',
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text:
"${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
),
SizedBox(height: 5.h),
// Kids + Price row
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
if (widget.checkoutData.childCount > 0)
Row(
children: [
Image.asset(
"assets/icons/kid.png",
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text:
"${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
)
else
const SizedBox(),
// Price
CustomText(
text:
"\$${subtotal.toStringAsFixed(2)}",
size: 20.sp,
weight: FontWeight.w500,
color: widget.checkoutData.themeColor, color: widget.checkoutData.themeColor,
), ),
), ],
),
);
},
)
: _fallbackImage(),
),
SizedBox(width: 6.66.w),
// ✅ Pass Details
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// City Name
CustomText(
text: widget.checkoutData.cityName,
weight: FontWeight.w500,
size: 16.sp,
),
SizedBox(height: 5.h),
// Validity (Days or Attractions)
CustomText(
text: widget.checkoutData.validityLabel,
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(height: 5.h),
// Adults
SizedBox(
width: MediaQuery.of(context).size.width * .5,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
// Adults
if (widget.checkoutData.adultCount > 0)
Row(
children: [
Image.asset(
'assets/icons/adult.png',
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text:
"${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
],
),
],
),
),
SizedBox(height: 5.h),
Row(
children: [
// Children
if (widget.checkoutData.childCount > 0) ...[
Image.asset(
"assets/icons/kid.png",
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text:
"${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(width: 53.w),
] else
SizedBox(width: 120.w),
// Total Price
CustomText(
text: "\$${subtotal.toStringAsFixed(2)}",
size: 24.sp,
weight: FontWeight.w500,
color: widget.checkoutData.themeColor,
), ),
], ],
), ),
], ),
), ],
], ),
), ),
// ✅ Card Type Label (Vertical) // ✅ Vertical label — fixed width, won't be squeezed
Container( Container(
width: 35.w, width: 35.w,
height: 140.h, height: 140.h,
@@ -472,6 +505,8 @@ class _CheckoutContentState extends State<_CheckoutContent> {
child: Center( child: Center(
child: Text( child: Text(
widget.checkoutData.cardDisplayName, widget.checkoutData.cardDisplayName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14.sp, fontSize: 14.sp,
@@ -490,8 +525,10 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// ✅ COUPON SECTION // ✅ COUPON SECTION
Container( Container(
width: double.infinity, width: double.infinity,
padding: padding: EdgeInsets.symmetric(
EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), horizontal: 16.w,
vertical: 16.h,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF95F62).withOpacity(0.06), color: const Color(0xFFF95F62).withOpacity(0.06),
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
@@ -502,142 +539,160 @@ class _CheckoutContentState extends State<_CheckoutContent> {
), ),
child: state is CheckoutCouponsLoadingState child: state is CheckoutCouponsLoadingState
? Row( ? Row(
children: [ children: [
SizedBox( SizedBox(
width: 16.w, width: 16.w,
height: 16.w, height: 16.w,
child: const CircularProgressIndicator( child: const CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Color(0xFFF95F62), color: Color(0xFFF95F62),
), ),
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
CustomText( CustomText(
text: "Loading coupons...", text: "Loading coupons...",
size: 12.sp, size: 12.sp,
color: Colors.grey, color: Colors.grey,
), ),
], ],
) )
: state is CheckoutCouponsErrorState : state is CheckoutCouponsErrorState
? CustomText( ? CustomText(
text: "Error loading coupons", text: "Error loading coupons",
size: 12.sp, size: 12.sp,
color: Colors.red, color: Colors.red,
) )
: state is CheckoutCouponsLoadedState : state is CheckoutCouponsLoadedState
? Row( ? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// LEFT CONTENT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CustomText( /// LEFT CONTENT
text: appliedCoupon != null Expanded(
? "Coupon Applied: ${appliedCoupon.couponCode}" child: Column(
: state.coupons.isNotEmpty crossAxisAlignment: CrossAxisAlignment.start,
? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}" children: [
: "No coupons available", CustomText(
color: const Color(0xFF262626), text: appliedCoupon != null
size: 14.sp, ? "Coupon Applied: ${appliedCoupon.couponCode}"
maxLines: 1, : state.coupons.isNotEmpty
overflow: TextOverflow.ellipsis, ? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}"
), : "No coupons available",
SizedBox(height: 7.h), color: const Color(0xFF262626),
GestureDetector( size: 14.sp,
onTap: () { maxLines: 1,
// ✅ Updated: Pass callback to bottomsheet overflow: TextOverflow.ellipsis,
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
), ),
builder: (_) => AllCouponsBottomsheet( SizedBox(height: 7.h),
onCouponSelected: (selectedCoupon) { GestureDetector(
final coupon = selectedCoupon as AllCouponsModel; onTap: () {
// Apply the selected coupon // ✅ Updated: Pass callback to bottomsheet
context.read<CheckoutBloc>().add( showModalBottomSheet(
ApplyCouponEvent( context: context,
coupon: selectedCoupon), isScrollControlled: true,
); backgroundColor: Colors.white,
context.read<CheckoutBloc>().add( shape: RoundedRectangleBorder(
ApplyCouponToBackendEvent( borderRadius: BorderRadius.vertical(
bookingId: widget.bookingId, top: Radius.circular(12.r),
couponCode: coupon.couponCode, ),
),
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: widget.bookingId,
couponCode: coupon.couponCode,
),
);
},
), ),
); );
}, },
), child: Row(
); mainAxisSize: MainAxisSize.min,
}, children: [
child: Row( CustomText(
mainAxisSize: MainAxisSize.min, text: "View all coupons",
children: [ color: const Color(0xFFF95F62),
CustomText( size: 12.sp,
text: "View all coupons", ),
color: const Color(0xFFF95F62), SizedBox(width: 3.w),
size: 12.sp, const Icon(
), Icons.arrow_right,
SizedBox(width: 3.w), size: 18,
const Icon( color: Color(0xFFF95F62),
Icons.arrow_right, ),
size: 18, ],
color: Color(0xFFF95F62), ),
), ),
], ],
), ),
), ),
],
),
),
SizedBox(width: 12.w), SizedBox(width: 12.w),
/// APPLY / REMOVE BUTTON /// APPLY / REMOVE BUTTON
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
if (appliedCoupon != null) { final isLogin =
context.read<CheckoutBloc>().add( await LocalPreference.getLogin();
RemoveCouponEvent(bookingId: widget.bookingId), if (isLogin == true) {
); if (appliedCoupon != null) {
} else if (state.coupons.isNotEmpty) { context.read<CheckoutBloc>().add(
// Apply coupon via backend API RemoveCouponEvent(
context.read<CheckoutBloc>().add( bookingId: widget.bookingId,
ApplyCouponToBackendEvent( ),
bookingId: widget.bookingId, );
couponCode: state.coupons[0].couponCode, } else if (state.coupons.isNotEmpty) {
// Apply coupon via backend API
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: widget.bookingId,
couponCode:
state.coupons[0].couponCode,
),
);
}
} else {
CustomSnackbar.showWarning(
context,
message: 'Please login to apply coupon',
useOverlay: true,
);
}
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 18.w,
vertical: 10.h,
),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFF95F62),
),
borderRadius: BorderRadius.circular(8.r),
),
child: CustomText(
text: state.isApplyingCoupon
? "Applying..."
: (appliedCoupon != null
? "Remove"
: "Apply"),
color: const Color(0xFFF95F62),
size: 14.sp,
),
), ),
);
}
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 18.w,
vertical: 10.h,
),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFF95F62),
), ),
borderRadius: BorderRadius.circular(8.r), ],
), )
child: CustomText(
text: state.isApplyingCoupon
? "Applying..."
: (appliedCoupon != null ? "Remove" : "Apply"),
color: const Color(0xFFF95F62),
size: 14.sp,
),
),
),
],
)
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
@@ -669,13 +724,12 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// Discount // Discount
if (discountPercentage > 0) ...[ if (discountPercentage > 0) ...[
Row( Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [ children: [
CustomText(text: "Discount", size: 14.sp), CustomText(text: "Discount", size: 14.sp),
CustomText( CustomText(
text: text:
"-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)", "-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
size: 14.sp, size: 14.sp,
weight: FontWeight.w500, weight: FontWeight.w500,
color: Colors.green, color: Colors.green,
@@ -699,14 +753,13 @@ class _CheckoutContentState extends State<_CheckoutContent> {
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [ children: [
CustomText(text: 'Total', size: 14.sp), CustomText(text: 'Total', size: 14.sp),
SizedBox(height: 4.h), SizedBox(height: 4.h),
CustomText( CustomText(
text: text:
"Including \$${taxAmount.toStringAsFixed(2)} in taxes", "Including \$${taxAmount.toStringAsFixed(2)} in taxes",
size: 12.sp, size: 12.sp,
color: Colors.black.withOpacity(0.6), color: Colors.black.withOpacity(0.6),
), ),
@@ -728,65 +781,76 @@ class _CheckoutContentState extends State<_CheckoutContent> {
future: LocalPreference.getLogin(), future: LocalPreference.getLogin(),
builder: (context, snapshot) { builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false; final isLoggedIn = snapshot.data ?? false;
final isDisabled = isInitiatingPayment || isConfirmingPayment; final isDisabled =
isInitiatingPayment || isConfirmingPayment;
return CustomFilledButton( return CustomFilledButton(
onTap: isDisabled onTap: isDisabled
? () {} // Empty callback when disabled ? () {} // Empty callback when disabled
: () async { : () async {
if (isLoggedIn) { if (isLoggedIn) {
if (widget.isPurchaseDetailsConfirmed) { if (widget.isPurchaseDetailsConfirmed) {
// 🆕 Initiate payment flow // 🆕 Initiate payment flow
context.read<CheckoutBloc>().add( context.read<CheckoutBloc>().add(
InitiatePaymentEvent( InitiatePaymentEvent(
bookingId: widget.bookingId), bookingId: widget.bookingId,
); ),
} else { );
// Show purchase details bottom sheet } else {
final result = await PassPurchaseBottomSheet.show( // Show purchase details bottom sheet
context, bookingId: widget.bookingId); final result =
await PassPurchaseBottomSheet.show(
context,
bookingId: widget.bookingId,
);
// ✅ Handle 'Buy for Myself' - user submitted details // ✅ Handle 'Buy for Myself' - user submitted details
if (result == 'success') { if (result == 'success') {
widget.onPurchaseDetailsChanged(true); widget.onPurchaseDetailsChanged(true);
} }
// ✅ Handle 'Gift the Pass' - navigate to AddDetailsView // ✅ Handle 'Gift the Pass' - navigate to AddDetailsView
else if (result == 'gift') { else if (result == 'gift') {
final giftResult = await Navigator.of(context).push<String>( final giftResult =
MaterialPageRoute( await Navigator.of(
builder: (_) => AddDetailsView(bookingId: widget.bookingId), context,
), ).push<String>(
); MaterialPageRoute(
builder: (_) => AddDetailsView(
bookingId: widget.bookingId,
),
),
);
// If gift details were successfully submitted, mark as confirmed // If gift details were successfully submitted, mark as confirmed
if (giftResult == 'success') { if (giftResult == 'success') {
widget.onPurchaseDetailsChanged(true); widget.onPurchaseDetailsChanged(true);
}
}
}
} else {
Navigator.pop(context);
// Show login bottom sheet if not logged in
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) =>
const LoginEmailBottomsheet(),
);
} }
} },
}
} else {
Navigator.pop(context);
// Show login bottom sheet if not logged in
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
}
},
width: double.infinity, width: double.infinity,
label: isLoggedIn label: isLoggedIn
? (widget.isPurchaseDetailsConfirmed ? (widget.isPurchaseDetailsConfirmed
? (isInitiatingPayment || isConfirmingPayment ? (isInitiatingPayment || isConfirmingPayment
? "Processing..." ? "Processing..."
: "Pay \$${finalTotal.toStringAsFixed(2)}") : "Pay \$${finalTotal.toStringAsFixed(2)}")
: "Checkout") : "Checkout")
: "Login to Checkout", : "Login to Checkout",
); );
}, },
@@ -807,11 +871,7 @@ class _CheckoutContentState extends State<_CheckoutContent> {
width: 105.w, width: 105.w,
height: 140.h, height: 140.h,
color: Colors.grey[200], color: Colors.grey[200],
child: Icon( child: Icon(Icons.card_travel, size: 40.sp, color: Colors.grey[400]),
Icons.card_travel,
size: 40.sp,
color: Colors.grey[400],
),
); );
} }
} }

View File

@@ -30,26 +30,26 @@ class AllCouponsBottomsheet extends StatelessWidget {
right: 20.w, right: 20.w,
bottom: MediaQuery.of(context).viewInsets.bottom, bottom: MediaQuery.of(context).viewInsets.bottom,
), ),
child: Column( child: SingleChildScrollView(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
/// --- Header --- children: [
Container( /// --- Header ---
height: 4.h, Container(
width: 40.w, height: 4.h,
decoration: BoxDecoration( width: 40.w,
color: Color(0xFF2D3134), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.r), color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
), ),
), SizedBox(height: 12.h),
SizedBox(height: 12.h), CustomText(
CustomText( text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
text: "All Coupons", size: 18.sp, weight: FontWeight.w500), SizedBox(height: 22.h),
SizedBox(height: 22.h),
/// --- Coupon list --- /// --- Coupon list ---
Flexible( BlocBuilder<AllCouponsBloc, AllCouponsState>(
child: BlocBuilder<AllCouponsBloc, AllCouponsState>(
builder: (context, state) { builder: (context, state) {
if (state is CouponsLoadingState) { if (state is CouponsLoadingState) {
return Center( return Center(
@@ -77,7 +77,7 @@ class AllCouponsBottomsheet extends StatelessWidget {
return ListView.separated( return ListView.separated(
shrinkWrap: true, shrinkWrap: true,
physics: const BouncingScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: state.coupons.length, itemCount: state.coupons.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h), separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
@@ -101,14 +101,15 @@ class AllCouponsBottomsheet extends StatelessWidget {
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( Expanded(
width: 220.w,
child: CustomText( child: CustomText(
text: "${coupon.discountPercent}% discount on ${coupon.title}", text:
"${coupon.discountPercent}% discount on ${coupon.title}",
size: 12.sp, size: 12.sp,
weight: FontWeight.w400, weight: FontWeight.w400,
), ),
), ),
SizedBox(width: 8.w),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
// Pass the selected coupon back to checkout view // Pass the selected coupon back to checkout view
@@ -118,8 +119,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
Navigator.pop(context); Navigator.pop(context);
}, },
child: Container( child: Container(
width: 110.w,
height: 44.h, height: 44.h,
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFF95F62), color: Color(0xFFF95F62),
borderRadius: borderRadius:
@@ -141,9 +143,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
height: 32.h, height: 32.h,
width: 83.w, width: 83.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: Color(0xFFF95F62).withOpacity(0.12),
Color(0xFFF95F62).withOpacity(0.12), border:
border: Border.all(color: Color(0xFFF95F62)), Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(6.r), borderRadius: BorderRadius.circular(6.r),
), ),
child: Center( child: Center(
@@ -165,8 +167,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
return SizedBox.shrink(); return SizedBox.shrink();
}, },
), ),
), SizedBox(height: 16.h),
], ],
),
), ),
), ),
); );

View File

@@ -275,9 +275,8 @@ class _PassPurchaseContent extends StatelessWidget {
? const SizedBox( ? const SizedBox(
height: 20, height: 20,
width: 20, width: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(color: Color(0xffF95F62),
strokeWidth: 2, strokeWidth: 2,
color: Colors.white,
), ),
) )
: Text( : Text(

View File

@@ -18,4 +18,4 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
emit(NavigationState(event.index)); emit(NavigationState(event.index));
}); });
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,7 +17,6 @@ class CommonAppBar extends StatelessWidget {
required this.isProfilePage, required this.isProfilePage,
this.showCart = true, this.showCart = true,
required this.showDivider, required this.showDivider,
this.imageUrl,
this.isSelectCity = false, this.isSelectCity = false,
}); });
@@ -24,14 +24,10 @@ class CommonAppBar extends StatelessWidget {
final bool isProfilePage; final bool isProfilePage;
final bool? showCart; final bool? showCart;
final bool showDivider; final bool showDivider;
final String? imageUrl;
final bool isSelectCity; final bool isSelectCity;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isPathIcon =
imageUrl != null && imageUrl!.isNotEmpty;
return Column( return Column(
children: [ children: [
Row( Row(
@@ -40,31 +36,62 @@ class CommonAppBar extends StatelessWidget {
/// LEFT SIDE /// LEFT SIDE
Row( Row(
children: [ children: [
/// ✅ LOGO / PATH ICON (SIZE CONTROLLED) /// ✅ LOGO (TAP ENABLED ONLY WHEN isSelectCity == true)
SizedBox( GestureDetector(
height: isPathIcon ? 40.h : 32.h, // 🔥 ONLY path icon bigger onTap: isSelectCity
child: isPathIcon ? () {
? Image.network( showModalBottomSheet(
imageUrl!, context: context,
fit: BoxFit.contain, isScrollControlled: true,
errorBuilder: (context, error, stackTrace) { backgroundColor: Colors.transparent,
return Image.asset( builder: (_) => const CitySelectionBottomSheet(),
isWhiteLogo );
? "assets/logo/logo_city_cards_white.png" }
: "assets/logo/logo_city_cards.png", : null,
fit: BoxFit.contain, child: FutureBuilder<String?>(
future: LocalPreference.getSelectedCityLogo(),
builder: (context, snapshot) {
final String? logoPath = snapshot.data;
final bool hasLogo =
snapshot.hasData &&
logoPath != null &&
logoPath.isNotEmpty;
final String? fullLogoUrl = hasLogo
? "${ApiUrls.baseUrl}$logoPath"
: null;
return SizedBox(
height: hasLogo ? 40.h : 32.h,
child: hasLogo && fullLogoUrl != null
? CachedNetworkImage(
imageUrl: fullLogoUrl,
fit: BoxFit.contain,
errorWidget: (context, url, error) => Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
placeholder: (context, url) => Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
)
: Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
); );
}, },
)
: Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
), ),
), ),
/// ✅ CITY DROPDOWN /// ✅ CITY DROPDOWN ICON (UNCHANGED)
if (isSelectCity) if (isSelectCity)
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -124,32 +151,26 @@ class CommonAppBar extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
String? imagePath; String? imagePath;
// ✅ Get image from profile state
if (state is ProfileLoaded) { if (state is ProfileLoaded) {
imagePath = state.profile.profileImage; imagePath = state.profile.profileImage;
} }
// ✅ Build full image URL
final String? imageUrl = final String? imageUrl =
(imagePath != null && imagePath.isNotEmpty) (imagePath != null && imagePath.isNotEmpty)
? "${ApiUrls.baseUrl}$imagePath" ? "${ApiUrls.baseUrl}$imagePath"
: null; : null;
return CircleAvatar( return CircleAvatar(
radius: 20.r, radius: 20.r,
backgroundColor: const Color(0xffFFDFDF), backgroundColor: const Color(0xffFFDFDF),
// ✅ Network image only if exists
backgroundImage: backgroundImage:
(imageUrl != null && imageUrl.isNotEmpty) (imageUrl != null && imageUrl.isNotEmpty)
? NetworkImage(imageUrl) ? NetworkImage(imageUrl)
: null, : null,
// ✅ Default fallback (unchanged)
child: (imageUrl == null || imageUrl.isEmpty) child: (imageUrl == null || imageUrl.isEmpty)
? Image.asset( ? Image.asset(
"assets/images/profile_default_img.png", "assets/images/profile_default_img.png",
) )
: null, : null,
); );
}, },
@@ -159,6 +180,7 @@ class CommonAppBar extends StatelessWidget {
), ),
], ],
), ),
/// DIVIDER /// DIVIDER
if (showDivider) if (showDivider)
Column( Column(

View File

@@ -2,23 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
Widget backWidget(BuildContext context, String title, Color? textColor){ Widget backWidget(BuildContext context, String title, Color? textColor){
return Row( return GestureDetector(
children: [ onTap: () {
GestureDetector( Navigator.pop(context);
onTap: () { },
Navigator.pop(context); child: Row(
}, children: [
child: Icon(Icons.arrow_back, size: 24.sp, color: textColor ?? Colors.black), Icon(Icons.arrow_back, size: 24.sp, color: textColor ?? Colors.black),
), SizedBox(width: 8.w),
SizedBox(width: 8.w), Text(
Text( title,
title, style: TextStyle(
style: TextStyle( fontSize: 12.sp,
fontSize: 12.sp, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: textColor ?? Colors.black
color: textColor ?? Colors.black ),
), ),
), ],
], ),
); );
} }

View File

@@ -13,7 +13,7 @@ class CustomBottomNavBar extends StatelessWidget {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xffFFF5F5), color: const Color(0xffFFF5F5),
border: Border.all(color: Color(0xffFDCDCE)), border: Border.all(color: const Color(0xffFDCDCE)),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24), topLeft: Radius.circular(24),
topRight: Radius.circular(24), topRight: Radius.circular(24),
@@ -26,10 +26,10 @@ class CustomBottomNavBar extends StatelessWidget {
), ),
], ],
), ),
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 16.w), padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 8.w),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
_buildNavItem( _buildNavItem(
context, context,
@@ -49,7 +49,7 @@ class CustomBottomNavBar extends StatelessWidget {
context, context,
index: 2, index: 2,
iconPath: 'assets/icons/pass_icon.png', iconPath: 'assets/icons/pass_icon.png',
label: 'My Passes', label: 'My Cards',
isActive: state.selectedIndex == 2, isActive: state.selectedIndex == 2,
), ),
_buildNavItem( _buildNavItem(
@@ -67,45 +67,66 @@ class CustomBottomNavBar extends StatelessWidget {
} }
Widget _buildNavItem( Widget _buildNavItem(
BuildContext context, { BuildContext context, {
required int index, required int index,
required String iconPath, required String iconPath,
required String label, required String label,
required bool isActive, required bool isActive,
}) { }) {
final color = isActive final color = isActive
? const Color(0xFFBB474A) ? const Color(0xFFBB474A)
: Color(0xFFBB474A).withOpacity(0.6); : const Color(0xFFBB474A).withOpacity(0.6);
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onTap: () =>
context.read<NavigationBloc>().add(NavigationTabChanged(index)), context.read<NavigationBloc>().add(NavigationTabChanged(index)),
child: Column( child: SizedBox(
mainAxisSize: MainAxisSize.min, width: 80.w,
children: [ child: Column(
if (isActive) mainAxisSize: MainAxisSize.min,
Container( mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Always reserve the same height for the indicator bar
// so all items stay vertically aligned
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
margin: EdgeInsets.only(bottom: 4.h), margin: EdgeInsets.only(bottom: 4.h),
height: 4.h, height: 4.h,
width: 50.w, width: isActive ? 50.w : 0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: isActive ? color : Colors.transparent,
borderRadius: BorderRadius.circular(2.r), borderRadius: BorderRadius.circular(2.r),
), ),
)
else
SizedBox(height: 7.h),
Image.asset(iconPath, scale: 4, color: color),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: isActive ? FontWeight.w500 : FontWeight.w500,
), ),
), AnimatedScale(
], scale: isActive ? 1.1 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Image.asset(iconPath, scale: 4, color: color),
),
SizedBox(height: 4.h),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
style: TextStyle(
color: color,
fontSize: 11.sp,
fontWeight: FontWeight.w500,
height: 1,
),
child: Text(
label,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
), ),
); );
} }
} }

View File

@@ -1,59 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DashedDivider extends StatelessWidget { class DashedDivider extends StatelessWidget {
/// The divider's height extent.
///
/// The divider itself is always drawn as a horizontal line that is centered
/// within the height specified by this value.
///
/// If this is null, then the [DividerThemeData.space] is used. If that is
/// also null, then this defaults to 16.0.
final double? height; final double? height;
/// The thickness of the line drawn within the divider.
///
/// A divider with a [thickness] of 0.0 is always drawn as a line with a
/// height of exactly one device pixel.
///
/// If this is null, then the [DividerThemeData.thickness] is used. If
/// that is also null, then this defaults to 0.0.
final double? thickness; final double? thickness;
/// The amount of empty space to the leading edge of the divider.
///
/// If this is null, then the [DividerThemeData.indent] is used. If that is
/// also null, then this defaults to 0.0.
final double? indent; final double? indent;
/// The amount of empty space to the trailing edge of the divider.
///
/// If this is null, then the [DividerThemeData.endIndent] is used. If that is
/// also null, then this defaults to 0.0.
final double? endIndent; final double? endIndent;
/// The color to use when painting the line.
///
/// If this is null, then the [DividerThemeData.color] is used. If that is
/// also null, then [ThemeData.dividerColor] is used.
final Color? color; final Color? color;
/// The length of each dash in the dashed line.
final double dashLength; final double dashLength;
/// The space between each dash in the dashed line.
final double dashSpace; final double dashSpace;
/// The offset along the main axis for the starting position of the dashes.
///
/// This value determines how far from the start the first dash will be drawn,
/// allowing for fine-tuning the positioning of the dashed line. A positive value
/// shifts the dashes forward, while a negative value moves them backward along
/// the main axis.
///
/// The default value is 0.0, meaning the dashes start at the beginning of the line.
final double mainAxisOffset; final double mainAxisOffset;
/// If true, shows the advanced pill-style dashed divider
final bool isAdvanced;
const DashedDivider({ const DashedDivider({
super.key, super.key,
this.height, this.height,
@@ -64,6 +23,7 @@ class DashedDivider extends StatelessWidget {
this.dashLength = 5, this.dashLength = 5,
this.dashSpace = 5, this.dashSpace = 5,
this.mainAxisOffset = 0.0, this.mainAxisOffset = 0.0,
this.isAdvanced = false,
}) : assert(height == null || height >= 0.0), }) : assert(height == null || height >= 0.0),
assert(thickness == null || thickness >= 0.0), assert(thickness == null || thickness >= 0.0),
assert(indent == null || indent >= 0.0), assert(indent == null || indent >= 0.0),
@@ -71,6 +31,17 @@ class DashedDivider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// ── Advanced pill-style divider ──
if (isAdvanced) {
return _AdvancedDashedDivider(
color: color ?? const Color(0xFFBEBEBE),
height: height ?? 20,
indent: indent ?? 0,
endIndent: endIndent ?? 0,
);
}
// ── Original dashed divider ──
final theme = DividerThemeProvider.of(context).withDefaults( final theme = DividerThemeProvider.of(context).withDefaults(
height: height, height: height,
thickness: thickness, thickness: thickness,
@@ -96,6 +67,72 @@ class DashedDivider extends StatelessWidget {
} }
} }
// ─────────────────────────────────────────────
// Advanced Pill-Style Dashed Divider
// ─────────────────────────────────────────────
class _AdvancedDashedDivider extends StatelessWidget {
final Color color;
final double height;
final double indent;
final double endIndent;
const _AdvancedDashedDivider({
required this.color,
required this.height,
required this.indent,
required this.endIndent,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: indent, right: endIndent),
height: height,
width: double.infinity,
child: CustomPaint(
painter: _PillDashedLinePainter(color: color),
),
);
}
}
class _PillDashedLinePainter extends CustomPainter {
final Color color;
_PillDashedLinePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
const pillWidth = 22.0;
const pillHeight = 10.0;
const gap = 6.0;
const radius = pillHeight / 2;
final centerY = size.height / 2;
double x = 0;
while (x + pillWidth <= size.width) {
final rect = RRect.fromRectAndRadius(
Rect.fromLTWH(x, centerY - radius, pillWidth, pillHeight),
const Radius.circular(radius),
);
canvas.drawRRect(rect, paint);
x += pillWidth + gap;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
// ─────────────────────────────────────────────
// Original Painter
// ─────────────────────────────────────────────
class DashedLinePainter extends CustomPainter { class DashedLinePainter extends CustomPainter {
final Color color; final Color color;
final double thickness; final double thickness;
@@ -142,31 +179,29 @@ class DashedLinePainter extends CustomPainter {
} }
} }
double _getMainAxisSize(Size size) { double _getMainAxisSize(Size size) =>
return isVertical ? size.height : size.width; isVertical ? size.height : size.width;
}
double _getCrossAxisPosition(Size size) { double _getCrossAxisPosition(Size size) =>
return isVertical ? size.width / 2 : size.height / 2; isVertical ? size.width / 2 : size.height / 2;
}
Offset _calculateStartOffset( Offset _calculateStartOffset(double crossAxisPosition, double currentPosition) =>
double crossAxisPosition, double currentPosition) { isVertical
return isVertical ? Offset(crossAxisPosition, currentPosition)
? Offset(crossAxisPosition, currentPosition) : Offset(currentPosition, crossAxisPosition);
: Offset(currentPosition, crossAxisPosition);
}
Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) { Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) =>
return isVertical isVertical
? Offset(crossAxisPosition, nextDashEnd) ? Offset(crossAxisPosition, nextDashEnd)
: Offset(nextDashEnd, crossAxisPosition); : Offset(nextDashEnd, crossAxisPosition);
}
@override @override
bool shouldRepaint(CustomPainter oldDelegate) => false; bool shouldRepaint(CustomPainter oldDelegate) => false;
} }
// ─────────────────────────────────────────────
// Theme Provider (unchanged)
// ─────────────────────────────────────────────
class DividerThemeProvider { class DividerThemeProvider {
final DividerThemeData _dividerTheme; final DividerThemeData _dividerTheme;
final ThemeData _theme; final ThemeData _theme;
@@ -204,35 +239,20 @@ class DividerThemeProvider {
_indent = indent ?? _indent; _indent = indent ?? _indent;
_endIndent = endIndent ?? _endIndent; _endIndent = endIndent ?? _endIndent;
_color = color ?? _color; _color = color ?? _color;
return this; return this;
} }
double get width => _width ?? _dividerTheme.space ?? _defaults.space!; double get width => _width ?? _dividerTheme.space ?? _defaults.space!;
double get height => _height ?? _dividerTheme.space ?? _defaults.space!; double get height => _height ?? _dividerTheme.space ?? _defaults.space!;
double get thickness => _thickness ?? _dividerTheme.thickness ?? _defaults.thickness!;
double get thickness =>
_thickness ?? _dividerTheme.thickness ?? _defaults.thickness!;
double get indent => _indent ?? _dividerTheme.indent ?? _defaults.indent!; double get indent => _indent ?? _dividerTheme.indent ?? _defaults.indent!;
double get endIndent => _endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!;
double get endIndent => Color get color => _color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor;
_endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!;
Color get color =>
_color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor;
} }
class _DividerDefaultsM3 extends DividerThemeData { class _DividerDefaultsM3 extends DividerThemeData {
const _DividerDefaultsM3(this.context) const _DividerDefaultsM3(this.context)
: super( : super(space: 16, thickness: 1.0, indent: 0, endIndent: 0);
space: 16,
thickness: 1.0,
indent: 0,
endIndent: 0,
);
final BuildContext context; final BuildContext context;
@override @override
@@ -241,13 +261,7 @@ class _DividerDefaultsM3 extends DividerThemeData {
class _DividerDefaultsM2 extends DividerThemeData { class _DividerDefaultsM2 extends DividerThemeData {
const _DividerDefaultsM2(this.context) const _DividerDefaultsM2(this.context)
: super( : super(space: 16, thickness: 0, indent: 0, endIndent: 0);
space: 16,
thickness: 0,
indent: 0,
endIndent: 0,
);
final BuildContext context; final BuildContext context;
@override @override

View File

@@ -6,48 +6,63 @@ class CustomFilledButton extends StatelessWidget {
final double? width; final double? width;
final String label; final String label;
final bool? showArrow; final bool? showArrow;
final GestureTapCallback onTap; final GestureTapCallback? onTap; // ✅ Made nullable
final double? height; final double? height;
final bool isLoading; // ✅ NEW
CustomFilledButton({ const CustomFilledButton({
super.key, super.key,
this.width = 266, this.width,
required this.onTap, required this.onTap,
required this.label, required this.label,
this.showArrow = false, this.showArrow = false,
this.height = 42 this.height,
this.isLoading = false, // ✅ NEW
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: isLoading ? null : onTap, // ✅ Disabled when loading
child: Container( child: Container(
height: height, height: height ?? 42.h,
width: width, width: width ?? 266.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFF95F62), color: isLoading
? Color(0xFFF95F62).withOpacity(0.6) // ✅ Dimmed when loading
: Color(0xFFF95F62),
borderRadius: BorderRadius.circular(38.r), borderRadius: BorderRadius.circular(38.r),
), ),
child: Center( child: Center(
child: Row( child: isLoading
? SizedBox(
height: 20.sp,
width: 20.sp,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
CustomText( CustomText(
text: label, text: label,
color: Colors.white, color: Colors.white,
size: 16.sp , size: 16.sp,
weight: FontWeight.w500, weight: FontWeight.w500,
), ),
if (showArrow!) SizedBox(width: 8),
if(showArrow!) if (showArrow!)
SizedBox(width: 8,), Icon(
if(showArrow!) Icons.arrow_forward_ios_rounded,
Icon(Icons.arrow_forward_ios_rounded,size: 18.sp, color: Colors.white,) size: 18.sp,
color: Colors.white,
),
], ],
), ),
), ),
), ),
); );
} }
} }

View File

@@ -8,6 +8,7 @@ class CustomText extends StatelessWidget {
final int? maxLines; final int? maxLines;
final TextOverflow? overflow; final TextOverflow? overflow;
final TextAlign? textAlign; final TextAlign? textAlign;
final Color asteriskColor; // ADD THIS
const CustomText({ const CustomText({
Key? key, Key? key,
@@ -18,20 +19,50 @@ class CustomText extends StatelessWidget {
this.maxLines, this.maxLines,
this.overflow, this.overflow,
this.textAlign, this.textAlign,
this.asteriskColor = Colors.red, // ADD THIS
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// ADD THIS BLOCK
if (asteriskColor != null && text.contains('*')) {
final parts = text.split('*');
return RichText(
text: TextSpan(
text: parts[0],
style: TextStyle(
fontWeight: weight,
color: color ?? Colors.black,
fontSize: size,
),
children: [
TextSpan(
text: '*',
style: TextStyle(
color: asteriskColor,
fontWeight: weight,
fontSize: size,
),
),
if (parts.length > 1) TextSpan(text: parts[1]),
],
),
maxLines: maxLines,
overflow: overflow ?? TextOverflow.clip,
textAlign: textAlign ?? TextAlign.start,
);
}
return Text( return Text(
text, text,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.lerp( fontWeight: FontWeight.lerp(
weight, weight,
FontWeight.values[ FontWeight.values[
(FontWeight.values.indexOf(weight??FontWeight.w400) + 1) (FontWeight.values.indexOf(weight ?? FontWeight.w400) + 1)
.clamp(0, FontWeight.values.length - 1) // prevent overflow .clamp(0, FontWeight.values.length - 1)
], ],
0.5, // t: pick between 0.0 and 1.0 0.5,
), ),
color: color, color: color,
fontSize: size, fontSize: size,

View File

@@ -13,11 +13,21 @@ class CustomTextField extends StatelessWidget {
final TextInputType? keyboardType; final TextInputType? keyboardType;
final bool obscureText; final bool obscureText;
final Widget? suffixIcon; final Widget? suffixIcon;
final Widget? prefixWidget;
final void Function(String)? onChanged; final void Function(String)? onChanged;
// ✅ NEW final int? maxLength;
final int? maxLength; // e.g. 10 final bool numbersOnly;
final bool numbersOnly; // allow only digits
final bool isMobileNumber;
final bool isEmail;
final bool onlyLetters;
final bool noSpace;
final bool noSpecialCharacters;
final bool isFirstLetterCapital;
final int mobileLength;
final bool isPreview;
const CustomTextField({ const CustomTextField({
super.key, super.key,
@@ -30,98 +40,402 @@ class CustomTextField extends StatelessWidget {
this.keyboardType, this.keyboardType,
this.obscureText = false, this.obscureText = false,
this.suffixIcon, this.suffixIcon,
this.prefixWidget,
this.onChanged, this.onChanged,
this.maxLength,
// ✅ NEW this.numbersOnly = false,
this.maxLength, // default = null (infinite) this.isMobileNumber = false,
this.numbersOnly = false, // default = false this.isEmail = false,
this.onlyLetters = false,
this.noSpace = false,
this.noSpecialCharacters = false,
this.isFirstLetterCapital = false,
this.mobileLength = 10,
this.isPreview = false,
}); });
void _capitalizeFirstLetter(String value) {
if (value.isEmpty) return;
final capitalized = value[0].toUpperCase() + value.substring(1);
if (capitalized != value) {
controller.value = controller.value.copyWith(
text: capitalized,
selection: TextSelection.collapsed(offset: capitalized.length),
);
}
}
String? _internalValidator(String? value) {
if (isPreview) return null;
if (value == null || value.trim().isEmpty) {
return 'Please enter $label';
}
if (isEmail) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Please enter a valid email address';
}
}
if (isMobileNumber) {
if (!RegExp(r'^\d+$').hasMatch(value)) {
return 'Only numbers are allowed';
}
if (value.length != mobileLength) {
return 'Mobile number must be $mobileLength digits';
}
}
if (noSpace && value.contains(' ')) {
return 'Spaces are not allowed';
}
if (noSpecialCharacters && !RegExp(r'^[a-zA-Z0-9\s]+$').hasMatch(value)) {
return 'Special characters are not allowed';
}
return null;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<TextInputFormatter> inputFormatters = [];
if (isPreview) {
inputFormatters.add(
TextInputFormatter.withFunction((oldValue, newValue) => oldValue),
);
} else {
if (isMobileNumber) {
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
inputFormatters.add(LengthLimitingTextInputFormatter(mobileLength));
} else {
if (numbersOnly) {
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
}
if (onlyLetters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
);
}
if (noSpecialCharacters) {
inputFormatters.add(
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),
);
}
if (noSpace) {
inputFormatters.add(FilteringTextInputFormatter.deny(RegExp(r'\s')));
}
if (maxLength != null) {
inputFormatters.add(LengthLimitingTextInputFormatter(maxLength));
}
}
}
final fillColor = isPreview
? Colors.grey.shade100
: enabled
? const Color(0xFFFFF5F5)
: Colors.grey.shade200;
final borderColor = const Color(0xBBC83B61).withOpacity(0.4);
// ✅ Full radius used for normal fields
// ✅ Only right-side radius when prefix is present (left side is the prefix container)
final borderRadius = prefixWidget != null
? BorderRadius.only(
topRight: Radius.circular(8.r),
bottomRight: Radius.circular(8.r),
)
: BorderRadius.circular(8.r);
return Padding( return Padding(
padding: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.only(bottom: 14.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CustomText( // Label
text: label, if (label.isNotEmpty) ...[
size: 14.sp, CustomText(text: label, size: 14.sp),
), SizedBox(height: 6.h),
SizedBox(height: 6.h), ],
SizedBox(
height: maxLines == 1 ? 42.h : null, if (prefixWidget != null)
child: TextFormField( // ✅ THE CORE FIX:
// We split the phone field into two parts:
// 1. The input ROW (prefix + text field) — wrapped in IntrinsicHeight
// so both sides match height perfectly
// 2. The error text — rendered OUTSIDE and BELOW the row
// so IntrinsicHeight is never affected by error text height
_PrefixFieldWithError(
prefixWidget: prefixWidget!,
fillColor: fillColor,
borderColor: borderColor,
borderRadius: borderRadius,
maxLines: maxLines,
obscureText: obscureText,
enabled: isPreview ? false : enabled,
controller: controller,
validator: validator ?? _internalValidator,
keyboardType: keyboardType ?? TextInputType.phone,
inputFormatters: inputFormatters,
hint: hint,
onChanged: (value) {
if (isFirstLetterCapital) _capitalizeFirstLetter(value);
if (onChanged != null) onChanged!(value);
},
suffixIcon: suffixIcon,
)
else
TextFormField(
controller: controller, controller: controller,
maxLines: obscureText ? 1 : maxLines, maxLines: obscureText ? 1 : maxLines,
enabled: enabled, enabled: isPreview ? false : enabled,
validator: validator,
keyboardType: keyboardType,
obscureText: obscureText, obscureText: obscureText,
onChanged: onChanged, validator: validator ?? _internalValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
// ✅ NEW keyboardType: keyboardType ??
maxLength: maxLength, (isMobileNumber
inputFormatters: [ ? TextInputType.phone
if (numbersOnly) : isEmail
FilteringTextInputFormatter.digitsOnly, ? TextInputType.emailAddress
if (maxLength != null) : TextInputType.name),
LengthLimitingTextInputFormatter(maxLength), inputFormatters: inputFormatters,
], onChanged: (value) {
if (isFirstLetterCapital) _capitalizeFirstLetter(value);
if (onChanged != null) onChanged!(value);
},
decoration: InputDecoration( decoration: InputDecoration(
hintText: hint, hintText: hint,
counterText: "", // ✅ hides 0/10 counter counterText: "",
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: const Color(0xFF8E8E8E), color: const Color(0xFF8E8E8E),
), ),
filled: true, filled: true,
fillColor: enabled fillColor: fillColor,
? const Color(0xFFFFF5F5)
: Colors.grey.shade200,
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: 24.w, horizontal: 24.w,
vertical: maxLines != null && maxLines! > 1 ? 12.h : 0, vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
), ),
suffixIcon: suffixIcon, suffixIcon: suffixIcon,
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: borderRadius,
borderSide: BorderSide( borderSide: BorderSide(color: borderColor, width: .4.w),
color: const Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: borderRadius,
borderSide: BorderSide( borderSide: BorderSide(
color: const Color(0xFFF95F62), color: const Color(0xFFF95F62), width: 1.w),
width: 1.w,
),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: borderRadius,
borderSide: BorderSide( borderSide: BorderSide(color: Colors.red, width: 1.w),
color: Colors.red,
width: 1.w,
),
), ),
focusedErrorBorder: OutlineInputBorder( focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: borderRadius,
borderSide: BorderSide( borderSide: BorderSide(color: Colors.red, width: 1.5.w),
color: Colors.red,
width: 1.5.w,
),
), ),
errorStyle: TextStyle( errorStyle: TextStyle(
fontSize: 11.sp, fontSize: 11.sp,
color: Colors.red, color: Colors.red,
height: 1.3,
), ),
), ),
), ),
),
], ],
), ),
); );
} }
}
/// ✅ Separate StatefulWidget for the prefix + field combo.
/// It manually manages validation state and renders error text
/// OUTSIDE the IntrinsicHeight row — this is the key to perfect alignment.
class _PrefixFieldWithError extends StatefulWidget {
final Widget prefixWidget;
final Color fillColor;
final Color borderColor;
final BorderRadius borderRadius;
final int? maxLines;
final bool obscureText;
final bool enabled;
final TextEditingController controller;
final String? Function(String?)? validator;
final TextInputType keyboardType;
final List<TextInputFormatter> inputFormatters;
final String hint;
final void Function(String) onChanged;
final Widget? suffixIcon;
const _PrefixFieldWithError({
required this.prefixWidget,
required this.fillColor,
required this.borderColor,
required this.borderRadius,
required this.maxLines,
required this.obscureText,
required this.enabled,
required this.controller,
required this.validator,
required this.keyboardType,
required this.inputFormatters,
required this.hint,
required this.onChanged,
required this.suffixIcon,
});
@override
State<_PrefixFieldWithError> createState() => _PrefixFieldWithErrorState();
}
class _PrefixFieldWithErrorState extends State<_PrefixFieldWithError> {
String? _errorText;
bool _hasInteracted = false;
void _validate(String value) {
if (!_hasInteracted) return;
setState(() {
_errorText = widget.validator?.call(value);
});
}
void _onChanged(String value) {
setState(() => _hasInteracted = true);
_validate(value);
widget.onChanged(value);
}
// Called by Form.validate() via FormField
String? _formValidator(String? value) {
setState(() => _hasInteracted = true);
final error = widget.validator?.call(value);
// Update error text after frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() => _errorText = error);
});
return error;
}
bool get _hasError => _errorText != null && _errorText!.isNotEmpty;
@override
Widget build(BuildContext context) {
final borderColor = widget.borderColor;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ✅ IntrinsicHeight ONLY wraps the input row (prefix + field)
// Error text is outside this, so IntrinsicHeight height is never affected by it
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Prefix container — matches field height perfectly via IntrinsicHeight
Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: widget.fillColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
// ✅ No right border — avoids double border line
border: Border(
top: BorderSide(
color: _hasError ? Colors.red : borderColor,
width: _hasError ? 1.w : 0.4.w,
),
bottom: BorderSide(
color: _hasError ? Colors.red : borderColor,
width: _hasError ? 1.w : 0.4.w,
),
left: BorderSide(
color: _hasError ? Colors.red : borderColor,
width: _hasError ? 1.w : 0.4.w,
),
),
),
child: widget.prefixWidget,
),
// Text field — takes remaining width
Expanded(
child: TextFormField(
controller: widget.controller,
maxLines: widget.obscureText ? 1 : widget.maxLines,
enabled: widget.enabled,
obscureText: widget.obscureText,
validator: _formValidator,
// ✅ No autovalidateMode here — we handle it manually
// so we can show error text outside the row
autovalidateMode: AutovalidateMode.disabled,
keyboardType: widget.keyboardType,
inputFormatters: widget.inputFormatters,
onChanged: _onChanged,
decoration: InputDecoration(
hintText: widget.hint,
counterText: "",
// ✅ errorText: null always — we render error ourselves below
errorText: null,
hintStyle: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
filled: true,
fillColor: widget.fillColor,
contentPadding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 10.h,
),
suffixIcon: widget.suffixIcon,
enabledBorder: OutlineInputBorder(
borderRadius: widget.borderRadius,
borderSide: BorderSide(
color: _hasError ? Colors.red : borderColor,
width: _hasError ? 1.w : 0.4.w,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: widget.borderRadius,
borderSide: BorderSide(
color: _hasError
? Colors.red
: const Color(0xFFF95F62),
width: 1.w,
),
),
errorBorder: OutlineInputBorder(
borderRadius: widget.borderRadius,
borderSide:
BorderSide(color: Colors.red, width: 1.w),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: widget.borderRadius,
borderSide:
BorderSide(color: Colors.red, width: 1.5.w),
),
),
),
),
],
),
),
// ✅ Error text rendered OUTSIDE the IntrinsicHeight row
// This is why the prefix box never grows when error appears
if (_hasError) ...[
SizedBox(height: 4.h),
Padding(
padding: EdgeInsets.only(left: 4.w),
child: Text(
_errorText!,
style: TextStyle(
fontSize: 11.sp,
color: Colors.red,
height: 1.3,
),
),
),
],
],
);
}
} }

View File

@@ -5,200 +5,197 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
class LanguageSelectionBottomsheet extends StatelessWidget { import '../localPreference/local_preference.dart';
LanguageSelectionBottomsheet({super.key});
List<String> languages = [ class LanguageSelectionBottomsheet extends StatefulWidget {
"English / Englis", const LanguageSelectionBottomsheet({super.key});
"Dutch / Nederlands",
"Spanish / Español", @override
"French / Français", State<LanguageSelectionBottomsheet> createState() =>
"Japanese / 日本語", _LanguageSelectionBottomsheetState();
}
class _LanguageSelectionBottomsheetState
extends State<LanguageSelectionBottomsheet> {
/// Each entry: display label → BCP-47 code for google_mlkit_translation
final List<Map<String, String>> languages = [
{'label': 'English / English', 'code': 'en'},
{'label': 'Dutch / Nederlands', 'code': 'nl'},
{'label': 'Spanish / Español', 'code': 'es'},
{'label': 'French / Français', 'code': 'fr'},
{'label': 'Japanese / 日本語', 'code': 'ja'},
]; ];
TextEditingController searchController = TextEditingController(); List<Map<String, String>> _filtered = [];
String? _pendingLabel; // highlighted in list but not yet saved
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_filtered = List.from(languages);
_searchController.addListener(_onSearch);
}
void _onSearch() {
final query = _searchController.text.toLowerCase();
setState(() {
_filtered = languages
.where((l) => l['label']!.toLowerCase().contains(query))
.toList();
});
}
Future<void> _onSave() async {
if (_pendingLabel == null) {
Navigator.of(context).pop();
return;
}
final selected = languages.firstWhere((l) => l['label'] == _pendingLabel);
final code = selected['code']!;
// Persist to SQLite
await LocalPreference.setLanguage(code);
// Update BLoC
if (mounted) {
context.read<LanguageBloc>().add(UpdateLanguage(_pendingLabel!));
Navigator.of(context).pop();
}
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), padding: EdgeInsets.only(
child: Column( left: 20.w,
mainAxisSize: MainAxisSize.min, right: 20.w,
children: [ top: 16.h,
Container( bottom: MediaQuery.of(context).viewInsets.bottom + 16.h,
width: 40.w, ),
height: 4.h, child: BlocBuilder<LanguageBloc, LanguageState>(
decoration: BoxDecoration( builder: (context, state) {
color: Color(0xFF2D3134), // Seed pending selection from current BLoC state on first build
borderRadius: BorderRadius.circular(4.r), _pendingLabel ??= state.selectedLanguage;
),
), return Column(
SizedBox(height: 20.h), mainAxisSize: MainAxisSize.min,
Align( children: [
alignment: Alignment.topLeft, /// Drag handle
child: Text( Container(
"Change Language", width: 40.w,
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w500), height: 4.h,
), decoration: BoxDecoration(
), color: const Color(0xFF2D3134),
SizedBox(height: 22.h), borderRadius: BorderRadius.circular(4.r),
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: "Search Languages",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Color(0xBBC83B61).withOpacity(0.4),
),
suffixIcon: Image.asset("assets/icons/search.png", scale: 4),
contentPadding: EdgeInsets.symmetric(horizontal: 24.w),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(
color: Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
), ),
), ),
focusedBorder: OutlineInputBorder( SizedBox(height: 20.h),
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w),
),
),
),
SizedBox(height: 12.h),
BlocBuilder<LanguageBloc, LanguageState>( /// Title
builder: (context, state) { Align(
return Expanded( alignment: Alignment.topLeft,
child: Text(
"Change Language",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 22.h),
/// Search field
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search Languages",
hintStyle: TextStyle(
fontSize: 14.sp,
color: const Color(0xBBC83B61).withOpacity(0.4),
),
suffixIcon:
Image.asset("assets/icons/search.png", scale: 4),
contentPadding:
EdgeInsets.symmetric(horizontal: 24.w),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: .4.w,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.r),
borderSide: BorderSide(
color: const Color(0xFFF95F62),
width: 1.w,
),
),
),
),
SizedBox(height: 12.h),
/// Language list (fixed height, scrollable)
ConstrainedBox(
constraints: BoxConstraints(maxHeight: 280.h),
child: ListView.builder( child: ListView.builder(
itemCount: languages.length,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), itemCount: _filtered.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = languages[index]; final item = _filtered[index];
final label = item['label']!;
final isSelected = _pendingLabel == label;
return ListTile( return ListTile(
dense: true, dense: true,
onTap: () => setState(() => _pendingLabel = label),
leading: GestureDetector( leading: GestureDetector(
onTap: () { onTap: () => setState(() => _pendingLabel = label),
context.read<LanguageBloc>().add( child: isSelected
UpdateLanguage(item),
);
Navigator.of(context).pop();
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (context) => Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 16.h,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 20.h),
Text(
"Are you sure you want to switch to",
style: TextStyle(
color: Colors.black.withOpacity(.6),
fontWeight: FontWeight.w400,
fontSize: 18.sp
),
),
SizedBox(height: 8.h),
Text(
item,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 20.h),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: OutlinedButton(
onPressed: () =>
Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
side: BorderSide(
color: Colors.transparent,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40.r),
),
minimumSize: Size(
double.infinity,
42.h,
),
),
child: Text(
"Cancel",
style: TextStyle(
color: Color(0xFFF95F62),
fontWeight: FontWeight.w500,
),
),
),
),
SizedBox(width: 16.w),
CustomFilledButton(
width: 166.w,
height: 42.h,
onTap: () {
Navigator.pop(context);
},
label: "Save",
),
],
),
SizedBox(height: 16.h),
],
),
),
);
},
child: state.selectedLanguage == item
? Image.asset( ? Image.asset(
"assets/icons/radio_button_checked.png", "assets/icons/radio_button_checked.png",
scale: 4, scale: 4,
) )
: Image.asset( : Image.asset(
"assets/icons/radio_button_unchecked.png", "assets/icons/radio_button_unchecked.png",
scale: 4, scale: 4,
), ),
), ),
title: CustomText( title: CustomText(
text: item, text: label,
size: 16.sp, size: 16.sp,
color: state.selectedLanguage == item color: isSelected
? Color(0xFFF95F62) ? const Color(0xFFF95F62)
: Color(0xFF000000).withOpacity(.6), : const Color(0xFF000000).withOpacity(.6),
), ),
); );
}, },
), ),
); ),
},
), SizedBox(height: 16.h),
],
/// Save button
CustomFilledButton(
width: double.infinity,
height: 48.h,
onTap: _onSave,
label: "Save",
),
SizedBox(height: 8.h),
],
);
},
), ),
); );
} }
} }

View File

@@ -35,6 +35,8 @@ import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart';
import '../my_pass/repository/my_passes_attractions_repository.dart'; import '../my_pass/repository/my_passes_attractions_repository.dart';
import '../my_pass/repository/my_passes_offers_repository.dart'; import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/pass_attraction_details_view.dart'; import '../my_pass/views/pass_attraction_details_view.dart';
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../profile/view/contact_us/contact_us_view.dart'; import '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/edit_profile/edit_profile_view.dart'; import '../profile/view/edit_profile/edit_profile_view.dart';
import '../profile/view/faq/faq_view.dart'; import '../profile/view/faq/faq_view.dart';
@@ -43,6 +45,8 @@ import '../profile/view/profile_page_view.dart';
import '../profile/view/terms_and_condition/terms_and_condition_view.dart'; import '../profile/view/terms_and_condition/terms_and_condition_view.dart';
import '../search_offers/bloc/offers_bloc.dart'; import '../search_offers/bloc/offers_bloc.dart';
import '../search_offers/repository/offers_repository.dart'; import '../search_offers/repository/offers_repository.dart';
import '../your_itinerary/bloc/yourItineraryDetails/your_itinerary_details_bloc.dart';
import 'global_keys.dart';
import 'route_constants.dart'; import 'route_constants.dart';
class AppRouter { class AppRouter {
@@ -68,6 +72,20 @@ class AppRouter {
}, },
); );
case RouteConstants.noInternet:
final onRetry = settings.arguments as Future<void> Function();
return MaterialPageRoute(
builder: (context) {
final bloc = GlobalKeys.navigatorKey.currentContext!
.read<NoInternetBloc>();
bloc.updateRetry(onRetry);
return BlocProvider.value(
value: bloc,
child: NoInternetScreen(onRetry: onRetry),
);
},
);
case RouteConstants.intro: case RouteConstants.intro:
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) { builder: (_) {
@@ -80,6 +98,7 @@ class AppRouter {
case RouteConstants.passAttractionsPage: case RouteConstants.passAttractionsPage:
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>; final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
final int cityId = args['cityId'] as int; final int cityId = args['cityId'] as int;
final int bookingId = args['bookingId'] as int;
final String source = args['source'] as String; final String source = args['source'] as String;
return MaterialPageRoute( return MaterialPageRoute(
@@ -91,6 +110,7 @@ class AppRouter {
child: PassAttractionsPage( child: PassAttractionsPage(
cityXid: cityId, cityXid: cityId,
source: source, source: source,
bookingId: bookingId,
), ),
); );
}, },
@@ -254,9 +274,14 @@ class AppRouter {
); );
case RouteConstants.yourItinerary: case RouteConstants.yourItinerary:
final itineraryId = settings.arguments as int;
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) { builder: (context) {
return YourItineraryView(); return BlocProvider(
create: (context) => YourItineraryDetailsBloc()
..add(FetchItineraryDetailsEvent(itineraryId: itineraryId)),
child: YourItineraryView(itineraryId: itineraryId,),
);
}, },
); );

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class GlobalKeys { class GlobalKeys {
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
GlobalKey<ScaffoldMessengerState>(); static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
} }

View File

@@ -28,15 +28,20 @@ import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/booking_page_view.dart'; import '../my_pass/views/booking_page_view.dart';
import '../my_pass/views/booking_successful_page_view.dart'; import '../my_pass/views/booking_successful_page_view.dart';
import '../my_pass/views/pass_details_page_view.dart'; import '../my_pass/views/pass_details_page_view.dart';
import '../networkApiServices/noInternet/bloc/no_internet_bloc.dart';
import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../offer_pass_detail/offer_pass_detail_view.dart'; import '../offer_pass_detail/offer_pass_detail_view.dart';
import '../postcard/blocs/postcard_creation_bloc.dart'; import '../postcard/blocs/postcard_creation_bloc.dart';
import '../postcard/views/postcard_creation_page_view.dart'; import '../postcard/views/postcard_creation_page_view.dart';
import '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/privacy/privacy_view.dart'; import '../profile/view/privacy/privacy_view.dart';
import '../search_offers/bloc/offers_bloc.dart'; import '../search_offers/bloc/offers_bloc.dart';
import '../search_offers/bloc/search_offers_listing_bloc.dart'; import '../search_offers/bloc/search_offers_listing_bloc.dart';
import '../search_offers/repository/offers_repository.dart'; import '../search_offers/repository/offers_repository.dart';
import '../search_offers/view/search_offers_with_listing.dart'; import '../search_offers/view/search_offers_with_listing.dart';
import '../your_itinerary/bloc/yourItineraryDetails/your_itinerary_details_bloc.dart';
import '../your_itinerary/view/your_itinerary_view.dart'; import '../your_itinerary/view/your_itinerary_view.dart';
import 'global_keys.dart';
Widget buildOffstageNavigator( Widget buildOffstageNavigator(
int index, int index,
@@ -58,6 +63,16 @@ Widget buildOffstageNavigator(
return IntroScreensView(); return IntroScreensView();
}); });
case RouteConstants.noInternet:
final onRetry = settings.arguments as Future<void> Function();
return MaterialPageRoute(
builder: (context) {
final bloc = GlobalKeys.navigatorKey.currentContext!.read<NoInternetBloc>();
bloc.updateRetry(onRetry);
return BlocProvider.value(value: bloc, child: NoInternetScreen(onRetry: onRetry));
},
);
// 🔹 Attractions Page // 🔹 Attractions Page
case RouteConstants.attractionsPage: case RouteConstants.attractionsPage:
final args = settings.arguments as String; final args = settings.arguments as String;
@@ -67,6 +82,7 @@ Widget buildOffstageNavigator(
case RouteConstants.passAttractionsPage: case RouteConstants.passAttractionsPage:
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>; final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
final int cityId = args['cityId'] as int; final int cityId = args['cityId'] as int;
final int bookingId = args['bookingId'] as int;
final String source = args['source'] as String; final String source = args['source'] as String;
return MaterialPageRoute( return MaterialPageRoute(
@@ -78,6 +94,7 @@ Widget buildOffstageNavigator(
child: PassAttractionsPage( child: PassAttractionsPage(
cityXid: cityId, cityXid: cityId,
source: source, source: source,
bookingId: bookingId,
), ),
); );
}, },
@@ -92,28 +109,34 @@ Widget buildOffstageNavigator(
); );
case RouteConstants.passAttractionDetails: case RouteConstants.passAttractionDetails:
final attractionID = settings.arguments as int; final args = settings.arguments as Map<String, dynamic>;
final attractionId = args['attractionId'] as int;
final bookingId = args['bookingId'] as int;
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) { builder: (_) => PassAttractionDetailsView(
return PassAttractionDetailsView(attractionId: attractionID); attractionId: attractionId,
}, bookingId: bookingId,
),
); );
case RouteConstants.makeBooking: case RouteConstants.makeBooking:
final args = settings.arguments as Map<String, dynamic>?;
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) { builder: (_) => MakeBookingView(
return MakeBookingView( title: args?['title'] ?? '',
title: 'Koh Rong Samloem', description: args?['description'] ?? '',
description: validUpto: args?['validUpto'] ?? '',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis.ß', attractionId: args?['attractionId'] ?? 0,
); bookingId: args?['bookingId'] ?? 0,
}, ),
); );
case RouteConstants.bookingSuccessful: case RouteConstants.bookingSuccessful:
final message = settings.arguments as String;
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) { builder: (_) {
return BookingSuccessfulPageView(); return BookingSuccessfulPageView(message: message,);
}, },
); );
@@ -152,6 +175,12 @@ Widget buildOffstageNavigator(
return const PrivacyPolicyPage(); return const PrivacyPolicyPage();
}, },
); );
case RouteConstants.contactUs:
return MaterialPageRoute(
builder: (_) {
return const ContactUsPage();
},
);
// 🔹 Upload Photo Page (start of postcard creation flow) // 🔹 Upload Photo Page (start of postcard creation flow)
case RouteConstants.uploadPhotoPage: case RouteConstants.uploadPhotoPage:
@@ -208,9 +237,14 @@ Widget buildOffstageNavigator(
); );
case RouteConstants.yourItinerary: case RouteConstants.yourItinerary:
final itineraryId = settings.arguments as int;
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) { builder: (context) {
return YourItineraryView(); return BlocProvider(
create: (context) => YourItineraryDetailsBloc()
..add(FetchItineraryDetailsEvent(itineraryId: itineraryId)),
child: YourItineraryView(itineraryId: itineraryId,),
);
}, },
); );

View File

@@ -3,6 +3,7 @@ class RouteConstants {
static const String intro = '/intro'; static const String intro = '/intro';
static const String splash = '/splash'; static const String splash = '/splash';
static const String noInternet = '/noInternet';
/****************************** HOME SECTION ************************************/ /****************************** HOME SECTION ************************************/

View File

@@ -25,6 +25,7 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
firstName: event.firstName, firstName: event.firstName,
lastName: event.lastName, lastName: event.lastName,
emailAddress: event.emailAddress, emailAddress: event.emailAddress,
isdCode: event.isdCode,
mobileNumber: event.mobileNumber, mobileNumber: event.mobileNumber,
address1: event.address1, address1: event.address1,
address2: event.address2, address2: event.address2,

View File

@@ -11,6 +11,7 @@ class CreateAccountSubmitted extends CreateAccountEvent {
final String firstName; final String firstName;
final String lastName; final String lastName;
final String emailAddress; final String emailAddress;
final String isdCode;
final String mobileNumber; final String mobileNumber;
final String address1; final String address1;
final String address2; final String address2;
@@ -23,6 +24,7 @@ class CreateAccountSubmitted extends CreateAccountEvent {
required this.firstName, required this.firstName,
required this.lastName, required this.lastName,
required this.emailAddress, required this.emailAddress,
required this.isdCode,
required this.mobileNumber, required this.mobileNumber,
required this.address1, required this.address1,
required this.address2, required this.address2,
@@ -37,6 +39,7 @@ class CreateAccountSubmitted extends CreateAccountEvent {
firstName, firstName,
lastName, lastName,
emailAddress, emailAddress,
isdCode,
mobileNumber, mobileNumber,
address1, address1,
address2, address2,

View File

@@ -8,6 +8,7 @@ class CreateAccountRepository {
required String firstName, required String firstName,
required String lastName, required String lastName,
required String emailAddress, required String emailAddress,
required String isdCode,
required String mobileNumber, required String mobileNumber,
required String address1, required String address1,
required String address2, required String address2,
@@ -23,6 +24,7 @@ class CreateAccountRepository {
"firstName": firstName, "firstName": firstName,
"lastName": lastName, "lastName": lastName,
"emailAddress": emailAddress, "emailAddress": emailAddress,
"isdCode": isdCode,
"mobileNumber": mobileNumber, "mobileNumber": mobileNumber,
"address1": address1, "address1": address1,
"address2": address2, "address2": address2,

View File

@@ -2,9 +2,15 @@ import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_textfield.dart'; import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:country_code_picker/country_code_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:phone_numbers_parser/phone_numbers_parser.dart';
import 'package:geocoding/geocoding.dart';
import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart';
import '../../core/route_constants.dart'; import '../../core/route_constants.dart';
import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart'; import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
@@ -36,18 +42,51 @@ class _CreateAccountViewState extends State<CreateAccountView> {
final TextEditingController cityController = TextEditingController(); final TextEditingController cityController = TextEditingController();
final TextEditingController postalController = TextEditingController(); final TextEditingController postalController = TextEditingController();
String? selectedState; // ── Replaced dropdowns with plain text controllers ─────────────────────────
String? selectedCountry; final TextEditingController stateController = TextEditingController();
final TextEditingController countryController = TextEditingController();
// ──────────────────────────────────────────────────────────────────────────
String _selectedIsdCode = '+61';
bool _isZipLoading = false;
// ── PRIMARY geocoding: zip → city, state, country ──────────────────────────
Future<void> fetchLocationFromZip(String zip) async {
if (zip.trim().length < 4) return; // wait for a meaningful zip length
setState(() => _isZipLoading = true);
try {
List<Location> locations = await locationFromAddress(zip);
if (locations.isNotEmpty) {
List<Placemark> placemarks = await placemarkFromCoordinates(
locations.first.latitude,
locations.first.longitude,
);
final place = placemarks.first;
setState(() {
cityController.text = place.locality ?? '';
stateController.text = place.administrativeArea ?? '';
countryController.text = place.country ?? '';
});
}
} catch (e) {
debugPrint("Zip lookup failed: $e");
} finally {
if (mounted) setState(() => _isZipLoading = false);
}
}
// ──────────────────────────────────────────────────────────────────────────
void _submitForm(BuildContext context) { void _submitForm(BuildContext context) {
// 1. Empty field check
if (firstNameController.text.trim().isEmpty || if (firstNameController.text.trim().isEmpty ||
lastNameController.text.trim().isEmpty || lastNameController.text.trim().isEmpty ||
emailController.text.trim().isEmpty || emailController.text.trim().isEmpty ||
phoneController.text.trim().isEmpty || phoneController.text.trim().isEmpty ||
addressController.text.trim().isEmpty || addressController.text.trim().isEmpty ||
cityController.text.trim().isEmpty || cityController.text.trim().isEmpty ||
selectedState == null || stateController.text.trim().isEmpty ||
selectedCountry == null || countryController.text.trim().isEmpty ||
postalController.text.trim().isEmpty) { postalController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please fill all fields')), const SnackBar(content: Text('Please fill all fields')),
@@ -55,17 +94,41 @@ class _CreateAccountViewState extends State<CreateAccountView> {
return; return;
} }
// 2. Phone validation against selected country code
final phone = phoneController.text.trim();
bool isValidPhone = false;
try {
final fullNumber = '$_selectedIsdCode$phone';
final parsed = PhoneNumber.parse(fullNumber);
isValidPhone = parsed.isValid();
} catch (_) {
isValidPhone = false;
}
if (!isValidPhone) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Enter a valid phone number for $_selectedIsdCode'),
backgroundColor: Colors.red,
),
);
return;
}
// 3. Submit
context.read<CreateAccountBloc>().add( context.read<CreateAccountBloc>().add(
CreateAccountSubmitted( CreateAccountSubmitted(
firstName: firstNameController.text.trim(), firstName: firstNameController.text.trim(),
lastName: lastNameController.text.trim(), lastName: lastNameController.text.trim(),
emailAddress: emailController.text.trim(), emailAddress: emailController.text.trim(),
mobileNumber: phoneController.text.trim(), isdCode: _selectedIsdCode,
mobileNumber: phone,
address1: addressController.text.trim(), address1: addressController.text.trim(),
address2: '', address2: '',
city: cityController.text.trim(), city: cityController.text.trim(),
state: selectedState!, state: stateController.text.trim(),
country: selectedCountry!, country: countryController.text.trim(),
postalCode: postalController.text.trim(), postalCode: postalController.text.trim(),
), ),
); );
@@ -80,6 +143,8 @@ class _CreateAccountViewState extends State<CreateAccountView> {
addressController.dispose(); addressController.dispose();
cityController.dispose(); cityController.dispose();
postalController.dispose(); postalController.dispose();
stateController.dispose();
countryController.dispose();
super.dispose(); super.dispose();
} }
@@ -98,14 +163,15 @@ class _CreateAccountViewState extends State<CreateAccountView> {
context.read<ProfileBloc>().add(CheckLoginStatusEvent()); context.read<ProfileBloc>().add(CheckLoginStatusEvent());
context.read<MyPostCardBloc>().add(CheckLoginStatus()); context.read<MyPostCardBloc>().add(CheckLoginStatus());
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary()); context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
// context.read<MyPostCardBloc>().add(FetchDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshDraftPostCards()); context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards()); context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses()); context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
context
.read<MyPostCardsCartBloc>()
.add(CheckLoginAndFetchPostcardsCart());
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of( ScaffoldMessenger.of(context)
context, .showSnackBar(SnackBar(content: Text(state.message)));
).showSnackBar(SnackBar(content: Text(state.message)));
} else if (state is CreateAccountFailure) { } else if (state is CreateAccountFailure) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -130,7 +196,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
), ),
), ),
/// 🔹 Scrollable content starts here /// Scrollable content
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20.w), padding: EdgeInsets.symmetric(horizontal: 20.w),
@@ -140,9 +206,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Row( Row(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () => Navigator.pop(context),
Navigator.pop(context);
},
child: const Icon(Icons.arrow_back), child: const Icon(Icons.arrow_back),
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
@@ -166,204 +230,183 @@ class _CreateAccountViewState extends State<CreateAccountView> {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "First Name", label: "First Name *",
hint: "Enter your first name", hint: "Enter your first name",
controller: firstNameController, controller: firstNameController,
onlyLetters: true,
noSpace: true,
maxLength: 50,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
), ),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Last Name", label: "Last Name *",
hint: "Enter your last name", hint: "Enter your last name",
controller: lastNameController, controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
), ),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Email", label: "Email *",
hint: "Enter your email address", hint: "Enter your email address",
controller: emailController, controller: emailController,
enabled: false, enabled: false,
keyboardType: TextInputType.emailAddress,
), ),
), ),
// ── Phone Number ──────────────────────────────────────
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Phone Number", label: "Phone Number *",
hint: "Enter your phone number", hint: "Enter phone number",
controller: phoneController, controller: phoneController,
keyboardType: TextInputType.number, keyboardType: TextInputType.phone,
maxLength: 10, maxLength: 12,
numbersOnly: true,
prefixWidget: CountryCodePicker(
onChanged: (country) {
setState(() => _selectedIsdCode = country.dialCode!);
},
initialSelection: 'AU',
favorite: const ['+61', '+1', '+44', '+91'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
flagWidth: 24.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
textStyle: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF2D3134),
),
dialogTextStyle: TextStyle(fontSize: 14.sp),
searchDecoration: const InputDecoration(
hintText: 'Search country...',
prefixIcon: Icon(Icons.search),
),
),
), ),
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
CustomText( CustomText(
text: "Location Details", text: "Location Details *",
size: 18.sp, size: 18.sp,
weight: FontWeight.w500, weight: FontWeight.w500,
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// ── Address ───────────────────────────────────────────
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Address", label: "Address *",
hint: "Enter address manually or tap to search", hint: "Enter your address",
controller: addressController, controller: addressController,
maxLength: 50,
), ),
), ),
SizedBox(height: 8.h),
// ── City (unchanged) ──────────────────────────────────
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "City", label: "City *",
hint: "Enter your city", hint: "Enter your city",
maxLength: 50,
// noSpace: true,
controller: cityController, controller: cityController,
isFirstLetterCapital: true,
), ),
), ),
// State Dropdown // ── State now a plain text field ────────────────────
Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "State", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedState,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select state",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedState = value;
});
},
items: [
"New South Wales",
"Victoria",
"Queensland",
"South Australia",
"Western Australia",
"Tasmania",
"Northern Territory",
"Australian Capital Territory"
].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),
// Country Dropdown
Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select country",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedCountry = value;
});
},
items: ["Australia"].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField( child: CustomTextField(
label: "Postal Code", label: "State *",
hint: "Enter postal / zip code", hint: "Enter your state",
controller: postalController, maxLength: 50,
keyboardType: TextInputType.number, // noSpace: true,
maxLength: 6, controller: stateController,
isFirstLetterCapital: true,
), ),
), ),
// ── Country now a plain text field ──────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Country *",
hint: "Enter your country",
maxLength: 50,
// noSpace: true,
controller: countryController,
isFirstLetterCapital: true,
),
),
// ── Zip Code → auto-fills City, State, Country ────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: CustomTextField(
controller: postalController,
keyboardType: TextInputType.number,
maxLength: 6,
onChanged: fetchLocationFromZip,
label: 'Zip Code *',
hint: 'Enter the zip code you reside in',
),
),
if (_isZipLoading)
Padding(
padding: EdgeInsets.only(right: 12.w),
child: SizedBox(
width: 18.w,
height: 18.h,
child:
const CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFC83B61),
),
),
),
],
),
SizedBox(height: 4.h),
Text(
"City, State & Country will auto-fill from zip",
style: TextStyle(
fontSize: 10.sp,
color: const Color(0xFF8E8E8E),
),
),
],
),
),
SizedBox(height: 20.h), SizedBox(height: 20.h),
BlocBuilder<CreateAccountBloc, CreateAccountState>( BlocBuilder<CreateAccountBloc, CreateAccountState>(
builder: (context, state) { builder: (context, state) {
if (state is CreateAccountLoading) { if (state is CreateAccountLoading) {

View File

@@ -12,307 +12,359 @@ class EsimOfferPage extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: Column(
child: Column( children: [
children: [ Container(
Container( color: Colors.white,
color: Colors.white, padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: CommonAppBar(
child: CommonAppBar(isWhiteLogo: false, isProfilePage: false,showDivider: true,), isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
), ),
),
/************************* Top Banner ***********************/ Expanded(
Stack( child: SingleChildScrollView(
children: [
Image.asset(
"assets/images/esim_top_bg.png",
width: double.infinity,
),
Positioned(
top: 32.h,
left: 24.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48.h,
width: 48.w,
decoration: BoxDecoration(
color: Color(0xFFFFFFFF).withOpacity(.2),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(Icons.wifi, color: Colors.white),
),
SizedBox(height: 24.h),
SizedBox(
width: 350.w,
child: CustomText(
text:
"Stay Connected Instantly with Your Complimentary eSIM",
size: 22.sp,
color: Color(0xFFFFFFFF),
),
),
SizedBox(height: 12.h),
SizedBox(
width: 350,
child: CustomText(
text:
"Because every unforgettable trip starts with seamless connectivity.",
size: 14.sp,
color: Colors.white,
),
),
SizedBox(height: 22.h),
Container(
height: 48.h,
width: 165.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(38.r),
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(4, 4),
blurRadius: 5,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: "View Plans",
size: 16.sp,
color: Color(0xFFF95F62),
),
SizedBox(width: 6.w),
Icon(
Icons.arrow_forward,
color: Color(0xFFF95F62),
size: 18,
),
],
),
),
],
),
),
],
),
SizedBox(height: 32.h),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "With your ",
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.w300,
),
),
TextSpan(
text: "eSIM",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 26.sp,
fontWeight: FontWeight.w700,
),
),
TextSpan(
text: ", you can:",
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.w300,
),
),
],
),
),
SizedBox(height: 37.h),
ServiceCard(
"assets/icons/esim_location.png",
"Navigate the city with ease",
"Access real-time maps and directions wherever you go",
),
SizedBox(height: 28.h),
ServiceCard(
"assets/icons/esim_phone.png",
"Book rides, access maps, and find attractions in real time",
"Stay connected to all essential travel services",
),
SizedBox(height: 28.h),
ServiceCard(
"assets/icons/esim_camera.png",
"Share photos and memories instantly",
"Upload and share your travel moments without delay",
),
SizedBox(height: 28.h),
ServiceCard(
"assets/icons/esim_people.png",
"Stay connected with friends, family, and travel plans",
"Never miss important updates or messages while traveling",
),
SizedBox(height: 75.h),
Container(
width: double.infinity,
padding: EdgeInsets.only(
left: 33.w,
right: 33.w,
top: 70.h,
bottom: 37.h,
),
color: Color(0xFFFFF5F5),
child: Column( child: Column(
children: [ children: [
/************************* Top Banner ***********************/
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24.r),
bottomRight: Radius.circular(24.r),
),
child: Container(
height: 340.h,
width: double.infinity,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
"assets/images/esim_top_bg.png",
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
// Black gradient overlay from top to bottom
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.6),
Colors.black.withOpacity(0.3),
Colors.transparent,
],
stops: [0.0, 0.4, 1.0],
),
),
),
),
Positioned(
top: 32.h,
left: 24.w,
right: 24.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48.h,
width: 48.w,
decoration: BoxDecoration(
color: Color(0xFFFFFFFF).withOpacity(.2),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(Icons.wifi, color: Colors.white),
),
SizedBox(height: 24.h),
SizedBox(
width: 350.w,
child: CustomText(
text:
"Connect instantly with your free eSIM",
size: 22.sp,
color: Color(0xFFFFFFFF),
),
),
SizedBox(height: 12.h),
SizedBox(
width: 350,
child: CustomText(
text:
"Every great journey begins with smooth connectivity.",
size: 14.sp,
color: Colors.white,
),
),
SizedBox(height: 22.h),
Container(
height: 48.h,
width: 165.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(38.r),
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(4, 4),
blurRadius: 5,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: "View Plans",
size: 16.sp,
color: Color(0xFFF95F62),
),
SizedBox(width: 6.w),
Icon(
Icons.arrow_forward,
color: Color(0xFFF95F62),
size: 18,
),
],
),
),
],
),
),
],
),
),
),
SizedBox(height: 32.h),
Text.rich( Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: "Simple ", text: "With your ",
style: TextStyle(fontSize: 26.sp), style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.w300,
),
), ),
TextSpan( TextSpan(
text: "3-Step Process", text: "eSIM",
style: TextStyle( style: TextStyle(
color: Color(0xFFF95F62), color: Color(0xFFF95F62),
fontSize: 26.sp, fontSize: 26.sp,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
TextSpan(
text: ", you can:",
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.w300,
),
),
], ],
), ),
), ),
SizedBox(height: 16.h), SizedBox(height: 37.h),
CustomText( ServiceCard(
text: "Get connected in seconds", "assets/icons/esim_location.png",
size: 17.5, "Navigate the city with ease",
color: Color(0xFF4B5563), "Access real-time maps and directions wherever you go",
),
SizedBox(height: 56.h),
ProcessCard(
"Receive QR Code",
"Get your unique eSIM QR code with your CityCard",
"1",
"assets/icons/process_qr.png",
), ),
SizedBox(height: 28.h), SizedBox(height: 28.h),
ProcessCard( ServiceCard(
"Scan Code", "assets/icons/esim_phone.png",
"Open your phone camera and scan the QR code", "Book rides, access maps, and find attractions in real time",
"2", "Stay connected to all essential travel services",
"assets/icons/process_phone.png",
), ),
SizedBox(height: 28.h), SizedBox(height: 28.h),
ProcessCard( ServiceCard(
"Connected", "assets/icons/esim_camera.png",
"You're online instantly - start exploring!", "Share photos and memories instantly",
"3", "Upload and share your travel moments without delay",
"assets/icons/process_wifi.png", ),
SizedBox(height: 28.h),
ServiceCard(
"assets/icons/esim_people.png",
"Stay connected with friends, family, and travel plans",
"Never miss important updates or messages while traveling",
),
SizedBox(height: 75.h),
Container(
width: double.infinity,
padding: EdgeInsets.only(
left: 33.w,
right: 33.w,
top: 70.h,
bottom: 37.h,
),
color: Color(0xFFFFF5F5),
child: Column(
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Simple ",
style: TextStyle(fontSize: 24.sp),
),
TextSpan(
text: "3-Step Process",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 24.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(height: 16.h),
CustomText(
text: "Get connected in seconds",
size: 16,
color: Color(0xFF4B5563),
),
SizedBox(height: 56.h),
ProcessCard(
"Receive QR Code",
"Get your unique eSIM QR code with your CityCard",
"1",
"assets/icons/process_qr.png",
),
SizedBox(height: 28.h),
ProcessCard(
"Scan Code",
"Open your phone camera and scan the QR code",
"2",
"assets/icons/process_phone.png",
),
SizedBox(height: 28.h),
ProcessCard(
"Connected",
"You're online instantly - start exploring!",
"3",
"assets/icons/process_wifi.png",
),
],
),
),
Stack(
children: [
Image.asset(
'assets/images/esim_bottom_banner.png',
fit: BoxFit.contain,
),
Positioned.fill(
child: Container(
height: double.infinity,
width: double.infinity,
color: Colors.black.withOpacity(.68),
),
),
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "It's one more way",
style: TextStyle(
color: Colors.white,
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " CityCards",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(
height: 4.h,
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "makes your journey",
style: TextStyle(
color: Colors.white,
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " smarter",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(
height: 4.h,
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "and more",
style: TextStyle(
color: Colors.white,
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " effortless",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(
height: 28.h,
),
CustomFilledButton(
onTap: () {},
label: "Start Your Journey Today",
height: 60.h,
width: 300.w,
showArrow: true,
),
],
),
),
],
), ),
], ],
), ),
), ),
),
Stack( ],
children: [
Image.asset(
'assets/images/esim_bottom_banner.png',
fit: BoxFit.contain,
),
Positioned.fill(
child: Container(
height: double.infinity,
width: double.infinity,
color: Colors.black.withOpacity(.68),
),
),
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: "It's one more way",
style: TextStyle(
color: Colors.white,
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " CityCard",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(height: 4.h,),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "makes your journey",
style: TextStyle(
color: Colors.white,
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " smarter",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(height: 4.h,),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "and more",
style: TextStyle(
color: Colors.white,
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " effortless",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
SizedBox(height: 28.h,),
CustomFilledButton(onTap: (){}, label: "Start Your Journey Today", height: 60.h, width: 300.w,showArrow: true,),
],
),
),
],
),
],
),
), ),
), ),
); );
@@ -401,4 +453,4 @@ class EsimOfferPage extends StatelessWidget {
), ),
); );
} }
} }

View File

@@ -8,25 +8,25 @@ class CityList {
if (json['cities'] != null) { if (json['cities'] != null) {
cities = <Cities>[]; cities = <Cities>[];
json['cities'].forEach((v) { json['cities'].forEach((v) {
cities!.add(new Cities.fromJson(v)); cities!.add(Cities.fromJson(v));
}); });
} }
if (json['upcomingCities'] != null) { if (json['upcomingCities'] != null) {
upcomingCities = <UpcomingCities>[]; upcomingCities = <UpcomingCities>[];
json['upcomingCities'].forEach((v) { json['upcomingCities'].forEach((v) {
upcomingCities!.add(new UpcomingCities.fromJson(v)); upcomingCities!.add(UpcomingCities.fromJson(v));
}); });
} }
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>(); final Map<String, dynamic> data = {};
if (this.cities != null) { if (cities != null) {
data['cities'] = this.cities!.map((v) => v.toJson()).toList(); data['cities'] = cities!.map((v) => v.toJson()).toList();
} }
if (this.upcomingCities != null) { if (upcomingCities != null) {
data['upcomingCities'] = data['upcomingCities'] =
this.upcomingCities!.map((v) => v.toJson()).toList(); upcomingCities!.map((v) => v.toJson()).toList();
} }
return data; return data;
} }
@@ -41,18 +41,27 @@ class Cities {
int? cityCardTicketAmt; int? cityCardTicketAmt;
int? saveAmount; int? saveAmount;
String? saveLabel; String? saveLabel;
// ✅ added safely
String? cityIconPath;
CityIcon? icon;
// ✅ kept to avoid breaking existing usage
List<UpcomingCities>? upcomingCities; List<UpcomingCities>? upcomingCities;
Cities( Cities({
{this.id, this.id,
this.cityName, this.cityName,
this.tagLine, this.tagLine,
this.bannerImage, this.bannerImage,
this.indivisualTicketAmt, this.indivisualTicketAmt,
this.cityCardTicketAmt, this.cityCardTicketAmt,
this.saveAmount, this.saveAmount,
this.saveLabel, this.saveLabel,
this.upcomingCities}); this.cityIconPath,
this.icon,
this.upcomingCities,
});
Cities.fromJson(Map<String, dynamic> json) { Cities.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
@@ -63,32 +72,55 @@ class Cities {
cityCardTicketAmt = json['cityCardTicketAmt']; cityCardTicketAmt = json['cityCardTicketAmt'];
saveAmount = json['saveAmount']; saveAmount = json['saveAmount'];
saveLabel = json['saveLabel']; saveLabel = json['saveLabel'];
cityIconPath = json['cityIconPath'];
icon = json['icon'] != null ? CityIcon.fromJson(json['icon']) : null;
if (json['upcomingCities'] != null) { if (json['upcomingCities'] != null) {
upcomingCities = <UpcomingCities>[]; upcomingCities = <UpcomingCities>[];
json['upcomingCities'].forEach((v) { json['upcomingCities'].forEach((v) {
upcomingCities!.add(new UpcomingCities.fromJson(v)); upcomingCities!.add(UpcomingCities.fromJson(v));
}); });
} }
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>(); final Map<String, dynamic> data = {};
data['id'] = this.id; data['id'] = id;
data['cityName'] = this.cityName; data['cityName'] = cityName;
data['tagLine'] = this.tagLine; data['tagLine'] = tagLine;
data['bannerImage'] = this.bannerImage; data['bannerImage'] = bannerImage;
data['indivisualTicketAmt'] = this.indivisualTicketAmt; data['indivisualTicketAmt'] = indivisualTicketAmt;
data['cityCardTicketAmt'] = this.cityCardTicketAmt; data['cityCardTicketAmt'] = cityCardTicketAmt;
data['saveAmount'] = this.saveAmount; data['saveAmount'] = saveAmount;
data['saveLabel'] = this.saveLabel; data['saveLabel'] = saveLabel;
if (this.upcomingCities != null) { data['cityIconPath'] = cityIconPath;
data['icon'] = icon?.toJson();
if (upcomingCities != null) {
data['upcomingCities'] = data['upcomingCities'] =
this.upcomingCities!.map((v) => v.toJson()).toList(); upcomingCities!.map((v) => v.toJson()).toList();
} }
return data; return data;
} }
} }
class CityIcon {
String? svg;
CityIcon({this.svg});
CityIcon.fromJson(Map<String, dynamic> json) {
svg = json['svg'];
}
Map<String, dynamic> toJson() {
return {
'svg': svg,
};
}
}
class UpcomingCities { class UpcomingCities {
int? id; int? id;
String? cityName; String? cityName;
@@ -103,10 +135,10 @@ class UpcomingCities {
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>(); return {
data['id'] = this.id; 'id': id,
data['cityName'] = this.cityName; 'cityName': cityName,
data['imgPathName'] = this.imgPathName; 'imgPathName': imgPathName,
return data; };
} }
} }

View File

@@ -76,7 +76,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: false), CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: false),
SizedBox(height: 140.h), SizedBox(height: 120.h),
Text( Text(
"CityCards.\nSee More,\nSpend Less.", "CityCards.\nSee More,\nSpend Less.",
style: TextStyle( style: TextStyle(
@@ -91,32 +91,35 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
ElevatedButton( SizedBox(
style: ElevatedButton.styleFrom( height: 50.h,
fixedSize: const Size(200, 50), width: 200.w,
padding: EdgeInsets.symmetric( child: ElevatedButton(
horizontal: 15.w, style: ElevatedButton.styleFrom(
vertical: 15.h, padding: EdgeInsets.symmetric(
), horizontal: 15.w,
backgroundColor: const Color(0xffF95F62), vertical: 15.h,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.r),
),
),
onPressed: _handleGetCityCard,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Get You'r CityCard",
style: TextStyle(color: Colors.white),
), ),
SizedBox(width: 10.w), backgroundColor: const Color(0xffF95F62),
Image.asset("assets/icons/arrow.png", height: 13.h), shape: RoundedRectangleBorder(
], borderRadius: BorderRadius.circular(25.r),
),
),
onPressed: _handleGetCityCard,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Get Your CityCards",
style: TextStyle(color: Colors.white,fontSize: 14.sp),
),
SizedBox(width: 10.w),
Image.asset("assets/icons/arrow.png", height: 13.h),
],
),
), ),
), ),
SizedBox(height: 80.h), SizedBox(height: 50.h),
Text.rich( Text.rich(
TextSpan( TextSpan(
children: [ children: [
@@ -205,6 +208,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
onTap: () async { onTap: () async {
await LocalPreference.updateOnboardingPage(2); await LocalPreference.updateOnboardingPage(2);
await LocalPreference.setSelectedCityId(city.id!); await LocalPreference.setSelectedCityId(city.id!);
await LocalPreference.setSelectedCityLogo(city.cityIconPath??"");
Navigator.pushReplacementNamed( Navigator.pushReplacementNamed(
context, context,
RouteConstants.home, RouteConstants.home,
@@ -317,8 +321,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
separatorBuilder: (_, __) => SizedBox(width: 16.w), separatorBuilder: (_, __) => SizedBox(width: 16.w),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final city = upcomingCities[index]; final city = upcomingCities[index];
final imageUrl = final imageUrl ='${ApiUrls.baseUrl}${city.imgPathName}';
'${ApiUrls.baseUrl}${city.imgPathName}';
return Column( return Column(
children: [ children: [

View File

@@ -4,11 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart'; import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart';
import 'package:citycards_customer/core/inside_bottom_navigator.dart'; import 'package:citycards_customer/core/inside_bottom_navigator.dart';
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart';
import 'package:citycards_customer/my_pass/views/my_pass_page_view.dart'; import 'package:citycards_customer/my_pass/views/my_pass_page_view.dart';
import 'package:citycards_customer/postcard/views/postcard_initial_page_view.dart';
import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../itinerary_creation/views/magic_itinerary_empty_view.dart';
import 'registered_user_home_page.dart'; import 'registered_user_home_page.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/home/widgets/e_sim_offer_section.dart'; import 'package:citycards_customer/home/widgets/e_sim_offer_section.dart';
import 'package:citycards_customer/home/widgets/hotel_offers_section.dart'; import 'package:citycards_customer/home/widgets/hotel_offers_section.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -6,6 +7,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/app_bar.dart'; import '../../common_packages/app_bar.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../common_packages/custom_text.dart';
import '../../core/route_constants.dart'; import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart'; import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/api_urls.dart';
@@ -31,7 +34,6 @@ class RegisteredUserHomePage extends StatefulWidget {
} }
class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> { class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
@override
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -39,6 +41,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
_checkAndShowCitySelection(); _checkAndShowCitySelection();
_loadProfileIfLoggedIn(); _loadProfileIfLoggedIn();
} }
Future<void> _loadProfileIfLoggedIn() async { Future<void> _loadProfileIfLoggedIn() async {
final userId = await LocalPreference.getUserId(); final userId = await LocalPreference.getUserId();
@@ -63,14 +66,11 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
Future<void> _checkAndShowCitySelection() async { Future<void> _checkAndShowCitySelection() async {
final int cityId = await LocalPreference.getSelectedCityId(); final int cityId = await LocalPreference.getSelectedCityId();
// If cityId is 1 (default) or invalid, show city selection
if (cityId == 0) { if (cityId == 0) {
// Use addPostFrameCallback to show bottom sheet after build is complete
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_showCitySelectionBottomSheet(); _showCitySelectionBottomSheet();
}); });
} else { } else {
// Load home data only if city is already selected
if (mounted) { if (mounted) {
context.read<HomeBloc>().add(FetchHomeData()); context.read<HomeBloc>().add(FetchHomeData());
} }
@@ -82,271 +82,323 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isDismissible: false, // Prevent dismissing without selecting a city isDismissible: false,
enableDrag: false, // Prevent dragging to close enableDrag: false,
builder: (_) => const CitySelectionBottomSheet(), builder: (_) => const CitySelectionBottomSheet(),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return Scaffold(
child: BlocBuilder<HomeBloc, HomeState>( backgroundColor: Colors.white,
builder: (context, state) { body: SafeArea(
if (state is HomeLoading) { child: RefreshIndicator(
return const Center(child: CircularProgressIndicator()); color: Color(0xffF95F62),
} onRefresh: () async {
await _checkAndShowCitySelection();
},
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state is HomeLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
}
if (state is HomeError) { if (state is HomeError) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${state.message}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<HomeBloc>().add(FetchHomeData());
},
child: const Text('Retry'),
),
],
),
);
}
if (state is HomeLoaded) {
final city = state.homeModel.city;
final attractions = state.homeModel.attraction ?? [];
final String? cityIconUrl =
city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
? "${ApiUrls.baseUrl}${city.cityIconPath}"
: null;
final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
? city!.cityBanners!.firstWhere(
(banner) => banner.isActive == true && banner.imageFilePath != null,
orElse: () => city.cityBanners!.first,
).imageFilePath
: null;
return SingleChildScrollView(
child: Stack(
children: [
// Background image - use city banner if available
_buildBannerImage(bannerImageUrl),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 40.h),
Icon(
Icons.error_outline,
size: 120.sp,
color: Colors.red.withOpacity(0.3),
),
SizedBox(height: 32.h),
CustomText(
text: "Oops! Something went wrong",
size: 18.sp,
weight: FontWeight.w600,
textAlign: TextAlign.center,
),
SizedBox(height: 12.h),
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column( child: CustomText(
crossAxisAlignment: CrossAxisAlignment.start, text: state.message,
children: [ size: 14.sp,
CommonAppBar( color: Color(0xFF656565),
isWhiteLogo: false, textAlign: TextAlign.center,
isProfilePage: false, ),
showDivider: false, ),
imageUrl: cityIconUrl,
isSelectCity: true,
),
SizedBox(height: 60.h),
// City name from API SizedBox(height: 32.h),
Text( CustomFilledButton(
city?.cityName ?? "City Name", onTap:() {
style: const TextStyle( context.read<HomeBloc>().add(FetchHomeData());
color: Colors.white, },
fontWeight: FontWeight.w500, label: "Try Again",
fontSize: 44, ),
), ],
), ),
SizedBox(height: 4.h), );
}
// City description from API if (state is HomeLoaded) {
Text( final city = state.homeModel.city;
city?.description ?? "City description", final attractions = state.homeModel.attraction ?? [];
style: TextStyle( final String? cityIconUrl =
color: Colors.white.withOpacity(0.9), city?.cityIconPath != null && city!.cityIconPath!.isNotEmpty
fontSize: 12, ? "${ApiUrls.baseUrl}${city.cityIconPath}"
fontWeight: FontWeight.w400, : null;
), final bannerImageUrl = city?.cityBanners?.isNotEmpty == true
maxLines: 2, ? city!.cityBanners!
overflow: TextOverflow.ellipsis, .firstWhere(
), (banner) =>
SizedBox(height: 12.h), banner.isActive == true &&
banner.imageFilePath != null,
orElse: () => city.cityBanners!.first,
)
.imageFilePath
: null;
// Category tags - you can customize this based on your needs return SingleChildScrollView(
Wrap( child: Stack(
spacing: 8, children: [
runSpacing: 8, // Background image - use city banner if available
children: (city?.cityHighlights ?? []) _buildBannerImage(bannerImageUrl),
.where((highlight) => highlight.isActive == true) Column(
.map( crossAxisAlignment: CrossAxisAlignment.start,
(highlight) => _buildTag( children: [
highlight.title ?? "", Padding(
), padding: EdgeInsets.all(10.r),
) child: Column(
.toList(), crossAxisAlignment: CrossAxisAlignment.start,
),
SizedBox(height: 60.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text.rich( CommonAppBar(
TextSpan( isWhiteLogo: false,
children: const [ isProfilePage: false,
showDivider: false,
// imageUrl: cityIconUrl,
isSelectCity: true,
),
SizedBox(height: 130.h),
// City name from API
Text(
city?.cityName ?? "City Name",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 44.sp,
),
),
SizedBox(height: 4.h),
// City description from API
Text(
city?.description ?? "City description",
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 12.h),
// Category tags
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: () {
final tags = (city?.cityHighlights ?? [])
.where((highlight) => highlight.isActive == true)
.map((highlight) => Padding(
padding: EdgeInsets.only(right: 8.w),
child: _buildTag(highlight.title ?? ""),
))
.toList();
return tags.isEmpty ? [_buildTag("No Highlights Available")] : tags;
}(),
),
),
SizedBox(height: 40.h),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan( TextSpan(
text: "Popular ", children: [
TextSpan(
text: "Popular ",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
),
),
TextSpan(
text: "Attractions",
style: TextStyle(
fontSize: 18.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
),
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.attractionsPage,
arguments: "home",
);
},
child: Text(
"View all",
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 12.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xffF95F62), color: Color(0xffF95F62),
), ),
), ),
TextSpan(
text: "Attractions",
style: TextStyle(
fontSize: 18,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
),
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.attractionsPage,
arguments: "home",
);
},
child: const Text(
"View all",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xffF95F62),
), ),
), ],
), ),
SizedBox(height: 12.h),
// Pass attractions from API
AttractionsListView(attractions: attractions),
], ],
), ),
const SizedBox(height: 12), ),
// Pass attractions from API InwardCurvedContainer(
AttractionsListView(attractions: attractions), child: Column(
], mainAxisSize: MainAxisSize.min,
),
),
InwardCurvedContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 40.h),
const ItineraryVideo(),
SizedBox(height: 20.h),
// Button section
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: 200,
child: ElevatedButton(
onPressed: () {
context.read<NavigationBloc>().add(NavigationTabChanged(1));
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Create my itinerary",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_forward, color: Colors.white),
],
),
),
),
),
],
),
),
ESimOfferSection(),
HotelOffersSection(),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [ children: [
InkWell( SizedBox(height: 40.h),
onTap: () { const ItineraryVideo(),
Navigator.of(context).pushNamed(RouteConstants.searchOffer); SizedBox(height: 20.h),
},
child: _buildFeatureCard( // Button section
image: "assets/images/claim_offers_bg.jpg", Container(
title: "Claim offers with your City Cards", margin: EdgeInsets.symmetric(horizontal: 16.w),
subtitle: "Lorem ipsum dolor sit amet...", child: SizedBox(
width: 240.w,
child: ElevatedButton(
onPressed: () {
context.read<NavigationBloc>().add(
NavigationTabChanged(1),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(
vertical: 14.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
30.r,
),
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Create My Magic Itinerary",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
),
),
SizedBox(width: 4.w),
const Icon(
Icons.arrow_forward,
color: Colors.white,
),
],
),
),
), ),
), ),
], ],
), ),
),
const SizedBox(height: 24), ESimOfferSection(),
ChooseYourPassSection( HotelOffersSection(),
cards: state.homeModel.city?.cards ?? [], Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.searchOffer,
);
},
child: _buildFeatureCard(
image:
"assets/images/claim_offers_bg.jpg",
title: "Claim offers with your City Cards",
subtitle: "Lorem ipsum dolor sit amet...",
),
),
],
),
SizedBox(height: 24.h),
ChooseYourPassSection(
cards: state.homeModel.city?.cards ?? [],
),
SizedBox(height: 20.h),
GetYourPassCard(),
SizedBox(height: 20.h),
],
), ),
const SizedBox(height: 20), ),
GetYourPassCard(), ],
],
),
), ),
], ],
), ),
], );
), }// Initial state
); return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
} },
),
// Initial state ),
return const Center(child: CircularProgressIndicator());
},
), ),
); );
} }
Widget _buildTag(String label) { Widget _buildTag(String label) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xffFFFFFF).withOpacity(0.29), color: const Color(0xffFFFFFF).withOpacity(0.29),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20.r),
), ),
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 12, fontSize: 12.sp,
), ),
), ),
); );
@@ -360,23 +412,23 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
return Stack( return Stack(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16.r),
child: Image.asset( child: Image.asset(
image, image,
height: 200, height: 220.h,
width: double.infinity, width: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
Positioned( Positioned(
left: 16, left: 16.w,
right: 16, right: 16.w,
bottom: 16, bottom: 16.h,
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: EdgeInsets.all(12.r),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9), color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16.r),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -389,9 +441,9 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
title, title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 18, fontSize: 18.sp,
), ),
), ),
Text( Text(
@@ -399,20 +451,20 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 14, fontSize: 14.sp,
color: Colors.black.withOpacity(0.6), color: Colors.black.withOpacity(0.6),
), ),
), ),
], ],
), ),
), ),
const SizedBox(width: 8), SizedBox(width: 8.w),
Container( Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xffFDCDCE), color: Color(0xffFDCDCE),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
padding: const EdgeInsets.all(12), padding: EdgeInsets.all(12.r),
child: Image.asset( child: Image.asset(
"assets/icons/arrow_angle_up.png", "assets/icons/arrow_angle_up.png",
scale: 4, scale: 4,
@@ -425,47 +477,36 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
], ],
); );
} }
Widget _buildBannerImage(String? imageUrl) {
if (imageUrl == null || imageUrl.isEmpty) {
// Use placeholder if no image URL
return Image.asset(
"assets/images/chicago.png",
height: 300.h,
width: double.infinity,
fit: BoxFit.cover,
);
}
return Image.network( Widget _buildBannerImage(String? imageUrl) {
imageUrl, return SizedBox(
height: 300.h, height: 350.h,
width: double.infinity, width: double.infinity,
fit: BoxFit.cover, child: (imageUrl == null || imageUrl.isEmpty)
loadingBuilder: (context, child, loadingProgress) { ? Image.asset(
if (loadingProgress == null) return child; "assets/images/chicago.png",
return Container( fit: BoxFit.cover,
height: 300.h, )
width: double.infinity, : CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
// 🔄 Loader (same as your loadingBuilder)
placeholder: (context, url) => Container(
color: Colors.grey[300], color: Colors.grey[300],
child: Center( child: const Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null color: Color(0xffF95F62),
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
), ),
), ),
); ),
},
errorBuilder: (context, error, stackTrace) { // ❌ Error fallback (same as errorBuilder)
// Use placeholder on error errorWidget: (context, url, error) => Image.asset(
return Image.asset(
"assets/images/chicago.png", "assets/images/chicago.png",
height: 300.h,
width: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
); ),
}, ),
); );
} }
} }

View File

@@ -1,7 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../attraction_details/views/attraction_details_view.dart'; import '../../attraction_details/views/attraction_details_view.dart';
import '../../core/route_constants.dart';
import '../model/home_model.dart'; import '../model/home_model.dart';
class AttractionsListView extends StatefulWidget { class AttractionsListView extends StatefulWidget {
@@ -39,14 +41,12 @@ class _AttractionsListViewState extends State<AttractionsListView> {
}); });
} }
// Get cover image from attraction galleries
String? _getCoverImage(Attraction attraction) { String? _getCoverImage(Attraction attraction) {
if (attraction.attractionGalleries == null || if (attraction.attractionGalleries == null ||
attraction.attractionGalleries!.isEmpty) { attraction.attractionGalleries!.isEmpty) {
return null; return null;
} }
// Try to find the cover image
final coverImage = attraction.attractionGalleries!.firstWhere( final coverImage = attraction.attractionGalleries!.firstWhere(
(gallery) => gallery.isCoverImage == true, (gallery) => gallery.isCoverImage == true,
orElse: () => attraction.attractionGalleries!.first, orElse: () => attraction.attractionGalleries!.first,
@@ -57,14 +57,13 @@ class _AttractionsListViewState extends State<AttractionsListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Show placeholder if no attractions
if (widget.attractions.isEmpty) { if (widget.attractions.isEmpty) {
return const Center( return Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(20.0), padding: EdgeInsets.all(20.w),
child: Text( child: Text(
'No attractions available', 'No attractions available',
style: TextStyle(fontSize: 16, color: Colors.grey), style: TextStyle(fontSize: 16.sp, color: Colors.grey),
), ),
), ),
); );
@@ -73,11 +72,11 @@ class _AttractionsListViewState extends State<AttractionsListView> {
return Column( return Column(
children: [ children: [
SizedBox( SizedBox(
height: 240, height: 240.h,
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(right: 16), padding: EdgeInsets.only(right: 16.w),
itemCount: widget.attractions.length, itemCount: widget.attractions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final attraction = widget.attractions[index]; final attraction = widget.attractions[index];
@@ -88,68 +87,65 @@ class _AttractionsListViewState extends State<AttractionsListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AttractionDetailsView(attractionId: attraction.id), builder: (context) =>
AttractionDetailsView(attractionId: attraction.id),
), ),
); );
}, },
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
margin: const EdgeInsets.only(right: 16), margin: EdgeInsets.only(right: 16.w),
padding: const EdgeInsets.all(4), padding: EdgeInsets.all(4.r),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: const Color(0xFFF95F62).withOpacity(0.24), color: const Color(0xFFF95F62).withOpacity(0.24),
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16.r),
), ),
child: Container( child: Container(
height: 232, height: 232.h,
width: 161, width: 161.w,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16.r),
color: Colors.grey[300], color: Colors.grey[300],
), ),
child: Stack( child: Stack(
children: [ children: [
// Image or placeholder // Image
if (imageUrl != null) if (imageUrl != null)
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16.r),
child: Image.network( child: CachedNetworkImage(
imageUrl, imageUrl: imageUrl,
height: 232, height: 232.h,
width: 161, width: 161.w,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { memCacheWidth: 400,
return _buildPlaceholder(); memCacheHeight: 600,
}, placeholder: (context, url) => const Center(
loadingBuilder: (context, child, loadingProgress) { child: CircularProgressIndicator(color: Color(0xffF95F62)),
if (loadingProgress == null) return child; ),
return Center( errorWidget: (context, url, error) => _buildPlaceholder(),
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
), ),
) )
else else
_buildPlaceholder(), _buildPlaceholder(),
// Title overlay // Title + Description Overlay
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: Container( child: Container(
padding: const EdgeInsets.all(12), width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16), bottomLeft: Radius.circular(16.r),
bottomRight: Radius.circular(16), bottomRight: Radius.circular(16.r),
), ),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
@@ -160,15 +156,34 @@ class _AttractionsListViewState extends State<AttractionsListView> {
], ],
), ),
), ),
child: Text( child: Column(
attraction.title ?? 'Untitled', crossAxisAlignment: CrossAxisAlignment.start,
style: GoogleFonts.poppins( mainAxisSize: MainAxisSize.min,
color: Colors.white, children: [
fontWeight: FontWeight.w600, Text(
fontSize: 14, attraction.title ?? 'Untitled',
), textAlign: TextAlign.left,
maxLines: 2, style: GoogleFonts.poppins(
overflow: TextOverflow.ellipsis, color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Text(
attraction.description ?? '',
textAlign: TextAlign.left,
style: GoogleFonts.poppins(
color:
Colors.white.withOpacity(0.8),
fontSize: 12.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
), ),
), ),
), ),
@@ -180,16 +195,16 @@ class _AttractionsListViewState extends State<AttractionsListView> {
}, },
), ),
), ),
const SizedBox(height: 20), SizedBox(height: 20.h),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SizedBox( child: SizedBox(
width: 200, width: 200.w,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10.r),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: _scrollProgress, value: _scrollProgress,
minHeight: 6, minHeight: 6.h,
backgroundColor: const Color(0xffFEE7E7), backgroundColor: const Color(0xffFEE7E7),
color: const Color(0xffF95F62), color: const Color(0xffF95F62),
), ),
@@ -203,16 +218,16 @@ class _AttractionsListViewState extends State<AttractionsListView> {
Widget _buildPlaceholder() { Widget _buildPlaceholder() {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16.r),
color: Colors.grey[300], color: Colors.grey[300],
), ),
child: const Center( child: Center(
child: Icon( child: Icon(
Icons.image_outlined, Icons.image_outlined,
size: 50, size: 50.sp,
color: Colors.grey, color: Colors.grey,
), ),
), ),
); );
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@@ -38,15 +39,22 @@ class ExploreCitiesCard extends StatelessWidget {
children: [ children: [
/// Background Image with fallback /// Background Image with fallback
_isNetworkImage _isNetworkImage
? Image.network( ? CachedNetworkImage(
imageUrl, imageUrl: imageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { placeholder: (context, url) => Container(
return Image.asset( color: Colors.grey.shade200,
'assets/images/city_sydney.png', child: const Center(
fit: BoxFit.cover, child: CircularProgressIndicator(
); color: Color(0xffF95F62),
}, strokeWidth: 2,
),
),
),
errorWidget: (context, url, error) => Image.asset(
'assets/images/city_sydney.png',
fit: BoxFit.cover,
),
) )
: Image.asset( : Image.asset(
'assets/images/city_sydney.png', 'assets/images/city_sydney.png',

View File

@@ -25,7 +25,7 @@ class GetYourPassCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
"Get your Pass", "Get Your Card",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -66,7 +66,7 @@ class GetYourPassCard extends StatelessWidget {
Text( Text(
"Attractions", "Attractions",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 13.sp, fontSize: 12.sp,
color: Colors.black, color: Colors.black,
fontWeight: FontWeight.w400 fontWeight: FontWeight.w400
), ),
@@ -79,7 +79,7 @@ class GetYourPassCard extends StatelessWidget {
Text( Text(
"From", "From",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 12.sp, fontSize: 11.sp,
color: Colors.black87, color: Colors.black87,
), ),
), ),
@@ -89,7 +89,7 @@ class GetYourPassCard extends StatelessWidget {
TextSpan( TextSpan(
text: "\$20", text: "\$20",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 14.sp, fontSize: 13.sp,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Colors.black, color: Colors.black,
), ),
@@ -97,7 +97,7 @@ class GetYourPassCard extends StatelessWidget {
TextSpan( TextSpan(
text: " /Adult", text: " /Adult",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 13.sp, fontSize: 12 .sp,
color: Colors.black87, color: Colors.black87,
), ),
), ),

View File

@@ -1,44 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; import 'package:lottie/lottie.dart';
class ItineraryVideo extends StatefulWidget { class ItineraryVideo extends StatelessWidget {
const ItineraryVideo({super.key}); const ItineraryVideo({super.key});
@override
State<ItineraryVideo> createState() => _ItineraryVideoState();
}
class _ItineraryVideoState extends State<ItineraryVideo> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.asset(
'assets/gif/itinenary_animation_for_citycards.mp4',
)
..initialize().then((_) {
_controller.setLooping(true);
_controller.play();
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: _controller.value.isInitialized child: Lottie.asset(
? AspectRatio( 'assets/intro/itinerary_animation.json', // 👈 your path
aspectRatio: _controller.value.aspectRatio, repeat: true,
child: VideoPlayer(_controller), animate: true,
) ),
: const CircularProgressIndicator(),
); );
} }
} }

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@@ -50,26 +52,26 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Choose your Pass", "Choose Your Card",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 18, fontSize: 18.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 8), SizedBox(height: 8.h),
Text( Text(
"Dive into an extensive selection of thrilling destinations, " "Dive into an extensive selection of thrilling destinations, "
"thoughtfully categorized to help you find the perfect getaway.", "thoughtfully categorized to help you find the perfect getaway.",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 13, fontSize: 13.sp,
color: Colors.grey[700], color: Colors.grey[700],
), ),
), ),
const SizedBox(height: 20), SizedBox(height: 20.h),
// ===== PAGEVIEW ===== // ===== PAGEVIEW =====
SizedBox( SizedBox(
height: 430, height: 430.h,
child: PageView.builder( child: PageView.builder(
controller: _pageController, controller: _pageController,
itemCount: widget.cards.length, itemCount: widget.cards.length,
@@ -79,7 +81,7 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
), ),
), ),
const SizedBox(height: 12), SizedBox(height: 12.h),
// ===== INDICATOR ===== // ===== INDICATOR =====
Center( Center(
@@ -89,11 +91,11 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
bool isActive = index == _currentPage; bool isActive = index == _currentPage;
return AnimatedContainer( return AnimatedContainer(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
margin: const EdgeInsets.symmetric(horizontal: 4), margin: EdgeInsets.symmetric(horizontal: 4.w),
width: isActive ? 40 : 20, width: isActive ? 40.w : 20.w,
height: 6, height: 6.h,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10.r),
color: isActive color: isActive
? const Color(0xffF95F62) ? const Color(0xffF95F62)
: const Color(0xffFEE7E7), : const Color(0xffFEE7E7),
@@ -111,108 +113,113 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
final Color primaryColor = final Color primaryColor =
index.isEven ? const Color(0xffF95FAF) : const Color(0xffF95F62); index.isEven ? const Color(0xffF95FAF) : const Color(0xffF95F62);
final Color bgColor =
index.isEven ? const Color(0xFFFDE7F1) : const Color(0xFFFFE8E8);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h),
padding: const EdgeInsets.all(20), child: ClipRRect(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.r),
color: bgColor, child: Stack(
border: Border.all(color: primaryColor.withOpacity(0.6)), children: [
borderRadius: BorderRadius.circular(20), // ===== BACKGROUND IMAGE =====
), Positioned.fill(
child: Column( child: ImageFiltered(
crossAxisAlignment: CrossAxisAlignment.start, imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
children: [ child: Image.asset(
// TITLE FROM API 'assets/images/card_bg.png', // 👈 Replace with your image path
Text( fit: BoxFit.cover,
card.title ?? "",
style: GoogleFonts.poppins(
fontSize: 22,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
const SizedBox(height: 6),
// PRICE FROM API
Text.rich(
TextSpan(
children: [
TextSpan(
text: "From ",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff535353),
),
),
TextSpan(
text: "\$${card.adultPrice ?? 0}",
style: TextStyle(
fontSize: 16.sp,
color: primaryColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 12),
// DESCRIPTION FROM API
Text(
card.description ?? "",
style: GoogleFonts.poppins(
fontSize: 12,
color: const Color(0xff5B5F62),
height: 1.4,
),
),
// const SizedBox(height: 16),
//
// // 🔒 STATIC TEXT (NOT REMOVED)
// const Text(
// "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n"
// "• Pellentesque vel nisl posuere, ullamcorper nibh.\n"
// "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.",
// style: TextStyle(
// fontSize: 12,
// color: Color(0xff5B5F62),
// height: 1.5,
// ),
// ),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed(RouteConstants.buyPass);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Text(
"Get a Pass",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
), ),
), ),
), ),
),
], // ===== DARK OVERLAY =====
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.45),
),
),
),
// ===== CARD CONTENT =====
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all(color: primaryColor.withOpacity(0.6)),
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.title ?? "",
style: GoogleFonts.poppins(
fontSize: 22.sp,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
SizedBox(height: 6.h),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "From ",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w400,
color: Colors.white, // 👈 changed for visibility
),
),
TextSpan(
text: "\$${card.adultPrice ?? 0}",
style: TextStyle(
fontSize: 16.sp,
color: primaryColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
SizedBox(height: 12.h),
Text(
card.description ?? "",
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.white, // 👈 changed for visibility
height: 1.4.h,
),
maxLines: 11,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
SizedBox(
width: double.infinity.w,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed(RouteConstants.buyPass);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
),
child: Text(
"Get a Card",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
color: Colors.white,
),
),
),
),
],
),
),
],
),
), ),
); );
} }

View File

@@ -7,7 +7,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart'; import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
import '../../my_pass/blocs/myPasses/my_passes_event.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../profile/bloc/profile/profile_bloc.dart';
import '../../profile/bloc/profile/profile_event.dart';
class CitySelectionBottomSheet extends StatelessWidget { class CitySelectionBottomSheet extends StatelessWidget {
const CitySelectionBottomSheet({super.key}); const CitySelectionBottomSheet({super.key});
@@ -65,15 +72,15 @@ class _CitySelectionView extends StatelessWidget {
if (cityId == 0) { if (cityId == 0) {
return SizedBox(width: 60.w); // Empty space to maintain layout return SizedBox(width: 60.w); // Empty space to maintain layout
} }
return Row( return GestureDetector(
children: [ onTap: () => Navigator.pop(context),
InkWell( child: Row(
onTap: () => Navigator.pop(context), children: [
child: const Icon(Icons.arrow_back, size: 18), const Icon(Icons.arrow_back, size: 18),
), SizedBox(width: 4.w),
SizedBox(width: 4.w), CustomText(text: "Back", size: 12.sp),
CustomText(text: "Back", size: 12.sp), ],
], ),
); );
}, },
), ),
@@ -272,6 +279,10 @@ class _CitySelectionView extends StatelessWidget {
await LocalPreference.setSelectedCityLogo(svgIcon!); await LocalPreference.setSelectedCityLogo(svgIcon!);
Navigator.pop(context); Navigator.pop(context);
context.read<HomeBloc>().add(FetchHomeData()); context.read<HomeBloc>().add(FetchHomeData());
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
context.read<MyPostCardBloc>().add(CheckLoginStatus());
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
debugPrint("Selected City ID: $cityId"); debugPrint("Selected City ID: $cityId");
}, },
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),

Some files were not shown because too many files have changed in this diff Show More