Compare commits
21 Commits
0abdd2b796
...
Anuj
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d962b3bde | |||
| c12464e89a | |||
| aeeb1c27e0 | |||
| c06c844210 | |||
| 9d27389bf2 | |||
| d1038e846e | |||
| 177f891a31 | |||
| adc737a6af | |||
| 265bddc784 | |||
| 60486e737a | |||
| 77aba2f1a0 | |||
| 06e60cfd57 | |||
| f59b14bec7 | |||
| cbe03f21b4 | |||
| a80a0ac790 | |||
|
|
cdfb9c74ca | ||
| 80b724d6d4 | |||
| 46906b04f4 | |||
| 8f7a68edbc | |||
| eb9ca9299e | |||
| e15a979c0c |
@@ -1,7 +1,7 @@
|
||||
# citycards_customer
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<application
|
||||
android:label="CityCard Customer"
|
||||
android:name="${applicationName}"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
663
android/build/reports/problems/problems-report.html
Normal file
BIN
assets/font/Poppins-Regular.ttf
Normal file
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 863 B After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/compass_outlined.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/icons/downlaod.png
Normal file
|
After Width: | Height: | Size: 991 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/location_outlined.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/icons/love_them.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/maybe.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/icons/no_kids.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/not_interested.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/icons/payment_summary_outlined.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
BIN
assets/icons/refresh.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
assets/icons/sounds_good.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icons/traveling_with_kids.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
BIN
assets/images/card_bg.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 95 KiB |
141
assets/intro/city_cards_splash_screen.json
Normal 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"
|
||||
}
|
||||
}]
|
||||
1
assets/intro/citycards_splash_screen.json
Normal file
1
assets/intro/itinerary_animation.json
Normal file
1
assets/intro/itinerary_creating.json
Normal file
228
ios/Podfile.lock
@@ -1,4 +1,6 @@
|
||||
PODS:
|
||||
- app_links (7.0.0):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_angle (0.3.8):
|
||||
- Flutter
|
||||
@@ -7,6 +9,8 @@ PODS:
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- FlutterAngle (0.0.8)
|
||||
- geocoding_ios (1.0.5):
|
||||
- Flutter
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -16,83 +20,287 @@ PODS:
|
||||
- Flutter
|
||||
- Google-Maps-iOS-Utils (< 7.0, >= 5.0)
|
||||
- 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/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):
|
||||
- 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):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- 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):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
|
||||
- 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`)
|
||||
- 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`)
|
||||
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- 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`)
|
||||
- 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`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FlutterAngle
|
||||
- Google-Maps-iOS-Utils
|
||||
- GoogleDataTransport
|
||||
- GoogleMaps
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GTMSessionFetcher
|
||||
- MLImage
|
||||
- MLKitCommon
|
||||
- MLKitNaturalLanguage
|
||||
- MLKitTranslate
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- SSZipArchive
|
||||
- Stripe
|
||||
- StripeApplePay
|
||||
- StripeCore
|
||||
- StripeFinancialConnections
|
||||
- StripePayments
|
||||
- StripePaymentSheet
|
||||
- StripePaymentsUI
|
||||
- StripeUICore
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_angle:
|
||||
:path: ".symlinks/plugins/flutter_angle/darwin"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
geocoding_ios:
|
||||
:path: ".symlinks/plugins/geocoding_ios/ios"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||
google_maps_flutter_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:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
open_filex:
|
||||
:path: ".symlinks/plugins/open_filex/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
: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:
|
||||
:path: ".symlinks/plugins/three_js_sensors/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_angle: 7b1a2b3e733221bf2e0325e42fc3edf95b5d44c4
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_angle: fc44e198cea1f07e1a5919bad1484049fab65c96
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18
|
||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||
geocoding_ios: eafacae6ad11a1eb56681f7d11df602a5fd49416
|
||||
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
|
||||
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
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
three_js_sensors: f516b092803411e05b1e3dc7625efa36acd8f455
|
||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
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
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
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 */; };
|
||||
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -44,13 +44,13 @@
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
D485254B2B32B84B17D20864 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -72,7 +72,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */,
|
||||
C9496C6E1D2E0AEA2083C14C /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -80,13 +80,22 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */,
|
||||
60A4FC1A895BADD3C1597C0B /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
227A5CBF4270AAC4960A7CAF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D485254B2B32B84B17D20864 /* Pods_Runner.framework */,
|
||||
6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -95,24 +104,15 @@
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D45FB84C63476582408C414 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */,
|
||||
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6D4A73F1E55857ADBD000C6A /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */,
|
||||
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */,
|
||||
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */,
|
||||
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */,
|
||||
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */,
|
||||
9EEFD1F245CF2AAD027ADE1E /* Pods-Runner.debug.xcconfig */,
|
||||
D8B9C9F2F3A8FEF639B5A528 /* Pods-Runner.release.xcconfig */,
|
||||
D719B3676BADD267F44F4A59 /* Pods-Runner.profile.xcconfig */,
|
||||
6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */,
|
||||
5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -136,7 +136,7 @@
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
6D4A73F1E55857ADBD000C6A /* Pods */,
|
||||
5D45FB84C63476582408C414 /* Frameworks */,
|
||||
227A5CBF4270AAC4960A7CAF /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -171,7 +171,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */,
|
||||
FCD91A1B9B088634E06F7C98 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
CF8A29BE993C0C902CB143AF /* Frameworks */,
|
||||
@@ -190,15 +190,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */,
|
||||
8B4387D9368AF1A601B17194 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */,
|
||||
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */,
|
||||
CA082AA85EC21FFAA42CE12F /* [CP] Embed Pods Frameworks */,
|
||||
3F2F0E04CC81D63DDD8C37A9 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -270,7 +270,40 @@
|
||||
/* End PBXResourcesBuildPhase 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;
|
||||
buildActionMask = 2147483647;
|
||||
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";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
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;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -325,22 +357,7 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
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 */ = {
|
||||
FCD91A1B9B088634E06F7C98 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
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";
|
||||
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 */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -515,7 +515,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
baseConfigurationReference = 6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -533,7 +533,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */;
|
||||
baseConfigurationReference = 18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -549,7 +549,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */;
|
||||
baseConfigurationReference = 5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,59 +1,65 @@
|
||||
<?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>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Citycards Customer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>citycards_customer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>3</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Citycard customer needs your location to find the closest place you can visit.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Citycard customer needs your location to find the closest place you can visit.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Scan your card to add it automatically</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>To scan cards</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Citycards Customer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>citycards_customer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>3</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>To scan cards</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Citycard customer needs your location to find the closest place you can visit.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Citycard customer needs your location to find the closest place you can visit.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -58,9 +58,10 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
|
||||
paymentIntentClientSecret: clientSecret,
|
||||
merchantDisplayName: "CityCards",
|
||||
style: ThemeMode.light,
|
||||
allowsDelayedPaymentMethods: true,
|
||||
),
|
||||
);
|
||||
|
||||
await Stripe.instance.presentPaymentSheet();
|
||||
emit(const StripePaymentSheetReady());
|
||||
|
||||
emit(const StripePaymentLoading(
|
||||
@@ -105,6 +106,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
|
||||
paymentIntentClientSecret: event.clientSecret,
|
||||
merchantDisplayName: "CityCards",
|
||||
style: ThemeMode.light,
|
||||
allowsDelayedPaymentMethods: true,
|
||||
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../bloc/stripe_payment_bloc.dart';
|
||||
import '../bloc/stripe_payment_event.dart';
|
||||
import '../bloc/stripe_payment_state.dart';
|
||||
@@ -346,6 +345,7 @@ class StripePaymentScreen extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
|
||||
),
|
||||
|
||||
@@ -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_text.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_screenutil/flutter_screenutil.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_event.dart';
|
||||
@@ -25,29 +27,46 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController cityController = TextEditingController();
|
||||
String? selectedCountry;
|
||||
final TextEditingController countryController = TextEditingController();
|
||||
|
||||
String _selectedIsdCode = '+61'; // ✅ NEW: tracks selected country dial code
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
firstNameController.dispose();
|
||||
lastNameController.dispose();
|
||||
emailController.dispose();
|
||||
countryController.dispose();
|
||||
phoneController.dispose();
|
||||
cityController.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) {
|
||||
// If already submitting, do nothing
|
||||
if (isSubmitting) return;
|
||||
|
||||
// Validate inputs
|
||||
if (firstNameController.text.isEmpty ||
|
||||
lastNameController.text.isEmpty ||
|
||||
emailController.text.isEmpty ||
|
||||
phoneController.text.isEmpty ||
|
||||
cityController.text.isEmpty ||
|
||||
selectedCountry == null) {
|
||||
countryController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please fill all fields'),
|
||||
@@ -57,17 +76,38 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
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(
|
||||
SubmitUserDetailsEvent(
|
||||
bookingId: widget.bookingId,
|
||||
isForSelf: false,
|
||||
recipientFirstName: firstNameController.text,
|
||||
recipientLastName: lastNameController.text,
|
||||
isdCode: _selectedIsdCode,
|
||||
recipientEmail: emailController.text,
|
||||
recipientPhone: phoneController.text,
|
||||
city: cityController.text,
|
||||
country: selectedCountry!,
|
||||
country: countryController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -78,21 +118,10 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
create: (_) => PurchaseDetailsBloc(),
|
||||
child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>(
|
||||
listener: (context, state) {
|
||||
// Handle API submission success
|
||||
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');
|
||||
}
|
||||
|
||||
// Handle API submission error
|
||||
if (state is PurchaseDetailsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -151,106 +180,100 @@ class _AddDetailsViewState extends State<AddDetailsView> {
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "First Name",
|
||||
label: "First Name *",
|
||||
hint: "Enter recipient's first name",
|
||||
controller: firstNameController,
|
||||
onlyLetters: true,
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Last Name",
|
||||
label: "Last Name *",
|
||||
hint: "Enter recipient's last name",
|
||||
controller: lastNameController,
|
||||
onlyLetters: true,
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Email",
|
||||
label: "Email *",
|
||||
hint: "Enter recipient's email address",
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
),
|
||||
|
||||
// ✅ NEW: Phone field with CountryCodePicker (replaces plain CustomTextField)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Phone Number",
|
||||
hint: "Enter recipient's phone number",
|
||||
label: "Phone Number *",
|
||||
hint: "Enter phone number",
|
||||
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: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "City",
|
||||
label: "City *",
|
||||
hint: "Enter the name of the city",
|
||||
controller: cityController,
|
||||
maxLength: 50,
|
||||
onlyLetters: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
|
||||
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: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Country *",
|
||||
hint: "Enter country name",
|
||||
controller: countryController,
|
||||
maxLength: 50,
|
||||
onlyLetters: true,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// Option 1: Pass empty function when disabled (doesn't change button appearance)
|
||||
CustomFilledButton(
|
||||
onTap: () => _handleSubmit(context, isSubmitting),
|
||||
label: isSubmitting ? "Submitting..." : "Continue",
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../core/route_constants.dart';
|
||||
import '../bloc/attraction_details_bloc.dart';
|
||||
@@ -33,7 +34,7 @@ class AttractionDetailsView extends StatelessWidget {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -150,12 +151,9 @@ class AttractionDetailsView extends StatelessWidget {
|
||||
right: 17.w,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) =>
|
||||
const ShareBottomSheet(),
|
||||
Share.share(
|
||||
'www.google.com',
|
||||
subject: 'Check this out',
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@@ -174,7 +172,7 @@ class AttractionDetailsView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'attractions_state.dart';
|
||||
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
|
||||
final AttractionsRepository repository;
|
||||
|
||||
AttractionsBloc({required this.repository})
|
||||
: super(AttractionsInitial()) {
|
||||
AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
|
||||
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
|
||||
on<SearchAttractions>(_onSearchAttractions);
|
||||
}
|
||||
|
||||
Future<void> _onFetchAttractionsByCategory(
|
||||
@@ -21,22 +21,50 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
|
||||
try {
|
||||
final AttractionsResponse response =
|
||||
await repository.fetchAttractionsByCategory(
|
||||
categoryXid: event.categoryXid, // Can be null now
|
||||
categoryXid: event.categoryXid,
|
||||
);
|
||||
|
||||
final allAttractions = response.attractions ?? [];
|
||||
|
||||
emit(
|
||||
AttractionsLoaded(
|
||||
attractions: response.attractions ?? [],
|
||||
attractions: allAttractions,
|
||||
allAttractions: allAttractions,
|
||||
categories: response.categories ?? [],
|
||||
selectedCategoryId: event.categoryXid, // Can be null
|
||||
selectedCategoryId: event.categoryXid,
|
||||
searchQuery: '',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
AttractionsError(
|
||||
e.toString(),
|
||||
),
|
||||
);
|
||||
emit(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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,19 @@ abstract class AttractionsEvent extends Equatable {
|
||||
}
|
||||
|
||||
class FetchAttractionsByCategory extends AttractionsEvent {
|
||||
final int? categoryXid; // Make it nullable
|
||||
final int? categoryXid;
|
||||
|
||||
const FetchAttractionsByCategory({this.categoryXid}); // Remove required
|
||||
const FetchAttractionsByCategory({this.categoryXid});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [categoryXid];
|
||||
}
|
||||
|
||||
class SearchAttractions extends AttractionsEvent {
|
||||
final String query;
|
||||
|
||||
const SearchAttractions(this.query);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [query];
|
||||
}
|
||||
@@ -14,17 +14,27 @@ class AttractionsLoading extends AttractionsState {}
|
||||
|
||||
class AttractionsLoaded extends AttractionsState {
|
||||
final List<Attraction> attractions;
|
||||
final List<Attraction> allAttractions; // Keep full list for local filtering
|
||||
final List<Category> categories;
|
||||
final int? selectedCategoryId; // Make it nullable
|
||||
final int? selectedCategoryId;
|
||||
final String searchQuery;
|
||||
|
||||
const AttractionsLoaded({
|
||||
required this.attractions,
|
||||
required this.allAttractions,
|
||||
required this.categories,
|
||||
this.selectedCategoryId, // Remove required
|
||||
this.selectedCategoryId,
|
||||
this.searchQuery = '',
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [attractions, categories, selectedCategoryId];
|
||||
List<Object?> get props => [
|
||||
attractions,
|
||||
allAttractions,
|
||||
categories,
|
||||
selectedCategoryId,
|
||||
searchQuery,
|
||||
];
|
||||
}
|
||||
|
||||
class AttractionsError extends AttractionsState {
|
||||
|
||||
@@ -56,8 +56,7 @@ class AttractionsPage extends StatelessWidget {
|
||||
hint: "Search attractions...",
|
||||
hintColor: Colors.grey.shade500,
|
||||
onChanged: (value) {
|
||||
// ❌ Search logic intentionally disabled
|
||||
// UI only, no API call
|
||||
bloc.add(SearchAttractions(value));
|
||||
},
|
||||
),
|
||||
|
||||
@@ -106,7 +105,7 @@ class AttractionsPage extends StatelessWidget {
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 60),
|
||||
child: CircularProgressIndicator(),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
),
|
||||
)
|
||||
else if (state is AttractionsLoaded)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -42,12 +43,13 @@ class AttractionCard extends StatelessWidget {
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
child: imageUrl.isNotEmpty
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
height: 94.h,
|
||||
width: 94.w,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _imageFallback(),
|
||||
placeholder: (context, url) => _imageFallback(),
|
||||
errorWidget: (_, __, ___) => _imageFallback(),
|
||||
)
|
||||
: _imageFallback(),
|
||||
),
|
||||
@@ -88,7 +90,7 @@ class AttractionCard extends StatelessWidget {
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "from \$${attraction.ticketPriceAdult}",
|
||||
text: "\$${attraction.ticketPriceAdult}",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -20,7 +20,18 @@ class BuyPassBloc extends Bloc<BuyPassEvent, BuyPassState> {
|
||||
on<UpdateChildCount>(_onUpdateChildCount);
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -29,4 +29,6 @@ class UpdateValidityDuration extends BuyPassEvent {
|
||||
final int duration;
|
||||
|
||||
UpdateValidityDuration(this.duration);
|
||||
}
|
||||
}
|
||||
class AddToCartLoading extends BuyPassEvent {}
|
||||
class AddToCartDone extends BuyPassEvent {}
|
||||
@@ -14,15 +14,17 @@ class BuyPassLoaded extends BuyPassState {
|
||||
final int selectedCardIndex;
|
||||
final int adultCount;
|
||||
final int childCount;
|
||||
final int validityDuration; // ✅ Added
|
||||
final int validityDuration;
|
||||
final bool isAddingToCart;
|
||||
|
||||
BuyPassLoaded({
|
||||
required this.data,
|
||||
this.selectedCardIndex = 0,
|
||||
this.adultCount = 1,
|
||||
this.childCount = 1,
|
||||
int? validityDuration, // ✅ Added as optional parameter
|
||||
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; // ✅ Initialize with minNumber
|
||||
int? validityDuration,
|
||||
this.isAddingToCart = false, // ✅ default false, NOT required
|
||||
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber;
|
||||
|
||||
/// Method to copy state with updated values
|
||||
BuyPassLoaded copyWith({
|
||||
@@ -30,14 +32,16 @@ class BuyPassLoaded extends BuyPassState {
|
||||
int? selectedCardIndex,
|
||||
int? adultCount,
|
||||
int? childCount,
|
||||
int? validityDuration, // ✅ Added
|
||||
int? validityDuration,
|
||||
bool? isAddingToCart,
|
||||
}) {
|
||||
return BuyPassLoaded(
|
||||
data: data ?? this.data,
|
||||
selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex,
|
||||
adultCount: adultCount ?? this.adultCount,
|
||||
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
|
||||
double get totalPrice {
|
||||
final card = selectedCard;
|
||||
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) * validityDuration.toDouble(); // ✅ Multiply by validityDuration
|
||||
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) *
|
||||
validityDuration.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,25 @@ class BuyPassView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class BuyPassContent extends StatelessWidget {
|
||||
class BuyPassContent extends StatefulWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -92,58 +108,49 @@ class BuyPassContent extends StatelessWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0.w),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
CustomText(text: "Buy a Pass", size: 12.sp),
|
||||
],
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_back),
|
||||
SizedBox(width: 8.w),
|
||||
CustomText(text: "Buy a Card", size: 12.sp),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 22.h),
|
||||
|
||||
// Pass Cards Horizontal List
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20.0.w),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
data.cards.length,
|
||||
(index) {
|
||||
final card = data.cards[index];
|
||||
final isSelected = index == state.selectedCardIndex;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<BuyPassBloc>().add(
|
||||
ChangeSelectedCard(index),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 12.w),
|
||||
child: PassCardView(
|
||||
themeColor: isSelected
|
||||
? Color(0xFFF97316)
|
||||
: Color(0xFF1E8AF6),
|
||||
city: data.city.name,
|
||||
heroImage: data.city.heroBanner.image,
|
||||
adultPrice: card.adultPrice,
|
||||
childPrice: card.childPrice,
|
||||
cardType: card.cardType.displayName,
|
||||
description: card.description,
|
||||
isSelected: isSelected,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Pass Cards Horizontal List
|
||||
SizedBox(
|
||||
height: 140.h,
|
||||
child: PageView.builder(
|
||||
controller: PageController(viewportFraction: 0.92),
|
||||
itemCount: data.cards.length,
|
||||
onPageChanged: (index) {
|
||||
context.read<BuyPassBloc>().add(ChangeSelectedCard(index));
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final card = data.cards[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
child: PassCardView(
|
||||
themeColor: card.cardType.name == "selective_pass"
|
||||
? const Color(0xFFF95FAF)
|
||||
: const Color(0xFFF95F62),
|
||||
city: data.city.name,
|
||||
heroImage: data.city.heroBanner.image,
|
||||
adultPrice: card.adultPrice,
|
||||
childPrice: card.childPrice,
|
||||
cardType: card.cardType.displayName,
|
||||
description: card.description,
|
||||
isSelected: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -159,9 +166,9 @@ class BuyPassContent extends StatelessWidget {
|
||||
heroImage: data.city.heroBanner.image,
|
||||
cardType: selectedCard.cardType.name,
|
||||
cardDisplayName: selectedCard.cardType.displayName,
|
||||
themeColor: state.selectedCardIndex == 0
|
||||
? Color(0xFFF97316)
|
||||
: Color(0xFF1E8AF6),
|
||||
themeColor: selectedCard.cardType.name == "selective_pass"
|
||||
? Color(0xFFF95FAF) // pink for flexi/selective pass
|
||||
: Color(0xFFF95F62),
|
||||
adultPrice: selectedCard.adultPrice.toDouble(),
|
||||
childPrice: selectedCard.childPrice.toDouble(),
|
||||
adults: state.adultCount,
|
||||
@@ -209,7 +216,7 @@ class BuyPassContent extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CustomText(text: "Card Offers", size: 18.sp),
|
||||
CustomText(text: "Member Privileges", size: 18.sp),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
@@ -246,12 +253,12 @@ class BuyPassContent extends StatelessWidget {
|
||||
final offer = selectedCard.offers[index];
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.offerPassDetail,
|
||||
arguments: offer.id, // ✅ pass offerId
|
||||
);
|
||||
},
|
||||
// onTap: () {
|
||||
// Navigator.of(context).pushNamed(
|
||||
// RouteConstants.offerPassDetail,
|
||||
// arguments: offer.id, // ✅ pass offerId
|
||||
// );
|
||||
// },
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 6.w,
|
||||
@@ -344,7 +351,7 @@ class BuyPassContent extends StatelessWidget {
|
||||
text: offer.description??"N/A",
|
||||
color: Colors.black.withOpacity(.6),
|
||||
size: 12.sp,
|
||||
maxLines: 2,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
@@ -426,7 +433,7 @@ class BuyPassContent extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62),strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
class PassCardView extends StatelessWidget {
|
||||
final Color? themeColor;
|
||||
final String? city;
|
||||
final String? heroImage; // ✅ heroBanner.image from API
|
||||
final String? heroImage;
|
||||
final num? adultPrice;
|
||||
final num? childPrice;
|
||||
final String? cardType;
|
||||
@@ -31,140 +31,143 @@ class PassCardView extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24),
|
||||
width: isSelected ? 2 : 1,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
/// -------- HERO BANNER IMAGE --------
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8.r),
|
||||
bottomLeft: Radius.circular(8.r),
|
||||
),
|
||||
child: Container(
|
||||
width: 103.w,
|
||||
height: 140.h,
|
||||
color: Colors.grey[200],
|
||||
child: heroImage != null && heroImage!.isNotEmpty
|
||||
? Image.network(
|
||||
heroImage!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _fallbackIcon();
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
/// -------- LEFT: IMAGE + DETAILS --------
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
/// HERO BANNER IMAGE
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8.r),
|
||||
bottomLeft: Radius.circular(8.r),
|
||||
),
|
||||
child: Container(
|
||||
width: 103.w,
|
||||
height: 140.h,
|
||||
color: Colors.grey[200],
|
||||
child: heroImage != null && heroImage!.isNotEmpty
|
||||
? Image.network(
|
||||
heroImage!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _fallbackIcon();
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 24.w,
|
||||
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 --------
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: city ?? "City",
|
||||
weight: FontWeight.w500,
|
||||
size: 16.sp,
|
||||
),
|
||||
|
||||
/// Adult Price
|
||||
Row(
|
||||
/// CARD DETAILS
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"From ",
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
CustomText(
|
||||
text: city ?? "City",
|
||||
weight: FontWeight.w500,
|
||||
size: 16.sp,
|
||||
),
|
||||
Text(
|
||||
"\$${adultPrice ?? 0}",
|
||||
style: TextStyle(
|
||||
color: themeColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 24.sp,
|
||||
),
|
||||
|
||||
/// Adult Price
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"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",
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.8),
|
||||
fontSize: 11.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
|
||||
/// 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: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(
|
||||
width: 35.w,
|
||||
height: 140.h,
|
||||
@@ -194,7 +197,7 @@ class PassCardView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// -------- FALLBACK ICON --------
|
||||
/// FALLBACK ICON
|
||||
Widget _fallbackIcon() {
|
||||
return Icon(
|
||||
Icons.card_travel,
|
||||
@@ -202,4 +205,4 @@ class PassCardView extends StatelessWidget {
|
||||
color: Colors.grey[400],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
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 '../../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 '../../checkout/view/checkout_view.dart';
|
||||
import '../repository/buy_pass_repository.dart'; // ✅ Import repository
|
||||
|
||||
class PaymentCard extends StatelessWidget {
|
||||
class PaymentCard extends StatefulWidget {
|
||||
final String city;
|
||||
final String heroImage;
|
||||
final String cardType;
|
||||
@@ -56,10 +59,16 @@ class PaymentCard extends StatelessWidget {
|
||||
required this.cardXid, // ✅ NEW
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentCard> createState() => _PaymentCardState();
|
||||
}
|
||||
|
||||
class _PaymentCardState extends State<PaymentCard> {
|
||||
bool _isLoading = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isUnlimitedCard = cardType == "unlimited_card";
|
||||
final bool isSelectivePass = cardType == "selective_pass";
|
||||
final bool isUnlimitedCard = widget.cardType == "unlimited_card";
|
||||
final bool isSelectivePass = widget.cardType == "selective_pass";
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
@@ -83,7 +92,7 @@ class PaymentCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: city,
|
||||
text: widget.city,
|
||||
size: 20.sp,
|
||||
weight: FontWeight.bold,
|
||||
),
|
||||
@@ -91,32 +100,32 @@ class PaymentCard extends StatelessWidget {
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95FAF),
|
||||
color: widget.themeColor.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: CustomText(
|
||||
text: cardDisplayName,
|
||||
text: widget.cardDisplayName,
|
||||
size: 12.sp,
|
||||
color: Colors.white,
|
||||
color: widget.themeColor,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
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),
|
||||
_buildCounterRow("No. of Children", children, onChildChanged),
|
||||
_buildCounterRow("No. of Children", widget.children, widget.onChildChanged, context),
|
||||
SizedBox(height: 10.h),
|
||||
if (isUnlimitedCard)
|
||||
_buildDropdownRow(
|
||||
label: "No. of Days",
|
||||
value: selectedValue,
|
||||
onChanged: onValidityChanged,
|
||||
value: widget.selectedValue,
|
||||
onChanged: widget.onValidityChanged,
|
||||
)
|
||||
else if (isSelectivePass)
|
||||
_buildDropdownRow(
|
||||
label: "No. of Attractions",
|
||||
value: selectedValue,
|
||||
onChanged: onValidityChanged,
|
||||
value: widget.selectedValue,
|
||||
onChanged: widget.onValidityChanged,
|
||||
),
|
||||
Divider(height: 30.h, thickness: 1),
|
||||
Row(
|
||||
@@ -128,7 +137,7 @@ class PaymentCard extends StatelessWidget {
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
CustomText(
|
||||
text: "\$${totalPrice.toStringAsFixed(0)}",
|
||||
text: "\$${widget.totalPrice.toStringAsFixed(0)}",
|
||||
size: 18.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
weight: FontWeight.bold,
|
||||
@@ -136,101 +145,111 @@ class PaymentCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
CustomFilledButton(
|
||||
onTap: () async {
|
||||
try {
|
||||
// ✅ Check login status first
|
||||
final bool isLoggedIn = await LocalPreference.getLogin();
|
||||
BlocBuilder<BuyPassBloc, BuyPassState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state is BuyPassLoaded && state.isAddingToCart;
|
||||
|
||||
// ✅ Create checkout data (needed for both cases)
|
||||
final checkoutData = CheckoutData(
|
||||
cityName: city,
|
||||
heroImage: heroImage,
|
||||
cardTypeName: cardType,
|
||||
cardDisplayName: cardDisplayName,
|
||||
themeColor: themeColor,
|
||||
adultCount: adults,
|
||||
childCount: children,
|
||||
adultPrice: adultPrice,
|
||||
childPrice: childPrice,
|
||||
validityDuration: selectedValue,
|
||||
totalPrice: totalPrice,
|
||||
description: description,
|
||||
);
|
||||
return CustomFilledButton(
|
||||
onTap: isLoading
|
||||
? null
|
||||
: () async {
|
||||
final bloc = context.read<BuyPassBloc>();
|
||||
bloc.add(AddToCartLoading());
|
||||
try {
|
||||
// ✅ Check login status first
|
||||
final bool isLoggedIn = await LocalPreference.getLogin();
|
||||
|
||||
// ✅ Save to local preference (for both logged in and guest users)
|
||||
await LocalPreference.setPassCart(
|
||||
cityName: city,
|
||||
heroImage: heroImage,
|
||||
cardTypeName: cardType,
|
||||
cardDisplayName: cardDisplayName,
|
||||
themeColor: themeColor.value,
|
||||
adultCount: adults,
|
||||
childCount: children,
|
||||
adultPrice: adultPrice,
|
||||
childPrice: childPrice,
|
||||
validityDuration: selectedValue,
|
||||
totalPrice: totalPrice,
|
||||
description: 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,
|
||||
),
|
||||
),
|
||||
// ✅ Create checkout data (needed for both cases)
|
||||
final checkoutData = CheckoutData(
|
||||
cityName: widget.city,
|
||||
heroImage: widget.heroImage,
|
||||
cardTypeName: widget.cardType,
|
||||
cardDisplayName: widget.cardDisplayName,
|
||||
themeColor: widget.themeColor,
|
||||
adultCount: widget.adults,
|
||||
childCount: widget.children,
|
||||
adultPrice: widget.adultPrice,
|
||||
childPrice: widget.childPrice,
|
||||
validityDuration: widget.selectedValue,
|
||||
totalPrice: widget.totalPrice,
|
||||
description: widget.description,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// ✅ User is NOT logged in - skip API, navigate directly
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CheckoutView(bookingId: 0), // or 0, depending on your CheckoutView implementation
|
||||
settings: RouteSettings(
|
||||
arguments: checkoutData,
|
||||
|
||||
if (isLoggedIn) {
|
||||
// ✅ User is logged in - hit API
|
||||
final repository = BuyPassRepository();
|
||||
final response = await repository.addToCartPasses(
|
||||
cityXid: widget.cityXid,
|
||||
cardTypeXid: widget.cardTypeXid,
|
||||
cardXid: widget.cardXid,
|
||||
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) {
|
||||
// ✅ 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: isLoading ? "Please wait..." : "Proceed to Pay",
|
||||
);
|
||||
},
|
||||
label: "Proceed to Pay",
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -244,8 +263,8 @@ class PaymentCard extends StatelessWidget {
|
||||
required Function(int) onChanged,
|
||||
}) {
|
||||
List<int> numbersList = List.generate(
|
||||
maxNumber - minNumber + 1,
|
||||
(index) => minNumber + index,
|
||||
widget.maxNumber - widget.minNumber + 1,
|
||||
(index) => widget.minNumber + index,
|
||||
);
|
||||
|
||||
return Row(
|
||||
@@ -305,7 +324,9 @@ class PaymentCard extends StatelessWidget {
|
||||
String label,
|
||||
int value,
|
||||
Function(int) onChanged,
|
||||
) {
|
||||
BuildContext context, {
|
||||
int minValue = 0,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -313,7 +334,22 @@ class PaymentCard extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
_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: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../model/my_passes_cart_mode.dart';
|
||||
import '../../model/my_passes_cart_model.dart';
|
||||
|
||||
abstract class MyPassCartState extends Equatable {
|
||||
const MyPassCartState();
|
||||
|
||||
57
lib/cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
27
lib/cart/blocs/myPostcardsCart/my_postcards_cart_state.dart
Normal 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});
|
||||
}
|
||||
@@ -35,14 +35,16 @@ class MyPassesCartModel {
|
||||
};
|
||||
}
|
||||
|
||||
/// ---------- CITY ----------
|
||||
/// ---------- TOP LEVEL CITY ----------
|
||||
class CartCity {
|
||||
int id;
|
||||
String name;
|
||||
String bannerImage;
|
||||
|
||||
CartCity({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.bannerImage,
|
||||
});
|
||||
|
||||
factory CartCity.fromJson(Map<String, dynamic>? json) {
|
||||
@@ -51,12 +53,14 @@ class CartCity {
|
||||
return CartCity(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
name: json['name']?.toString() ?? "",
|
||||
bannerImage: json['bannerImage']?.toString() ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"bannerImage": bannerImage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,6 +69,7 @@ class CartItem {
|
||||
int id;
|
||||
String bookingNumber;
|
||||
String cardMode;
|
||||
String displayCardMode;
|
||||
int noOfDays;
|
||||
int noOfAttractions;
|
||||
int totalAdult;
|
||||
@@ -74,6 +79,7 @@ class CartItem {
|
||||
num totalAmount;
|
||||
String bookingStatus;
|
||||
bool isForSelf;
|
||||
|
||||
String recipientFirstName;
|
||||
String recipientLastName;
|
||||
String recipientEmail;
|
||||
@@ -81,18 +87,22 @@ class CartItem {
|
||||
String recipientCity;
|
||||
String recipientCountry;
|
||||
String giftMessage;
|
||||
|
||||
bool isPaymentRequired;
|
||||
int couponXid;
|
||||
num couponDiscountAmount;
|
||||
num couponDiscountPercent;
|
||||
String paymentStatus;
|
||||
String createdAt;
|
||||
|
||||
Coupon? coupon;
|
||||
ItemCity city;
|
||||
|
||||
CartItem({
|
||||
required this.id,
|
||||
required this.bookingNumber,
|
||||
required this.cardMode,
|
||||
required this.displayCardMode,
|
||||
required this.noOfDays,
|
||||
required this.noOfAttractions,
|
||||
required this.totalAdult,
|
||||
@@ -115,6 +125,7 @@ class CartItem {
|
||||
required this.couponDiscountPercent,
|
||||
required this.paymentStatus,
|
||||
required this.createdAt,
|
||||
required this.coupon,
|
||||
required this.city,
|
||||
});
|
||||
|
||||
@@ -125,6 +136,7 @@ class CartItem {
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
bookingNumber: json['bookingNumber']?.toString() ?? "",
|
||||
cardMode: json['cardMode']?.toString() ?? "",
|
||||
displayCardMode: json['displayCardMode']?.toString() ?? "",
|
||||
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
|
||||
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
|
||||
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
|
||||
@@ -147,6 +159,8 @@ class CartItem {
|
||||
couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
|
||||
paymentStatus: json['paymentStatus']?.toString() ?? "",
|
||||
createdAt: json['createdAt']?.toString() ?? "",
|
||||
coupon:
|
||||
json['coupon'] == null ? null : Coupon.fromJson(json['coupon']),
|
||||
city: ItemCity.fromJson(json['city']),
|
||||
);
|
||||
}
|
||||
@@ -155,6 +169,7 @@ class CartItem {
|
||||
"id": id,
|
||||
"bookingNumber": bookingNumber,
|
||||
"cardMode": cardMode,
|
||||
"displayCardMode": displayCardMode,
|
||||
"noOfDays": noOfDays,
|
||||
"noOfAttractions": noOfAttractions,
|
||||
"totalAdult": totalAdult,
|
||||
@@ -177,18 +192,49 @@ class CartItem {
|
||||
"couponDiscountPercent": couponDiscountPercent,
|
||||
"paymentStatus": paymentStatus,
|
||||
"createdAt": createdAt,
|
||||
"coupon": coupon?.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 ----------
|
||||
class ItemCity {
|
||||
int id;
|
||||
String cityName;
|
||||
List<CityBanner> cityBanners;
|
||||
|
||||
ItemCity({
|
||||
required this.id,
|
||||
required this.cityName,
|
||||
required this.cityBanners,
|
||||
});
|
||||
|
||||
factory ItemCity.fromJson(Map<String, dynamic>? json) {
|
||||
@@ -197,11 +243,35 @@ class ItemCity {
|
||||
return ItemCity(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
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() => {
|
||||
"id": id,
|
||||
"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,
|
||||
};
|
||||
}
|
||||
163
lib/cart/model/my_postcards_cart_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
class PassModel {
|
||||
final String title;
|
||||
final String imageUrl;
|
||||
final String duration;
|
||||
final int adults;
|
||||
final int kids;
|
||||
final int quantity;
|
||||
final double price;
|
||||
final double discount;
|
||||
|
||||
PassModel({
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
required this.duration,
|
||||
required this.adults,
|
||||
required this.kids,
|
||||
required this.quantity,
|
||||
required this.price,
|
||||
required this.discount,
|
||||
});
|
||||
}
|
||||
// class PassModel {
|
||||
// final String title;
|
||||
// final String imageUrl;
|
||||
// final String duration;
|
||||
// final int adults;
|
||||
// final int kids;
|
||||
// final int quantity;
|
||||
// final double price;
|
||||
// final double discount;
|
||||
//
|
||||
// PassModel({
|
||||
// required this.title,
|
||||
// required this.imageUrl,
|
||||
// required this.duration,
|
||||
// required this.adults,
|
||||
// required this.kids,
|
||||
// required this.quantity,
|
||||
// required this.price,
|
||||
// required this.discount,
|
||||
// });
|
||||
// }
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
import '../model/my_passes_cart_mode.dart';
|
||||
import '../model/my_passes_cart_model.dart';
|
||||
|
||||
class MyPassCartRepository {
|
||||
final NetworkApiService _apiService = NetworkApiService();
|
||||
|
||||
35
lib/cart/repository/my_postcards_cart_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../common_packages/back_widget.dart';
|
||||
import '../blocs/myPassCart/my_pass_cart_bloc.dart';
|
||||
import '../blocs/myPassCart/my_pass_cart_event.dart';
|
||||
import '../blocs/postcard_bloc.dart';
|
||||
import '../repository/my_pass_cart_repository.dart';
|
||||
import '../blocs/myPostcardsCart/my_postcards_cart_bloc.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 {
|
||||
const MyCartPage({super.key});
|
||||
@@ -20,62 +19,61 @@ class MyCartPage extends StatefulWidget {
|
||||
class _MyCartPageState extends State<MyCartPage> {
|
||||
int selectedTab = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
|
||||
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => PostCardBloc()..add(LoadPostCards()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => MyPassCartBloc(
|
||||
repository: MyPassCartRepository(),
|
||||
)..add(const FetchPassCartEvent()),
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
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),
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showCart: false,
|
||||
showDivider: true,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_tabButton("My Passes", 0),
|
||||
_tabButton("My Post Cards", 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: selectedTab == 0
|
||||
? const MyPassesPage()
|
||||
: const MyPostCardsPage(),
|
||||
backWidget(context, "Your Cart", Colors.black),
|
||||
SizedBox(height: 24.h),
|
||||
Container(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFEE7E7),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
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(
|
||||
onTap: () => setState(() => selectedTab = index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
decoration: BoxDecoration(
|
||||
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: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xff2A2A2A),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
fontSize: 13.sp,
|
||||
color: const Color(0xff2A2A2A),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -105,4 +113,4 @@ class _MyCartPageState extends State<MyCartPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
501
lib/cart/views/my_postcard_cart_page_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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_screenutil/flutter_screenutil.dart';
|
||||
import '../model/my_postcards_cart_model.dart';
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@@ -13,43 +22,101 @@ class TicketCard extends StatelessWidget {
|
||||
child: ClipPath(
|
||||
clipper: TicketClipper(),
|
||||
child: Container(
|
||||
width: 270.w,
|
||||
height: 400.h,
|
||||
padding: EdgeInsets.all(16.w),
|
||||
width: 240.w,
|
||||
height: 340.h,
|
||||
padding: EdgeInsets.all(14.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
borderRadius: BorderRadius.circular(24.r), // ← was 12.r
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── Postcard Image ──
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: Image.asset(
|
||||
'assets/images/card_banner.png',
|
||||
width: 237.w,
|
||||
height: 198.h,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
child: cartItem.pcImagePath.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl:
|
||||
'${ApiUrls.baseUrl}${cartItem.pcImagePath}',
|
||||
width: 210.w,
|
||||
height: 170.h,
|
||||
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(
|
||||
width: 200.w,
|
||||
child: DashedDivider(
|
||||
color: const Color(0xFFBEBEBE),
|
||||
thickness: 2.h,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
SizedBox(height: 8.h),
|
||||
|
||||
// ── Title ──
|
||||
Text(
|
||||
"Melbourne",
|
||||
cartItem.pcTitle.isNotEmpty ? cartItem.pcTitle : 'No Title',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontSize: 13.sp,
|
||||
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) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 6.h),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: const Color(0xFF808080), fontSize: 12.sp),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 12.sp),
|
||||
),
|
||||
],
|
||||
Widget _placeholderImage() {
|
||||
return Container(
|
||||
width: 210.w,
|
||||
height: 170.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
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
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final notchRadius = 23.r;
|
||||
final dividerY = 240.h;
|
||||
final paint = Paint()
|
||||
..color = const Color(0xffF95F62)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
|
||||
final ticketPath = Path()
|
||||
..moveTo(12.w, 0)
|
||||
..lineTo(size.width - 12.w, 0)
|
||||
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r))
|
||||
..lineTo(size.width, dividerY - notchRadius)
|
||||
..arcToPoint(
|
||||
Offset(size.width, dividerY + notchRadius),
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..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();
|
||||
// Dashes from left notch center to right notch center.
|
||||
// Card is 240.w wide, notchRadius = 28.w. We hardcode these because
|
||||
// size.width here is the inner column width (240-2*14=212.w), not the card width.
|
||||
final double startX = 30.w; // 2.w gap from notch edge
|
||||
final double endX = 240.w - 30.w; // 2.w gap from notch edge
|
||||
final double dashH = 6.h;
|
||||
final double dashW = 12.w;
|
||||
final double gap = 5.w;
|
||||
final double top = (size.height - dashH) / 2;
|
||||
final double span = endX - startX;
|
||||
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.3)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.outer, 8);
|
||||
// Fit exact number of dashes: n*dashW + (n-1)*gap <= span
|
||||
final int count = ((span + gap) / (dashW + gap)).floor();
|
||||
|
||||
canvas.drawPath(ticketPath, shadowPaint);
|
||||
// Recalculate actual gap to distribute evenly
|
||||
final double actualGap = count > 1 ? (span - count * dashW) / (count - 1) : 0;
|
||||
|
||||
final cardPaint = Paint()
|
||||
..color = const Color(0xFFFAC9CA).withOpacity(0.12)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawPath(ticketPath, cardPaint);
|
||||
double x = startX;
|
||||
for (int i = 0; i < count; i++) {
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(x, top, dashW, dashH),
|
||||
Radius.circular(dashH / 2),
|
||||
),
|
||||
paint,
|
||||
);
|
||||
x += dashW + actualGap;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class TicketClipper extends CustomClipper<Path> {
|
||||
// ─────────────────────────────────────────────
|
||||
// Ticket Painter (shadow + fill)
|
||||
// ─────────────────────────────────────────────
|
||||
class TicketPainter extends CustomPainter {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final notchRadius = 23.r;
|
||||
final dividerY = 240.h;
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final notchRadius = 28.r;
|
||||
final dividerY = 218.h;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(12.w, 0)
|
||||
..lineTo(size.width - 12.w, 0)
|
||||
..arcToPoint(Offset(size.width, 12.h), radius: Radius.circular(12.r))
|
||||
final ticketPath = _buildPath(size, notchRadius, dividerY);
|
||||
|
||||
// Shadow
|
||||
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)
|
||||
..arcToPoint(
|
||||
Offset(size.width, dividerY + notchRadius),
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..lineTo(size.width, size.height - 12.h)
|
||||
..lineTo(size.width, size.height - 24.h) // ← was 12.h
|
||||
..arcToPoint(
|
||||
Offset(size.width - 12.w, size.height),
|
||||
radius: Radius.circular(12.r),
|
||||
Offset(size.width - 24.w, size.height), // ← was 12.w
|
||||
radius: Radius.circular(24.r), // ← was 12.r
|
||||
)
|
||||
..lineTo(12.w, size.height)
|
||||
..lineTo(24.w, size.height) // ← was 12.w
|
||||
..arcToPoint(
|
||||
Offset(0, size.height - 12.h),
|
||||
radius: Radius.circular(12.r),
|
||||
Offset(0, size.height - 24.h), // ← was 12.h
|
||||
radius: Radius.circular(24.r), // ← was 12.r
|
||||
)
|
||||
..lineTo(0, dividerY + notchRadius)
|
||||
..arcToPoint(
|
||||
@@ -163,13 +249,55 @@ class TicketClipper extends CustomClipper<Path> {
|
||||
radius: Radius.circular(notchRadius),
|
||||
clockwise: false,
|
||||
)
|
||||
..lineTo(0, 12.h)
|
||||
..arcToPoint(Offset(12.w, 0), radius: Radius.circular(12.r))
|
||||
..lineTo(0, 24.h) // ← was 12.h
|
||||
..arcToPoint(Offset(24.w, 0), radius: Radius.circular(24.r)) // ← was 12
|
||||
..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
|
||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ class PurchaseDetailsBloc
|
||||
isForSelf: event.isForSelf,
|
||||
recipientFirstName: event.recipientFirstName,
|
||||
recipientLastName: event.recipientLastName,
|
||||
isdCode: event.isdCode,
|
||||
recipientEmail: event.recipientEmail,
|
||||
recipientPhone: event.recipientPhone,
|
||||
city: event.city,
|
||||
|
||||
@@ -19,6 +19,7 @@ class SubmitUserDetailsEvent extends PassPurchaseDetailsEvent {
|
||||
final bool isForSelf;
|
||||
final String? recipientFirstName;
|
||||
final String? recipientLastName;
|
||||
final String? isdCode;
|
||||
final String? recipientEmail;
|
||||
final String? recipientPhone;
|
||||
final String? city;
|
||||
@@ -29,6 +30,7 @@ class SubmitUserDetailsEvent extends PassPurchaseDetailsEvent {
|
||||
required this.isForSelf,
|
||||
this.recipientFirstName,
|
||||
this.recipientLastName,
|
||||
this.isdCode,
|
||||
this.recipientEmail,
|
||||
this.recipientPhone,
|
||||
this.city,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
|
||||
@@ -8,63 +5,34 @@ class PassPurchaseDetailsRepository {
|
||||
final NetworkApiService _apiServices = NetworkApiService();
|
||||
|
||||
/// Submit user details for pass purchase
|
||||
/// POST https://devapi.citycards.betadelivery.com/mobile/passes/{bookingId}/user-details
|
||||
Future<Map<String, dynamic>> submitUserDetails({
|
||||
required int bookingId,
|
||||
required bool isForSelf,
|
||||
String? recipientFirstName,
|
||||
String? recipientLastName,
|
||||
String? isdCode,
|
||||
String? recipientEmail,
|
||||
String? recipientPhone,
|
||||
String? city,
|
||||
String? country,
|
||||
}) async {
|
||||
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(
|
||||
url: url,
|
||||
data: requestBody,
|
||||
url: '${ApiUrls.baseUrl}/mobile/passes/$bookingId/user-details',
|
||||
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>;
|
||||
} catch (e, stackTrace) {
|
||||
log(
|
||||
'❌ submitUserDetails FAILED',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to submit user details: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../StripePayment/view/stripe_payment.dart';
|
||||
import '../../add_details/add_details_view.dart';
|
||||
import '../../buy_a_pass/models/checkout_model.dart';
|
||||
import '../../common_packages/custom_snackbar.dart';
|
||||
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
|
||||
@@ -24,7 +25,8 @@ import '../models/all_coupons_model.dart';
|
||||
|
||||
class CheckoutView extends StatefulWidget {
|
||||
final int bookingId;
|
||||
const CheckoutView({super.key, required this.bookingId});
|
||||
final int? couponId;
|
||||
const CheckoutView({super.key, required this.bookingId, this.couponId});
|
||||
|
||||
@override
|
||||
State<CheckoutView> createState() => _CheckoutViewState();
|
||||
@@ -93,6 +95,7 @@ class _CheckoutViewState extends State<CheckoutView> {
|
||||
child: _CheckoutContent(
|
||||
checkoutData: checkoutData,
|
||||
bookingId: widget.bookingId,
|
||||
couponId: widget.couponId,
|
||||
isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed,
|
||||
onPurchaseDetailsChanged: (value) {
|
||||
setState(() {
|
||||
@@ -107,12 +110,14 @@ class _CheckoutViewState extends State<CheckoutView> {
|
||||
class _CheckoutContent extends StatefulWidget {
|
||||
final CheckoutData checkoutData;
|
||||
final int bookingId;
|
||||
final int? couponId;
|
||||
final bool isPurchaseDetailsConfirmed;
|
||||
final Function(bool) onPurchaseDetailsChanged;
|
||||
|
||||
const _CheckoutContent({
|
||||
required this.checkoutData,
|
||||
required this.bookingId,
|
||||
this.couponId,
|
||||
required this.isPurchaseDetailsConfirmed,
|
||||
required this.onPurchaseDetailsChanged,
|
||||
});
|
||||
@@ -123,9 +128,16 @@ class _CheckoutContent extends StatefulWidget {
|
||||
|
||||
class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
bool _hasHandledPaymentResult = false;
|
||||
bool _hasAutoAppliedCoupon = false;
|
||||
|
||||
/// 🆕 Handle payment flow with client secret
|
||||
/// 🆕 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(
|
||||
context: context,
|
||||
clientSecret: clientSecret,
|
||||
@@ -179,11 +191,11 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
|
||||
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Payment confirmed successfully!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
const SnackBar(
|
||||
content: Text('Payment confirmed successfully!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,37 +208,60 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
// 🔒 CHECK: Prevent duplicate payment flow initiation
|
||||
if (state.clientSecret != null &&
|
||||
state.clientSecret!.isNotEmpty &&
|
||||
!_hasHandledPaymentResult) { // 🔒 Only proceed if not already handled
|
||||
|
||||
// 🔒 MARK: Set flag immediately to prevent re-entry
|
||||
!_hasHandledPaymentResult) {
|
||||
_hasHandledPaymentResult = true;
|
||||
|
||||
// ✅ Calculate finalTotal here
|
||||
double discountPercentage = 0.0;
|
||||
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 totalBeforeTax = subtotal - discountAmount;
|
||||
final double taxAmount = 2;
|
||||
final double finalTotal = totalBeforeTax + taxAmount;
|
||||
|
||||
// ✅ Trigger payment flow with finalTotal
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handlePaymentFlow(
|
||||
context,
|
||||
state.clientSecret!,
|
||||
state.bookingId ?? 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
|
||||
if (state.isPaymentConfirmed) {
|
||||
// Navigate to success page or back
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
@@ -248,20 +283,14 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
// 🆕 Handle payment initiation error
|
||||
if (state is CheckoutPaymentInitiationErrorState) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.error),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
SnackBar(content: Text(state.error), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
|
||||
// 🆕 Handle payment confirmation error
|
||||
if (state is CheckoutPaymentConfirmationErrorState) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.error),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
SnackBar(content: Text(state.error), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -332,131 +361,135 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// ✅ Hero Image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8.r),
|
||||
bottomLeft: Radius.circular(8.r),
|
||||
// ✅ Expanded forces left side to only take remaining space after the 35.w label
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Hero Image
|
||||
ClipRRect(
|
||||
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(
|
||||
widget.checkoutData.heroImage,
|
||||
width: 105.w,
|
||||
height: 140.h,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _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(
|
||||
strokeWidth: 2,
|
||||
|
||||
SizedBox(width: 6.66.w),
|
||||
|
||||
// ✅ Expanded so text column doesn't overflow
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: widget.checkoutData.cityName,
|
||||
weight: FontWeight.w500,
|
||||
size: 16.sp,
|
||||
),
|
||||
SizedBox(height: 5.h),
|
||||
CustomText(
|
||||
text: widget.checkoutData.validityLabel,
|
||||
color: const Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
SizedBox(height: 5.h),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: _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(
|
||||
width: 35.w,
|
||||
height: 140.h,
|
||||
@@ -472,6 +505,8 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.checkoutData.cardDisplayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
@@ -490,8 +525,10 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
// ✅ COUPON SECTION
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w,
|
||||
vertical: 16.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF95F62).withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
@@ -502,142 +539,160 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
),
|
||||
child: state is CheckoutCouponsLoadingState
|
||||
? Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16.w,
|
||||
height: 16.w,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
CustomText(
|
||||
text: "Loading coupons...",
|
||||
size: 12.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
)
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16.w,
|
||||
height: 16.w,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
CustomText(
|
||||
text: "Loading coupons...",
|
||||
size: 12.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
)
|
||||
: state is CheckoutCouponsErrorState
|
||||
? CustomText(
|
||||
text: "Error loading coupons",
|
||||
size: 12.sp,
|
||||
color: Colors.red,
|
||||
)
|
||||
text: "Error loading coupons",
|
||||
size: 12.sp,
|
||||
color: Colors.red,
|
||||
)
|
||||
: state is CheckoutCouponsLoadedState
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// LEFT CONTENT
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: appliedCoupon != null
|
||||
? "Coupon Applied: ${appliedCoupon.couponCode}"
|
||||
: state.coupons.isNotEmpty
|
||||
? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}"
|
||||
: "No coupons available",
|
||||
color: const Color(0xFF262626),
|
||||
size: 14.sp,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 7.h),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// ✅ Updated: Pass callback to bottomsheet
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
/// LEFT CONTENT
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: appliedCoupon != null
|
||||
? "Coupon Applied: ${appliedCoupon.couponCode}"
|
||||
: state.coupons.isNotEmpty
|
||||
? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}"
|
||||
: "No coupons available",
|
||||
color: const Color(0xFF262626),
|
||||
size: 14.sp,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
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,
|
||||
SizedBox(height: 7.h),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// ✅ Updated: Pass callback to bottomsheet
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
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: [
|
||||
CustomText(
|
||||
text: "View all coupons",
|
||||
color: const Color(0xFFF95F62),
|
||||
size: 12.sp,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
const Icon(
|
||||
Icons.arrow_right,
|
||||
size: 18,
|
||||
color: Color(0xFFF95F62),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "View all coupons",
|
||||
color: const Color(0xFFF95F62),
|
||||
size: 12.sp,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
const Icon(
|
||||
Icons.arrow_right,
|
||||
size: 18,
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
/// APPLY / REMOVE BUTTON
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (appliedCoupon != null) {
|
||||
context.read<CheckoutBloc>().add(
|
||||
RemoveCouponEvent(bookingId: widget.bookingId),
|
||||
);
|
||||
} else if (state.coupons.isNotEmpty) {
|
||||
// Apply coupon via backend API
|
||||
context.read<CheckoutBloc>().add(
|
||||
ApplyCouponToBackendEvent(
|
||||
bookingId: widget.bookingId,
|
||||
couponCode: state.coupons[0].couponCode,
|
||||
/// APPLY / REMOVE BUTTON
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final isLogin =
|
||||
await LocalPreference.getLogin();
|
||||
if (isLogin == true) {
|
||||
if (appliedCoupon != null) {
|
||||
context.read<CheckoutBloc>().add(
|
||||
RemoveCouponEvent(
|
||||
bookingId: widget.bookingId,
|
||||
),
|
||||
);
|
||||
} 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(),
|
||||
),
|
||||
|
||||
@@ -669,13 +724,12 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
// Discount
|
||||
if (discountPercentage > 0) ...[
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CustomText(text: "Discount", size: 14.sp),
|
||||
CustomText(
|
||||
text:
|
||||
"-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
|
||||
"-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
|
||||
size: 14.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: Colors.green,
|
||||
@@ -699,14 +753,13 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(text: 'Total', size: 14.sp),
|
||||
SizedBox(height: 4.h),
|
||||
CustomText(
|
||||
text:
|
||||
"Including \$${taxAmount.toStringAsFixed(2)} in taxes",
|
||||
"Including \$${taxAmount.toStringAsFixed(2)} in taxes",
|
||||
size: 12.sp,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
@@ -728,65 +781,76 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
future: LocalPreference.getLogin(),
|
||||
builder: (context, snapshot) {
|
||||
final isLoggedIn = snapshot.data ?? false;
|
||||
final isDisabled = isInitiatingPayment || isConfirmingPayment;
|
||||
final isDisabled =
|
||||
isInitiatingPayment || isConfirmingPayment;
|
||||
|
||||
return CustomFilledButton(
|
||||
onTap: isDisabled
|
||||
? () {} // Empty callback when disabled
|
||||
: () async {
|
||||
if (isLoggedIn) {
|
||||
if (widget.isPurchaseDetailsConfirmed) {
|
||||
// 🆕 Initiate payment flow
|
||||
context.read<CheckoutBloc>().add(
|
||||
InitiatePaymentEvent(
|
||||
bookingId: widget.bookingId),
|
||||
);
|
||||
} else {
|
||||
// Show purchase details bottom sheet
|
||||
final result = await PassPurchaseBottomSheet.show(
|
||||
context, bookingId: widget.bookingId);
|
||||
if (isLoggedIn) {
|
||||
if (widget.isPurchaseDetailsConfirmed) {
|
||||
// 🆕 Initiate payment flow
|
||||
context.read<CheckoutBloc>().add(
|
||||
InitiatePaymentEvent(
|
||||
bookingId: widget.bookingId,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Show purchase details bottom sheet
|
||||
final result =
|
||||
await PassPurchaseBottomSheet.show(
|
||||
context,
|
||||
bookingId: widget.bookingId,
|
||||
);
|
||||
|
||||
// ✅ Handle 'Buy for Myself' - user submitted details
|
||||
if (result == 'success') {
|
||||
widget.onPurchaseDetailsChanged(true);
|
||||
}
|
||||
// ✅ Handle 'Gift the Pass' - navigate to AddDetailsView
|
||||
else if (result == 'gift') {
|
||||
final giftResult = await Navigator.of(context).push<String>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AddDetailsView(bookingId: widget.bookingId),
|
||||
),
|
||||
);
|
||||
// ✅ Handle 'Buy for Myself' - user submitted details
|
||||
if (result == 'success') {
|
||||
widget.onPurchaseDetailsChanged(true);
|
||||
}
|
||||
// ✅ Handle 'Gift the Pass' - navigate to AddDetailsView
|
||||
else if (result == 'gift') {
|
||||
final giftResult =
|
||||
await Navigator.of(
|
||||
context,
|
||||
).push<String>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AddDetailsView(
|
||||
bookingId: widget.bookingId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// If gift details were successfully submitted, mark as confirmed
|
||||
if (giftResult == 'success') {
|
||||
widget.onPurchaseDetailsChanged(true);
|
||||
// If gift details were successfully submitted, mark as confirmed
|
||||
if (giftResult == 'success') {
|
||||
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,
|
||||
label: isLoggedIn
|
||||
? (widget.isPurchaseDetailsConfirmed
|
||||
? (isInitiatingPayment || isConfirmingPayment
|
||||
? "Processing..."
|
||||
: "Pay \$${finalTotal.toStringAsFixed(2)}")
|
||||
: "Checkout")
|
||||
? (isInitiatingPayment || isConfirmingPayment
|
||||
? "Processing..."
|
||||
: "Pay \$${finalTotal.toStringAsFixed(2)}")
|
||||
: "Checkout")
|
||||
: "Login to Checkout",
|
||||
);
|
||||
},
|
||||
@@ -807,11 +871,7 @@ class _CheckoutContentState extends State<_CheckoutContent> {
|
||||
width: 105.w,
|
||||
height: 140.h,
|
||||
color: Colors.grey[200],
|
||||
child: Icon(
|
||||
Icons.card_travel,
|
||||
size: 40.sp,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
child: Icon(Icons.card_travel, size: 40.sp, color: Colors.grey[400]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,26 +30,26 @@ class AllCouponsBottomsheet extends StatelessWidget {
|
||||
right: 20.w,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// --- Header ---
|
||||
Container(
|
||||
height: 4.h,
|
||||
width: 40.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF2D3134),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// --- Header ---
|
||||
Container(
|
||||
height: 4.h,
|
||||
width: 40.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF2D3134),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
CustomText(
|
||||
text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
|
||||
SizedBox(height: 22.h),
|
||||
SizedBox(height: 12.h),
|
||||
CustomText(
|
||||
text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
|
||||
SizedBox(height: 22.h),
|
||||
|
||||
/// --- Coupon list ---
|
||||
Flexible(
|
||||
child: BlocBuilder<AllCouponsBloc, AllCouponsState>(
|
||||
/// --- Coupon list ---
|
||||
BlocBuilder<AllCouponsBloc, AllCouponsState>(
|
||||
builder: (context, state) {
|
||||
if (state is CouponsLoadingState) {
|
||||
return Center(
|
||||
@@ -77,7 +77,7 @@ class AllCouponsBottomsheet extends StatelessWidget {
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: state.coupons.length,
|
||||
separatorBuilder: (_, __) => SizedBox(height: 12.h),
|
||||
itemBuilder: (context, index) {
|
||||
@@ -101,14 +101,15 @@ class AllCouponsBottomsheet extends StatelessWidget {
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 220.w,
|
||||
Expanded(
|
||||
child: CustomText(
|
||||
text: "${coupon.discountPercent}% discount on ${coupon.title}",
|
||||
text:
|
||||
"${coupon.discountPercent}% discount on ${coupon.title}",
|
||||
size: 12.sp,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Pass the selected coupon back to checkout view
|
||||
@@ -118,8 +119,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Container(
|
||||
width: 110.w,
|
||||
height: 44.h,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62),
|
||||
borderRadius:
|
||||
@@ -141,9 +143,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
|
||||
height: 32.h,
|
||||
width: 83.w,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Color(0xFFF95F62).withOpacity(0.12),
|
||||
border: Border.all(color: Color(0xFFF95F62)),
|
||||
color: Color(0xFFF95F62).withOpacity(0.12),
|
||||
border:
|
||||
Border.all(color: Color(0xFFF95F62)),
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
child: Center(
|
||||
@@ -165,8 +167,9 @@ class AllCouponsBottomsheet extends StatelessWidget {
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 16.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -275,9 +275,8 @@ class _PassPurchaseContent extends StatelessWidget {
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62),
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
|
||||
@@ -18,4 +18,4 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
|
||||
emit(NavigationState(event.index));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
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_bloc/flutter_bloc.dart';
|
||||
@@ -16,7 +17,6 @@ class CommonAppBar extends StatelessWidget {
|
||||
required this.isProfilePage,
|
||||
this.showCart = true,
|
||||
required this.showDivider,
|
||||
this.imageUrl,
|
||||
this.isSelectCity = false,
|
||||
});
|
||||
|
||||
@@ -24,14 +24,10 @@ class CommonAppBar extends StatelessWidget {
|
||||
final bool isProfilePage;
|
||||
final bool? showCart;
|
||||
final bool showDivider;
|
||||
final String? imageUrl;
|
||||
final bool isSelectCity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isPathIcon =
|
||||
imageUrl != null && imageUrl!.isNotEmpty;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -40,31 +36,62 @@ class CommonAppBar extends StatelessWidget {
|
||||
/// LEFT SIDE
|
||||
Row(
|
||||
children: [
|
||||
/// ✅ LOGO / PATH ICON (SIZE CONTROLLED)
|
||||
SizedBox(
|
||||
height: isPathIcon ? 40.h : 32.h, // 🔥 ONLY path icon bigger
|
||||
child: isPathIcon
|
||||
? Image.network(
|
||||
imageUrl!,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(
|
||||
isWhiteLogo
|
||||
? "assets/logo/logo_city_cards_white.png"
|
||||
: "assets/logo/logo_city_cards.png",
|
||||
fit: BoxFit.contain,
|
||||
/// ✅ LOGO (TAP ENABLED ONLY WHEN isSelectCity == true)
|
||||
GestureDetector(
|
||||
onTap: isSelectCity
|
||||
? () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => const CitySelectionBottomSheet(),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
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)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
@@ -124,32 +151,26 @@ class CommonAppBar extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
String? imagePath;
|
||||
|
||||
// ✅ Get image from profile state
|
||||
if (state is ProfileLoaded) {
|
||||
imagePath = state.profile.profileImage;
|
||||
}
|
||||
|
||||
// ✅ Build full image URL
|
||||
final String? imageUrl =
|
||||
(imagePath != null && imagePath.isNotEmpty)
|
||||
(imagePath != null && imagePath.isNotEmpty)
|
||||
? "${ApiUrls.baseUrl}$imagePath"
|
||||
: null;
|
||||
|
||||
return CircleAvatar(
|
||||
radius: 20.r,
|
||||
backgroundColor: const Color(0xffFFDFDF),
|
||||
|
||||
// ✅ Network image only if exists
|
||||
backgroundImage:
|
||||
(imageUrl != null && imageUrl.isNotEmpty)
|
||||
(imageUrl != null && imageUrl.isNotEmpty)
|
||||
? NetworkImage(imageUrl)
|
||||
: null,
|
||||
|
||||
// ✅ Default fallback (unchanged)
|
||||
child: (imageUrl == null || imageUrl.isEmpty)
|
||||
? Image.asset(
|
||||
"assets/images/profile_default_img.png",
|
||||
)
|
||||
"assets/images/profile_default_img.png",
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
@@ -159,6 +180,7 @@ class CommonAppBar extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
/// DIVIDER
|
||||
if (showDivider)
|
||||
Column(
|
||||
|
||||
@@ -2,23 +2,23 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
Widget backWidget(BuildContext context, String title, Color? textColor){
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Icon(Icons.arrow_back, size: 24.sp, color: textColor ?? Colors.black),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textColor ?? Colors.black
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_back, size: 24.sp, color: textColor ?? Colors.black),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textColor ?? Colors.black
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFFF5F5),
|
||||
border: Border.all(color: Color(0xffFDCDCE)),
|
||||
border: Border.all(color: const Color(0xffFDCDCE)),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: 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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
context,
|
||||
@@ -49,7 +49,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
context,
|
||||
index: 2,
|
||||
iconPath: 'assets/icons/pass_icon.png',
|
||||
label: 'My Passes',
|
||||
label: 'My Cards',
|
||||
isActive: state.selectedIndex == 2,
|
||||
),
|
||||
_buildNavItem(
|
||||
@@ -67,45 +67,66 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildNavItem(
|
||||
BuildContext context, {
|
||||
required int index,
|
||||
required String iconPath,
|
||||
required String label,
|
||||
required bool isActive,
|
||||
}) {
|
||||
BuildContext context, {
|
||||
required int index,
|
||||
required String iconPath,
|
||||
required String label,
|
||||
required bool isActive,
|
||||
}) {
|
||||
final color = isActive
|
||||
? const Color(0xFFBB474A)
|
||||
: Color(0xFFBB474A).withOpacity(0.6);
|
||||
: const Color(0xFFBB474A).withOpacity(0.6);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () =>
|
||||
context.read<NavigationBloc>().add(NavigationTabChanged(index)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isActive)
|
||||
Container(
|
||||
child: SizedBox(
|
||||
width: 80.w,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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),
|
||||
height: 4.h,
|
||||
width: 50.w,
|
||||
width: isActive ? 50.w : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
color: isActive ? color : Colors.transparent,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// The length of each dash in the dashed line.
|
||||
final double dashLength;
|
||||
|
||||
/// The space between each dash in the dashed line.
|
||||
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;
|
||||
|
||||
/// If true, shows the advanced pill-style dashed divider
|
||||
final bool isAdvanced;
|
||||
|
||||
const DashedDivider({
|
||||
super.key,
|
||||
this.height,
|
||||
@@ -64,6 +23,7 @@ class DashedDivider extends StatelessWidget {
|
||||
this.dashLength = 5,
|
||||
this.dashSpace = 5,
|
||||
this.mainAxisOffset = 0.0,
|
||||
this.isAdvanced = false,
|
||||
}) : assert(height == null || height >= 0.0),
|
||||
assert(thickness == null || thickness >= 0.0),
|
||||
assert(indent == null || indent >= 0.0),
|
||||
@@ -71,6 +31,17 @@ class DashedDivider extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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(
|
||||
height: height,
|
||||
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 {
|
||||
final Color color;
|
||||
final double thickness;
|
||||
@@ -142,31 +179,29 @@ class DashedLinePainter extends CustomPainter {
|
||||
}
|
||||
}
|
||||
|
||||
double _getMainAxisSize(Size size) {
|
||||
return isVertical ? size.height : size.width;
|
||||
}
|
||||
double _getMainAxisSize(Size size) =>
|
||||
isVertical ? size.height : size.width;
|
||||
|
||||
double _getCrossAxisPosition(Size size) {
|
||||
return isVertical ? size.width / 2 : size.height / 2;
|
||||
}
|
||||
double _getCrossAxisPosition(Size size) =>
|
||||
isVertical ? size.width / 2 : size.height / 2;
|
||||
|
||||
Offset _calculateStartOffset(
|
||||
double crossAxisPosition, double currentPosition) {
|
||||
return isVertical
|
||||
? Offset(crossAxisPosition, currentPosition)
|
||||
: Offset(currentPosition, crossAxisPosition);
|
||||
}
|
||||
Offset _calculateStartOffset(double crossAxisPosition, double currentPosition) =>
|
||||
isVertical
|
||||
? Offset(crossAxisPosition, currentPosition)
|
||||
: Offset(currentPosition, crossAxisPosition);
|
||||
|
||||
Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) {
|
||||
return isVertical
|
||||
? Offset(crossAxisPosition, nextDashEnd)
|
||||
: Offset(nextDashEnd, crossAxisPosition);
|
||||
}
|
||||
Offset _calculateEndOffset(double crossAxisPosition, double nextDashEnd) =>
|
||||
isVertical
|
||||
? Offset(crossAxisPosition, nextDashEnd)
|
||||
: Offset(nextDashEnd, crossAxisPosition);
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Theme Provider (unchanged)
|
||||
// ─────────────────────────────────────────────
|
||||
class DividerThemeProvider {
|
||||
final DividerThemeData _dividerTheme;
|
||||
final ThemeData _theme;
|
||||
@@ -204,35 +239,20 @@ class DividerThemeProvider {
|
||||
_indent = indent ?? _indent;
|
||||
_endIndent = endIndent ?? _endIndent;
|
||||
_color = color ?? _color;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
double get width => _width ?? _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 endIndent =>
|
||||
_endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!;
|
||||
|
||||
Color get color =>
|
||||
_color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor;
|
||||
double get endIndent => _endIndent ?? _dividerTheme.endIndent ?? _defaults.endIndent!;
|
||||
Color get color => _color ?? _dividerTheme.color ?? _defaults.color ?? _theme.dividerColor;
|
||||
}
|
||||
|
||||
class _DividerDefaultsM3 extends DividerThemeData {
|
||||
const _DividerDefaultsM3(this.context)
|
||||
: super(
|
||||
space: 16,
|
||||
thickness: 1.0,
|
||||
indent: 0,
|
||||
endIndent: 0,
|
||||
);
|
||||
|
||||
: super(space: 16, thickness: 1.0, indent: 0, endIndent: 0);
|
||||
final BuildContext context;
|
||||
|
||||
@override
|
||||
@@ -241,13 +261,7 @@ class _DividerDefaultsM3 extends DividerThemeData {
|
||||
|
||||
class _DividerDefaultsM2 extends DividerThemeData {
|
||||
const _DividerDefaultsM2(this.context)
|
||||
: super(
|
||||
space: 16,
|
||||
thickness: 0,
|
||||
indent: 0,
|
||||
endIndent: 0,
|
||||
);
|
||||
|
||||
: super(space: 16, thickness: 0, indent: 0, endIndent: 0);
|
||||
final BuildContext context;
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,48 +6,63 @@ class CustomFilledButton extends StatelessWidget {
|
||||
final double? width;
|
||||
final String label;
|
||||
final bool? showArrow;
|
||||
final GestureTapCallback onTap;
|
||||
final GestureTapCallback? onTap; // ✅ Made nullable
|
||||
final double? height;
|
||||
final bool isLoading; // ✅ NEW
|
||||
|
||||
CustomFilledButton({
|
||||
const CustomFilledButton({
|
||||
super.key,
|
||||
this.width = 266,
|
||||
this.width,
|
||||
required this.onTap,
|
||||
required this.label,
|
||||
this.showArrow = false,
|
||||
this.height = 42
|
||||
this.height,
|
||||
this.isLoading = false, // ✅ NEW
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
onTap: isLoading ? null : onTap, // ✅ Disabled when loading
|
||||
child: Container(
|
||||
height: height,
|
||||
width: width,
|
||||
height: height ?? 42.h,
|
||||
width: width ?? 266.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62),
|
||||
color: isLoading
|
||||
? Color(0xFFF95F62).withOpacity(0.6) // ✅ Dimmed when loading
|
||||
: Color(0xFFF95F62),
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Row(
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: 20.sp,
|
||||
width: 20.sp,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: label,
|
||||
color: Colors.white,
|
||||
size: 16.sp ,
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
|
||||
if(showArrow!)
|
||||
SizedBox(width: 8,),
|
||||
if(showArrow!)
|
||||
Icon(Icons.arrow_forward_ios_rounded,size: 18.sp, color: Colors.white,)
|
||||
if (showArrow!) SizedBox(width: 8),
|
||||
if (showArrow!)
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 18.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ class CustomText extends StatelessWidget {
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
final TextAlign? textAlign;
|
||||
final Color asteriskColor; // ADD THIS
|
||||
|
||||
const CustomText({
|
||||
Key? key,
|
||||
@@ -18,20 +19,50 @@ class CustomText extends StatelessWidget {
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
this.textAlign,
|
||||
this.asteriskColor = Colors.red, // ADD THIS
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
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(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.lerp(
|
||||
weight,
|
||||
FontWeight.values[
|
||||
(FontWeight.values.indexOf(weight??FontWeight.w400) + 1)
|
||||
.clamp(0, FontWeight.values.length - 1) // prevent overflow
|
||||
(FontWeight.values.indexOf(weight ?? FontWeight.w400) + 1)
|
||||
.clamp(0, FontWeight.values.length - 1)
|
||||
],
|
||||
0.5, // t: pick between 0.0 and 1.0
|
||||
0.5,
|
||||
),
|
||||
color: color,
|
||||
fontSize: size,
|
||||
|
||||
@@ -13,11 +13,21 @@ class CustomTextField extends StatelessWidget {
|
||||
final TextInputType? keyboardType;
|
||||
final bool obscureText;
|
||||
final Widget? suffixIcon;
|
||||
final Widget? prefixWidget;
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
// ✅ NEW
|
||||
final int? maxLength; // e.g. 10
|
||||
final bool numbersOnly; // allow only digits
|
||||
final int? maxLength;
|
||||
final bool numbersOnly;
|
||||
|
||||
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({
|
||||
super.key,
|
||||
@@ -30,98 +40,402 @@ class CustomTextField extends StatelessWidget {
|
||||
this.keyboardType,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.prefixWidget,
|
||||
this.onChanged,
|
||||
|
||||
// ✅ NEW
|
||||
this.maxLength, // default = null (infinite)
|
||||
this.numbersOnly = false, // default = false
|
||||
this.maxLength,
|
||||
this.numbersOnly = false,
|
||||
this.isMobileNumber = 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
|
||||
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(
|
||||
padding: EdgeInsets.only(bottom: 12.h),
|
||||
padding: EdgeInsets.only(bottom: 14.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: label,
|
||||
size: 14.sp,
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
SizedBox(
|
||||
height: maxLines == 1 ? 42.h : null,
|
||||
child: TextFormField(
|
||||
// Label
|
||||
if (label.isNotEmpty) ...[
|
||||
CustomText(text: label, size: 14.sp),
|
||||
SizedBox(height: 6.h),
|
||||
],
|
||||
|
||||
if (prefixWidget != null)
|
||||
// ✅ 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,
|
||||
maxLines: obscureText ? 1 : maxLines,
|
||||
enabled: enabled,
|
||||
validator: validator,
|
||||
keyboardType: keyboardType,
|
||||
enabled: isPreview ? false : enabled,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
|
||||
// ✅ NEW
|
||||
maxLength: maxLength,
|
||||
inputFormatters: [
|
||||
if (numbersOnly)
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
if (maxLength != null)
|
||||
LengthLimitingTextInputFormatter(maxLength),
|
||||
],
|
||||
|
||||
validator: validator ?? _internalValidator,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
keyboardType: keyboardType ??
|
||||
(isMobileNumber
|
||||
? TextInputType.phone
|
||||
: isEmail
|
||||
? TextInputType.emailAddress
|
||||
: TextInputType.name),
|
||||
inputFormatters: inputFormatters,
|
||||
onChanged: (value) {
|
||||
if (isFirstLetterCapital) _capitalizeFirstLetter(value);
|
||||
if (onChanged != null) onChanged!(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
counterText: "", // ✅ hides 0/10 counter
|
||||
counterText: "",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFF8E8E8E),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: enabled
|
||||
? const Color(0xFFFFF5F5)
|
||||
: Colors.grey.shade200,
|
||||
fillColor: fillColor,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 0,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xBBC83B61).withOpacity(0.4),
|
||||
width: .4.w,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(color: borderColor, width: .4.w),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xFFF95F62),
|
||||
width: 1.w,
|
||||
),
|
||||
color: const Color(0xFFF95F62), width: 1.w),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.w,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(color: Colors.red, width: 1.w),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.5.w,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(color: Colors.red, width: 1.5.w),
|
||||
),
|
||||
errorStyle: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,200 +5,197 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class LanguageSelectionBottomsheet extends StatelessWidget {
|
||||
LanguageSelectionBottomsheet({super.key});
|
||||
import '../localPreference/local_preference.dart';
|
||||
|
||||
List<String> languages = [
|
||||
"English / Englis",
|
||||
"Dutch / Nederlands",
|
||||
"Spanish / Español",
|
||||
"French / Français",
|
||||
"Japanese / 日本語",
|
||||
class LanguageSelectionBottomsheet extends StatefulWidget {
|
||||
const LanguageSelectionBottomsheet({super.key});
|
||||
|
||||
@override
|
||||
State<LanguageSelectionBottomsheet> createState() =>
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
return 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),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
"Change Language",
|
||||
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 22.h),
|
||||
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,
|
||||
padding: EdgeInsets.only(
|
||||
left: 20.w,
|
||||
right: 20.w,
|
||||
top: 16.h,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 16.h,
|
||||
),
|
||||
child: BlocBuilder<LanguageBloc, LanguageState>(
|
||||
builder: (context, state) {
|
||||
// Seed pending selection from current BLoC state on first build
|
||||
_pendingLabel ??= state.selectedLanguage;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Drag handle
|
||||
Container(
|
||||
width: 40.w,
|
||||
height: 4.h,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D3134),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
BlocBuilder<LanguageBloc, LanguageState>(
|
||||
builder: (context, state) {
|
||||
return Expanded(
|
||||
/// Title
|
||||
Align(
|
||||
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(
|
||||
itemCount: languages.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = languages[index];
|
||||
final item = _filtered[index];
|
||||
final label = item['label']!;
|
||||
final isSelected = _pendingLabel == label;
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
onTap: () => setState(() => _pendingLabel = label),
|
||||
leading: GestureDetector(
|
||||
onTap: () {
|
||||
context.read<LanguageBloc>().add(
|
||||
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
|
||||
onTap: () => setState(() => _pendingLabel = label),
|
||||
child: isSelected
|
||||
? Image.asset(
|
||||
"assets/icons/radio_button_checked.png",
|
||||
scale: 4,
|
||||
)
|
||||
"assets/icons/radio_button_checked.png",
|
||||
scale: 4,
|
||||
)
|
||||
: Image.asset(
|
||||
"assets/icons/radio_button_unchecked.png",
|
||||
scale: 4,
|
||||
),
|
||||
"assets/icons/radio_button_unchecked.png",
|
||||
scale: 4,
|
||||
),
|
||||
),
|
||||
title: CustomText(
|
||||
text: item,
|
||||
text: label,
|
||||
size: 16.sp,
|
||||
color: state.selectedLanguage == item
|
||||
? Color(0xFFF95F62)
|
||||
: Color(0xFF000000).withOpacity(.6),
|
||||
color: isSelected
|
||||
? const Color(0xFFF95F62)
|
||||
: 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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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_offers_repository.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/edit_profile/edit_profile_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 '../search_offers/bloc/offers_bloc.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';
|
||||
|
||||
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:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
@@ -80,6 +98,7 @@ class AppRouter {
|
||||
case RouteConstants.passAttractionsPage:
|
||||
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
|
||||
final int cityId = args['cityId'] as int;
|
||||
final int bookingId = args['bookingId'] as int;
|
||||
final String source = args['source'] as String;
|
||||
|
||||
return MaterialPageRoute(
|
||||
@@ -91,6 +110,7 @@ class AppRouter {
|
||||
child: PassAttractionsPage(
|
||||
cityXid: cityId,
|
||||
source: source,
|
||||
bookingId: bookingId,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -254,9 +274,14 @@ class AppRouter {
|
||||
);
|
||||
|
||||
case RouteConstants.yourItinerary:
|
||||
final itineraryId = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return YourItineraryView();
|
||||
builder: (context) {
|
||||
return BlocProvider(
|
||||
create: (context) => YourItineraryDetailsBloc()
|
||||
..add(FetchItineraryDetailsEvent(itineraryId: itineraryId)),
|
||||
child: YourItineraryView(itineraryId: itineraryId,),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlobalKeys {
|
||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
GlobalKey<ScaffoldMessengerState>();
|
||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
static final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
|
||||
}
|
||||
@@ -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_successful_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 '../postcard/blocs/postcard_creation_bloc.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 '../search_offers/bloc/offers_bloc.dart';
|
||||
import '../search_offers/bloc/search_offers_listing_bloc.dart';
|
||||
import '../search_offers/repository/offers_repository.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 'global_keys.dart';
|
||||
|
||||
Widget buildOffstageNavigator(
|
||||
int index,
|
||||
@@ -58,6 +63,16 @@ Widget buildOffstageNavigator(
|
||||
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
|
||||
case RouteConstants.attractionsPage:
|
||||
final args = settings.arguments as String;
|
||||
@@ -67,6 +82,7 @@ Widget buildOffstageNavigator(
|
||||
case RouteConstants.passAttractionsPage:
|
||||
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
|
||||
final int cityId = args['cityId'] as int;
|
||||
final int bookingId = args['bookingId'] as int;
|
||||
final String source = args['source'] as String;
|
||||
|
||||
return MaterialPageRoute(
|
||||
@@ -78,6 +94,7 @@ Widget buildOffstageNavigator(
|
||||
child: PassAttractionsPage(
|
||||
cityXid: cityId,
|
||||
source: source,
|
||||
bookingId: bookingId,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -92,28 +109,34 @@ Widget buildOffstageNavigator(
|
||||
);
|
||||
|
||||
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(
|
||||
builder: (_) {
|
||||
return PassAttractionDetailsView(attractionId: attractionID);
|
||||
},
|
||||
builder: (_) => PassAttractionDetailsView(
|
||||
attractionId: attractionId,
|
||||
bookingId: bookingId,
|
||||
),
|
||||
);
|
||||
|
||||
case RouteConstants.makeBooking:
|
||||
final args = settings.arguments as Map<String, dynamic>?;
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return MakeBookingView(
|
||||
title: 'Koh Rong Samloem',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis.ß',
|
||||
);
|
||||
},
|
||||
builder: (_) => MakeBookingView(
|
||||
title: args?['title'] ?? '',
|
||||
description: args?['description'] ?? '',
|
||||
validUpto: args?['validUpto'] ?? '',
|
||||
attractionId: args?['attractionId'] ?? 0,
|
||||
bookingId: args?['bookingId'] ?? 0,
|
||||
),
|
||||
);
|
||||
|
||||
case RouteConstants.bookingSuccessful:
|
||||
final message = settings.arguments as String;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return BookingSuccessfulPageView();
|
||||
return BookingSuccessfulPageView(message: message,);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -152,6 +175,12 @@ Widget buildOffstageNavigator(
|
||||
return const PrivacyPolicyPage();
|
||||
},
|
||||
);
|
||||
case RouteConstants.contactUs:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return const ContactUsPage();
|
||||
},
|
||||
);
|
||||
|
||||
// 🔹 Upload Photo Page (start of postcard creation flow)
|
||||
case RouteConstants.uploadPhotoPage:
|
||||
@@ -208,9 +237,14 @@ Widget buildOffstageNavigator(
|
||||
);
|
||||
|
||||
case RouteConstants.yourItinerary:
|
||||
final itineraryId = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return YourItineraryView();
|
||||
builder: (context) {
|
||||
return BlocProvider(
|
||||
create: (context) => YourItineraryDetailsBloc()
|
||||
..add(FetchItineraryDetailsEvent(itineraryId: itineraryId)),
|
||||
child: YourItineraryView(itineraryId: itineraryId,),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ class RouteConstants {
|
||||
|
||||
static const String intro = '/intro';
|
||||
static const String splash = '/splash';
|
||||
static const String noInternet = '/noInternet';
|
||||
|
||||
/****************************** HOME SECTION ************************************/
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
|
||||
firstName: event.firstName,
|
||||
lastName: event.lastName,
|
||||
emailAddress: event.emailAddress,
|
||||
isdCode: event.isdCode,
|
||||
mobileNumber: event.mobileNumber,
|
||||
address1: event.address1,
|
||||
address2: event.address2,
|
||||
|
||||
@@ -11,6 +11,7 @@ class CreateAccountSubmitted extends CreateAccountEvent {
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String emailAddress;
|
||||
final String isdCode;
|
||||
final String mobileNumber;
|
||||
final String address1;
|
||||
final String address2;
|
||||
@@ -23,6 +24,7 @@ class CreateAccountSubmitted extends CreateAccountEvent {
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.emailAddress,
|
||||
required this.isdCode,
|
||||
required this.mobileNumber,
|
||||
required this.address1,
|
||||
required this.address2,
|
||||
@@ -37,6 +39,7 @@ class CreateAccountSubmitted extends CreateAccountEvent {
|
||||
firstName,
|
||||
lastName,
|
||||
emailAddress,
|
||||
isdCode,
|
||||
mobileNumber,
|
||||
address1,
|
||||
address2,
|
||||
|
||||
@@ -8,6 +8,7 @@ class CreateAccountRepository {
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String emailAddress,
|
||||
required String isdCode,
|
||||
required String mobileNumber,
|
||||
required String address1,
|
||||
required String address2,
|
||||
@@ -23,6 +24,7 @@ class CreateAccountRepository {
|
||||
"firstName": firstName,
|
||||
"lastName": lastName,
|
||||
"emailAddress": emailAddress,
|
||||
"isdCode": isdCode,
|
||||
"mobileNumber": mobileNumber,
|
||||
"address1": address1,
|
||||
"address2": address2,
|
||||
|
||||
@@ -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_text.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_screenutil/flutter_screenutil.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 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart';
|
||||
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
|
||||
@@ -36,18 +42,51 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
final TextEditingController cityController = TextEditingController();
|
||||
final TextEditingController postalController = TextEditingController();
|
||||
|
||||
String? selectedState;
|
||||
String? selectedCountry;
|
||||
// ── Replaced dropdowns with plain text controllers ─────────────────────────
|
||||
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) {
|
||||
// 1. Empty field check
|
||||
if (firstNameController.text.trim().isEmpty ||
|
||||
lastNameController.text.trim().isEmpty ||
|
||||
emailController.text.trim().isEmpty ||
|
||||
phoneController.text.trim().isEmpty ||
|
||||
addressController.text.trim().isEmpty ||
|
||||
cityController.text.trim().isEmpty ||
|
||||
selectedState == null ||
|
||||
selectedCountry == null ||
|
||||
stateController.text.trim().isEmpty ||
|
||||
countryController.text.trim().isEmpty ||
|
||||
postalController.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please fill all fields')),
|
||||
@@ -55,17 +94,41 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
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(
|
||||
CreateAccountSubmitted(
|
||||
firstName: firstNameController.text.trim(),
|
||||
lastName: lastNameController.text.trim(),
|
||||
emailAddress: emailController.text.trim(),
|
||||
mobileNumber: phoneController.text.trim(),
|
||||
isdCode: _selectedIsdCode,
|
||||
mobileNumber: phone,
|
||||
address1: addressController.text.trim(),
|
||||
address2: '',
|
||||
city: cityController.text.trim(),
|
||||
state: selectedState!,
|
||||
country: selectedCountry!,
|
||||
state: stateController.text.trim(),
|
||||
country: countryController.text.trim(),
|
||||
postalCode: postalController.text.trim(),
|
||||
),
|
||||
);
|
||||
@@ -80,6 +143,8 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
addressController.dispose();
|
||||
cityController.dispose();
|
||||
postalController.dispose();
|
||||
stateController.dispose();
|
||||
countryController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -98,14 +163,15 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
|
||||
context.read<MyPostCardBloc>().add(CheckLoginStatus());
|
||||
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
|
||||
// context.read<MyPostCardBloc>().add(FetchDraftPostCards());
|
||||
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
|
||||
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
|
||||
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
|
||||
context
|
||||
.read<MyPostCardsCartBloc>()
|
||||
.add(CheckLoginAndFetchPostcardsCart());
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.message)));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(state.message)));
|
||||
} else if (state is CreateAccountFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -130,7 +196,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
),
|
||||
),
|
||||
|
||||
/// 🔹 Scrollable content starts here
|
||||
/// Scrollable content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
@@ -140,9 +206,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
@@ -166,204 +230,183 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "First Name",
|
||||
label: "First Name *",
|
||||
hint: "Enter your first name",
|
||||
controller: firstNameController,
|
||||
onlyLetters: true,
|
||||
noSpace: true,
|
||||
maxLength: 50,
|
||||
keyboardType: TextInputType.name,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Last Name",
|
||||
label: "Last Name *",
|
||||
hint: "Enter your last name",
|
||||
controller: lastNameController,
|
||||
onlyLetters: true,
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
keyboardType: TextInputType.name,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Email",
|
||||
label: "Email *",
|
||||
hint: "Enter your email address",
|
||||
controller: emailController,
|
||||
enabled: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
),
|
||||
|
||||
// ── Phone Number ──────────────────────────────────────
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Phone Number",
|
||||
hint: "Enter your phone number",
|
||||
label: "Phone Number *",
|
||||
hint: "Enter phone number",
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
CustomText(
|
||||
text: "Location Details",
|
||||
text: "Location Details *",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// ── Address ───────────────────────────────────────────
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Address",
|
||||
hint: "Enter address manually or tap to search",
|
||||
label: "Address *",
|
||||
hint: "Enter your address",
|
||||
controller: addressController,
|
||||
maxLength: 50,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 8.h),
|
||||
|
||||
// ── City (unchanged) ──────────────────────────────────
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "City",
|
||||
label: "City *",
|
||||
hint: "Enter your city",
|
||||
maxLength: 50,
|
||||
// noSpace: true,
|
||||
controller: cityController,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
),
|
||||
|
||||
// State Dropdown
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── State – now a plain text field ────────────────────
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Postal Code",
|
||||
hint: "Enter postal / zip code",
|
||||
controller: postalController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
label: "State *",
|
||||
hint: "Enter your state",
|
||||
maxLength: 50,
|
||||
// noSpace: true,
|
||||
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),
|
||||
|
||||
BlocBuilder<CreateAccountBloc, CreateAccountState>(
|
||||
builder: (context, state) {
|
||||
if (state is CreateAccountLoading) {
|
||||
|
||||
@@ -12,307 +12,359 @@ class EsimOfferPage extends StatelessWidget {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: CommonAppBar(isWhiteLogo: false, isProfilePage: false,showDivider: true,),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
child: CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
|
||||
/************************* Top Banner ***********************/
|
||||
Stack(
|
||||
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),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
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(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Simple ",
|
||||
style: TextStyle(fontSize: 26.sp),
|
||||
text: "With your ",
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: "3-Step Process",
|
||||
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: 16.h),
|
||||
CustomText(
|
||||
text: "Get connected in seconds",
|
||||
size: 17.5,
|
||||
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: 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),
|
||||
ProcessCard(
|
||||
"Scan Code",
|
||||
"Open your phone camera and scan the QR code",
|
||||
"2",
|
||||
"assets/icons/process_phone.png",
|
||||
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),
|
||||
ProcessCard(
|
||||
"Connected",
|
||||
"You're online instantly - start exploring!",
|
||||
"3",
|
||||
"assets/icons/process_wifi.png",
|
||||
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(
|
||||
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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,25 +8,25 @@ class CityList {
|
||||
if (json['cities'] != null) {
|
||||
cities = <Cities>[];
|
||||
json['cities'].forEach((v) {
|
||||
cities!.add(new Cities.fromJson(v));
|
||||
cities!.add(Cities.fromJson(v));
|
||||
});
|
||||
}
|
||||
if (json['upcomingCities'] != null) {
|
||||
upcomingCities = <UpcomingCities>[];
|
||||
json['upcomingCities'].forEach((v) {
|
||||
upcomingCities!.add(new UpcomingCities.fromJson(v));
|
||||
upcomingCities!.add(UpcomingCities.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
if (this.cities != null) {
|
||||
data['cities'] = this.cities!.map((v) => v.toJson()).toList();
|
||||
final Map<String, dynamic> data = {};
|
||||
if (cities != null) {
|
||||
data['cities'] = cities!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
if (this.upcomingCities != null) {
|
||||
if (upcomingCities != null) {
|
||||
data['upcomingCities'] =
|
||||
this.upcomingCities!.map((v) => v.toJson()).toList();
|
||||
upcomingCities!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -41,18 +41,27 @@ class Cities {
|
||||
int? cityCardTicketAmt;
|
||||
int? saveAmount;
|
||||
String? saveLabel;
|
||||
|
||||
// ✅ added safely
|
||||
String? cityIconPath;
|
||||
CityIcon? icon;
|
||||
|
||||
// ✅ kept to avoid breaking existing usage
|
||||
List<UpcomingCities>? upcomingCities;
|
||||
|
||||
Cities(
|
||||
{this.id,
|
||||
this.cityName,
|
||||
this.tagLine,
|
||||
this.bannerImage,
|
||||
this.indivisualTicketAmt,
|
||||
this.cityCardTicketAmt,
|
||||
this.saveAmount,
|
||||
this.saveLabel,
|
||||
this.upcomingCities});
|
||||
Cities({
|
||||
this.id,
|
||||
this.cityName,
|
||||
this.tagLine,
|
||||
this.bannerImage,
|
||||
this.indivisualTicketAmt,
|
||||
this.cityCardTicketAmt,
|
||||
this.saveAmount,
|
||||
this.saveLabel,
|
||||
this.cityIconPath,
|
||||
this.icon,
|
||||
this.upcomingCities,
|
||||
});
|
||||
|
||||
Cities.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
@@ -63,32 +72,55 @@ class Cities {
|
||||
cityCardTicketAmt = json['cityCardTicketAmt'];
|
||||
saveAmount = json['saveAmount'];
|
||||
saveLabel = json['saveLabel'];
|
||||
|
||||
cityIconPath = json['cityIconPath'];
|
||||
icon = json['icon'] != null ? CityIcon.fromJson(json['icon']) : null;
|
||||
|
||||
if (json['upcomingCities'] != null) {
|
||||
upcomingCities = <UpcomingCities>[];
|
||||
json['upcomingCities'].forEach((v) {
|
||||
upcomingCities!.add(new UpcomingCities.fromJson(v));
|
||||
upcomingCities!.add(UpcomingCities.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['id'] = this.id;
|
||||
data['cityName'] = this.cityName;
|
||||
data['tagLine'] = this.tagLine;
|
||||
data['bannerImage'] = this.bannerImage;
|
||||
data['indivisualTicketAmt'] = this.indivisualTicketAmt;
|
||||
data['cityCardTicketAmt'] = this.cityCardTicketAmt;
|
||||
data['saveAmount'] = this.saveAmount;
|
||||
data['saveLabel'] = this.saveLabel;
|
||||
if (this.upcomingCities != null) {
|
||||
final Map<String, dynamic> data = {};
|
||||
data['id'] = id;
|
||||
data['cityName'] = cityName;
|
||||
data['tagLine'] = tagLine;
|
||||
data['bannerImage'] = bannerImage;
|
||||
data['indivisualTicketAmt'] = indivisualTicketAmt;
|
||||
data['cityCardTicketAmt'] = cityCardTicketAmt;
|
||||
data['saveAmount'] = saveAmount;
|
||||
data['saveLabel'] = saveLabel;
|
||||
data['cityIconPath'] = cityIconPath;
|
||||
data['icon'] = icon?.toJson();
|
||||
|
||||
if (upcomingCities != null) {
|
||||
data['upcomingCities'] =
|
||||
this.upcomingCities!.map((v) => v.toJson()).toList();
|
||||
upcomingCities!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
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 {
|
||||
int? id;
|
||||
String? cityName;
|
||||
@@ -103,10 +135,10 @@ class UpcomingCities {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['id'] = this.id;
|
||||
data['cityName'] = this.cityName;
|
||||
data['imgPathName'] = this.imgPathName;
|
||||
return data;
|
||||
return {
|
||||
'id': id,
|
||||
'cityName': cityName,
|
||||
'imgPathName': imgPathName,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: false),
|
||||
SizedBox(height: 140.h),
|
||||
SizedBox(height: 120.h),
|
||||
Text(
|
||||
"CityCards.\nSee More,\nSpend Less.",
|
||||
style: TextStyle(
|
||||
@@ -91,32 +91,35 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
fixedSize: const Size(200, 50),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 15.w,
|
||||
vertical: 15.h,
|
||||
),
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
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(
|
||||
height: 50.h,
|
||||
width: 200.w,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 15.w,
|
||||
vertical: 15.h,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Image.asset("assets/icons/arrow.png", height: 13.h),
|
||||
],
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
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(
|
||||
TextSpan(
|
||||
children: [
|
||||
@@ -205,6 +208,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
onTap: () async {
|
||||
await LocalPreference.updateOnboardingPage(2);
|
||||
await LocalPreference.setSelectedCityId(city.id!);
|
||||
await LocalPreference.setSelectedCityLogo(city.cityIconPath??"");
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
RouteConstants.home,
|
||||
@@ -317,8 +321,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
separatorBuilder: (_, __) => SizedBox(width: 16.w),
|
||||
itemBuilder: (context, index) {
|
||||
final city = upcomingCities[index];
|
||||
final imageUrl =
|
||||
'${ApiUrls.baseUrl}${city.imgPathName}';
|
||||
final imageUrl ='${ApiUrls.baseUrl}${city.imgPathName}';
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
||||
@@ -4,11 +4,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_bottom_navbar.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/postcard/views/postcard_initial_page_view.dart';
|
||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../itinerary_creation/views/magic_itinerary_empty_view.dart';
|
||||
import 'registered_user_home_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
|
||||
@@ -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/hotel_offers_section.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 '../../common_bloc/bottom_navigation_bloc.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 '../../localPreference/local_preference.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
@@ -31,7 +34,6 @@ class RegisteredUserHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -39,6 +41,7 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
_checkAndShowCitySelection();
|
||||
_loadProfileIfLoggedIn();
|
||||
}
|
||||
|
||||
Future<void> _loadProfileIfLoggedIn() async {
|
||||
final userId = await LocalPreference.getUserId();
|
||||
|
||||
@@ -63,14 +66,11 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
Future<void> _checkAndShowCitySelection() async {
|
||||
final int cityId = await LocalPreference.getSelectedCityId();
|
||||
|
||||
// If cityId is 1 (default) or invalid, show city selection
|
||||
if (cityId == 0) {
|
||||
// Use addPostFrameCallback to show bottom sheet after build is complete
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_showCitySelectionBottomSheet();
|
||||
});
|
||||
} else {
|
||||
// Load home data only if city is already selected
|
||||
if (mounted) {
|
||||
context.read<HomeBloc>().add(FetchHomeData());
|
||||
}
|
||||
@@ -82,271 +82,323 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
isDismissible: false, // Prevent dismissing without selecting a city
|
||||
enableDrag: false, // Prevent dragging to close
|
||||
isDismissible: false,
|
||||
enableDrag: false,
|
||||
builder: (_) => const CitySelectionBottomSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) {
|
||||
if (state is HomeLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
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) {
|
||||
return Center(
|
||||
child: Column(
|
||||
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,
|
||||
if (state is HomeError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: false,
|
||||
imageUrl: cityIconUrl,
|
||||
isSelectCity: true,
|
||||
),
|
||||
SizedBox(height: 60.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: CustomText(
|
||||
text: state.message,
|
||||
size: 14.sp,
|
||||
color: Color(0xFF656565),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
// City name from API
|
||||
Text(
|
||||
city?.cityName ?? "City Name",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 44,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
SizedBox(height: 32.h),
|
||||
CustomFilledButton(
|
||||
onTap:() {
|
||||
context.read<HomeBloc>().add(FetchHomeData());
|
||||
},
|
||||
label: "Try Again",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// City description from API
|
||||
Text(
|
||||
city?.description ?? "City description",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
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;
|
||||
|
||||
// Category tags - you can customize this based on your needs
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: (city?.cityHighlights ?? [])
|
||||
.where((highlight) => highlight.isActive == true)
|
||||
.map(
|
||||
(highlight) => _buildTag(
|
||||
highlight.title ?? "",
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
SizedBox(height: 60.h),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
return SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background image - use city banner if available
|
||||
_buildBannerImage(bannerImageUrl),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: const [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
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(
|
||||
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(
|
||||
fontSize: 18,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
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
|
||||
AttractionsListView(attractions: attractions),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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(
|
||||
InwardCurvedContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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: 40.h),
|
||||
const ItineraryVideo(),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// Button section
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
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),
|
||||
ChooseYourPassSection(
|
||||
cards: state.homeModel.city?.cards ?? [],
|
||||
ESimOfferSection(),
|
||||
HotelOffersSection(),
|
||||
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());
|
||||
},
|
||||
);
|
||||
}// Initial state
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62),));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFFFFFF).withOpacity(0.29),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -360,23 +412,23 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
child: Image.asset(
|
||||
image,
|
||||
height: 200,
|
||||
height: 220.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
left: 16.w,
|
||||
right: 16.w,
|
||||
bottom: 16.h,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: EdgeInsets.all(12.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -389,9 +441,9 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
fontSize: 18.sp,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -399,20 +451,20 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 8.w),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffFDCDCE),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: EdgeInsets.all(12.r),
|
||||
child: Image.asset(
|
||||
"assets/icons/arrow_angle_up.png",
|
||||
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(
|
||||
imageUrl,
|
||||
height: 300.h,
|
||||
Widget _buildBannerImage(String? imageUrl) {
|
||||
return SizedBox(
|
||||
height: 350.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
height: 300.h,
|
||||
width: double.infinity,
|
||||
child: (imageUrl == null || imageUrl.isEmpty)
|
||||
? Image.asset(
|
||||
"assets/images/chicago.png",
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
|
||||
// 🔄 Loader (same as your loadingBuilder)
|
||||
placeholder: (context, url) => Container(
|
||||
color: Colors.grey[300],
|
||||
child: Center(
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// Use placeholder on error
|
||||
return Image.asset(
|
||||
),
|
||||
|
||||
// ❌ Error fallback (same as errorBuilder)
|
||||
errorWidget: (context, url, error) => Image.asset(
|
||||
"assets/images/chicago.png",
|
||||
height: 300.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../../attraction_details/views/attraction_details_view.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../model/home_model.dart';
|
||||
|
||||
class AttractionsListView extends StatefulWidget {
|
||||
@@ -39,14 +41,12 @@ class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
});
|
||||
}
|
||||
|
||||
// Get cover image from attraction galleries
|
||||
String? _getCoverImage(Attraction attraction) {
|
||||
if (attraction.attractionGalleries == null ||
|
||||
attraction.attractionGalleries!.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find the cover image
|
||||
final coverImage = attraction.attractionGalleries!.firstWhere(
|
||||
(gallery) => gallery.isCoverImage == true,
|
||||
orElse: () => attraction.attractionGalleries!.first,
|
||||
@@ -57,14 +57,13 @@ class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Show placeholder if no attractions
|
||||
if (widget.attractions.isEmpty) {
|
||||
return const Center(
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
padding: EdgeInsets.all(20.w),
|
||||
child: Text(
|
||||
'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(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 240,
|
||||
height: 240.h,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
padding: EdgeInsets.only(right: 16.w),
|
||||
itemCount: widget.attractions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final attraction = widget.attractions[index];
|
||||
@@ -88,68 +87,65 @@ class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AttractionDetailsView(attractionId: attraction.id),
|
||||
builder: (context) =>
|
||||
AttractionDetailsView(attractionId: attraction.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.all(4),
|
||||
margin: EdgeInsets.only(right: 16.w),
|
||||
padding: EdgeInsets.all(4.r),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF95F62).withOpacity(0.24),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Container(
|
||||
height: 232,
|
||||
width: 161,
|
||||
height: 232.h,
|
||||
width: 161.w,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Image or placeholder
|
||||
// Image
|
||||
if (imageUrl != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
height: 232,
|
||||
width: 161,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
height: 232.h,
|
||||
width: 161.w,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _buildPlaceholder();
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
memCacheWidth: 400,
|
||||
memCacheHeight: 600,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
),
|
||||
errorWidget: (context, url, error) => _buildPlaceholder(),
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildPlaceholder(),
|
||||
|
||||
// Title overlay
|
||||
// Title + Description Overlay
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16.r),
|
||||
bottomRight: Radius.circular(16.r),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
@@ -160,15 +156,34 @@ class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
attraction.title ?? 'Untitled',
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
attraction.title ?? 'Untitled',
|
||||
textAlign: TextAlign.left,
|
||||
style: GoogleFonts.poppins(
|
||||
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(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
width: 200.w,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
child: LinearProgressIndicator(
|
||||
value: _scrollProgress,
|
||||
minHeight: 6,
|
||||
minHeight: 6.h,
|
||||
backgroundColor: const Color(0xffFEE7E7),
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
@@ -203,16 +218,16 @@ class _AttractionsListViewState extends State<AttractionsListView> {
|
||||
Widget _buildPlaceholder() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 50,
|
||||
size: 50.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -38,15 +39,22 @@ class ExploreCitiesCard extends StatelessWidget {
|
||||
children: [
|
||||
/// Background Image with fallback
|
||||
_isNetworkImage
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(
|
||||
'assets/images/city_sydney.png',
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
placeholder: (context, url) => Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Image.asset(
|
||||
'assets/images/city_sydney.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Image.asset(
|
||||
'assets/images/city_sydney.png',
|
||||
|
||||
@@ -25,7 +25,7 @@ class GetYourPassCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Get your Pass",
|
||||
"Get Your Card",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -66,7 +66,7 @@ class GetYourPassCard extends StatelessWidget {
|
||||
Text(
|
||||
"Attractions",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400
|
||||
),
|
||||
@@ -79,7 +79,7 @@ class GetYourPassCard extends StatelessWidget {
|
||||
Text(
|
||||
"From",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
fontSize: 11.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
@@ -89,7 +89,7 @@ class GetYourPassCard extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: "\$20",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
@@ -97,7 +97,7 @@ class GetYourPassCard extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: " /Adult",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
fontSize: 12 .sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,44 +1,17 @@
|
||||
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});
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: _controller.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
child: Lottie.asset(
|
||||
'assets/intro/itinerary_animation.json', // 👈 your path
|
||||
repeat: true,
|
||||
animate: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -50,26 +52,26 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Choose your Pass",
|
||||
"Choose Your Card",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
"Dive into an extensive selection of thrilling destinations, "
|
||||
"thoughtfully categorized to help you find the perfect getaway.",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontSize: 13.sp,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// ===== PAGEVIEW =====
|
||||
SizedBox(
|
||||
height: 430,
|
||||
height: 430.h,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: widget.cards.length,
|
||||
@@ -79,7 +81,7 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// ===== INDICATOR =====
|
||||
Center(
|
||||
@@ -89,11 +91,11 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
|
||||
bool isActive = index == _currentPage;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 40 : 20,
|
||||
height: 6,
|
||||
margin: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
width: isActive ? 40.w : 20.w,
|
||||
height: 6.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
color: isActive
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffFEE7E7),
|
||||
@@ -111,108 +113,113 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
|
||||
final Color primaryColor =
|
||||
index.isEven ? const Color(0xffF95FAF) : const Color(0xffF95F62);
|
||||
|
||||
final Color bgColor =
|
||||
index.isEven ? const Color(0xFFFDE7F1) : const Color(0xFFFFE8E8);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
border: Border.all(color: primaryColor.withOpacity(0.6)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// TITLE FROM API
|
||||
Text(
|
||||
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,
|
||||
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.h),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
child: Stack(
|
||||
children: [
|
||||
// ===== BACKGROUND IMAGE =====
|
||||
Positioned.fill(
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
|
||||
child: Image.asset(
|
||||
'assets/images/card_bg.png', // 👈 Replace with your image path
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// ===== 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../../itinerary_creation/bloc/get_itinerary_bloc.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 {
|
||||
const CitySelectionBottomSheet({super.key});
|
||||
@@ -65,15 +72,15 @@ class _CitySelectionView extends StatelessWidget {
|
||||
if (cityId == 0) {
|
||||
return SizedBox(width: 60.w); // Empty space to maintain layout
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.arrow_back, size: 18),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(text: "Back", size: 12.sp),
|
||||
],
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_back, size: 18),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(text: "Back", size: 12.sp),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -272,6 +279,10 @@ class _CitySelectionView extends StatelessWidget {
|
||||
await LocalPreference.setSelectedCityLogo(svgIcon!);
|
||||
Navigator.pop(context);
|
||||
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");
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
|
||||