5 Commits

Author SHA1 Message Date
aeeb1c27e0 first commit 2026-03-24 16:56:07 +05:30
46906b04f4 Merge remote-tracking branch 'origin/raj' into Anuj 2026-02-13 20:06:48 +05:30
8f7a68edbc first commit 2026-02-13 20:06:04 +05:30
eb9ca9299e Merge remote-tracking branch 'origin/raj' into Anuj 2026-02-06 19:11:29 +05:30
e15a979c0c first commit 2026-02-06 19:11:09 +05:30
292 changed files with 10071 additions and 37225 deletions

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -1,141 +0,0 @@
[{
"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_launcher_icons:
image_path: "assets/icons/citycards_customer_logo.jpg"
image_path: "assets/logo/logo_city_cards.png"
android: "launcher_icon"
# image_path_android: "assets/icon/icon.png"

View File

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

View File

@@ -1,6 +1,4 @@
PODS:
- app_links (7.0.0):
- Flutter
- Flutter (1.0.0)
- flutter_angle (0.3.8):
- Flutter
@@ -25,15 +23,11 @@ PODS:
- GoogleMaps/Maps (9.4.0)
- image_picker_ios (0.0.1):
- Flutter
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -97,14 +91,11 @@ PODS:
- StripeCore (= 25.0.1)
- three_js_sensors (0.1.2):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- Flutter (from `Flutter`)
- flutter_angle (from `.symlinks/plugins/flutter_angle/darwin`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -112,15 +103,12 @@ DEPENDENCIES:
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_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`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- stripe_ios (from `.symlinks/plugins/stripe_ios/ios`)
- three_js_sensors (from `.symlinks/plugins/three_js_sensors/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
SPEC REPOS:
@@ -138,8 +126,6 @@ SPEC REPOS:
- StripeUICore
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
Flutter:
:path: Flutter
flutter_angle:
@@ -154,14 +140,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
@@ -170,13 +152,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/stripe_ios/ios"
three_js_sensors:
:path: ".symlinks/plugins/three_js_sensors/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
SPEC CHECKSUMS:
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_angle: fc44e198cea1f07e1a5919bad1484049fab65c96
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
@@ -187,10 +166,8 @@ SPEC CHECKSUMS:
google_maps_flutter_ios: e31555a04d1986ab130f2b9f24b6cdc861acc6d3
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
Stripe: 4728e3e0dd8df134e4a420ab504e929a93a815f0
@@ -203,7 +180,6 @@ SPEC CHECKSUMS:
StripePaymentsUI: 626726a01255a6458c35436f7f6431dacee82684
StripeUICore: 30f8352fd7a5cf1541b7777a57b3ad1133bf6763
three_js_sensors: ab5f24fbeb97ab5c5ce2978c3e63a25d67a076f5
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86
PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506

View File

@@ -7,15 +7,15 @@
objects = {
/* Begin PBXBuildFile section */
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */; };
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75864C28F633B337B6CD7995 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -46,14 +46,13 @@
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
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>"; };
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>"; };
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>"; };
@@ -62,9 +61,10 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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>"; };
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>"; };
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -72,7 +72,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */,
00C1AB7B0C8F1922F3F1AE65 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -80,7 +80,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */,
81D638B66EB4658C8192CA0D /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -95,15 +95,24 @@
path = RunnerTests;
sourceTree = "<group>";
};
5D45FB84C63476582408C414 /* Frameworks */ = {
isa = PBXGroup;
children = (
54C8901E9D1856D980DFFE46 /* Pods_Runner.framework */,
445696AB37183A7C63CB7E98 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
6D4A73F1E55857ADBD000C6A /* Pods */ = {
isa = PBXGroup;
children = (
369614DBDD277BF9018C34BC /* Pods-Runner.debug.xcconfig */,
6BD7534B4533D500F969D46C /* Pods-Runner.release.xcconfig */,
6997591091A0E8DA4E4776AA /* Pods-Runner.profile.xcconfig */,
62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */,
AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */,
AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */,
B691822B373AD22ECA93B798 /* Pods-Runner.debug.xcconfig */,
4FD33ADDA221C4BBA29FA3D6 /* Pods-Runner.release.xcconfig */,
D56ABB8F306EF9F6809C0C1E /* Pods-Runner.profile.xcconfig */,
E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */,
626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */,
C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -127,7 +136,7 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
6D4A73F1E55857ADBD000C6A /* Pods */,
F3A521C4EE6E75D0D8A88556 /* Frameworks */,
5D45FB84C63476582408C414 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -155,15 +164,6 @@
path = Runner;
sourceTree = "<group>";
};
F3A521C4EE6E75D0D8A88556 /* Frameworks */ = {
isa = PBXGroup;
children = (
75864C28F633B337B6CD7995 /* Pods_Runner.framework */,
BA7A98D7E1CD160163E28329 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -171,7 +171,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */,
BC66FA7BADCD3982DC87655E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
CF8A29BE993C0C902CB143AF /* Frameworks */,
@@ -190,15 +190,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */,
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */,
5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */,
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */,
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -270,45 +270,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
42DBF8C3008CA78F0E130EA1 /* [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-RunnerTests-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;
};
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */ = {
3825EC0F330C0B58EA2A8981 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -330,21 +292,37 @@
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 */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
41FC0A605EBADE26C841287E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
"${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-resources.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
@@ -362,21 +340,43 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */ = {
BC66FA7BADCD3982DC87655E /* [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";
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\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;
};
D10E98BB568B7005161E1ABD /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -515,7 +515,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = E2E6DC2B6718F55E3BF165E7 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -533,7 +533,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 626B072D1717B50A277DA3C7 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -549,7 +549,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = C1FCB3EF88270ED76DFA3FBD /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -6,6 +6,10 @@
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>NSCameraUsageDescription</key>
<string>Scan your card to add it automatically</string>
<key>NSCameraUsageDescription</key>
<string>To scan cards</string>
<key>CFBundleDisplayName</key>
<string>Citycards Customer</string>
<key>CFBundleExecutable</key>
@@ -24,37 +28,18 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>3</string>
<key>LSApplicationCategoryType</key>
<string></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>
<string>To scan cards</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Citycard customer needs your location to find the closest place you can visit.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your camera for taking photos for profile and to build a postcard.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
@@ -66,10 +51,15 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

@@ -11,39 +11,6 @@ class AttractionDetailsBloc
required this.repository,
}) : super(AttractionDetailsInitial()) {
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(

View File

@@ -17,19 +17,3 @@ class FetchAttractionDetails extends AttractionDetailsEvent {
@override
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,33 +15,13 @@ class AttractionDetailsLoading extends AttractionDetailsState {}
class AttractionDetailsLoaded extends AttractionDetailsState {
final AttractionDetailsModel attractionDetails;
final bool isExpanded;
final int galleryIndex;
final int fullScreenGalleryIndex;
const AttractionDetailsLoaded({
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
List<Object?> get props => [attractionDetails, isExpanded, galleryIndex, fullScreenGalleryIndex];
List<Object?> get props => [attractionDetails];
}
class AttractionDetailsError extends AttractionDetailsState {

View File

@@ -1,6 +1,3 @@
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/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
@@ -9,10 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import '../../l10n/app_localizations.dart';
import '../../common_packages/shimmer_animation.dart';
import '../../core/route_constants.dart';
import '../bloc/attraction_details_bloc.dart';
import '../bloc/attraction_details_event.dart';
@@ -39,7 +33,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
child: CircularProgressIndicator(),
),
);
}
@@ -72,156 +66,104 @@ class AttractionDetailsView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
children: [
// ── Hero image ──────────────────────────────────────
CachedNetworkImage(
imageUrl: coverImage,
height: 280.h,
Image.network(
coverImage,
height: 377.h,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (context, url) => SkeletonWidget(
width: double.infinity,
height: 280.h,
borderRadius: 0,
),
errorWidget: (context, url, error) => Image.asset(
'assets/images/koh_rong_samloem_banner.png',
height: 280.h,
width: double.infinity,
fit: BoxFit.cover,
),
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/koh_rong_samloem_banner.png',
height: 377.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(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // 🔽 reduced
child: Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w, // 🔽 smaller
vertical: 8.h, // 🔽 smaller
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r), // 🔽 slightly smaller
boxShadow: [
BoxShadow(
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,
size: 16.sp, // 🔽 smaller icon
color: Colors.black,
),
SizedBox(width: 6.w), // 🔽 smaller spacing
Text(
'Back to attractions',
style: TextStyle(
fontSize: 13.sp, // 🔽 slightly smaller
fontWeight: FontWeight.w600, // ✅ bold
color: Colors.black,
),
),
],
),
padding: EdgeInsets.symmetric(
horizontal: 20.w, vertical: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: true,
isProfilePage: false,
showDivider: true,
),
),
SizedBox(height: 10.h),
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.arrow_back,
size: 24.sp,
color: Colors.white,
),
),
SizedBox(width: 8.w),
Expanded(
child: Text(
attraction.title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
),
),
// ── Bottom-left: attraction title (smaller, over fade) ─
Positioned(
bottom: 48.h,
left: 14.w,
right: 60.w,
bottom: 31.h,
left: 12.w,
right: 60.w, // Add this - leaves space for share button
child: Text(
attraction.title,
style: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
height: 1.25,
fontSize: 44.sp,
fontWeight: FontWeight.w500,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// ── Bottom-right: share button ───────────────────────
Positioned(
bottom: 48.h,
right: 14.w,
bottom: 31.h,
right: 17.w,
child: GestureDetector(
onTap: () {
Share.share(
'www.google.com',
subject: AppLocalizations.of(context)!.checkThisOut,
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const ShareBottomSheet(),
);
},
child: Container(
height: 42.h,
width: 42.w,
height: 36.h,
width: 36.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(21.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
borderRadius: BorderRadius.circular(20.r),
),
child: Center(
child: Icon(
@@ -236,106 +178,29 @@ 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
Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
padding:
EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.aboutTitle,
"About",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 12.32.h),
LayoutBuilder(
builder: (context, constraints) {
final textSpan = TextSpan(
text: attraction.description,
style: TextStyle(
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,
),
),
),
],
],
);
},
Text(
attraction.description,
style: TextStyle(
color: Color(0xFF262626),
fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
),
],
),
@@ -505,10 +370,10 @@ class AttractionDetailsView extends StatelessWidget {
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
AppLocalizations.of(context)!.whatIsIncluded,
"What is included",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.h),
@@ -530,17 +395,17 @@ class AttractionDetailsView extends StatelessWidget {
),
SizedBox(height: 30.h),
// Divider(color: Colors.black.withOpacity(0.2)),
// SizedBox(height: 30.h),
SizedBox(height: 30.h),
Text(
AppLocalizations.of(context)!.exactLocation,
"Exact Location",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 8.h),
CustomText(
text: AppLocalizations.of(context)!.viewOnMap,
text: "View the location on map",
size: 12.sp,
color: Colors.black.withOpacity(.6),
),
@@ -600,29 +465,28 @@ class AttractionDetailsView extends StatelessWidget {
color: Colors.black.withOpacity(0.6),
),
SizedBox(height: 30.h),
if (attraction.attractionFaqs.isNotEmpty) ...[
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
AppLocalizations.of(context)!.peopleFrequentlyAsk,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
"People frequently ask",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
SizedBox(height: 15.h),
Column(
children: attraction.attractionFaqs.map((faq) {
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: faqBox(
title: faq.faqQuestion,
desc: faq.faqAnswer,
),
);
}).toList(),
),
],
),
SizedBox(height: 15.h),
Column(
children: attraction.attractionFaqs.map((faq) {
return Padding(
padding: EdgeInsets.only(bottom: 15.h),
child: faqBox(
title: faq.faqQuestion,
desc: faq.faqAnswer,
),
);
}).toList(),
),
],
),
),
@@ -637,7 +501,7 @@ class AttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text(AppLocalizations.of(context)!.somethingWentWrong),
child: Text("Something went wrong"),
),
);
},
@@ -728,276 +592,4 @@ 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> {
final AttractionsRepository repository;
AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
AttractionsBloc({required this.repository})
: super(AttractionsInitial()) {
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
on<SearchAttractions>(_onSearchAttractions);
}
Future<void> _onFetchAttractionsByCategory(
@@ -21,50 +21,22 @@ class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
try {
final AttractionsResponse response =
await repository.fetchAttractionsByCategory(
categoryXid: event.categoryXid,
categoryXid: event.categoryXid, // Can be null now
);
final allAttractions = response.attractions ?? [];
emit(
AttractionsLoaded(
attractions: allAttractions,
allAttractions: allAttractions,
attractions: response.attractions ?? [],
categories: response.categories ?? [],
selectedCategoryId: event.categoryXid,
searchQuery: '',
selectedCategoryId: event.categoryXid, // Can be null
),
);
} catch (e) {
emit(AttractionsError(e.toString()));
emit(
AttractionsError(
e.toString(),
),
);
}
}
void _onSearchAttractions(
SearchAttractions event,
Emitter<AttractionsState> emit,
) {
final currentState = state;
if (currentState is! AttractionsLoaded) return;
final query = event.query.trim().toLowerCase();
final filtered = query.isEmpty
? currentState.allAttractions
: currentState.allAttractions.where((attraction) {
final name = (attraction.title ?? '').toLowerCase();
final description = (attraction.description ?? '').toLowerCase();
return name.contains(query) || description.contains(query);
}).toList();
emit(
AttractionsLoaded(
attractions: filtered,
allAttractions: currentState.allAttractions,
categories: currentState.categories,
selectedCategoryId: currentState.selectedCategoryId,
searchQuery: event.query,
),
);
}
}

View File

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

View File

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

View File

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

View File

@@ -20,18 +20,7 @@ class BuyPassBloc extends Bloc<BuyPassEvent, BuyPassState> {
on<UpdateChildCount>(_onUpdateChildCount);
/// Handle update validity duration event
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
on<UpdateValidityDuration>(_onUpdateValidityDuration); // ✅ Added
}
/// Fetch buy pass data from repository

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,57 +0,0 @@
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

@@ -1,6 +0,0 @@
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

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

View File

@@ -1,163 +0,0 @@
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 {
// final String title;
// final String imageUrl;
// final String duration;
// final int adults;
// final int kids;
// final int quantity;
// final double price;
// final double discount;
//
// PassModel({
// required this.title,
// required this.imageUrl,
// required this.duration,
// required this.adults,
// required this.kids,
// required this.quantity,
// required this.price,
// required this.discount,
// });
// }
class PassModel {
final String title;
final String imageUrl;
final String duration;
final int adults;
final int kids;
final int quantity;
final double price;
final double discount;
PassModel({
required this.title,
required this.imageUrl,
required this.duration,
required this.adults,
required this.kids,
required this.quantity,
required this.price,
required this.discount,
});
}

View File

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

View File

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

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