22 Commits

Author SHA1 Message Date
Raj.Ghag
496287716a bugs solved and more fixes 2026-04-27 13:35:17 +05:30
Raj.Ghag
d0ecd48407 added pinput package for better otp enter experience 2026-04-27 12:35:46 +05:30
Raj.Ghag
092fa1215f chnages and bug fixes 2026-04-24 10:35:13 +05:30
Raj.Ghag
3ca76d0c26 send otp snackbar updated 2026-04-20 15:26:13 +05:30
Raj.Ghag
54f9a4b2ad sprint three chnagees done 2026-04-15 19:06:30 +05:30
Raj.Ghag
b37bb3bf2b apply filte delay solved and refeesh token logic updated 2026-04-01 11:50:00 +05:30
b78c83cc4a translator removed 2026-03-25 18:03:50 +05:30
c4e28decb9 horizonal rotate disable and progressbar added in create intinerary and more fixes 2026-03-25 18:00:42 +05:30
6038d450e4 horizonal rotate disable and progressbar added in create intinerary and more fixes 2026-03-25 17:59:22 +05:30
c06c844210 bug fixes done 2026-03-24 13:21:37 +05:30
9d27389bf2 bug fixes done 2026-03-23 19:25:32 +05:30
d1038e846e bug fixes done 2026-03-20 18:08:52 +05:30
177f891a31 added check in and Qr scaner and bookng with api and more fixes and changes 2026-03-17 17:07:14 +05:30
adc737a6af magic itinerary api intigration and more fixes 2026-03-13 11:11:53 +05:30
265bddc784 added create magic itinerary with api and more and bug fixes 2026-03-05 19:02:22 +05:30
60486e737a bug fixes 2026-02-26 15:54:57 +05:30
77aba2f1a0 added fixes of city catds word 2026-02-26 11:29:06 +05:30
06e60cfd57 my cart with postcads added and more fixes. 2026-02-26 10:20:34 +05:30
f59b14bec7 bug fixes and ui updates and my passses cart updated. 2026-02-20 18:50:28 +05:30
cbe03f21b4 pull taken from shreeyash 2026-02-17 15:24:10 +05:30
a80a0ac790 bug fixes and more 2026-02-17 15:18:45 +05:30
80b724d6d4 bug fixes and more 2026-02-16 19:13:08 +05:30
282 changed files with 35695 additions and 10201 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/downlaod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/icons/love_them.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/maybe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
assets/icons/no_kids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/icons/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/card_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
# flutter pub run flutter_launcher_icons # flutter pub run flutter_launcher_icons
flutter_launcher_icons: flutter_launcher_icons:
image_path: "assets/logo/logo_city_cards.png" image_path: "assets/icons/citycards_customer_logo.jpg"
android: "launcher_icon" android: "launcher_icon"
# image_path_android: "assets/icon/icon.png" # image_path_android: "assets/icon/icon.png"

View File

@@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,4 +1,6 @@
PODS: PODS:
- app_links (7.0.0):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_angle (0.3.8): - flutter_angle (0.3.8):
- Flutter - Flutter
@@ -7,6 +9,8 @@ PODS:
- flutter_native_splash (2.4.3): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- FlutterAngle (0.0.8) - FlutterAngle (0.0.8)
- geocoding_ios (1.0.5):
- Flutter
- geolocator_apple (1.2.0): - geolocator_apple (1.2.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -21,31 +25,102 @@ PODS:
- GoogleMaps/Maps (9.4.0) - GoogleMaps/Maps (9.4.0)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- Stripe (25.0.1):
- StripeApplePay (= 25.0.1)
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripePaymentsUI (= 25.0.1)
- StripeUICore (= 25.0.1)
- stripe_ios (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- stripe_ios/stripe_ios (= 0.0.1)
- stripe_ios/stripe_objc (= 0.0.1)
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- stripe_ios/stripe_ios (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- stripe_ios/stripe_objc
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- stripe_ios/stripe_objc (0.0.1):
- Flutter
- Stripe (~> 25.0.1)
- StripeApplePay (~> 25.0.1)
- StripeFinancialConnections (~> 25.0.1)
- StripePayments (~> 25.0.1)
- StripePaymentSheet (~> 25.0.1)
- StripePaymentsUI (~> 25.0.1)
- StripeApplePay (25.0.1):
- StripeCore (= 25.0.1)
- StripeCore (25.0.1)
- StripeFinancialConnections (25.0.1):
- StripeCore (= 25.0.1)
- StripeUICore (= 25.0.1)
- StripePayments (25.0.1):
- StripeCore (= 25.0.1)
- StripePayments/Stripe3DS2 (= 25.0.1)
- StripePayments/Stripe3DS2 (25.0.1):
- StripeCore (= 25.0.1)
- StripePaymentSheet (25.0.1):
- StripeApplePay (= 25.0.1)
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripePaymentsUI (= 25.0.1)
- StripePaymentsUI (25.0.1):
- StripeCore (= 25.0.1)
- StripePayments (= 25.0.1)
- StripeUICore (= 25.0.1)
- StripeUICore (25.0.1):
- StripeCore (= 25.0.1)
- three_js_sensors (0.1.2): - three_js_sensors (0.1.2):
- Flutter - Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`) - flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- geocoding_ios (from `.symlinks/plugins/geocoding_ios/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)
- three_js_sensors (from `.symlinks/plugins/three_js_sensors/ios`) - three_js_sensors (from `.symlinks/plugins/three_js_sensors/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
SPEC REPOS: SPEC REPOS:
@@ -53,46 +128,83 @@ SPEC REPOS:
- FlutterAngle - FlutterAngle
- Google-Maps-iOS-Utils - Google-Maps-iOS-Utils
- GoogleMaps - GoogleMaps
- Stripe
- StripeApplePay
- StripeCore
- StripeFinancialConnections
- StripePayments
- StripePaymentSheet
- StripePaymentsUI
- StripeUICore
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_angle: flutter_angle:
:path: ".symlinks/plugins/flutter_angle/darwin" :path: ".symlinks/plugins/flutter_angle/darwin"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
geocoding_ios:
:path: ".symlinks/plugins/geocoding_ios/ios"
geolocator_apple: geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin" :path: ".symlinks/plugins/geolocator_apple/darwin"
google_maps_flutter_ios: google_maps_flutter_ios:
:path: ".symlinks/plugins/google_maps_flutter_ios/ios" :path: ".symlinks/plugins/google_maps_flutter_ios/ios"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
stripe_ios:
:path: ".symlinks/plugins/stripe_ios/ios"
three_js_sensors: three_js_sensors:
:path: ".symlinks/plugins/three_js_sensors/ios" :path: ".symlinks/plugins/three_js_sensors/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_angle: 7b1a2b3e733221bf2e0325e42fc3edf95b5d44c4 flutter_angle: fc44e198cea1f07e1a5919bad1484049fab65c96
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18 FlutterAngle: c810891af800750361b1d0e7cc944f2338d5ae18
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e geocoding_ios: eafacae6ad11a1eb56681f7d11df602a5fd49416
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96 Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264 google_maps_flutter_ios: e31555a04d1986ab130f2b9f24b6cdc861acc6d3
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
three_js_sensors: f516b092803411e05b1e3dc7625efa36acd8f455 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
Stripe: 4728e3e0dd8df134e4a420ab504e929a93a815f0
stripe_ios: c552a249333c2e810e02539140dba366c7f0683f
StripeApplePay: 43997281ace138a1c75a8f2d7be11925ea28644c
StripeCore: 457c30e2fd3a7c4b274a5ad53d1ff03661eef2a0
StripeFinancialConnections: 8c2e326f767fb014b53174b3a5f8592c0a45fa56
StripePayments: 6955de4298a5265e66f02cffcc7954475ac7f6c8
StripePaymentSheet: 3f93ce6ea84afde770d3c7e18a9b8f99aed63896
StripePaymentsUI: 626726a01255a6458c35436f7f6431dacee82684
StripeUICore: 30f8352fd7a5cf1541b7777a57b3ad1133bf6763
three_js_sensors: ab5f24fbeb97ab5c5ce2978c3e63a25d67a076f5
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86
PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506 PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506

View File

@@ -7,15 +7,15 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */; }; 94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75864C28F633B337B6CD7995 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -46,13 +46,14 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
369614DBDD277BF9018C34BC /* 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>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 62ED1D923084D6092BECB5AC /* 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>"; };
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>"; }; 6997591091A0E8DA4E4776AA /* 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>"; };
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6BD7534B4533D500F969D46C /* 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>"; };
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
75864C28F633B337B6CD7995 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -61,10 +62,9 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; AB77C0F975F5B780954288AA /* 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>"; };
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>"; }; AE2DC54B7F4682B91B6259C6 /* 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>"; }; BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -72,7 +72,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */, B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -80,7 +80,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */, 94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -95,24 +95,15 @@
path = RunnerTests; path = RunnerTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5D45FB84C63476582408C414 /* Frameworks */ = {
isa = PBXGroup;
children = (
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */,
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
6D4A73F1E55857ADBD000C6A /* Pods */ = { 6D4A73F1E55857ADBD000C6A /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */, 369614DBDD277BF9018C34BC /* Pods-Runner.debug.xcconfig */,
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */, 6BD7534B4533D500F969D46C /* Pods-Runner.release.xcconfig */,
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */, 6997591091A0E8DA4E4776AA /* Pods-Runner.profile.xcconfig */,
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */, 62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */,
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */, AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */,
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */, AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -136,7 +127,7 @@
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
6D4A73F1E55857ADBD000C6A /* Pods */, 6D4A73F1E55857ADBD000C6A /* Pods */,
5D45FB84C63476582408C414 /* Frameworks */, F3A521C4EE6E75D0D8A88556 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -164,6 +155,15 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
F3A521C4EE6E75D0D8A88556 /* Frameworks */ = {
isa = PBXGroup;
children = (
75864C28F633B337B6CD7995 /* Pods_Runner.framework */,
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -171,7 +171,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */, 42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
CF8A29BE993C0C902CB143AF /* Frameworks */, CF8A29BE993C0C902CB143AF /* Frameworks */,
@@ -190,15 +190,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */, 46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */, E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */,
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */, 5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -270,28 +270,6 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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 */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -308,39 +286,7 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */ = { 42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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 */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -362,7 +308,29 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */ = { 46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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;
};
5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -379,6 +347,38 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0; 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";
};
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -515,7 +515,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -533,7 +533,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -549,7 +549,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;

View File

@@ -2,12 +2,15 @@ import Flutter
import UIKit import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,59 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict>
<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>UIApplicationSceneManifest</key>
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>UIApplicationSupportsMultipleScenes</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/> <false/>
<key>UISupportedInterfaceOrientations</key> <key>UISceneConfigurations</key>
<array> <dict>
<string>UIInterfaceOrientationPortrait</string> <key>UIWindowSceneSessionRoleApplication</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <array>
<string>UIInterfaceOrientationLandscapeRight</string> <dict>
</array> <key>UISceneClassName</key>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIWindowScene</string>
<array> <key>UISceneConfigurationName</key>
<string>UIInterfaceOrientationPortrait</string> <string>flutter</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <key>UISceneDelegateClassName</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>FlutterSceneDelegate</string>
<string>UIInterfaceOrientationLandscapeRight</string> <key>UISceneStoryboardFile</key>
</array> <string>Main</string>
</dict>
</array>
</dict>
</dict> </dict>
<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>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist> </plist>

5
l10n.yaml Normal file
View File

@@ -0,0 +1,5 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,39 @@ class AttractionDetailsBloc
required this.repository, required this.repository,
}) : super(AttractionDetailsInitial()) { }) : super(AttractionDetailsInitial()) {
on<FetchAttractionDetails>(_onFetchAttractionDetails); on<FetchAttractionDetails>(_onFetchAttractionDetails);
on<ToggleDescriptionExpanded>(_onToggleDescriptionExpanded);
on<UpdateGalleryIndex>(_onUpdateGalleryIndex);
on<UpdateFullScreenGalleryIndex>(_onUpdateFullScreenGalleryIndex);
}
void _onToggleDescriptionExpanded(
ToggleDescriptionExpanded event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(isExpanded: !currentState.isExpanded));
}
}
void _onUpdateGalleryIndex(
UpdateGalleryIndex event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(galleryIndex: event.index));
}
}
void _onUpdateFullScreenGalleryIndex(
UpdateFullScreenGalleryIndex event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(fullScreenGalleryIndex: event.index));
}
} }
Future<void> _onFetchAttractionDetails( Future<void> _onFetchAttractionDetails(

View File

@@ -17,3 +17,19 @@ class FetchAttractionDetails extends AttractionDetailsEvent {
@override @override
List<Object?> get props => [attractionId]; List<Object?> get props => [attractionId];
} }
class ToggleDescriptionExpanded extends AttractionDetailsEvent {}
class UpdateGalleryIndex extends AttractionDetailsEvent {
final int index;
const UpdateGalleryIndex({required this.index});
@override
List<Object?> get props => [index];
}
class UpdateFullScreenGalleryIndex extends AttractionDetailsEvent {
final int index;
const UpdateFullScreenGalleryIndex({required this.index});
@override
List<Object?> get props => [index];
}

View File

@@ -15,13 +15,33 @@ class AttractionDetailsLoading extends AttractionDetailsState {}
class AttractionDetailsLoaded extends AttractionDetailsState { class AttractionDetailsLoaded extends AttractionDetailsState {
final AttractionDetailsModel attractionDetails; final AttractionDetailsModel attractionDetails;
final bool isExpanded;
final int galleryIndex;
final int fullScreenGalleryIndex;
const AttractionDetailsLoaded({ const AttractionDetailsLoaded({
required this.attractionDetails, required this.attractionDetails,
this.isExpanded = false,
this.galleryIndex = 0,
this.fullScreenGalleryIndex = 0,
}); });
AttractionDetailsLoaded copyWith({
AttractionDetailsModel? attractionDetails,
bool? isExpanded,
int? galleryIndex,
int? fullScreenGalleryIndex,
}) {
return AttractionDetailsLoaded(
attractionDetails: attractionDetails ?? this.attractionDetails,
isExpanded: isExpanded ?? this.isExpanded,
galleryIndex: galleryIndex ?? this.galleryIndex,
fullScreenGalleryIndex: fullScreenGalleryIndex ?? this.fullScreenGalleryIndex,
);
}
@override @override
List<Object?> get props => [attractionDetails]; List<Object?> get props => [attractionDetails, isExpanded, galleryIndex, fullScreenGalleryIndex];
} }
class AttractionDetailsError extends AttractionDetailsState { class AttractionDetailsError extends AttractionDetailsState {

View File

@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart'; import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart';
import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_text.dart';
@@ -6,7 +9,10 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import '../../l10n/app_localizations.dart';
import '../../common_packages/shimmer_animation.dart';
import '../../core/route_constants.dart'; import '../../core/route_constants.dart';
import '../bloc/attraction_details_bloc.dart'; import '../bloc/attraction_details_bloc.dart';
import '../bloc/attraction_details_event.dart'; import '../bloc/attraction_details_event.dart';
@@ -33,7 +39,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Center( body: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(color: Color(0xffF95F62)),
), ),
); );
} }
@@ -66,104 +72,156 @@ class AttractionDetailsView extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// ── White app bar above the image ───────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: false,
),
),
SizedBox(
height: 20.h,
),
Stack( Stack(
children: [ children: [
Image.network( // ── Hero image ──────────────────────────────────────
coverImage, CachedNetworkImage(
height: 377.h, imageUrl: coverImage,
height: 280.h,
width: double.infinity, width: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { placeholder: (context, url) => SkeletonWidget(
return Image.asset( width: double.infinity,
'assets/images/koh_rong_samloem_banner.png', height: 280.h,
height: 377.h, borderRadius: 0,
width: double.infinity, ),
fit: BoxFit.cover, errorWidget: (context, url, error) => Image.asset(
); 'assets/images/koh_rong_samloem_banner.png',
}, height: 280.h,
width: double.infinity,
fit: BoxFit.cover,
),
), ),
// ── Bottom fade gradient ─────────────────────────────
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 180.h,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Color(0xCC000000), // ~80% black at bottom
Colors.transparent,
],
),
),
),
),
// ── Top: pill-style back button (no AppBar) ──────────
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: SafeArea( child: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // 🔽 reduced
horizontal: 20.w, vertical: 10.h), child: Align(
child: Column( alignment: Alignment.centerLeft,
crossAxisAlignment: CrossAxisAlignment.start, child: GestureDetector(
children: [ onTap: () => Navigator.pop(context),
CommonAppBar( child: Container(
isWhiteLogo: true, padding: EdgeInsets.symmetric(
isProfilePage: false, horizontal: 12.w, // 🔽 smaller
showDivider: true, vertical: 8.h, // 🔽 smaller
), ),
SizedBox(height: 10.h), decoration: BoxDecoration(
Row( color: Colors.white,
children: [ borderRadius: BorderRadius.circular(24.r), // 🔽 slightly smaller
GestureDetector( boxShadow: [
onTap: () => Navigator.pop(context), BoxShadow(
child: Icon( color: Colors.black.withOpacity(0.10), // 🔽 lighter shadow
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.arrow_back, Icons.arrow_back,
size: 24.sp, size: 16.sp, // 🔽 smaller icon
color: Colors.white, color: Colors.black,
), ),
), SizedBox(width: 6.w), // 🔽 smaller spacing
SizedBox(width: 8.w), Text(
Expanded( 'Back to attractions',
child: Text(
attraction.title,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 13.sp, // 🔽 slightly smaller
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600, // ✅ bold
color: Colors.white, color: Colors.black,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
), ],
], ),
), ),
], ),
), ),
), ),
), ),
), ),
// ── Bottom-left: attraction title (smaller, over fade) ─
Positioned( Positioned(
bottom: 31.h, bottom: 48.h,
left: 12.w, left: 14.w,
right: 60.w, // Add this - leaves space for share button right: 60.w,
child: Text( child: Text(
attraction.title, attraction.title,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 44.sp, fontSize: 28.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w600,
height: 1.2, height: 1.25,
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
// ── Bottom-right: share button ───────────────────────
Positioned( Positioned(
bottom: 31.h, bottom: 48.h,
right: 17.w, right: 14.w,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
showModalBottomSheet( Share.share(
context: context, 'www.google.com',
isScrollControlled: true, subject: AppLocalizations.of(context)!.checkThisOut,
backgroundColor: Colors.transparent,
builder: (context) =>
const ShareBottomSheet(),
); );
}, },
child: Container( child: Container(
height: 36.h, height: 42.h,
width: 36.w, width: 42.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20.r), borderRadius: BorderRadius.circular(21.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
), ),
child: Center( child: Center(
child: Icon( child: Icon(
@@ -178,29 +236,106 @@ class AttractionDetailsView extends StatelessWidget {
], ],
), ),
// ── Gallery Section (All Images) ──────────────────────────
if (attraction.attractionGalleries.length > 1)
Padding(
padding: EdgeInsets.only(top: 20.h, left: 16.w, right: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gallery',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 12.h),
_GalleryStrip(
galleries: attraction.attractionGalleries,
currentIndex: state.galleryIndex,
onTap: (index) => showFullScreenGallery(
context,
attraction.attractionGalleries
.map((g) => g.filePathUrl)
.toList(),
index,
),
),
],
),
),
// About Section // About Section
Padding( Padding(
padding: padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"About", AppLocalizations.of(context)!.aboutTitle,
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox(height: 12.32.h), SizedBox(height: 12.32.h),
Text(
attraction.description, LayoutBuilder(
style: TextStyle( builder: (context, constraints) {
color: Color(0xFF262626), final textSpan = TextSpan(
fontWeight: FontWeight.w400, text: attraction.description,
fontSize: 14.sp, style: TextStyle(
height: 1.5, color: Color(0xFF262626),
), fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
);
final textPainter = TextPainter(
text: textSpan,
maxLines: 3,
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: constraints.maxWidth);
final isTextOverflowing = textPainter.didExceedMaxLines;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attraction.description,
maxLines: state.isExpanded ? null : (isTextOverflowing ? 3 : null),
overflow: state.isExpanded ? TextOverflow.visible : (isTextOverflowing ? TextOverflow.ellipsis : TextOverflow.visible),
style: TextStyle(
color: Color(0xFF262626),
fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
),
if (isTextOverflowing) ...[
SizedBox(height: 6.h),
GestureDetector(
onTap: () {
context.read<AttractionDetailsBloc>().add(ToggleDescriptionExpanded());
},
child: Text(
state.isExpanded ? "See less" : "See more",
style: TextStyle(
color: Color(0xFFF95F62), // your theme color
fontSize: 13.sp,
fontWeight: FontWeight.w500,
),
),
),
],
],
);
},
), ),
], ],
), ),
@@ -370,10 +505,10 @@ class AttractionDetailsView extends StatelessWidget {
Divider(color: Colors.black.withOpacity(0.2)), Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h), SizedBox(height: 30.h),
Text( Text(
"What is included", AppLocalizations.of(context)!.whatIsIncluded,
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 18.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
@@ -395,17 +530,17 @@ class AttractionDetailsView extends StatelessWidget {
), ),
SizedBox(height: 30.h), SizedBox(height: 30.h),
// Divider(color: Colors.black.withOpacity(0.2)), // Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h), // SizedBox(height: 30.h),
Text( Text(
"Exact Location", AppLocalizations.of(context)!.exactLocation,
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
CustomText( CustomText(
text: "View the location on map", text: AppLocalizations.of(context)!.viewOnMap,
size: 12.sp, size: 12.sp,
color: Colors.black.withOpacity(.6), color: Colors.black.withOpacity(.6),
), ),
@@ -465,28 +600,29 @@ class AttractionDetailsView extends StatelessWidget {
color: Colors.black.withOpacity(0.6), color: Colors.black.withOpacity(0.6),
), ),
SizedBox(height: 30.h), SizedBox(height: 30.h),
Divider(color: Colors.black.withOpacity(0.2)), if (attraction.attractionFaqs.isNotEmpty) ...[
SizedBox(height: 30.h), Divider(color: Colors.black.withOpacity(0.2)),
Text( SizedBox(height: 30.h),
"People frequently ask", Text(
style: TextStyle( AppLocalizations.of(context)!.peopleFrequentlyAsk,
fontSize: 18.sp, style: TextStyle(
fontWeight: FontWeight.w400, fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
), ),
), SizedBox(height: 15.h),
SizedBox(height: 15.h), Column(
Column( children: attraction.attractionFaqs.map((faq) {
children: attraction.attractionFaqs.map((faq) { return Padding(
return Padding( padding: EdgeInsets.only(bottom: 15.h),
padding: EdgeInsets.only(bottom: 15.h), child: faqBox(
child: faqBox( title: faq.faqQuestion,
title: faq.faqQuestion, desc: faq.faqAnswer,
desc: faq.faqAnswer, ),
), );
); }).toList(),
}).toList(), ),
), ],
], ],
), ),
), ),
@@ -501,7 +637,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Center( body: Center(
child: Text("Something went wrong"), child: Text(AppLocalizations.of(context)!.somethingWentWrong),
), ),
); );
}, },
@@ -592,4 +728,276 @@ class AttractionDetailsView extends StatelessWidget {
), ),
); );
} }
// ── Full Screen Swipeable Gallery Viewer ────────────────────────────
void showFullScreenGallery(BuildContext context, List<String> imageUrls, int initialIndex) {
final bloc = context.read<AttractionDetailsBloc>();
bloc.add(UpdateFullScreenGalleryIndex(index: initialIndex));
showDialog(
context: context,
barrierColor: Colors.black,
builder: (ctx) => BlocProvider.value(
value: bloc,
child: _FullScreenGallery(
imageUrls: imageUrls,
initialIndex: initialIndex,
),
),
);
}
}
// ── Auto-scroll Gallery Strip with Dot Indicators ───────────────────
class _GalleryStrip extends StatefulWidget {
final List galleries;
final int currentIndex;
final void Function(int index) onTap;
const _GalleryStrip({required this.galleries, required this.currentIndex, required this.onTap});
@override
State<_GalleryStrip> createState() => _GalleryStripState();
}
class _GalleryStripState extends State<_GalleryStrip> {
late final PageController _pageController;
Timer? _timer;
late int _currentPage;
@override
void initState() {
super.initState();
// Start at a high number divisible by length to allow infinite scrolling in both directions
int initialPage = widget.galleries.isNotEmpty ? widget.galleries.length * 1000 : 0;
_currentPage = initialPage;
_pageController = PageController(
viewportFraction: 0.38, // shows partial next/prev image
initialPage: initialPage,
);
_startAutoScroll();
}
void _startAutoScroll() {
_timer = Timer.periodic(const Duration(seconds: 3), (_) {
if (!mounted) return;
_pageController.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
});
}
@override
void dispose() {
_timer?.cancel();
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.galleries.isEmpty) return const SizedBox.shrink();
return Column(
children: [
SizedBox(
height: 120.h,
child: PageView.builder(
controller: _pageController,
// No itemCount to allow infinite scrolling
onPageChanged: (i) {
_currentPage = i;
context.read<AttractionDetailsBloc>().add(UpdateGalleryIndex(index: i % widget.galleries.length));
},
itemBuilder: (context, index) {
final realIndex = index % widget.galleries.length;
final gallery = widget.galleries[realIndex];
return Padding(
padding: EdgeInsets.only(right: 12.w),
child: GestureDetector(
onTap: () => widget.onTap(realIndex),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: CachedNetworkImage(
imageUrl: gallery.filePathUrl,
width: 120.w,
height: 120.h,
fit: BoxFit.cover,
placeholder: (context, url) => SkeletonWidget(
width: 120.w,
height: 120.h,
borderRadius: 12.r,
),
errorWidget: (context, url, error) => Container(
width: 120.w,
height: 120.h,
color: Colors.grey[300],
child: const Icon(Icons.broken_image, color: Colors.grey),
),
),
),
),
);
},
),
),
SizedBox(height: 10.h),
// Dot indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.galleries.length, (index) {
final isActive = index == widget.currentIndex;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: EdgeInsets.symmetric(horizontal: 3.w),
width: isActive ? 18.w : 6.w,
height: 6.h,
decoration: BoxDecoration(
color: isActive
? const Color(0xFFF95F62)
: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4.r),
),
);
}),
),
],
);
}
}
// ── Full Screen Swipeable Gallery ───────────────────────────────────
class _FullScreenGallery extends StatefulWidget {
final List<String> imageUrls;
final int initialIndex;
const _FullScreenGallery({
required this.imageUrls,
required this.initialIndex,
});
@override
State<_FullScreenGallery> createState() => _FullScreenGalleryState();
}
class _FullScreenGalleryState extends State<_FullScreenGallery> {
late final PageController _controller;
@override
void initState() {
super.initState();
_controller = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AttractionDetailsBloc, AttractionDetailsState>(
builder: (context, state) {
int currentIndex = widget.initialIndex;
if (state is AttractionDetailsLoaded) {
currentIndex = state.fullScreenGalleryIndex;
}
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Swipeable images
PageView.builder(
controller: _controller,
itemCount: widget.imageUrls.length,
onPageChanged: (i) => context.read<AttractionDetailsBloc>().add(UpdateFullScreenGalleryIndex(index: i)),
itemBuilder: (context, index) {
return InteractiveViewer(
child: Center(
child: CachedNetworkImage(
imageUrl: widget.imageUrls[index],
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
errorWidget: (context, url, error) => const Icon(
Icons.broken_image,
color: Colors.white,
size: 50,
),
),
),
);
},
),
// Close button + counter
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.25),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(Icons.close, color: Colors.white, size: 24.sp),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
'${currentIndex + 1} / ${widget.imageUrls.length}',
style: TextStyle(color: Colors.white, fontSize: 14.sp),
),
),
],
),
),
),
),
// Bottom dot indicators
Positioned(
bottom: 30.h,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.imageUrls.length, (index) {
final isActive = index == currentIndex;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: EdgeInsets.symmetric(horizontal: 3.w),
width: isActive ? 18.w : 6.w,
height: 6.h,
decoration: BoxDecoration(
color: isActive ? const Color(0xFFF95F62) : Colors.white54,
borderRadius: BorderRadius.circular(4.r),
),
);
}),
),
),
],
),
);
},
);
}
} }

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import 'package:citycards_customer/common_packages/back_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../l10n/app_localizations.dart';
import '../../common_packages/custom_search_field.dart'; import '../../common_packages/custom_search_field.dart';
import '../blocs/attractions_bloc.dart'; import '../blocs/attractions_bloc.dart';
@@ -37,117 +38,125 @@ class AttractionsPage extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: RefreshIndicator(
padding: const EdgeInsets.all(16), color: Color(0xffF95F62),
child: Column( onRefresh: () async {
crossAxisAlignment: CrossAxisAlignment.start, bloc.add(
children: [ const FetchAttractionsByCategory(),
// App bar );
CommonAppBar( },
isWhiteLogo: false, child: SingleChildScrollView(
isProfilePage: false, padding: const EdgeInsets.all(16),
showDivider: true, physics: const AlwaysScrollableScrollPhysics(),
), child: Column(
backWidget(context, "Your Attraction", Colors.black), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 20), children: [
// App bar
// 🔍 Search field (UI kept, logic disabled) CommonAppBar(
CommonSearchField( isWhiteLogo: false,
hint: "Search attractions...", isProfilePage: false,
hintColor: Colors.grey.shade500, showDivider: true,
onChanged: (value) {
// ❌ Search logic intentionally disabled
// UI only, no API call
},
),
const SizedBox(height: 16),
// 🏖️ Category chips row - DYNAMIC
if (state is AttractionsLoaded)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: state.categories
.map(
(category) => buildCategoryChip(
category.categoryName ?? '',
isSelected: state.selectedCategoryId == category.id,
onTap: () {
bloc.add(
FetchAttractionsByCategory(
categoryXid: category.id,
),
);
},
),
)
.toList(),
),
), ),
// else backWidget(context, AppLocalizations.of(context)!.yourAttractionTitle, Colors.black),
// // Show placeholder chips while loading const SizedBox(height: 20),
// SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(
// children: [
// buildCategoryChip("Beach", isSelected: true, onTap: () {}),
// buildCategoryChip("Hike", isSelected: false, onTap: () {}),
// buildCategoryChip("Adventure", isSelected: false, onTap: () {}),
// buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}),
// ],
// ),
// ),
const SizedBox(height: 10), // 🔍 Search field (UI kept, logic disabled)
CommonSearchField(
hint: AppLocalizations.of(context)!.searchAttractionsHint,
hintColor: Colors.grey.shade500,
onChanged: (value) {
bloc.add(SearchAttractions(value));
},
),
// 🙏️ Attraction list const SizedBox(height: 16),
if (state is AttractionsLoading)
const Center( // 🏖️ Category chips row - DYNAMIC
child: Padding( if (state is AttractionsLoaded)
padding: EdgeInsets.only(top: 60), SingleChildScrollView(
child: CircularProgressIndicator(), scrollDirection: Axis.horizontal,
), child: Row(
) children: state.categories
else if (state is AttractionsLoaded) .map(
state.attractions.isEmpty (category) => buildCategoryChip(
? Center( category.categoryName ?? '',
child: Padding( isSelected: state.selectedCategoryId == category.id,
padding: const EdgeInsets.only(top: 60), onTap: () {
child: Text( bloc.add(
"No attractions found", FetchAttractionsByCategory(
style: TextStyle( categoryXid: category.id,
color: Colors.grey, ),
fontSize: 14.sp, );
), },
),
)
.toList(),
), ),
), ),
) // else
: Column( // // Show placeholder chips while loading
children: state.attractions // SingleChildScrollView(
.map( // scrollDirection: Axis.horizontal,
(attraction) => AttractionCard( // child: Row(
attraction: attraction, // children: [
// buildCategoryChip("Beach", isSelected: true, onTap: () {}),
// buildCategoryChip("Hike", isSelected: false, onTap: () {}),
// buildCategoryChip("Adventure", isSelected: false, onTap: () {}),
// buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}),
// ],
// ),
// ),
const SizedBox(height: 10),
// 🙏️ Attraction list
if (state is AttractionsLoading)
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
), ),
) )
.toList(), else if (state is AttractionsLoaded)
) state.attractions.isEmpty
else if (state is AttractionsError) ? Center(
Center(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 60), padding: const EdgeInsets.only(top: 60),
child: Text( child: Text(
state.message, AppLocalizations.of(context)!.noAttractionsFound,
style: TextStyle( style: TextStyle(
color: Colors.red, color: Colors.grey,
fontSize: 14.sp, fontSize: 14.sp,
), ),
), ),
), ),
) )
else : Column(
const SizedBox(), children: state.attractions
], .map(
(attraction) => AttractionCard(
attraction: attraction,
),
)
.toList(),
)
else if (state is AttractionsError)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
state.message,
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
),
),
),
)
else
const SizedBox(),
],
),
), ),
), ),
), ),

View File

@@ -1,7 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/common_app_texts.dart'; import '../../common_packages/common_app_texts.dart';
import '../../common_packages/shimmer_animation.dart';
import '../../l10n/app_localizations.dart';
import '../../core/route_constants.dart'; import '../../core/route_constants.dart';
import '../models/attraction_model.dart'; import '../models/attraction_model.dart';
@@ -42,12 +45,17 @@ class AttractionCard extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
child: imageUrl.isNotEmpty child: imageUrl.isNotEmpty
? Image.network( ? CachedNetworkImage(
imageUrl, imageUrl: imageUrl,
height: 94.h, height: 94.h,
width: 94.w, width: 94.w,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageFallback(), placeholder: (context, url) => SkeletonWidget(
width: 94.w,
height: 94.h,
borderRadius: 8.r,
),
errorWidget: (_, __, ___) => _imageFallback(),
) )
: _imageFallback(), : _imageFallback(),
), ),
@@ -69,18 +77,18 @@ class AttractionCard extends StatelessWidget {
), ),
), ),
SizedBox(height: 6.h), // SizedBox(height: 6.h),
//
Text( // Text(
attraction.address, // attraction.address,
maxLines: 1, // maxLines: 1,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins( // style: GoogleFonts.poppins(
fontSize: 12.sp, // fontSize: 12.sp,
fontWeight: FontWeight.w400, // fontWeight: FontWeight.w400,
color: const Color(0xff464646), // color: const Color(0xff464646),
), // ),
), // ),
SizedBox(height: 6.h), SizedBox(height: 6.h),
@@ -88,7 +96,7 @@ class AttractionCard extends StatelessWidget {
TextSpan( TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: "from \$${attraction.ticketPriceAdult}", text: "\$${attraction.ticketPriceAdult}",
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -96,7 +104,7 @@ class AttractionCard extends StatelessWidget {
), ),
), ),
TextSpan( TextSpan(
text: "/person", text: AppLocalizations.of(context)!.perPersonSuffix,
style: TextStyle( style: TextStyle(
fontSize: 10.sp, fontSize: 10.sp,
color: Colors.black, color: Colors.black,

View File

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

View File

@@ -30,3 +30,5 @@ class UpdateValidityDuration extends BuyPassEvent {
UpdateValidityDuration(this.duration); UpdateValidityDuration(this.duration);
} }
class AddToCartLoading extends BuyPassEvent {}
class AddToCartDone extends BuyPassEvent {}

View File

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

View File

@@ -7,11 +7,13 @@ import 'package:citycards_customer/core/route_constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../common_packages/back_widget.dart';
import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/api_urls.dart';
import '../bloc/buy_pass_bloc.dart'; import '../bloc/buy_pass_bloc.dart';
import '../bloc/buy_pass_event.dart'; import '../bloc/buy_pass_event.dart';
import '../bloc/buy_pass_state.dart'; import '../bloc/buy_pass_state.dart';
import '../repository/buy_pass_repository.dart'; import '../repository/buy_pass_repository.dart';
import '../../l10n/app_localizations.dart';
class BuyPassView extends StatelessWidget { class BuyPassView extends StatelessWidget {
const BuyPassView({super.key}); const BuyPassView({super.key});
@@ -19,16 +21,34 @@ class BuyPassView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => BuyPassBloc(repository: BuyPassRepository()) create: (context) =>
..add(FetchBuyPassData()), BuyPassBloc(repository: BuyPassRepository())..add(FetchBuyPassData()),
child: const BuyPassContent(), child: const BuyPassContent(),
); );
} }
} }
class BuyPassContent extends StatelessWidget { class BuyPassContent extends StatefulWidget {
const BuyPassContent({super.key}); const BuyPassContent({super.key});
@override
State<BuyPassContent> createState() => _BuyPassContentState();
}
class _BuyPassContentState extends State<BuyPassContent> {
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 0.85);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -38,9 +58,7 @@ class BuyPassContent extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
if (state is BuyPassLoading) { if (state is BuyPassLoading) {
return const Center( return const Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(color: Color(0xFFF95F62)),
color: Color(0xFFF95F62),
),
); );
} }
@@ -52,7 +70,7 @@ class BuyPassContent extends StatelessWidget {
Icon(Icons.error_outline, size: 60.sp, color: Colors.red), Icon(Icons.error_outline, size: 60.sp, color: Colors.red),
SizedBox(height: 16.h), SizedBox(height: 16.h),
CustomText( CustomText(
text: "Error loading data", text: AppLocalizations.of(context)!.errorLoadingDataTitle,
size: 16.sp, size: 16.sp,
color: Colors.red, color: Colors.red,
), ),
@@ -67,7 +85,9 @@ class BuyPassContent extends StatelessWidget {
onPressed: () { onPressed: () {
context.read<BuyPassBloc>().add(FetchBuyPassData()); context.read<BuyPassBloc>().add(FetchBuyPassData());
}, },
child: const Text("Retry"), child: Text(
AppLocalizations.of(context)!.retryButtonLabel,
),
), ),
], ],
), ),
@@ -91,77 +111,86 @@ class BuyPassContent extends StatelessWidget {
), ),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0.w), padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row( child: backWidget(
context,
AppLocalizations.of(context)!.buyACardTitle,
Colors.black,
),
),
SizedBox(height: 20.h),
// Pass Cards Horizontal List — with next-card peek + scroll hint
SizedBox(
height: 140.h,
child: PageView.builder(
controller: PageController(viewportFraction: 0.85),
clipBehavior: Clip.none,
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,
),
);
},
),
),
// "Scroll to reveal more" hint — only visible when there are multiple cards
if (data.cards.length > 1) ...[
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
GestureDetector( Icon(
onTap: () { Icons.arrow_forward,
Navigator.pop(context); size: 18.sp,
}, color: const Color(0xFFF95F62),
child: const Icon(Icons.arrow_back), ),
SizedBox(width: 6.w),
Text(
'Scroll to reveal more',
style: TextStyle(
fontSize: 13.sp,
color: const Color(0xFFF95F62),
fontWeight: FontWeight.w500,
),
), ),
SizedBox(width: 8.w),
CustomText(text: "Buy a Pass", size: 12.sp),
], ],
), ),
), ],
SizedBox(height: 22.h),
// Pass Cards Horizontal List SizedBox(height: 16.h),
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,
),
),
);
},
),
),
),
),
SizedBox(height: 30.h),
// Payment Card // Payment Card
// ✅ UPDATED PAYMENT CARD SECTION IN buy_pass_view.dart // ✅ UPDATED PAYMENT CARD SECTION IN buy_pass_view.dart
// Replace the existing PaymentCard widget (around line 154) with this: // Replace the existing PaymentCard widget (around line 154) with this:
Center( Center(
child: PaymentCard( child: PaymentCard(
city: data.city.name, city: data.city.name,
heroImage: data.city.heroBanner.image, heroImage: data.city.heroBanner.image,
cardType: selectedCard.cardType.name, cardType: selectedCard.cardType.name,
cardDisplayName: selectedCard.cardType.displayName, cardDisplayName: selectedCard.cardType.displayName,
themeColor: state.selectedCardIndex == 0 themeColor:
? Color(0xFFF97316) selectedCard.cardType.name == "selective_pass"
: Color(0xFF1E8AF6), ? Color(0xFFF95FAF) // pink for flexi/selective pass
: Color(0xFFF95F62),
adultPrice: selectedCard.adultPrice.toDouble(), adultPrice: selectedCard.adultPrice.toDouble(),
childPrice: selectedCard.childPrice.toDouble(), childPrice: selectedCard.childPrice.toDouble(),
adults: state.adultCount, adults: state.adultCount,
@@ -209,14 +238,21 @@ class BuyPassContent extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
CustomText(text: "Card Offers", size: 18.sp), CustomText(
text: AppLocalizations.of(
context,
)!.memberPrivilegesTitle,
size: 18.sp,
),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.pushNamed( Navigator.pushNamed(
context, RouteConstants.searchOffer); context,
RouteConstants.searchOffer,
);
}, },
child: CustomText( child: CustomText(
text: "View All", text: AppLocalizations.of(context)!.viewAll,
size: 14.sp, size: 14.sp,
color: Color(0xFFFF5757), color: Color(0xFFFF5757),
), ),
@@ -233,12 +269,13 @@ class BuyPassContent extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 20.w), padding: EdgeInsets.symmetric(horizontal: 20.w),
child: GridView.builder( child: GridView.builder(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate:
crossAxisCount: 2, SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 16.w, crossAxisCount: 2,
mainAxisSpacing: 22.h, crossAxisSpacing: 16.w,
childAspectRatio: 0.65, mainAxisSpacing: 22.h,
), childAspectRatio: 0.65,
),
itemCount: selectedCard.offers.length > 2 itemCount: selectedCard.offers.length > 2
? 2 ? 2
: selectedCard.offers.length, : selectedCard.offers.length,
@@ -246,12 +283,12 @@ class BuyPassContent extends StatelessWidget {
final offer = selectedCard.offers[index]; final offer = selectedCard.offers[index];
return GestureDetector( return GestureDetector(
onTap: () { // onTap: () {
Navigator.of(context).pushNamed( // Navigator.of(context).pushNamed(
RouteConstants.offerPassDetail, // RouteConstants.offerPassDetail,
arguments: offer.id, // ✅ pass offerId // arguments: offer.id, // ✅ pass offerId
); // );
}, // },
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 6.w, horizontal: 6.w,
@@ -259,7 +296,9 @@ class BuyPassContent extends StatelessWidget {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: const Color(0xFFF95F62).withOpacity(.24), color: const Color(
0xFFF95F62,
).withOpacity(.24),
), ),
borderRadius: BorderRadius.circular(12.sp), borderRadius: BorderRadius.circular(12.sp),
), ),
@@ -269,62 +308,75 @@ class BuyPassContent extends StatelessWidget {
/// Image /// Image
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8.sp), borderRadius: BorderRadius.circular(8.sp),
child: offer.mobileBannerImage != null && child:
offer.mobileBannerImage!.isNotEmpty offer.mobileBannerImage != null &&
offer
.mobileBannerImage!
.isNotEmpty
? Image.network( ? Image.network(
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}', '${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
width: double.infinity, width: double.infinity,
height: 120.5.h, height: 120.5.h,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { errorBuilder:
return Container( (context, error, stackTrace) {
width: double.infinity, return Container(
height: 120.5.h, width: double.infinity,
color: const Color(0xFFFEE7E7), height: 120.5.h,
child: Icon( color: const Color(
Icons.local_offer, 0xFFFEE7E7,
size: 40.sp, ),
color: child: Icon(
const Color(0xFFF95F62).withOpacity(.6), Icons.local_offer,
), size: 40.sp,
); color: const Color(
}, 0xFFF95F62,
loadingBuilder: ).withOpacity(.6),
(context, child, loadingProgress) { ),
if (loadingProgress == null) return child; );
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null)
return child;
return Container( return Container(
width: double.infinity, width: double.infinity,
height: 120.5.h, height: 120.5.h,
color: const Color(0xFFFEE7E7), color: const Color(
child: Center( 0xFFFEE7E7,
child: CircularProgressIndicator( ),
strokeWidth: 2, child: Center(
color: const Color(0xFFF95F62), child: CircularProgressIndicator(
value: loadingProgress strokeWidth: 2,
.expectedTotalBytes != color: const Color(
null 0xFFF95F62,
? loadingProgress ),
.cumulativeBytesLoaded / value:
loadingProgress loadingProgress
.expectedTotalBytes! .expectedTotalBytes !=
: null, null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
),
),
);
},
)
: Container(
width: double.infinity,
height: 120.5.h,
color: const Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color: const Color(
0xFFF95F62,
).withOpacity(.6),
), ),
), ),
);
},
)
: Container(
width: double.infinity,
height: 120.5.h,
color: const Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color:
const Color(0xFFF95F62).withOpacity(.6),
),
),
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@@ -341,10 +393,10 @@ class BuyPassContent extends StatelessWidget {
/// Offer Code /// Offer Code
CustomText( CustomText(
text: offer.description??"N/A", text: offer.description ?? "N/A",
color: Colors.black.withOpacity(.6), color: Colors.black.withOpacity(.6),
size: 12.sp, size: 12.sp,
maxLines: 2, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
@@ -359,7 +411,7 @@ class BuyPassContent extends StatelessWidget {
height: 100.h, height: 100.h,
alignment: Alignment.center, alignment: Alignment.center,
child: CustomText( child: CustomText(
text: "No offers available", text: AppLocalizations.of(context)!.noOffersAvailable,
size: 14.sp, size: 14.sp,
color: Colors.grey, color: Colors.grey,
), ),
@@ -376,7 +428,11 @@ class BuyPassContent extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0.w), padding: EdgeInsets.symmetric(horizontal: 20.0.w),
child: CustomText( child: CustomText(
text: "Available Attractions", size: 18.sp), text: AppLocalizations.of(
context,
)!.availableAttractionsTitle,
size: 18.sp,
),
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
@@ -397,7 +453,9 @@ class BuyPassContent extends StatelessWidget {
width: 104.w, width: 104.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(
8.r,
),
), ),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
@@ -407,35 +465,60 @@ class BuyPassContent extends StatelessWidget {
); );
}, },
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(
child: attraction.thumbnail != null && 8.r,
attraction.thumbnail!.isNotEmpty
? Image.network(
attraction.thumbnail!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.location_on,
size: 40.sp,
color: Colors.grey[400],
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
},
)
: Icon(
Icons.location_on,
size: 40.sp,
color: Colors.grey[400],
), ),
child:
attraction.thumbnail != null &&
attraction
.thumbnail!
.isNotEmpty
? Image.network(
attraction.thumbnail!,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stackTrace,
) {
return Icon(
Icons.location_on,
size: 40.sp,
color:
Colors.grey[400],
);
},
loadingBuilder:
(
context,
child,
loadingProgress,
) {
if (loadingProgress ==
null)
return child;
return Center(
child: SizedBox(
width: 20.w,
height: 20.w,
child:
CircularProgressIndicator(
color: Color(
0xffF95F62,
),
strokeWidth:
2,
),
),
);
},
)
: Icon(
Icons.location_on,
size: 40.sp,
color: Colors.grey[400],
),
), ),
), ),
), ),
@@ -461,7 +544,9 @@ class BuyPassContent extends StatelessWidget {
height: 100.h, height: 100.h,
alignment: Alignment.center, alignment: Alignment.center,
child: CustomText( child: CustomText(
text: "No attractions available", text: AppLocalizations.of(
context,
)!.noAttractionsAvailable,
size: 14.sp, size: 14.sp,
color: Colors.grey, color: Colors.grey,
), ),
@@ -478,7 +563,7 @@ class BuyPassContent extends StatelessWidget {
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: CustomText( child: CustomText(
text: "View All", text: AppLocalizations.of(context)!.viewAll,
size: 12.sp, size: 12.sp,
color: Color(0xFFF95F62), color: Color(0xFFF95F62),
), ),

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/common_app_texts.dart'; import '../../common_packages/common_app_texts.dart';
import '../../l10n/app_localizations.dart';
class FeatureTable extends StatelessWidget { class FeatureTable extends StatelessWidget {
const FeatureTable({super.key}); const FeatureTable({super.key});
@@ -9,15 +9,15 @@ class FeatureTable extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final features = [ final features = [
FeatureModel('Access to attractions', true, true), FeatureModel(AppLocalizations.of(context)!.featureAccessToAttractions, true, true),
FeatureModel('Entry to attractions', true, true), FeatureModel(AppLocalizations.of(context)!.featureEntryToAttractions, true, true),
FeatureModel('Access to experiences', true, true), FeatureModel(AppLocalizations.of(context)!.featureAccessToExperiences, true, true),
FeatureModel('Entry to sites', false, true), FeatureModel(AppLocalizations.of(context)!.featureEntryToSites, false, true),
FeatureModel('Access to venues', true, true), FeatureModel(AppLocalizations.of(context)!.featureAccessToVenues, true, true),
FeatureModel('Entry to events', true, true), FeatureModel(AppLocalizations.of(context)!.featureEntryToEvents, true, true),
FeatureModel('Access to experiences', false, true), FeatureModel(AppLocalizations.of(context)!.featureAccessToExperiences, false, true),
FeatureModel('Access to Itinerary creation', false, true), FeatureModel(AppLocalizations.of(context)!.featureAccessToItineraryCreation, false, true),
FeatureModel('Access to postcard creation', false, true), FeatureModel(AppLocalizations.of(context)!.featureAccessToPostcardCreation, false, true),
]; ];
return Center( return Center(
@@ -44,7 +44,7 @@ class FeatureTable extends StatelessWidget {
}, },
defaultVerticalAlignment: TableCellVerticalAlignment.middle, defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [ children: [
_buildHeaderRow(), _buildHeaderRow(context),
...features.map(_buildFeatureRow).toList(), ...features.map(_buildFeatureRow).toList(),
], ],
), ),
@@ -54,13 +54,13 @@ class FeatureTable extends StatelessWidget {
} }
// HEADER ROW // HEADER ROW
TableRow _buildHeaderRow() { TableRow _buildHeaderRow(BuildContext context) {
return TableRow( return TableRow(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.only(bottom: 12.h),
child: Text( child: Text(
'Features', AppLocalizations.of(context)!.featuresTitle,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 15.sp, fontSize: 15.sp,
@@ -68,7 +68,7 @@ class FeatureTable extends StatelessWidget {
), ),
), ),
_buildHeaderText(CommonAppText.selectiveCard), _buildHeaderText(CommonAppText.selectiveCard),
_buildHeaderText('Unlimited'), _buildHeaderText(AppLocalizations.of(context)!.unlimitedTitle),
], ],
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,503 @@
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';
import '../../l10n/app_localizations.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.
double get _cardItemHeight => 330.h + 20.h;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
final offset = _scrollController.offset;
// Use round() but based on dynamic height, not hardcoded
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(
AppLocalizations.of(context)!.purchaseOnePostcardAtTime,
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: AppLocalizations.of(context)!.proceedToCheckoutLabel,
),
),
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: AppLocalizations.of(context)!.notLoggedInYet,
size: 22.sp,
color: const Color(0xFFF95F62),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
Text(
AppLocalizations.of(context)!.loginToAccessPostcardsCart,
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: AppLocalizations.of(context)!.loginToCheckoutLabel,
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// 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(
AppLocalizations.of(context)!.youDoNotHaveAnyPostcards,
style: GoogleFonts.poppins(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 8.h),
Text(
AppLocalizations.of(context)!.emptyPostcardsDescription,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: const Color(0xFF656565),
),
),
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
Navigator.pop(context);
},
label: AppLocalizations.of(context)!.designMyPostcardLabel,
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────
// 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(
AppLocalizations.of(context)!.somethingWentWrong,
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(
AppLocalizations.of(context)!.retryLabel,
style: TextStyle(
color: const Color(0xffF95F62),
fontSize: 14.sp,
),
),
),
],
),
),
);
}
}

View File

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

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