11 Commits

Author SHA1 Message Date
9d962b3bde first commit 2026-03-27 11:29:45 +05:30
c12464e89a Merge remote-tracking branch 'origin/raj' into Anuj
# Conflicts:
#	ios/Podfile.lock
#	ios/Runner/Info.plist
2026-03-24 17:03:07 +05:30
aeeb1c27e0 first commit 2026-03-24 16:56:07 +05:30
c06c844210 bug fixes done 2026-03-24 13:21:37 +05:30
9d27389bf2 bug fixes done 2026-03-23 19:25:32 +05:30
d1038e846e bug fixes done 2026-03-20 18:08:52 +05:30
177f891a31 added check in and Qr scaner and bookng with api and more fixes and changes 2026-03-17 17:07:14 +05:30
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
79 changed files with 4146 additions and 1804 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.

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -10,12 +10,12 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
60A4FC1A895BADD3C1597C0B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7A98D7E1CD160163E28329 /* 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 */; };
C9496C6E1D2E0AEA2083C14C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D485254B2B32B84B17D20864 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -44,16 +44,15 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
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; };
9EEFD1F245CF2AAD027ADE1E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D485254B2B32B84B17D20864 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D719B3676BADD267F44F4A59 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
D8B9C9F2F3A8FEF639B5A528 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -72,7 +72,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B7B14C5E8DB2459D45E2AD2E /* Pods_Runner.framework in Frameworks */,
C9496C6E1D2E0AEA2083C14C /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -80,13 +80,22 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
94B491F6EAAA79D2947A02BD /* Pods_RunnerTests.framework in Frameworks */,
60A4FC1A895BADD3C1597C0B /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
227A5CBF4270AAC4960A7CAF /* Frameworks */ = {
isa = PBXGroup;
children = (
D485254B2B32B84B17D20864 /* Pods_Runner.framework */,
6E2BCDF72F2D56D34CC9E967 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@@ -98,12 +107,12 @@
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 */,
9EEFD1F245CF2AAD027ADE1E /* Pods-Runner.debug.xcconfig */,
D8B9C9F2F3A8FEF639B5A528 /* Pods-Runner.release.xcconfig */,
D719B3676BADD267F44F4A59 /* Pods-Runner.profile.xcconfig */,
6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */,
18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */,
5C263389B0D2FA3EC95111B1 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -127,7 +136,7 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
6D4A73F1E55857ADBD000C6A /* Pods */,
F3A521C4EE6E75D0D8A88556 /* Frameworks */,
227A5CBF4270AAC4960A7CAF /* 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 */,
FCD91A1B9B088634E06F7C98 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
CF8A29BE993C0C902CB143AF /* Frameworks */,
@@ -190,15 +190,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */,
8B4387D9368AF1A601B17194 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */,
5BB9E9D50E854F4D876D849A /* [CP] Copy Pods Resources */,
CA082AA85EC21FFAA42CE12F /* [CP] Embed Pods Frameworks */,
3F2F0E04CC81D63DDD8C37A9 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -286,29 +286,24 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
42DBF8C3008CA78F0E130EA1 /* [CP] Check Pods Manifest.lock */ = {
3F2F0E04CC81D63DDD8C37A9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
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";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
46DBB6E51DCB00168B7FED03 /* [CP] Check Pods Manifest.lock */ = {
8B4387D9368AF1A601B17194 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -330,23 +325,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
5BB9E9D50E854F4D876D849A /* [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;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -362,7 +340,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
E0E7566711BD38D2F6C5330A /* [CP] Embed Pods Frameworks */ = {
CA082AA85EC21FFAA42CE12F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -379,6 +357,28 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
FCD91A1B9B088634E06F7C98 /* [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;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -515,7 +515,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 62ED1D923084D6092BECB5AC /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 6F51EB881CD063E2C9A71BA6 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -533,7 +533,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AB77C0F975F5B780954288AA /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 18E5A2491D54EBB2484B6D9E /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -549,7 +549,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AE2DC54B7F4682B91B6259C6 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = 5C263389B0D2FA3EC95111B1 /* 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

@@ -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,10 +28,12 @@
<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>
@@ -45,13 +51,15 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

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

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../bloc/stripe_payment_bloc.dart';
import '../bloc/stripe_payment_event.dart';
import '../bloc/stripe_payment_state.dart';

View File

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

View File

@@ -116,7 +116,7 @@ class _BuyPassContentState extends State<BuyPassContent> {
children: [
const Icon(Icons.arrow_back),
SizedBox(width: 8.w),
CustomText(text: "Buy a Pass", size: 12.sp),
CustomText(text: "Buy a Card", size: 12.sp),
],
),
),

View File

@@ -47,7 +47,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
// ========== HANDLE API DATA (LOGGED IN USER) ==========
else if (state is MyPassCartApiLoaded) {
final apiCartData = state.apiCartData;
@@ -73,15 +72,15 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
final String cityName = cartItem.city.cityName;
final String cardDisplayName = cartItem.displayCardMode;
final String cardTypeName = cartItem.cardMode;
final int themeColor =
isFlexiCard ? 0xFFF95FAF : 0xFFF95F62;
final int themeColor = isFlexiCard ? 0xFFF95FAF : 0xFFF95F62;
final int adultCount = cartItem.totalAdult;
final int childCount = cartItem.totalChild;
final int validityDuration = cartItem.noOfDays;
final double totalPrice = cartItem.totalAmount.toDouble();
final bool isUnlimitedCard =
cardTypeName.toLowerCase().contains("unlimited");
final bool isUnlimitedCard = cardTypeName
.toLowerCase()
.contains("unlimited");
final String validityLabel = isUnlimitedCard
? "$validityDuration Days"
: "${cartItem.noOfAttractions} Attractions";
@@ -111,9 +110,7 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
bookingId: cartItem.id,
couponId: cartItem.couponXid,
),
settings: RouteSettings(
arguments: checkoutData,
),
settings: RouteSettings(arguments: checkoutData),
),
);
},
@@ -135,7 +132,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
),
);
}
// ========== HANDLE LOCAL DATA (NOT LOGGED IN) ==========
else if (state is MyPassCartLoaded) {
final cartData = state.cartData;
@@ -146,8 +142,7 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
cartData['card_type_name'] as String? ?? '';
final String cardDisplayName =
cartData['card_display_name'] as String? ?? '';
final int themeColor =
cartData['theme_color'] as int? ?? 0xFFF95FAF;
final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF;
final int adultCount = cartData['adult_count'] as int? ?? 0;
final int childCount = cartData['child_count'] as int? ?? 0;
final double adultPrice =
@@ -193,7 +188,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
),
);
}
// ========== EMPTY STATE ==========
else if (state is MyPassCartEmpty) {
return Padding(
@@ -221,7 +215,7 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
Navigator.pop(context);
},
label: "Buy a Pass",
),
@@ -230,7 +224,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
),
);
}
// ========== ERROR STATE ==========
else if (state is MyPassCartError) {
return Center(
@@ -290,9 +283,7 @@ class _CartItemCard extends StatelessWidget {
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Color(themeColor).withOpacity(0.2),
),
border: Border.all(color: Color(themeColor).withOpacity(0.2)),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
@@ -307,32 +298,32 @@ class _CartItemCard extends StatelessWidget {
),
child: heroImage.isNotEmpty
? CachedNetworkImage(
imageUrl: heroImage,
width: 105.w,
height: 130.h,
fit: BoxFit.cover,
errorWidget: (context, url, error) => Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
placeholder: (context, url) => Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
)
imageUrl: heroImage,
width: 105.w,
height: 130.h,
fit: BoxFit.cover,
errorWidget: (context, url, error) => Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
placeholder: (context, url) => Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
)
: Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
),
SizedBox(width: 6.66.w),
@@ -367,7 +358,7 @@ class _CartItemCard extends StatelessWidget {
SizedBox(width: 4.w),
CustomText(
text:
"$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
"$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -388,7 +379,7 @@ class _CartItemCard extends StatelessWidget {
SizedBox(width: 4.w),
CustomText(
text:
"$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
"$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -428,10 +419,7 @@ class _CartItemCard extends StatelessWidget {
children: [
TextSpan(
text: "$cardDisplayName ",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
),
style: TextStyle(color: Colors.white, fontSize: 14.sp),
),
],
),
@@ -443,4 +431,4 @@ class _CartItemCard extends StatelessWidget {
),
);
}
}
}

View File

@@ -425,7 +425,7 @@ class _EmptyCartScreen extends StatelessWidget {
SizedBox(height: 40.h),
CustomFilledButton(
onTap: () {
Navigator.pop(context);
},
label: "Design my postcard",
),

View File

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

View File

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

View File

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

View File

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

View File

@@ -98,6 +98,7 @@ class AppRouter {
case RouteConstants.passAttractionsPage:
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
final int cityId = args['cityId'] as int;
final int bookingId = args['bookingId'] as int;
final String source = args['source'] as String;
return MaterialPageRoute(
@@ -109,6 +110,7 @@ class AppRouter {
child: PassAttractionsPage(
cityXid: cityId,
source: source,
bookingId: bookingId,
),
);
},

View File

@@ -33,6 +33,7 @@ import '../networkApiServices/noInternet/view/no_internet_screen.dart';
import '../offer_pass_detail/offer_pass_detail_view.dart';
import '../postcard/blocs/postcard_creation_bloc.dart';
import '../postcard/views/postcard_creation_page_view.dart';
import '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/privacy/privacy_view.dart';
import '../search_offers/bloc/offers_bloc.dart';
import '../search_offers/bloc/search_offers_listing_bloc.dart';
@@ -81,6 +82,7 @@ Widget buildOffstageNavigator(
case RouteConstants.passAttractionsPage:
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
final int cityId = args['cityId'] as int;
final int bookingId = args['bookingId'] as int;
final String source = args['source'] as String;
return MaterialPageRoute(
@@ -92,6 +94,7 @@ Widget buildOffstageNavigator(
child: PassAttractionsPage(
cityXid: cityId,
source: source,
bookingId: bookingId,
),
);
},
@@ -106,28 +109,34 @@ Widget buildOffstageNavigator(
);
case RouteConstants.passAttractionDetails:
final attractionID = settings.arguments as int;
final args = settings.arguments as Map<String, dynamic>;
final attractionId = args['attractionId'] as int;
final bookingId = args['bookingId'] as int;
return MaterialPageRoute(
builder: (_) {
return PassAttractionDetailsView(attractionId: attractionID);
},
builder: (_) => PassAttractionDetailsView(
attractionId: attractionId,
bookingId: bookingId,
),
);
case RouteConstants.makeBooking:
final args = settings.arguments as Map<String, dynamic>?;
return MaterialPageRoute(
builder: (_) {
return MakeBookingView(
title: 'Koh Rong Samloem',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis.ß',
);
},
builder: (_) => MakeBookingView(
title: args?['title'] ?? '',
description: args?['description'] ?? '',
validUpto: args?['validUpto'] ?? '',
attractionId: args?['attractionId'] ?? 0,
bookingId: args?['bookingId'] ?? 0,
),
);
case RouteConstants.bookingSuccessful:
final message = settings.arguments as String;
return MaterialPageRoute(
builder: (_) {
return BookingSuccessfulPageView();
return BookingSuccessfulPageView(message: message,);
},
);
@@ -166,6 +175,12 @@ Widget buildOffstageNavigator(
return const PrivacyPolicyPage();
},
);
case RouteConstants.contactUs:
return MaterialPageRoute(
builder: (_) {
return const ContactUsPage();
},
);
// 🔹 Upload Photo Page (start of postcard creation flow)
case RouteConstants.uploadPhotoPage:

View File

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

View File

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

View File

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

View File

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

View File

@@ -84,7 +84,7 @@ class EsimOfferPage extends StatelessWidget {
width: 350.w,
child: CustomText(
text:
"Stay Connected Instantly with Your Complimentary eSIM",
"Connect instantly with your free eSIM",
size: 22.sp,
color: Color(0xFFFFFFFF),
),
@@ -94,7 +94,7 @@ class EsimOfferPage extends StatelessWidget {
width: 350,
child: CustomText(
text:
"Because every unforgettable trip starts with seamless connectivity.",
"Every great journey begins with smooth connectivity.",
size: 14.sp,
color: Colors.white,
),
@@ -285,7 +285,7 @@ class EsimOfferPage extends StatelessWidget {
),
),
TextSpan(
text: " CityCard",
text: " CityCards",
style: TextStyle(
color: Color(0xFFF95F62),
fontSize: 21.sp,

View File

@@ -109,9 +109,9 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Get Your CityCard",
style: TextStyle(color: Colors.white),
Text(
"Get Your CityCards",
style: TextStyle(color: Colors.white,fontSize: 14.sp),
),
SizedBox(width: 10.w),
Image.asset("assets/icons/arrow.png", height: 13.h),

View File

@@ -25,7 +25,7 @@ class GetYourPassCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Get your Pass",
"Get Your Card",
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w500,

View File

@@ -52,7 +52,7 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Choose your card",
"Choose Your Card",
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
@@ -206,7 +206,7 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
),
),
child: Text(
"Get a Pass",
"Get a Card",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 14.sp,

View File

@@ -176,7 +176,7 @@ class HotelOfferView extends StatelessWidget {
"Choose from a wide variety of Marriott hotels — from elegant urban hideaways and premium city-centre locations to luxurious five-star experiences — all designed to make your trip ",
),
TextSpan(
text: "effortless, comfortable and memorable",
text: "effortless, comfortable",
style: TextStyle(
color: const Color(0xFFF95F62),
fontWeight: FontWeight.w600,

View File

@@ -31,7 +31,7 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
}
final response = await _repository.fetchMyItineraries();
print("🔍 isUnlimitedPass = ${response.isUnlimitedPass}");
// Check if user has unlimited pass
if (!response.isUnlimitedPass) {
emit(GetItineraryRequiresPass(itineraries: response.itineraries));

View File

@@ -85,7 +85,7 @@ class ItineraryCreationStartPage extends StatelessWidget {
label: "Lets explore together!",
),
SizedBox(height: 35.h),
SizedBox(height: 10.h),
/// Footer Text
CustomText(

View File

@@ -216,13 +216,20 @@ class _ItineraryCompletionViewState extends State<ItineraryCompletionView>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Lottie.asset(
'assets/intro/itinerary_creating.json',
width: 260.w,
height: 260.w,
fit: BoxFit.contain,
),
SizedBox(height: 24.h),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: GoogleFonts.poppins(fontSize: 24.sp),
children: const [
TextSpan(
text: 'Building\n',
text: 'Creating\n',
style: TextStyle(
color: Color(0xFF364153),
fontWeight: FontWeight.bold,
@@ -238,13 +245,6 @@ class _ItineraryCompletionViewState extends State<ItineraryCompletionView>
],
),
),
SizedBox(height: 24.h),
Lottie.asset(
'assets/intro/itinerary_creating.json',
width: 260.w,
height: 260.w,
fit: BoxFit.contain,
),
],
),
),

View File

@@ -1,4 +1,4 @@
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/core/route_constants.dart';

View File

@@ -0,0 +1,40 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/check_in_repository.dart';
part 'check_in_event.dart';
part 'check_in_state.dart';
class CheckInBloc extends Bloc<CheckInEvent, CheckInState> {
final CheckInRepository _checkInRepository;
CheckInBloc({CheckInRepository? checkInRepository})
: _checkInRepository = checkInRepository ?? CheckInRepository(),
super(const CheckInInitial()) {
on<DoCheckInEvent>(_onDoCheckIn);
on<ResetCheckInEvent>(_onReset);
}
Future<void> _onDoCheckIn(
DoCheckInEvent event,
Emitter<CheckInState> emit,
) async {
emit(const CheckInLoading());
try {
final response = await _checkInRepository.checkIn(
passId: event.passId,
attractionId: event.attractionId,
);
emit(CheckInSuccess(data: response.data));
} catch (e) {
emit(CheckInFailure(error: e.toString()));
}
}
void _onReset(
ResetCheckInEvent event,
Emitter<CheckInState> emit,
) {
emit(const CheckInInitial());
}
}

View File

@@ -0,0 +1,27 @@
part of 'check_in_bloc.dart';
abstract class CheckInEvent extends Equatable {
const CheckInEvent();
@override
List<Object?> get props => [];
}
/// Trigger check-in
class DoCheckInEvent extends CheckInEvent {
final int passId;
final int attractionId;
const DoCheckInEvent({
required this.passId,
required this.attractionId,
});
@override
List<Object?> get props => [passId, attractionId];
}
/// Reset state back to initial
class ResetCheckInEvent extends CheckInEvent {
const ResetCheckInEvent();
}

View File

@@ -0,0 +1,38 @@
part of 'check_in_bloc.dart';
abstract class CheckInState extends Equatable {
const CheckInState();
@override
List<Object?> get props => [];
}
/// Initial state
class CheckInInitial extends CheckInState {
const CheckInInitial();
}
/// Loading state
class CheckInLoading extends CheckInState {
const CheckInLoading();
}
/// Success state
class CheckInSuccess extends CheckInState {
final dynamic data;
const CheckInSuccess({required this.data});
@override
List<Object?> get props => [data];
}
/// Failure state
class CheckInFailure extends CheckInState {
final String error;
const CheckInFailure({required this.error});
@override
List<Object?> get props => [error];
}

View File

@@ -1,35 +1,80 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'make_booking_events.dart';
import 'make_booking_state.dart';
import '../repository/make_booking_repository.dart'; // adjust path if needed
class MakeBookingBloc extends Bloc<MakeBookingEvent, MakeBookingState> {
final BookingRepository _repository = BookingRepository();
MakeBookingBloc() : super(const MakeBookingState(loading: true)) {
on<LoadAvailableDates>(_onLoadAvailableDates);
on<SelectDate>(_onSelectDate);
on<ConfirmBooking>(_onConfirmBooking); // NEW
}
void _onLoadAvailableDates(
LoadAvailableDates event, Emitter<MakeBookingState> emit) async {
emit(state.copyWith(loading: true));
// Simulate API load delay
await Future.delayed(const Duration(milliseconds: 500));
// Dummy available dates
final now = DateTime.now();
final available = [
now.add(const Duration(days: 2)),
now.add(const Duration(days: 5)),
now.add(const Duration(days: 7)),
now.add(const Duration(days: 10)),
now.add(const Duration(days: 11)),
now.add(const Duration(days: 13)),
];
// Parse "dd-MM-yyyy" → DateTime
final parts = event.validUpto.split('-');
final validUptoDate = DateTime(
int.parse(parts[2]), // year
int.parse(parts[1]), // month
int.parse(parts[0]), // day
);
emit(state.copyWith(availableDates: available, loading: false));
emit(state.copyWith(
availableDates: [],
validUptoDate: validUptoDate,
loading: false,
));
}
void _onSelectDate(SelectDate event, Emitter<MakeBookingState> emit) {
emit(state.copyWith(startDate: event.startDate, endDate: event.endDate));
}
}
// NEW — calls repository, emits isConfirmed + successMessage on success
Future<void> _onConfirmBooking(
ConfirmBooking event, Emitter<MakeBookingState> emit) async {
emit(state.copyWith(isConfirming: true, error: null));
try {
// Format DateTime → "yyyy-MM-dd" as required by API
final bookingStartDate = _formatDate(event.startDate);
final bookingEndDate = _formatDate(event.endDate);
final response = await _repository.confirmBookingDate(
attractionId: event.attractionId,
bookingId: event.bookingId,
bookingStartDate: bookingStartDate,
bookingEndDate: bookingEndDate,
);
// API response: { "message": "Your booking has been confirmed on 17-03-2026" }
final message = response['message'] as String? ?? 'Booking confirmed!';
emit(state.copyWith(
isConfirming: false,
isConfirmed: true,
successMessage: message,
));
} catch (e) {
emit(state.copyWith(
isConfirming: false,
error: e.toString(),
));
}
}
/// Formats DateTime to "yyyy-MM-dd" e.g. "2026-03-20"
String _formatDate(DateTime date) {
final y = date.year.toString().padLeft(4, '0');
final m = date.month.toString().padLeft(2, '0');
final d = date.day.toString().padLeft(2, '0');
return '$y-$m-$d';
}
}

View File

@@ -5,7 +5,14 @@ abstract class MakeBookingEvent extends Equatable {
List<Object?> get props => [];
}
class LoadAvailableDates extends MakeBookingEvent {}
class LoadAvailableDates extends MakeBookingEvent {
final String validUpto; // format: "dd-MM-yyyy" e.g. "21-03-2026"
LoadAvailableDates({required this.validUpto});
@override
List<Object?> get props => [validUpto];
}
class SelectDate extends MakeBookingEvent {
final DateTime startDate;
@@ -16,3 +23,21 @@ class SelectDate extends MakeBookingEvent {
@override
List<Object?> get props => [startDate, endDate];
}
// NEW — fired when user taps "Confirm Booking"
class ConfirmBooking extends MakeBookingEvent {
final int attractionId;
final int bookingId;
final DateTime startDate;
final DateTime endDate;
ConfirmBooking({
required this.attractionId,
required this.bookingId,
required this.startDate,
required this.endDate,
});
@override
List<Object?> get props => [attractionId, bookingId, startDate, endDate];
}

View File

@@ -5,12 +5,24 @@ class MakeBookingState extends Equatable {
final DateTime? startDate;
final DateTime? endDate;
final bool loading;
final DateTime? validUptoDate;
// NEW fields
final bool isConfirming; // true while API call is in-progress
final bool isConfirmed; // true once API returns success
final String? successMessage; // e.g. "Your booking has been confirmed on 17-03-2026"
final String? error; // non-null when API call fails
const MakeBookingState({
this.availableDates = const [],
this.startDate,
this.endDate,
this.loading = false,
this.validUptoDate,
this.isConfirming = false,
this.isConfirmed = false,
this.successMessage,
this.error,
});
MakeBookingState copyWith({
@@ -18,15 +30,35 @@ class MakeBookingState extends Equatable {
DateTime? startDate,
DateTime? endDate,
bool? loading,
DateTime? validUptoDate,
bool? isConfirming,
bool? isConfirmed,
String? successMessage,
String? error,
}) {
return MakeBookingState(
availableDates: availableDates ?? this.availableDates,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
loading: loading ?? this.loading,
validUptoDate: validUptoDate ?? this.validUptoDate,
isConfirming: isConfirming ?? this.isConfirming,
isConfirmed: isConfirmed ?? this.isConfirmed,
successMessage: successMessage ?? this.successMessage,
error: error ?? this.error,
);
}
@override
List<Object?> get props => [availableDates, startDate, endDate, loading];
}
List<Object?> get props => [
availableDates,
startDate,
endDate,
loading,
validUptoDate,
isConfirming,
isConfirmed,
successMessage,
error,
];
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../models/pass_attraction_details_model.dart';
import '../../repository/pass_attraction_details_repository.dart';
part 'pass_attraction_details_event.dart';
part 'pass_attraction_details_state.dart';
class PassAttractionDetailsBloc
extends Bloc<PassAttractionDetailsEvent, PassAttractionDetailsState> {
final PassAttractionDetailsRepository _repository;
PassAttractionDetailsBloc({PassAttractionDetailsRepository? repository})
: _repository = repository ?? PassAttractionDetailsRepository(),
super(PassAttractionDetailsInitial()) {
on<FetchPassAttractionDetailsEvent>(_onFetchPassAttractionDetails);
on<ResetPassAttractionDetailsEvent>(_onResetPassAttractionDetails);
}
/// Handle fetching attraction details
Future<void> _onFetchPassAttractionDetails(
FetchPassAttractionDetailsEvent event,
Emitter<PassAttractionDetailsState> emit,
) async {
emit(PassAttractionDetailsLoading());
try {
final PassAttractionDetailsModel attractionDetails =
await _repository.fetchPassAttractionDetails(
attractionId: event.attractionId,
bookingId: event.bookingId,
);
emit(PassAttractionDetailsLoaded(attractionDetails: attractionDetails));
} catch (e) {
emit(PassAttractionDetailsError(
message: e.toString(),
));
}
}
/// Handle resetting state back to initial
void _onResetPassAttractionDetails(
ResetPassAttractionDetailsEvent event,
Emitter<PassAttractionDetailsState> emit,
) {
emit(PassAttractionDetailsInitial());
}
}

View File

@@ -0,0 +1,15 @@
part of 'pass_attraction_details_bloc.dart';
abstract class PassAttractionDetailsEvent {}
class FetchPassAttractionDetailsEvent extends PassAttractionDetailsEvent {
final int attractionId;
final int bookingId;
FetchPassAttractionDetailsEvent({
required this.attractionId,
required this.bookingId,
});
}
class ResetPassAttractionDetailsEvent extends PassAttractionDetailsEvent {}

View File

@@ -0,0 +1,19 @@
part of 'pass_attraction_details_bloc.dart';
abstract class PassAttractionDetailsState {}
class PassAttractionDetailsInitial extends PassAttractionDetailsState {}
class PassAttractionDetailsLoading extends PassAttractionDetailsState {}
class PassAttractionDetailsLoaded extends PassAttractionDetailsState {
final PassAttractionDetailsModel attractionDetails;
PassAttractionDetailsLoaded({required this.attractionDetails});
}
class PassAttractionDetailsError extends PassAttractionDetailsState {
final String message;
PassAttractionDetailsError({required this.message});
}

View File

@@ -90,6 +90,7 @@ class Attraction {
final num? ticketPriceChild;
final String? bookingEmail;
final String? bookingPhoneNumber;
final bool isBookingRequired; // ✅ added
final String image;
Attraction({
@@ -100,6 +101,7 @@ class Attraction {
this.ticketPriceChild,
this.bookingEmail,
this.bookingPhoneNumber,
required this.isBookingRequired,
required this.image,
});
@@ -112,6 +114,7 @@ class Attraction {
ticketPriceChild: json?['ticketPriceChild'],
bookingEmail: json?['bookingEmail'],
bookingPhoneNumber: json?['bookingPhoneNumber'],
isBookingRequired: json?['isBookingRequired'] ?? false, // ✅ safe
image: json?['image'] ?? '',
);
}
@@ -125,6 +128,7 @@ class Attraction {
'ticketPriceChild': ticketPriceChild,
'bookingEmail': bookingEmail,
'bookingPhoneNumber': bookingPhoneNumber,
'isBookingRequired': isBookingRequired,
'image': image,
};
}

View File

@@ -0,0 +1,282 @@
class PassAttractionDetailsModel {
final int id;
final String title;
final String description;
final int cityXid;
final int? cardTypeXid;
final int partnerXid;
final String? productCode;
final String subTitle;
final String urlSlug;
final bool isBookingRequired;
final bool isPartnerAccess;
final String? bookingEmail;
final String? bookingPhoneNumber;
final String address;
final double latitudeCoordinate;
final double longitudeCoordinate;
final double ticketPriceAdult;
final double ticketPriceChild;
final int durations;
final int groupSize;
final String ageRange;
final String seoTitle;
final String seoDescription;
final String attractionStatus;
final bool isActive;
final String createdAt;
final String updatedAt;
final List<AttractionGallery> attractionGalleries;
final List<AttractionInclusion> attractionInclusions;
final List<AttractionFaq> attractionFaqs;
final Qr qr;
PassAttractionDetailsModel({
required this.id,
required this.title,
required this.description,
required this.cityXid,
this.cardTypeXid,
required this.partnerXid,
this.productCode,
required this.subTitle,
required this.urlSlug,
required this.isBookingRequired,
required this.isPartnerAccess,
this.bookingEmail,
this.bookingPhoneNumber,
required this.address,
required this.latitudeCoordinate,
required this.longitudeCoordinate,
required this.ticketPriceAdult,
required this.ticketPriceChild,
required this.durations,
required this.groupSize,
required this.ageRange,
required this.seoTitle,
required this.seoDescription,
required this.attractionStatus,
required this.isActive,
required this.createdAt,
required this.updatedAt,
required this.attractionGalleries,
required this.attractionInclusions,
required this.attractionFaqs,
required this.qr,
});
factory PassAttractionDetailsModel.fromJson(Map<String, dynamic> json) {
return PassAttractionDetailsModel(
id: json['id'] ?? 0,
title: json['title'] ?? "N/A",
description: json['description'] ?? "N/A",
cityXid: json['cityXid'] ?? 0,
cardTypeXid: json['cardTypeXid'],
partnerXid: json['partnerXid'] ?? 0,
productCode: json['productCode'],
subTitle: json['subTitle'] ?? "N/A",
urlSlug: json['urlSlug'] ?? "N/A",
isBookingRequired: json['isBookingRequired'] ?? false,
isPartnerAccess: json['isPartnerAccess'] ?? false,
bookingEmail: json['bookingEmail'],
bookingPhoneNumber: json['bookingPhoneNumber'],
address: json['address'] ?? "N/A",
latitudeCoordinate: (json['latitudeCoordinate'] is num)
? (json['latitudeCoordinate'] as num).toDouble()
: 0.0,
longitudeCoordinate: (json['longitudeCoordinate'] is num)
? (json['longitudeCoordinate'] as num).toDouble()
: 0.0,
ticketPriceAdult: (json['ticketPriceAdult'] is num)
? (json['ticketPriceAdult'] as num).toDouble()
: 0.0,
ticketPriceChild: (json['ticketPriceChild'] is num)
? (json['ticketPriceChild'] as num).toDouble()
: 0.0,
durations: json['durations'] ?? 0,
groupSize: json['groupSize'] ?? 0,
ageRange: json['ageRange'] ?? "N/A",
seoTitle: json['seoTitle'] ?? "N/A",
seoDescription: json['seoDescription'] ?? "N/A",
attractionStatus: json['attractionStatus'] ?? "N/A",
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] ?? "N/A",
updatedAt: json['updatedAt'] ?? "N/A",
attractionGalleries: List<AttractionGallery>.from(
(json['attractionGalleries'] ?? [])
.map((e) => AttractionGallery.fromJson(e)),
),
attractionInclusions: List<AttractionInclusion>.from(
(json['attractionInclusions'] ?? [])
.map((e) => AttractionInclusion.fromJson(e)),
),
attractionFaqs: List<AttractionFaq>.from(
(json['attractionFaqs'] ?? [])
.map((e) => AttractionFaq.fromJson(e)),
),
qr: json['qr'] != null ? Qr.fromJson(json['qr']) : Qr.empty(),
);
}
}
class AttractionGallery {
final int id;
final int attractionXid;
final String fileType;
final String filePathUrl;
final String altText;
final bool isCoverImage;
final bool isActive;
final String createdAt;
final String updatedAt;
AttractionGallery({
required this.id,
required this.attractionXid,
required this.fileType,
required this.filePathUrl,
required this.altText,
required this.isCoverImage,
required this.isActive,
required this.createdAt,
required this.updatedAt,
});
factory AttractionGallery.fromJson(Map<String, dynamic> json) {
return AttractionGallery(
id: json['id'] ?? 0,
attractionXid: json['attractionXid'] ?? 0,
fileType: json['fileType'] ?? "N/A",
filePathUrl: json['filePathUrl'] ?? "",
altText: json['altText'] ?? "",
isCoverImage: json['isCoverImage'] ?? false,
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] ?? "",
updatedAt: json['updatedAt'] ?? "",
);
}
}
class AttractionInclusion {
final int id;
final int attractionXid;
final String title;
final String description;
final int? iconXid;
final bool isInclusion;
final bool isActive;
final String createdAt;
final String updatedAt;
AttractionInclusion({
required this.id,
required this.attractionXid,
required this.title,
required this.description,
this.iconXid,
required this.isInclusion,
required this.isActive,
required this.createdAt,
required this.updatedAt,
});
factory AttractionInclusion.fromJson(Map<String, dynamic> json) {
return AttractionInclusion(
id: json['id'] ?? 0,
attractionXid: json['attractionXid'] ?? 0,
title: json['title'] ?? "",
description: json['description'] ?? "",
iconXid: json['iconXid'],
isInclusion: json['isInclusion'] ?? false,
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] ?? "",
updatedAt: json['updatedAt'] ?? "",
);
}
}
class AttractionFaq {
final int id;
final int attractionXid;
final String faqQuestion;
final String faqAnswer;
final int displayOrder;
final bool isActive;
AttractionFaq({
required this.id,
required this.attractionXid,
required this.faqQuestion,
required this.faqAnswer,
required this.displayOrder,
required this.isActive,
});
factory AttractionFaq.fromJson(Map<String, dynamic> json) {
return AttractionFaq(
id: json['id'] ?? 0,
attractionXid: json['attractionXid'] ?? 0,
faqQuestion: json['faqQuestion'] ?? "",
faqAnswer: json['faqAnswer'] ?? "",
displayOrder: json['displayOrder'] ?? 0,
isActive: json['isActive'] ?? false,
);
}
}
class Qr {
final String qrCode;
final String qrStatus;
final String qrExpiresAt;
final bool isQrActive;
final String qrNumber;
final String? checkedInDatetime;
final int qrRemainingMinutes;
final String validUpto;
Qr({
required this.qrCode,
required this.qrStatus,
required this.qrExpiresAt,
required this.isQrActive,
required this.qrNumber,
this.checkedInDatetime,
required this.qrRemainingMinutes,
required this.validUpto,
});
factory Qr.fromJson(Map<String, dynamic> json) {
return Qr(
qrCode: json['qrCode'] ?? "N/A",
qrStatus: json['qrStatus'] ?? "N/A",
qrExpiresAt: json['qrExpiresAt'] ?? "N/A",
isQrActive: json['isQrActive'] ?? false,
qrNumber: json['qrNumber'] ?? "N/A",
checkedInDatetime: json['checkedInDatetime'],
qrRemainingMinutes: json['qrRemainingMinutes'] ?? 0,
validUpto: json['validUpto'] ?? "N/A",
);
}
factory Qr.empty() {
return Qr(
qrCode: "N/A",
qrStatus: "N/A",
qrExpiresAt: "N/A",
isQrActive: false,
qrNumber: "N/A",
checkedInDatetime: null,
qrRemainingMinutes: 0,
validUpto: "N/A",
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
class CheckInRepository {
final NetworkApiService _apiService = NetworkApiService();
Future<Response> checkIn({
required int passId,
required int attractionId,
}) async {
final url = '${ApiUrls.checkIn}/$attractionId/$passId';
return await _apiService.postApi(url: url);
}
}

View File

@@ -0,0 +1,27 @@
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
class BookingRepository {
final NetworkApiService _apiServices = NetworkApiService();
Future<Map<String, dynamic>> confirmBookingDate({
required int attractionId,
required int bookingId,
required String bookingStartDate,
required String bookingEndDate,
}) async {
try {
final response = await _apiServices.postApi(
url: '${ApiUrls.booking}/$attractionId/$bookingId', // add this key in ApiUrls
data: {
"bookingStartDate": bookingStartDate,
"bookingEndDate": bookingEndDate,
},
);
return response.data as Map<String, dynamic>;
} catch (e) {
throw Exception('Failed to confirm booking date: $e');
}
}
}

View File

@@ -0,0 +1,18 @@
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
import '../models/pass_attraction_details_model.dart';
class PassAttractionDetailsRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch attraction details by attractionId
Future<PassAttractionDetailsModel> fetchPassAttractionDetails({
required int attractionId,
required int bookingId,
}) async {
final response = await _apiService.getApi(
url: '${ApiUrls.passAttractionDetails}/$attractionId/$bookingId',
);
return PassAttractionDetailsModel.fromJson(response.data);
}
}

View File

@@ -10,224 +10,392 @@ import 'package:syncfusion_flutter_datepicker/datepicker.dart';
import '../blocs/make_booking_bloc.dart';
import '../blocs/make_booking_events.dart';
import '../blocs/make_booking_state.dart';
import '../blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart';
class MakeBookingView extends StatelessWidget {
class MakeBookingView extends StatefulWidget {
final String title;
final String description;
final String validUpto;
final int attractionId;
final int bookingId;
const MakeBookingView({
super.key,
required this.title,
required this.description,
required this.validUpto,
required this.attractionId,
required this.bookingId,
});
@override
State<MakeBookingView> createState() => _MakeBookingViewState();
}
class _MakeBookingViewState extends State<MakeBookingView> {
// true = user tapped Confirm without selecting both dates → show red border + message
bool _showValidationError = false;
// true = user picked start date but hasn't picked end date yet → show orange hint
bool _onlyStartSelected = false;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => MakeBookingBloc()..add(LoadAvailableDates()),
child: BlocBuilder<MakeBookingBloc, MakeBookingState>(
builder: (context, state) {
if (state.loading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
create: (_) => MakeBookingBloc()
..add(LoadAvailableDates(validUpto: widget.validUpto)),
child: BlocListener<MakeBookingBloc, MakeBookingState>(
listenWhen: (previous, current) =>
previous.isConfirmed != current.isConfirmed ||
previous.error != current.error,
listener: (context, state) {
if (state.isConfirmed && state.successMessage != null) {
Navigator.of(context).pushReplacementNamed(
RouteConstants.bookingSuccessful,
arguments: state.successMessage,
);
// context.read<PassAttractionDetailsBloc>().add(
// FetchPassAttractionDetailsEvent(
// attractionId: widget.attractionId,
// bookingId: widget.bookingId,
// ),
// );
}
if (state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error!),
backgroundColor: Colors.red,
),
);
}
},
final bloc = context.read<MakeBookingBloc>();
final now = DateTime.now();
child: BlocBuilder<MakeBookingBloc, MakeBookingState>(
builder: (context, state) {
if (state.loading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
return SafeArea(
child: Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
final bloc = context.read<MakeBookingBloc>();
final now = DateTime.now();
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
backWidget(context, "Make Booking", Colors.black),
SizedBox(
height: 20.h,
),
final bool hasStartDate = state.startDate != null;
final bool hasEndDate = state.endDate != null;
final bool bothSelected = hasStartDate && hasEndDate;
// 🏝 Title
Text(
title,
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
return SafeArea(
child: Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: 20.w, vertical: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
),
SizedBox(height: 4.h),
backWidget(context, "Make Booking", Colors.black),
SizedBox(height: 20.h),
// 📄 Description
Text(
description,
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.black54,
Text(
widget.title,
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
SizedBox(height: 24.h),
SizedBox(height: 4.h),
// 📅 Calendar Container
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 10.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.06),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
Text(
widget.description,
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.black54,
),
),
child: Column(
children: [
Text(
"When are you visiting?",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
SizedBox(height: 24.h),
// ── Calendar Card ──────────────────────────────────────
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
vertical: 12.h, horizontal: 10.w),
decoration: BoxDecoration(
color: Colors.white,
// VALIDATION 1: red border when user tapped Confirm without both dates
border: _showValidationError
? Border.all(
color: Colors.red.shade300, width: 1.2)
: null,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.06),
blurRadius: 12,
offset: const Offset(0, 4),
),
),
SizedBox(height: 8.h),
// 🗓 SfDateRangePicker
SfDateRangePicker(
view: DateRangePickerView.month,
selectionMode: DateRangePickerSelectionMode.range,
minDate: now,
maxDate: now.add(const Duration(days: 365)),
enablePastDates: false,
backgroundColor: Colors.white,
showNavigationArrow: true,
// ✅ Put the background color here
headerStyle: DateRangePickerHeaderStyle(
backgroundColor: Colors.white, // <-- removes the purple strip
textAlign: TextAlign.center,
textStyle: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
],
),
monthViewSettings: DateRangePickerMonthViewSettings(
firstDayOfWeek: 7,
viewHeaderStyle: DateRangePickerViewHeaderStyle(
textStyle: GoogleFonts.poppins(
color: Colors.grey.shade600,
fontSize: 11.sp,
fontWeight: FontWeight.w500,
),
),
blackoutDates: _getUnavailableDates(state.availableDates, now),
),
monthCellStyle: DateRangePickerMonthCellStyle(
textStyle: GoogleFonts.poppins(fontSize: 12.sp, color: Colors.black87),
todayTextStyle: GoogleFonts.poppins(
fontSize: 12.sp, color: Colors.black, fontWeight: FontWeight.w500),
blackoutDateTextStyle: GoogleFonts.poppins(
fontSize: 12.sp, color: Colors.grey.shade400,
decoration: TextDecoration.lineThrough),
),
rangeTextStyle: GoogleFonts.poppins(
fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500),
startRangeSelectionColor: const Color(0xffFF5A5F),
endRangeSelectionColor: const Color(0xffFF5A5F),
rangeSelectionColor: const Color(0xffFF5A5F).withOpacity(0.15),
selectionTextStyle: GoogleFonts.poppins(
fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500),
initialSelectedRange: state.startDate != null && state.endDate != null
? PickerDateRange(state.startDate, state.endDate)
: null,
onSelectionChanged: (DateRangePickerSelectionChangedArgs args) {
if (args.value is PickerDateRange) {
final start = args.value.startDate;
final end = args.value.endDate;
if (start != null && end != null) {
bloc.add(SelectDate(start, end));
}
}
},
),
],
),
),
SizedBox(height: 40.h),
// ✅ Confirm button
GestureDetector(
onTap: () {
if (state.startDate != null && state.endDate != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Booking confirmed from "
"${state.startDate!.toLocal().toString().split(' ')[0]} "
"to ${state.endDate!.toLocal().toString().split(' ')[0]}",
child: Column(
children: [
Text(
"When are you visiting?",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
);
Navigator.of(context).pushNamed(RouteConstants.bookingSuccessful);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a valid date range"),
SizedBox(height: 8.h),
SfDateRangePicker(
view: DateRangePickerView.month,
selectionMode:
DateRangePickerSelectionMode.range,
minDate: now.add(const Duration(days: 1)),
maxDate: state.validUptoDate ??
now.add(const Duration(days: 365)),
enablePastDates: false,
backgroundColor: Colors.white,
showNavigationArrow: true,
headerStyle: DateRangePickerHeaderStyle(
backgroundColor: Colors.white,
textAlign: TextAlign.center,
textStyle: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
monthViewSettings:
DateRangePickerMonthViewSettings(
firstDayOfWeek: 7,
viewHeaderStyle:
DateRangePickerViewHeaderStyle(
textStyle: GoogleFonts.poppins(
color: Colors.grey.shade600,
fontSize: 11.sp,
fontWeight: FontWeight.w500,
),
),
blackoutDates: const [],
),
monthCellStyle: DateRangePickerMonthCellStyle(
textStyle: GoogleFonts.poppins(
fontSize: 12.sp, color: Colors.black87),
todayTextStyle: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.black,
fontWeight: FontWeight.w500),
blackoutDateTextStyle: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.grey.shade400,
decoration: TextDecoration.lineThrough),
),
rangeTextStyle: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.white,
fontWeight: FontWeight.w500),
startRangeSelectionColor:
const Color(0xffFF5A5F),
endRangeSelectionColor:
const Color(0xffFF5A5F),
rangeSelectionColor:
const Color(0xffFF5A5F).withOpacity(0.15),
selectionTextStyle: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.white,
fontWeight: FontWeight.w500),
initialSelectedRange: bothSelected
? PickerDateRange(
state.startDate, state.endDate)
: null,
// VALIDATION 2: detect partial vs full selection
onSelectionChanged:
(DateRangePickerSelectionChangedArgs args) {
if (args.value is PickerDateRange) {
final start = args.value.startDate;
final end = args.value.endDate;
if (start != null && end != null) {
// ✅ Both selected — clear all error states
setState(() {
_onlyStartSelected = false;
_showValidationError = false;
});
bloc.add(SelectDate(start, end));
} else if (start != null && end == null) {
// ⚠️ Only start tapped — guide user to pick end
setState(() {
_onlyStartSelected = true;
_showValidationError = false;
});
} else {
// Selection cleared
setState(() {
_onlyStartSelected = false;
});
}
}
},
),
);
}
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 14.h),
decoration: BoxDecoration(
color: const Color(0xffFF5A5F),
borderRadius: BorderRadius.circular(30.r),
// VALIDATION 3: inline hint message below calendar
if (_onlyStartSelected || _showValidationError)
Padding(
padding: EdgeInsets.only(
top: 8.h, left: 4.w, right: 4.w),
child: Row(
children: [
Icon(
Icons.info_outline_rounded,
size: 14.sp,
// orange for guidance, red for submit error
color: _showValidationError
? Colors.red
: Colors.orange.shade700,
),
SizedBox(width: 6.w),
Expanded(
child: Text(
_showValidationError && !hasStartDate
? "Please select a check-in date to continue"
: _showValidationError &&
hasStartDate &&
!hasEndDate
? "Please also select a check-out date"
: "Now tap an end date to complete your range",
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: _showValidationError
? Colors.red
: Colors.orange.shade700,
),
),
),
],
),
),
],
),
child: Center(
child: Text(
"Confirm Booking",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
// VALIDATION 4: selected range summary chip (only when both selected)
if (bothSelected) ...[
SizedBox(height: 12.h),
Center(
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 14.w, vertical: 8.h),
decoration: BoxDecoration(
color:
const Color(0xffFF5A5F).withOpacity(0.08),
borderRadius: BorderRadius.circular(10.r),
border: Border.all(
color: const Color(0xffFF5A5F)
.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle_outline_rounded,
size: 15.sp,
color: const Color(0xffFF5A5F)),
SizedBox(width: 6.w),
Text(
"${_fmt(state.startDate!)}${_fmt(state.endDate!)}",
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: const Color(0xffFF5A5F),
),
),
],
),
),
), // close Center
],
SizedBox(height: 40.h),
// ── Confirm Button ─────────────────────────────────────
GestureDetector(
onTap: state.isConfirming
? null
: () {
// VALIDATION 5: check both dates before firing API
if (!hasStartDate || !hasEndDate) {
setState(() {
_showValidationError = true;
_onlyStartSelected = false;
});
return; // stop here — don't call API
}
// ✅ Both dates present — dispatch to bloc
bloc.add(ConfirmBooking(
attractionId: widget.attractionId,
bookingId: widget.bookingId,
startDate: state.startDate!,
endDate: state.endDate!,
));
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 14.h),
decoration: BoxDecoration(
color: state.isConfirming
? const Color(0xffFF5A5F).withOpacity(0.6)
: const Color(0xffFF5A5F),
borderRadius: BorderRadius.circular(30.r),
),
child: Center(
child: state.isConfirming
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
"Confirm Booking",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
],
],
),
),
),
),
);
},
);
},
),
),
);
}
/// Marks unavailable days (those not in availableDates) as blackout
List<DateTime> _getUnavailableDates(List<DateTime> available, DateTime start) {
final end = start.add(const Duration(days: 365));
final allDays = List.generate(
end.difference(start).inDays,
(i) => DateTime(start.year, start.month, start.day + i),
);
return allDays
.where((day) => !available.any((a) =>
a.year == day.year && a.month == day.month && a.day == day.day))
.toList();
/// "20 Mar 2026"
String _fmt(DateTime d) {
const months = [
'Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov','Dec'
];
return '${d.day} ${months[d.month - 1]} ${d.year}';
}
}
}

View File

@@ -6,7 +6,10 @@ import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/back_widget.dart';
class BookingSuccessfulPageView extends StatelessWidget {
const BookingSuccessfulPageView({super.key});
final String message;
const BookingSuccessfulPageView({super.key, required this.message});
@override
Widget build(BuildContext context) {
@@ -39,7 +42,7 @@ class BookingSuccessfulPageView extends StatelessWidget {
SizedBox(height: 20.h),
Text(
"Your booking has been Confirmed on 08/01/2025",
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.sp,

View File

@@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:ui';
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';
@@ -7,30 +10,92 @@ 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:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import '../../attraction_details/bloc/attraction_details_bloc.dart';
import '../../attraction_details/bloc/attraction_details_event.dart';
import '../../attraction_details/bloc/attraction_details_state.dart';
import '../../attraction_details/repository/attraction_details_repository.dart';
import '../../core/route_constants.dart';
import '../blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart';
import '../repository/pass_attraction_details_repository.dart';
import '../widgets/check_in_bottom_sheet.dart';
import '../widgets/how_to_redeem_bottomsheet.dart';
class PassAttractionDetailsView extends StatelessWidget {
final int? attractionId;
class PassAttractionDetailsView extends StatefulWidget {
final int attractionId;
final int bookingId;
const PassAttractionDetailsView({
super.key,
required this.attractionId,
required this.bookingId,
});
@override
State<PassAttractionDetailsView> createState() =>
_PassAttractionDetailsViewState();
}
class _PassAttractionDetailsViewState extends State<PassAttractionDetailsView> {
bool _isCheckedIn = false;
int _remainingSeconds = 0;
Timer? _countdownTimer;
@override
void dispose() {
_countdownTimer?.cancel();
super.dispose();
}
void _startCountdown(int minutes) {
_countdownTimer?.cancel();
setState(() {
_isCheckedIn = true;
_remainingSeconds = minutes * 60;
});
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) {
timer.cancel();
return;
}
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
timer.cancel();
if (!mounted) return;
setState(() {
_isCheckedIn = false;
});
context.read<PassAttractionDetailsBloc>().add(
FetchPassAttractionDetailsEvent(
attractionId: widget.attractionId,
bookingId: widget.bookingId,
),
);
}
});
});
}
String get _timerLabel {
final m = _remainingSeconds ~/ 60;
final s = _remainingSeconds % 60;
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => AttractionDetailsBloc(
repository: AttractionDetailsRepository(),
)..add(FetchAttractionDetails(attractionId: attractionId??0)),
child: BlocBuilder<AttractionDetailsBloc, AttractionDetailsState>(
create: (_) =>
PassAttractionDetailsBloc(
repository: PassAttractionDetailsRepository(),
)..add(
FetchPassAttractionDetailsEvent(
attractionId: widget.attractionId ?? 0,
bookingId: widget.bookingId ?? 0,
),
),
child: BlocBuilder<PassAttractionDetailsBloc, PassAttractionDetailsState>(
builder: (context, state) {
if (state is AttractionDetailsLoading) {
if (state is PassAttractionDetailsLoading) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
@@ -39,25 +104,22 @@ class PassAttractionDetailsView extends StatelessWidget {
);
}
if (state is AttractionDetailsError) {
if (state is PassAttractionDetailsError) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text(
state.message,
style: TextStyle(color: Colors.red),
),
child: Text(state.message, style: TextStyle(color: Colors.red)),
),
);
}
if (state is AttractionDetailsLoaded) {
if (state is PassAttractionDetailsLoaded) {
final attraction = state.attractionDetails;
final coverImage = attraction.attractionGalleries
.firstWhere(
(gallery) => gallery.isCoverImage,
orElse: () => attraction.attractionGalleries.first,
)
orElse: () => attraction.attractionGalleries.first,
)
.filePathUrl;
return Scaffold(
@@ -90,7 +152,9 @@ class PassAttractionDetailsView extends StatelessWidget {
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.w, vertical: 10.h),
horizontal: 20.w,
vertical: 10.h,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -133,7 +197,8 @@ class PassAttractionDetailsView extends StatelessWidget {
Positioned(
bottom: 31.h,
left: 12.w,
right: 60.w, // Add this - leaves space for share button
right: 60
.w, // Add this - leaves space for share button
child: Text(
attraction.title,
style: TextStyle(
@@ -177,7 +242,10 @@ class PassAttractionDetailsView extends StatelessWidget {
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h),
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 24.h,
),
child: Container(
width: double.infinity,
padding: EdgeInsets.all(20.w),
@@ -191,14 +259,48 @@ class PassAttractionDetailsView extends StatelessWidget {
),
child: Column(
children: [
if (_isCheckedIn)
Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 6.h,
),
decoration: BoxDecoration(
color: const Color(
0xFFF95F62,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: const Color(0xFFF95F62),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.access_time_rounded,
color: const Color(0xFFF95F62),
size: 16.sp,
),
SizedBox(width: 6.w),
Text(
_timerLabel,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFFF95F62),
),
),
],
),
),
Text(
"Scan this at the site of the attraction",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Color(0xFFF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 20.h),
// QR Code Image
@@ -208,11 +310,15 @@ class PassAttractionDetailsView extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Image.asset(
'assets/images/qr_image.png',
height: 200.h,
width: 200.w,
fit: BoxFit.contain,
child: ImageFiltered(
imageFilter: _isCheckedIn
? ImageFilter.blur(sigmaX: 0, sigmaY: 0) // clear when checked in
: ImageFilter.blur(sigmaX: 6, sigmaY: 6), // blurred before check-in
child: QrImageView(
data: "Details:\nQR No. : ${attraction.qr.qrNumber}\nQR Code : ${attraction.qr.qrCode}\nStatus : ${attraction.qr.qrStatus}\nExpires At: ${attraction.qr.qrExpiresAt}\nChecked In: ${attraction.qr.checkedInDatetime}\nRemaining : ${attraction.qr.qrRemainingMinutes} mins\nIs Active : ${attraction.qr.isQrActive ? "Yes" : "No"}",
version: QrVersions.auto,
size: 200.w,
),
),
),
SizedBox(height: 16.h),
@@ -221,7 +327,7 @@ class PassAttractionDetailsView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"IYFHHVN254ADSD",
attraction.qr.qrNumber,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
@@ -232,10 +338,18 @@ class PassAttractionDetailsView extends StatelessWidget {
SizedBox(width: 8.w),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: "IYFHHVN254ADSD"));
ScaffoldMessenger.of(context).showSnackBar(
Clipboard.setData(
ClipboardData(
text: attraction.qr.qrNumber,
),
);
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text('Code copied to clipboard'),
content: Text(
'Code copied to clipboard',
),
duration: Duration(seconds: 2),
backgroundColor: Color(0xFFF95F62),
),
@@ -251,27 +365,92 @@ class PassAttractionDetailsView extends StatelessWidget {
),
SizedBox(height: 20.h),
// Check in Button
// AFTER
SizedBox(
width: double.infinity,
height: 50.h,
child: ElevatedButton(
onPressed: () {
// Add your check-in logic here
},
onPressed: _isCheckedIn
? null // ← not tappable after check-in
: () async {
final result =
await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top: Radius.circular(
20.r,
),
),
),
builder: (_) =>
CheckInBottomSheet(
attractionName:
attraction.title,
minuteTime: attraction
.qr
.qrRemainingMinutes,
bookingId:
widget.bookingId,
attractionId:
widget.attractionId,
),
);
if (result == true) {
context
.read<
PassAttractionDetailsBloc
>()
.add(
FetchPassAttractionDetailsEvent(
attractionId:
widget.attractionId,
bookingId: widget.bookingId,
),
);
_startCountdown(
attraction.qr.qrRemainingMinutes,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFF95F62),
backgroundColor: _isCheckedIn
? Colors
.grey
.shade400 // ← greyed out
: const Color(0xFFF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.r),
),
elevation: 0,
disabledBackgroundColor:
Colors.grey.shade400,
),
child: Text(
"Check in",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isCheckedIn) ...[
Icon(
Icons.check_circle_outline,
color: Colors.white,
size: 18.sp,
),
SizedBox(width: 8.w),
],
Text(
_isCheckedIn
? "Checked In"
: "Check in",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
@@ -289,7 +468,18 @@ class PassAttractionDetailsView extends StatelessWidget {
),
GestureDetector(
onTap: () {
// Add your help/support navigation here
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20.r),
),
),
builder: (_) => HowToRedeemBottomSheet(
attractionName: attraction.title,
),
);
},
child: Text(
"Click Here",
@@ -310,8 +500,7 @@ class PassAttractionDetailsView extends StatelessWidget {
// About Section
Padding(
padding:
EdgeInsets.only(left: 16.w, right: 16.w,),
padding: EdgeInsets.only(left: 16.w, right: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -371,7 +560,7 @@ class PassAttractionDetailsView extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [
CustomText(
text: "Contact Number",
@@ -381,7 +570,9 @@ class PassAttractionDetailsView extends StatelessWidget {
),
SizedBox(height: 6.h),
CustomText(
text: attraction.bookingPhoneNumber??"N/A",
text:
attraction.bookingPhoneNumber ??
"N/A",
color: Colors.black,
size: 14.sp,
weight: FontWeight.w600,
@@ -420,7 +611,7 @@ class PassAttractionDetailsView extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [
CustomText(
text: "Email",
@@ -430,7 +621,8 @@ class PassAttractionDetailsView extends StatelessWidget {
),
SizedBox(height: 6.h),
CustomText(
text: attraction.bookingEmail??"N/A",
text:
attraction.bookingEmail ?? "N/A",
color: Colors.black,
size: 14.sp,
weight: FontWeight.w600,
@@ -451,8 +643,16 @@ class PassAttractionDetailsView extends StatelessWidget {
SizedBox(height: 16.h),
InkWell(
onTap: () {
Navigator.of(context)
.pushNamed(RouteConstants.makeBooking);
Navigator.of(context).pushNamed(
RouteConstants.makeBooking,
arguments: {
"title": attraction.title,
"description": attraction.description,
"validUpto": attraction.qr.validUpto,
"attractionId": attraction.id,
"bookingId": widget.bookingId,
},
);
},
child: Container(
padding: EdgeInsets.symmetric(
@@ -465,12 +665,12 @@ class PassAttractionDetailsView extends StatelessWidget {
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [
CustomText(
text: "Via CityCards",
@@ -516,11 +716,11 @@ class PassAttractionDetailsView extends StatelessWidget {
.where((inclusion) => inclusion.isInclusion)
.map(
(inclusion) => includedBox(
"assets/icons/bus.png",
inclusion.title,
inclusion.description,
),
)
"assets/icons/bus.png",
inclusion.title,
inclusion.description,
),
)
.toList(),
),
SizedBox(height: 30.h),
@@ -559,13 +759,17 @@ class PassAttractionDetailsView extends StatelessWidget {
),
initialZoom: 15.0,
interactionOptions: InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
flags:
InteractiveFlag.all &
~InteractiveFlag.rotate,
),
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.citycards_customer',
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName:
'com.example.citycards_customer',
),
MarkerLayer(
markers: [
@@ -616,7 +820,6 @@ class PassAttractionDetailsView extends StatelessWidget {
);
}).toList(),
),
],
),
),
@@ -630,9 +833,7 @@ class PassAttractionDetailsView extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text("Something went wrong"),
),
body: Center(child: Text("Something went wrong")),
);
},
),
@@ -680,10 +881,7 @@ class PassAttractionDetailsView extends StatelessWidget {
);
}
Widget faqBox({
required String title,
required String desc,
}) {
Widget faqBox({required String title, required String desc}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
decoration: BoxDecoration(
@@ -713,13 +911,9 @@ class PassAttractionDetailsView extends StatelessWidget {
],
),
SizedBox(height: 9.h),
CustomText(
text: desc,
size: 11.sp,
color: const Color(0xFF7D7D7D),
),
CustomText(text: desc, size: 11.sp, color: const Color(0xFF7D7D7D)),
],
),
);
}
}
}

View File

@@ -14,11 +14,13 @@ import '../repository/my_passes_attractions_repository.dart';
class PassAttractionsPage extends StatelessWidget {
final int cityXid;
final int bookingId;
final String source;
const PassAttractionsPage({
super.key,
required this.cityXid,
required this.bookingId,
required this.source,
});
@@ -156,7 +158,7 @@ class PassAttractionsPage extends StatelessWidget {
children: state.filteredAttractions
.map(
(attraction) => PassAttractionCard(
attraction: attraction,
attraction: attraction, bookingId: bookingId,
),
)
.toList(),

View File

@@ -229,7 +229,10 @@ class _PassDetailsViewState extends State<PassDetailsView> {
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.passAttractionDetails,
arguments: attraction.id,
arguments: {
'attractionId': attraction.id,
'bookingId': widget.bookingId, // pass your actual bookingId here
},
);
},
child: _attractionCard(
@@ -238,9 +241,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
image: attraction.image,
ticketPriceAdult: attraction.ticketPriceAdult,
ticketPriceChild: attraction.ticketPriceChild,
bookingEmail: attraction.bookingEmail,
bookingPhoneNumber:
attraction.bookingPhoneNumber,
isBookingRequired: attraction.isBookingRequired,
),
),
),
@@ -252,8 +253,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
image: '',
ticketPriceAdult: null,
ticketPriceChild: null,
bookingEmail: null,
bookingPhoneNumber: null,
isBookingRequired: false,
),
],
SizedBox(height: 16.h),
@@ -261,7 +261,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
Navigator.pushNamed(
context,
RouteConstants.passAttractionsPage,
arguments: {'cityId': city?.id, 'source': 'my_passes'},
arguments: {'cityId': city?.id, 'source': 'my_passes', 'bookingId': widget.bookingId},
);
}),
@@ -416,14 +416,8 @@ class _PassDetailsViewState extends State<PassDetailsView> {
required String image,
num? ticketPriceAdult,
num? ticketPriceChild,
String? bookingEmail,
String? bookingPhoneNumber,
required bool isBookingRequired,
}) {
// Check if booking is required (both email and phone are empty/null)
final bool isBookingRequired =
(bookingEmail == null || bookingEmail.isEmpty) &&
(bookingPhoneNumber == null || bookingPhoneNumber.isEmpty);
// Format the price display
String priceText = ticketPriceAdult != null
? "\$$ticketPriceAdult/person"
@@ -437,36 +431,34 @@ class _PassDetailsViewState extends State<PassDetailsView> {
),
child: Row(
children: [
/// 🔥 Attraction Image (Real Image Style Box)
/// 🔥 Attraction Image
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: image.isNotEmpty
? CachedNetworkImage(
imageUrl: image,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
placeholder: (context, url) => Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
errorWidget: (context, url, error) => Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
)
imageUrl: image,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
placeholder: (context, url) => Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
errorWidget: (context, url, error) => Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
)
: Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
),
SizedBox(width: 12.w),
@@ -482,6 +474,8 @@ class _PassDetailsViewState extends State<PassDetailsView> {
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2.h),
@@ -508,7 +502,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
SizedBox(height: 6.h),
// Show "Booking Required" tag only if both email and phone are null/empty
/// 🔥 Booking Required Tag
if (isBookingRequired)
Container(
padding: EdgeInsets.symmetric(
@@ -516,14 +510,16 @@ class _PassDetailsViewState extends State<PassDetailsView> {
vertical: 4.h,
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
color: const Color(0xffC1D2F8),
border: Border.all(color: const Color(0xff2563EB)),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
"Booking Required",
style: GoogleFonts.poppins(
fontSize: 10.sp,
color: Colors.blue.shade700,
fontSize: 11.sp,
color: const Color(0xff1A1A1A),
fontWeight: FontWeight.w400,
),
),
),
@@ -533,12 +529,12 @@ class _PassDetailsViewState extends State<PassDetailsView> {
SizedBox(width: 8.w),
/// 🔥 QR Code Circle (Proper UI like Design)
/// 🔥 QR Code Circle
Container(
height: 44.w,
width: 44.w,
decoration: BoxDecoration(
color: const Color(0xffF8EDED), // light pink circle bg
decoration: const BoxDecoration(
color: Color(0xffF8EDED),
shape: BoxShape.circle,
),
child: Padding(

View File

@@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/custom_filled_button.dart';
import '../blocs/checkIn/check_in_bloc.dart';
import '../repository/check_in_repository.dart';
class CheckInBottomSheet extends StatelessWidget {
final String attractionName;
final int minuteTime;
final int bookingId;
final int attractionId;
const CheckInBottomSheet({
super.key,
required this.attractionName,
required this.minuteTime,
required this.bookingId,
required this.attractionId,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CheckInBloc(checkInRepository: CheckInRepository()),
child: BlocConsumer<CheckInBloc, CheckInState>(
listener: (context, state) {
if (state is CheckInSuccess) {
Navigator.pop(context, true); // close sheet
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Checked In Successful"),
backgroundColor: const Color(0xFF22C55E),
behavior: SnackBarBehavior.floating,
),
);
} else if (state is CheckInFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error),
backgroundColor: const Color(0xFFF95F62),
behavior: SnackBarBehavior.floating,
),
);
}
},
builder: (context, state) {
final isLoading = state is CheckInLoading;
return AnimatedPadding(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
padding: EdgeInsets.only(
top: 24.h,
left: 20.w,
right: 20.w,
bottom: MediaQuery.of(context).viewInsets.bottom + 24.h,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// --- Drag Handle ---
Container(
height: 4.h,
width: 40.w,
decoration: BoxDecoration(
color: const Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 20.h),
/// --- Title ---
CustomText(
text: "Ready to check in?",
size: 22.sp,
weight: FontWeight.w700,
),
SizedBox(height: 16.h),
/// --- Subtitle with attraction name highlighted ---
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xFF6B7280),
height: 1.5,
),
children: [
const TextSpan(
text: "Only activate when you are at the entrance of ",
),
TextSpan(
text: "$attractionName.",
style: TextStyle(
color: const Color(0xFFF95F62),
fontWeight: FontWeight.w700,
fontSize: 15.sp,
),
),
],
),
),
SizedBox(height: 20.h),
/// --- Timer Info Card ---
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 14.h,
),
decoration: BoxDecoration(
color: const Color(0xFFF95F62).withOpacity(0.08),
borderRadius: BorderRadius.circular(14.r),
border: Border.all(color: const Color(0xFFF95F62)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 36.h,
width: 36.w,
decoration: BoxDecoration(
color: const Color(0xFFF95F62).withOpacity(0.12),
shape: BoxShape.circle,
),
child: Center(
child: Icon(
Icons.access_time_rounded,
color: const Color(0xFFF95F62),
size: 20.sp,
),
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "$minuteTime minute timer",
size: 15.sp,
weight: FontWeight.w700,
color: const Color(0xFFF95F62),
),
SizedBox(height: 4.h),
CustomText(
text:
"Once activated, the pass is valid for $minuteTime minutes. This action cannot be undone",
size: 11.sp,
weight: FontWeight.w400,
color: const Color(0xFFF95F62).withOpacity(0.8),
maxLines: 3,
),
],
),
),
],
),
),
SizedBox(height: 24.h),
/// --- Activate Button ---
isLoading
? SizedBox(
height: 52.h,
child: const Center(
child: CircularProgressIndicator(
color: Color(0xFFF95F62),
),
),
)
: CustomFilledButton(
label: "Activate Pass Now",
width: double.infinity,
height: 52.h,
showArrow: true,
onTap: () {
context.read<CheckInBloc>().add(
DoCheckInEvent(
passId: bookingId,
attractionId: attractionId,
),
);
},
),
SizedBox(height: 16.h),
/// --- Dismiss Text Button ---
GestureDetector(
onTap: isLoading ? null : () => Navigator.pop(context),
child: CustomText(
text: "I'm not at the entrance yet",
size: 14.sp,
weight: FontWeight.w500,
color: isLoading
? const Color(0xFFF95F62).withOpacity(0.4)
: const Color(0xFFF95F62),
),
),
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../core/route_constants.dart';
class HowToRedeemBottomSheet extends StatelessWidget {
final String attractionName;
const HowToRedeemBottomSheet({
super.key,
required this.attractionName,
});
@override
Widget build(BuildContext context) {
return AnimatedPadding(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
padding: EdgeInsets.only(
top: 24.h,
left: 20.w,
right: 20.w,
bottom: MediaQuery.of(context).viewInsets.bottom + 24.h,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// --- Drag Handle ---
Container(
height: 4.h,
width: 40.w,
decoration: BoxDecoration(
color: const Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
SizedBox(height: 20.h),
/// --- Title ---
CustomText(
text: "How to redeem my attraction pass?",
size: 20.sp,
weight: FontWeight.w700,
textAlign: TextAlign.center,
),
SizedBox(height: 20.h),
/// --- Body with attraction name highlighted ---
RichText(
textAlign: TextAlign.start,
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xFF3D3D3D),
height: 1.6,
),
children: [
const TextSpan(
text:
"To redeem your attraction pass, present the QR code at the entrance. Our staff will scan it, granting you access to the wonders within. Enjoy your adventure at ",
),
TextSpan(
text: "$attractionName!",
style: GoogleFonts.poppins(
color: const Color(0xFFF95F62),
fontWeight: FontWeight.w700,
fontSize: 14.sp,
),
),
],
),
),
SizedBox(height: 24.h),
/// --- Trouble text ---
CustomText(
text: "Having trouble redeeming the pass?",
size: 14.sp,
weight: FontWeight.w400,
color: Colors.black54,
textAlign: TextAlign.center,
),
SizedBox(height: 16.h),
/// --- Contact Support Button ---
CustomFilledButton(
label: "Contact Support",
width: double.infinity,
height: 52.h,
onTap: () {
Navigator.pushNamed(context, RouteConstants.contactUs);
},
),
],
),
);
}
}

View File

@@ -8,7 +8,8 @@ import '../../core/route_constants.dart';
class PassAttractionCard extends StatelessWidget {
final Attraction attraction;
const PassAttractionCard({super.key, required this.attraction});
final int bookingId;
const PassAttractionCard({super.key, required this.attraction, required this.bookingId});
@override
Widget build(BuildContext context) {
@@ -22,10 +23,11 @@ class PassAttractionCard extends StatelessWidget {
final String imageUrl = attraction.coverImageUrl;
/// Show "Booking Required" when both email and phone are empty/null
final bool showBookingRequired =
(attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) ||
(attraction.bookingPhoneNumber.isEmpty ||
attraction.bookingPhoneNumber == null);
// final bool showBookingRequired =
// (attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) ||
// (attraction.bookingPhoneNumber.isEmpty ||
// attraction.bookingPhoneNumber == null);
final bool showBookingRequired = attraction.isBookingRequired;
/// Format the price display
String priceText = attraction.ticketPriceAdult != null
@@ -36,7 +38,10 @@ class PassAttractionCard extends StatelessWidget {
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.passAttractionDetails,
arguments: attraction.id,
arguments: {
'attractionId': attraction.id,
'bookingId': bookingId, // pass your actual bookingId here
},
);
},
child: Container(

View File

@@ -1,17 +1,17 @@
class ApiUrls {
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";
static const cityList = "$baseUrl/mobile/city_list";
// static const upcomingCityList = "$baseUrl/mobile/upcoming_cities";
static const searchCityList = "$baseUrl/mobile/city-selection";
static const attractionsList = "$baseUrl/mobile/list/all";
static const passAttractionsList = "$baseUrl/mobile/passes/mobile/list";
static const attractionDetails = "$baseUrl/mobile/list";
static const passAttractionDetails = "$baseUrl/mobile/passes/attractionDetail";
static const home = "$baseUrl/mobile";
static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data";
static const userProfile = "$baseUrl/mobile/user";
@@ -37,5 +37,7 @@ class ApiUrls {
static const submitTicket = "$baseUrl/mobile/user/support";
static const createPostCard = "$baseUrl/mobile/postcards";
static const addToCartPasses = "$baseUrl/mobile/passes/add-to-cart";
static const checkIn = "$baseUrl/mobile/passes/start-checkin";
static const booking = "$baseUrl/mobile/passes/booking-date-confirm";
static const createItinerary = "$baseUrl/mobile/itinerary";
}

View File

@@ -19,8 +19,8 @@ class NetworkApiService {
NetworkApiService._internal() {
_dio = Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',

View File

@@ -239,6 +239,7 @@ class PostcardCreationBloc
userProfileFullName: event.fullName,
userProfileEmail: event.email,
userProfilePhone: event.phone,
isdCode: event.isdCode,
userProfileAddress: event.address,
userProfileCity: event.city,
userProfileState: event.state,

View File

@@ -82,6 +82,7 @@ class StoreUserProfileData extends PostcardCreationEvent {
final String? fullName;
final String? email;
final String? phone;
final String? isdCode;
final String? address;
final String? city;
final String? state;
@@ -92,6 +93,7 @@ class StoreUserProfileData extends PostcardCreationEvent {
this.fullName,
this.email,
this.phone,
this.isdCode,
this.address,
this.city,
this.state,

View File

@@ -15,6 +15,7 @@ class PostcardCreationState {
final String? fullName;
final String? emailId;
final String? phoneNumber;
final String? isdCode;
final String address;
final String? city;
final String? country;
@@ -51,6 +52,7 @@ class PostcardCreationState {
this.fullName,
this.emailId,
this.phoneNumber,
this.isdCode,
this.city,
this.country,
this.state,
@@ -86,6 +88,7 @@ class PostcardCreationState {
String? fullName,
String? emailId,
String? phoneNumber,
String? isdCode,
String? address,
String? city,
String? country,
@@ -120,6 +123,7 @@ class PostcardCreationState {
fullName: fullName ?? this.fullName,
emailId: emailId ?? this.emailId,
phoneNumber: phoneNumber ?? this.phoneNumber,
isdCode: isdCode ?? this.isdCode,
address: address ?? this.address,
city: city ?? this.city,
country: country ?? this.country,

View File

@@ -5,6 +5,7 @@ import 'package:citycards_customer/postcard/widgets/front_card_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../StripePayment/bloc/stripe_payment_bloc.dart';
import '../../StripePayment/bloc/stripe_payment_event.dart';
@@ -128,251 +129,105 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
/// 🆕 Handle payment flow with client secret
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret) async {
// Show payment bottom sheet with BLoC
final paymentSuccess = await showModalBottomSheet<bool>(
context: context,
isDismissible: false,
enableDrag: false,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (bottomSheetContext) {
return BlocProvider(
create: (_) => StripePaymentBloc(stripeService: StripeService())
..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)),
child: BlocConsumer<StripePaymentBloc, StripePaymentState>(
listener: (context, state) {
if (state is StripePaymentSuccess) {
Navigator.of(bottomSheetContext).pop(true);
} else if (state is StripePaymentFailure || state is StripePaymentCancelled) {
Navigator.of(bottomSheetContext).pop(false);
}
},
builder: (context, state) {
return Container(
height: MediaQuery.of(context).size.height * 0.5,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (state is StripePaymentLoading) ...[
const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFFF95F62),
),
),
const SizedBox(height: 24),
const Text(
"Processing payment...",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
] else if (state is StripePaymentSuccess) ...[
const Icon(
Icons.check_circle,
color: Colors.green,
size: 64,
),
const SizedBox(height: 16),
const Text(
"Payment Successful!",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
] else if (state is StripePaymentFailure) ...[
const Icon(
Icons.error,
color: Colors.red,
size: 64,
),
const SizedBox(height: 16),
const Text(
"Payment Failed",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
const SizedBox(height: 8),
Text(
state.error,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
] else if (state is StripePaymentCancelled) ...[
const Icon(
Icons.cancel,
color: Colors.orange,
size: 64,
),
const SizedBox(height: 16),
const Text(
"Payment Cancelled",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
],
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFE0E0E0),
),
),
child: Column(
children: [
Text(
"Payment Amount",
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
"\$${widget.totalAmount.toStringAsFixed(2)}",
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
],
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.lock_outline,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 6),
Text(
"Secured by Stripe",
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
],
),
),
),
);
},
try {
// Step 1: Initialize Stripe's native payment sheet
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: clientSecret,
merchantDisplayName: 'CityCards',
allowsDelayedPaymentMethods: true, // ← enables UPI, netbanking, etc.
style: ThemeMode.light,
),
);
// Step 2: Present Stripe's native UI (card, UPI, wallets, etc.)
await Stripe.instance.presentPaymentSheet();
// Step 3: Payment succeeded
if (!mounted) return;
_onPaymentSuccess(context);
} on StripeException catch (e) {
if (!mounted) return;
if (e.error.code == FailureCode.Canceled) {
_onPaymentCancelled(context);
} else {
_onPaymentFailed(context, e.error.message ?? 'Payment failed');
}
} catch (e) {
if (!mounted) return;
_onPaymentFailed(context, e.toString());
}
}
void _onPaymentSuccess(BuildContext context) {
context.read<MyPostCardBloc>().add(CheckLoginStatus());
if (widget.isEditMode) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => OrderSuccessPageView(
isEditMode: true,
isCartMode: widget.isCartMode,
pcImage: widget.pcImage,
pcContent: widget.pcContent,
pcState: widget.stateName,
pcCountry: widget.countryName,
pcCity: widget.cityName,
pcZipCode: widget.zipCode,
pcName: widget.fullname,
pcAddress: widget.address1,
senderName: widget.senderName,
senderCity: widget.senderCity,
senderCountry: widget.senderCountry,
),
);
},
),
);
context.read<PostcardCheckoutBloc>().add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
} else {
context.read<PostcardCreationBloc>().add(GoToNextStep());
context.read<PostcardCheckoutBloc>().add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
}
}
void _onPaymentCancelled(BuildContext context) {
// User dismissed the sheet — just save draft if in edit mode
if (widget.isEditMode) {
context.read<PostcardCheckoutBloc>().add(SaveAsDraftEvent());
context.read<PostcardCheckoutBloc>().add(
UpdateCheckoutDataEvent(
postcardId: widget.postcardId,
),
);
}
}
void _onPaymentFailed(BuildContext context, String error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Payment failed: $error'),
backgroundColor: Colors.red,
),
);
// Handle payment result
if (!mounted) return;
if (paymentSuccess == true) {
context.read<MyPostCardBloc>().add(CheckLoginStatus());
if (widget.isEditMode) {
// For edit mode, navigate directly to OrderSuccessPageView
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => OrderSuccessPageView(
isEditMode: true,
isCartMode: widget.isCartMode,
// Front
pcImage: widget.pcImage,
// Back
pcContent: widget.pcContent,
pcState: widget.stateName,
pcCountry: widget.countryName,
pcCity: widget.cityName,
pcZipCode: widget.zipCode,
pcName: widget.fullname,
pcAddress: widget.address1,
senderName: widget.senderName,
senderCity: widget.senderCity,
senderCountry: widget.senderCountry,
),
),
);
final bloc = context.read<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
} else {
// For new orders, use the normal step flow
context.read<PostcardCreationBloc>().add(GoToNextStep());
final bloc = context.read<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'succeeded',
paymentStatus: 'success',
),
);
}
} else {
if (widget.isEditMode) {
context.read<PostcardCheckoutBloc>().add(SaveAsDraftEvent());
context.read<PostcardCheckoutBloc>().add(
UpdateCheckoutDataEvent(
postcardId: widget.postcardId, // pass the id from widget
),
);
}
// Payment failed or cancelled - go to MyPostCardsView
// Navigator.pushReplacement(
// context,
// MaterialPageRoute(
// builder: (context) => const MyPostCardsView(),
// ),
// );
// final bloc = context.read<PostcardCheckoutBloc>();
// bloc.add(
// ConfirmPaymentEvent(
// stripeStatus: 'requires_payment_method',
// paymentStatus: 'failed',
// ),
// );
if (widget.isEditMode) {
context.read<PostcardCheckoutBloc>().add(SaveAsDraftEvent());
context.read<PostcardCheckoutBloc>().add(
UpdateCheckoutDataEvent(
postcardId: widget.postcardId,
),
);
}
}

View File

@@ -62,8 +62,9 @@ class PostcardCreationPage extends StatelessWidget {
initialSenderFullName: state.isGift ? state.userProfileFullName : null, // ⬅️ ADD
initialSenderCity: state.isGift ? state.userProfileCity : null, // ⬅️ ADD
initialSenderCountry: state.isGift ? state.userProfileCountry : null,
initialSenderEmail: state.isGift ? state.userProfileEmail : null,
initialSenderPhone: state.isGift ? state.userProfilePhone : null,
initialSenderEmail: state.userProfileEmail,
initialSenderPhone: state.userProfilePhone,
initialSenderisdCode: state.isdCode,
);
break;
case PostcardStep.checkout:

View File

@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:geocoding/geocoding.dart';
import '../../common_packages/app_bar.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_event.dart';
@@ -20,10 +21,11 @@ class PostcardPurchaseFormPageView extends StatefulWidget {
final String? initialState;
final String? initialZipCode;
final String? initialCountry;
final String? initialSenderFullName; // ⬅️ ADD
final String? initialSenderCity; // ⬅️ ADD
final String? initialSenderFullName;
final String? initialSenderCity;
final String? initialSenderCountry;
final String? initialSenderEmail;
final String? initialSenderisdCode;
final String? initialSenderPhone;
const PostcardPurchaseFormPageView({
@@ -31,6 +33,7 @@ class PostcardPurchaseFormPageView extends StatefulWidget {
this.initialFullName,
this.initialSenderEmail,
this.initialSenderPhone,
this.initialSenderisdCode,
this.initialAddress,
this.initialCity,
this.initialState,
@@ -42,41 +45,46 @@ class PostcardPurchaseFormPageView extends StatefulWidget {
});
@override
State<PostcardPurchaseFormPageView> createState() => _PostcardPurchaseFormPageViewState();
State<PostcardPurchaseFormPageView> createState() =>
_PostcardPurchaseFormPageViewState();
}
class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageView> {
class _PostcardPurchaseFormPageViewState
extends State<PostcardPurchaseFormPageView> {
final _formKey = GlobalKey<FormState>();
// Sender controllers
final _senderFullNameController = TextEditingController();
final _senderCityController = TextEditingController();
final _senderEmailController = TextEditingController();
final _senderPhoneController = TextEditingController();
String? _senderSelectedCountry;
// Controllers
final _senderCountryController = TextEditingController(); // ← was dropdown
// Recipient controllers
final _titleController = TextEditingController();
final _recipientFullNameController = TextEditingController();
final _recipientAddressController = TextEditingController();
final _recipientCityController = TextEditingController();
final _recipientZipCodeController = TextEditingController();
String? _recipientSelectedCountry;
String? _recipientSelectedState;
final _recipientStateController = TextEditingController(); // ← was dropdown
final _recipientCountryController = TextEditingController(); // ← was dropdown
// Zip auto-fill loading flag
bool _isZipLoading = false;
@override
void initState() {
super.initState();
// Initialize controllers with prefill values
_recipientFullNameController.text = widget.initialFullName ?? '';
_recipientAddressController.text = widget.initialAddress ?? '';
_recipientCityController.text = widget.initialCity ?? '';
_recipientZipCodeController.text = widget.initialZipCode ?? '';
_recipientSelectedState = widget.initialState;
_recipientSelectedCountry = widget.initialCountry;
_recipientStateController.text = widget.initialState ?? '';
_recipientCountryController.text = widget.initialCountry ?? '';
_senderFullNameController.text = widget.initialSenderFullName ?? '';
_senderCityController.text = widget.initialSenderCity ?? '';
_senderSelectedCountry = widget.initialSenderCountry;
_senderCountryController.text = widget.initialSenderCountry ?? '';
_senderEmailController.text = widget.initialSenderEmail ?? '';
_senderPhoneController.text = widget.initialSenderPhone ?? '';
}
@@ -90,9 +98,40 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
_recipientAddressController.dispose();
_recipientCityController.dispose();
_recipientZipCodeController.dispose();
_recipientStateController.dispose();
_recipientCountryController.dispose();
_senderCountryController.dispose();
_senderFullNameController.dispose();
_senderCityController.dispose();
super.dispose();
}
// ── Zip → City, State, Country auto-fill ──────────────────────────────────
Future<void> _fetchLocationFromZip(String zip) async {
if (zip.trim().length < 4) return;
setState(() => _isZipLoading = true);
try {
final locations = await locationFromAddress(zip);
if (locations.isNotEmpty) {
final placemarks = await placemarkFromCoordinates(
locations.first.latitude,
locations.first.longitude,
);
final place = placemarks.first;
setState(() {
_recipientCityController.text = place.locality ?? '';
_recipientStateController.text = place.administrativeArea ?? '';
_recipientCountryController.text = place.country ?? '';
});
}
} catch (e) {
debugPrint("Zip lookup failed: $e");
} finally {
if (mounted) setState(() => _isZipLoading = false);
}
}
// ─────────────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
@@ -102,13 +141,9 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
return BlocListener<AddToCartPostCardBloc, AddToCartPostCardState>(
listener: (context, cartState) {
if (cartState is AddToCartPostCardSuccess) {
// Update the postcard number in creation bloc
creationBloc.add(UpdatePostcardNumber(cartState.pcNumber));
// Navigate to next step (checkout)
creationBloc.add(GoToNextStep());
} else if (cartState is AddToCartPostCardFailure) {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(cartState.message),
@@ -132,13 +167,15 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
context
.read<PostcardCreationBloc>()
.add(GoToPreviousStep());
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
@@ -151,6 +188,8 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
),
),
// ── Postcard image + title ─────────────────────────────
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -174,8 +213,6 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
),
const SizedBox(width: 16),
/// 👇 Title input with heading on top
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -234,72 +271,60 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
],
),
const SizedBox(height: 28),
if(state.isGift)...[
Text(
"Your Details",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
// ── Sender section (gift only) ─────────────────────────
if (state.isGift) ...[
Text(
"Your Details",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
),
),
),
const SizedBox(height: 6),
Text(
state.isGift
? "Enter the address of the person who will receive this postcard"
: "Enter your contact details for this postcard.",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff7A7A7A),
const SizedBox(height: 6),
Text(
"Enter your details as the sender of this postcard",
style: GoogleFonts.poppins(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff7A7A7A),
),
),
),
const SizedBox(height: 16),
_buildInputField(
label:"Full Name *",
hint: "Enter the full name",
controller: _senderFullNameController,
maxLength: 50,
onlyLetters: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
// _buildInputField(
// label: "Email",
// hint: "eg: Jay@gmail.com",
// controller: _senderEmailController,
// keyboardType: TextInputType.emailAddress,
// isEmail: true,
// ),
// _buildInputField(
// label: "Phone number",
// hint: "eg: 9999 999 999",
// controller: _senderPhoneController,
// keyboardType: TextInputType.number,
// maxLength: 10,
// isMobileNumber: true,
// ),
_buildInputField(
label: "City *",
hint: "Enter the name of your city",
controller: _senderCityController,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
onlyLetters: true,
),
_buildDropdownField(
label: "Country *",
hint: "Select your country",
value: _senderSelectedCountry,
onChanged: (val) {
setState(() {
_senderSelectedCountry = val;
});
},
),],
// Personal details section
const SizedBox(height: 16),
_buildInputField(
label: "Full Name *",
hint: "Enter the full name",
controller: _senderFullNameController,
maxLength: 50,
onlyLetters: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
_buildInputField(
label: "City *",
hint: "Enter the name of your city",
controller: _senderCityController,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
onlyLetters: true,
),
// ← Country now a plain text field (was dropdown)
_buildInputField(
label: "Country *",
hint: "Enter your country",
controller: _senderCountryController,
maxLength: 50,
onlyLetters: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
],
// ── Recipient / Self section ───────────────────────────
Text(
state.isGift ? "Recipient Details" : "Your Details",
style: TextStyle(
@@ -335,51 +360,151 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
hint: "Enter the recipient's Address",
controller: _recipientAddressController,
maxLength: 50,
// noSpecialCharacters: true,
),
// ── Zip Code with auto-fill ────────────────────────────
Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: 'Zip Code',
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
],
),
),
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
controller: _recipientZipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
onChanged: _fetchLocationFromZip,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: InputDecoration(
hintText:
"Enter the Zip Code you reside in",
counterText: "",
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
contentPadding:
const EdgeInsets.symmetric(
vertical: 14, horizontal: 12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return 'Please enter Zip Code';
}
return null;
},
),
),
if (_isZipLoading)
const Padding(
padding: EdgeInsets.only(left: 10),
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFC83B61),
),
),
),
],
),
const SizedBox(height: 4),
Text(
"City, State & Country will auto-fill from zip",
style: TextStyle(
fontSize: 10.sp,
color: const Color(0xFF8E8E8E),
),
),
],
),
),
// ← City, State, Country — auto-filled but editable
_buildInputField(
label: "City *",
hint: "Enter the name of your city",
controller: _recipientCityController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true
),
_buildDropdownField(
label: "State *",
hint: "Select your state",
value: _recipientSelectedState,
onChanged: (val) {
setState(() {
_recipientSelectedState = val;
});
},
isFirstLetterCapital: true,
),
// ← State now a plain text field (was dropdown)
_buildInputField(
label: "Zip Code *",
hint: "Enter the Zip Code you reside in",
controller: _recipientZipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
label: "State *",
hint: "Enter your state",
controller: _recipientStateController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
_buildDropdownField(
// ← Country now a plain text field (was dropdown)
_buildInputField(
label: "Country *",
hint: "Select your country",
value: _recipientSelectedCountry,
onChanged: (val) {
setState(() {
_recipientSelectedCountry = val;
});
},
hint: "Enter your country",
controller: _recipientCountryController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
const SizedBox(height: 24),
// Next button
// ── Next button ───────────────────────────────────────
BlocBuilder<AddToCartPostCardBloc, AddToCartPostCardState>(
builder: (context, cartState) {
final isLoading = cartState is AddToCartPostCardLoading;
final addToCartBloc = context.read<AddToCartPostCardBloc>();
final isLoading =
cartState is AddToCartPostCardLoading;
final addToCartBloc =
context.read<AddToCartPostCardBloc>();
return SizedBox(
width: double.infinity,
@@ -390,50 +515,79 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
creationBloc.add(
UpdatePurchaseFormData(
pcTitle: _titleController.text,
fullName: _recipientFullNameController.text,
fullName:
_recipientFullNameController.text,
emailId: _senderEmailController.text,
phoneNumber: _senderPhoneController.text,
address: _recipientAddressController.text,
phoneNumber:
_senderPhoneController.text,
address:
_recipientAddressController.text,
city: _recipientCityController.text,
state: _recipientSelectedState,
zipCode: _recipientZipCodeController.text,
country: _recipientSelectedCountry,
senderName: _senderFullNameController.text,
senderCity: _senderCityController.text,
senderCountry: _senderSelectedCountry,
state: _recipientStateController.text,
zipCode:
_recipientZipCodeController.text,
country:
_recipientCountryController.text,
senderName:
_senderFullNameController.text,
senderCity:
_senderCityController.text,
senderCountry:
_senderCountryController.text,
),
);
if (_formKey.currentState!.validate()) {
final currentDate = DateFormat('yyyy-MM-dd').format(DateTime.now());
final currentDate =
DateFormat('yyyy-MM-dd')
.format(DateTime.now());
addToCartBloc.add(
AddToCartPostCardRequested(
countryName: _recipientSelectedCountry ?? '',
cityName: _recipientCityController.text,
stateName: _recipientSelectedState ?? '',
zipCode: _recipientZipCodeController.text,
address1: _recipientAddressController.text,
countryName:
_recipientCountryController.text,
cityName:
_recipientCityController.text,
stateName:
_recipientStateController.text,
zipCode:
_recipientZipCodeController.text,
address1:
_recipientAddressController.text,
address2: null,
pcTitle: _titleController.text,
pcContent: creationBloc.getFormattedMessage(),
pcImageFile: File(state.imagePath!),
pcContent: creationBloc
.getFormattedMessage(),
pcImageFile:
File(state.imagePath!),
pcNumber: '12',
pcDatetime: currentDate,
fullname: _recipientFullNameController.text,
isdCode: '+91',
fullname:
_recipientFullNameController
.text,
isdCode:
widget.initialSenderisdCode ??
"",
isForSelf: !state.isGift,
senderFullName: _senderFullNameController.text, // ⬅️ ADD
senderCityName: _senderCityController.text, // ⬅️ ADD
senderCountryName: _senderSelectedCountry,
emailAddress: _senderEmailController.text,
mobileNumber: _senderPhoneController.text,
senderFullName:
_senderFullNameController.text,
senderCityName:
_senderCityController.text,
senderCountryName:
_senderCountryController.text,
emailAddress:
widget.initialSenderEmail ??
_senderEmailController.text,
mobileNumber:
widget.initialSenderPhone ??
_senderPhoneController.text,
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
padding:
EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
@@ -444,7 +598,6 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
width: 20,
child: CircularProgressIndicator(
color: Color(0xffF95F62),
// color: Colors.white,
strokeWidth: 2,
),
)
@@ -484,7 +637,8 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
bool onlyLetters = false,
bool noSpace = false,
bool isFirstLetterCapital = false,
bool noSpecialCharacters = false, // ✅ NEW
bool noSpecialCharacters = false,
ValueChanged<String>? onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
@@ -516,6 +670,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
const SizedBox(height: 6),
TextFormField(
controller: controller,
onChanged: onChanged,
keyboardType: keyboardType ??
(isMobileNumber
? TextInputType.phone
@@ -525,26 +680,13 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
? TextCapitalization.words
: TextCapitalization.none,
inputFormatters: [
if (isMobileNumber)
FilteringTextInputFormatter.digitsOnly,
if (isMobileNumber) FilteringTextInputFormatter.digitsOnly,
if (onlyLetters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z ]'),
),
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z ]')),
if (noSpace)
FilteringTextInputFormatter.deny(
RegExp(r'\s'),
),
// ✅ NO SPECIAL CHARACTERS
FilteringTextInputFormatter.deny(RegExp(r'\s')),
if (noSpecialCharacters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 ]'),
),
// ✅ Capitalize first letter of each word
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 ]')),
if (isFirstLetterCapital)
TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.isEmpty) return newValue;
@@ -594,7 +736,6 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
if (value == null || value.trim().isEmpty) {
return 'Please enter $label';
}
if (isEmail) {
final emailRegex =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
@@ -602,7 +743,6 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
return 'Please enter a valid email address';
}
}
if (isMobileNumber) {
if (!RegExp(r'^\d+$').hasMatch(value)) {
return 'Only numbers are allowed';
@@ -611,122 +751,19 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
return 'Mobile number must be $mobileLength digits';
}
}
if (onlyLetters) {
if (!RegExp(r'^[a-zA-Z ]+$').hasMatch(value)) {
return 'Only letters are allowed';
}
}
if (noSpace && value.contains(' ')) {
return 'Spaces are not allowed';
}
// ✅ VALIDATION FOR SPECIAL CHARACTERS
if (noSpecialCharacters) {
if (!RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(value)) {
return 'Special characters are not allowed';
}
}
return null;
},
),
],
),
);
}
/// 🔹 Dropdown input
Widget _buildDropdownField({
required String label,
required String hint,
required String? value,
required Function(String?) onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: label.replaceAll(' *', ''),
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: label.contains('*')
? [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
]
: [],
),
),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.keyboard_arrow_down,
color: Color(0xffFDCDCE)),
hint: Text(
hint,
style: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
),
items: label == "Country *"
? const [
DropdownMenuItem(value: "Australia", child: Text("Australia")),
]
: label == "State *"
? const [
DropdownMenuItem(value: "New South Wales", child: Text("New South Wales")),
DropdownMenuItem(value: "Victoria", child: Text("Victoria")),
DropdownMenuItem(value: "Queensland", child: Text("Queensland")),
DropdownMenuItem(value: "South Australia", child: Text("South Australia")),
DropdownMenuItem(value: "Western Australia", child: Text("Western Australia")),
DropdownMenuItem(value: "Tasmania", child: Text("Tasmania")),
DropdownMenuItem(value: "Northern Territory", child: Text("Northern Territory")),
DropdownMenuItem(value: "Australian Capital Territory", child: Text("Australian Capital Territory")),
]
: const [
DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")),
],
onChanged: onChanged,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select $label';
}
return null;
},
),

View File

@@ -48,7 +48,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
TextPosition(offset: _controller.text.length));
final fonts = [
{"name": "Default", "font": GoogleFonts.caveat(), "cleanName": "Caveat"},
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
{"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:geocoding/geocoding.dart';
class EditYourdetails extends StatefulWidget {
final TextEditingController fullNameController;
@@ -18,6 +19,7 @@ class EditYourdetails extends StatefulWidget {
final TextEditingController senderCityController;
final String selectedSenderCountry;
final Function(String) selectSenderCountry;
const EditYourdetails({
super.key,
required this.fullNameController,
@@ -41,51 +43,76 @@ class EditYourdetails extends StatefulWidget {
}
class _EditYourdetailsState extends State<EditYourdetails> {
String? _selectedState;
String? _selectedCountry;
String? _selectedSenderCountry;
late TextEditingController _stateController;
late TextEditingController _countryController;
late TextEditingController _senderCountryController;
final List<String> countries = ['Australia'];
final List<String> states = [
'New South Wales',
'Victoria',
'Queensland',
'South Australia',
'Western Australia',
'Tasmania',
'Northern Territory',
'Australian Capital Territory',
];
bool _isZipLoading = false;
@override
void initState() {
setState(() {
_selectedState = states.contains(widget.selectedState)
? widget.selectedState
: null;
_selectedCountry = countries.contains(widget.selectedCountry)
? widget.selectedCountry
: null;
_selectedSenderCountry = countries.contains(widget.selectedSenderCountry)
? widget.selectedSenderCountry
: null;
});
super.initState();
_stateController = TextEditingController(text: widget.selectedState);
_countryController = TextEditingController(text: widget.selectedCountry);
_senderCountryController =
TextEditingController(text: widget.selectedSenderCountry);
}
@override
void dispose() {
_stateController.dispose();
_countryController.dispose();
_senderCountryController.dispose();
super.dispose();
}
// ── Zip → City, State, Country (mirrors CreateAccountView logic) ──────────
Future<void> _fetchLocationFromZip(String zip) async {
if (zip.trim().length < 4) return;
setState(() => _isZipLoading = true);
try {
final locations = await locationFromAddress(zip);
if (locations.isNotEmpty) {
final placemarks = await placemarkFromCoordinates(
locations.first.latitude,
locations.first.longitude,
);
final place = placemarks.first;
final city = place.locality ?? '';
final state = place.administrativeArea ?? '';
final country = place.country ?? '';
setState(() {
widget.cityController.text = city;
_stateController.text = state;
_countryController.text = country;
});
// Notify parent of new values
if (state.isNotEmpty) widget.selectState(state);
if (country.isNotEmpty) widget.selectCountry(country);
}
} catch (e) {
debugPrint("Zip lookup failed: $e");
} finally {
if (mounted) setState(() => _isZipLoading = false);
}
}
// ─────────────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// At the top of the Column children list, BEFORE the existing fields:
// ── Sender section (only when isForSelf == false) ──────────────────
if (!widget.isForSelf) ...[
Text(
"Your Details",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
color: const Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
@@ -94,7 +121,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
Text(
"Enter your details as the sender of this postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
color: const Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
@@ -115,22 +142,22 @@ class _EditYourdetailsState extends State<EditYourdetails> {
onlyLetters: true,
noSpace: true,
),
_buildDropdownField(
_buildInputField(
label: "Country *",
hint: "Select your country",
value: _selectedSenderCountry,
items: countries,
onChanged: (val) {
setState(() => _selectedSenderCountry = val);
widget.selectSenderCountry(val!);
},
hint: "Enter your country",
controller: _senderCountryController,
maxLength: 50,
onlyLetters: true,
onChanged: (val) => widget.selectSenderCountry(val),
),
const SizedBox(height: 8),
],
// ── Recipient / Self section ───────────────────────────────────────
Text(
widget.isForSelf ? "Your Details" : "Recipient Details",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
color: const Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
@@ -141,7 +168,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
? "Enter your address to receive this postcard"
: "Enter the address of the person who will receive this postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
color: const Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
@@ -160,8 +187,110 @@ class _EditYourdetailsState extends State<EditYourdetails> {
hint: "Enter the recipient's Address",
controller: widget.addressController,
maxLength: 50,
// noSpecialCharacters: true,
),
// ── Zip Code with spinner + auto-fill hint ─────────────────────────
Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: 'Zip Code',
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
],
),
),
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
controller: widget.zipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
onChanged: _fetchLocationFromZip,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: InputDecoration(
hintText: "Enter the Zip Code you reside in",
counterText: "",
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14, horizontal: 12),
enabledBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter Zip Code';
}
return null;
},
),
),
if (_isZipLoading)
const Padding(
padding: EdgeInsets.only(left: 10),
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFC83B61),
),
),
),
],
),
const SizedBox(height: 4),
Text(
"City, State & Country will auto-fill from zip",
style: TextStyle(
fontSize: 10.sp,
color: const Color(0xFF8E8E8E),
),
),
],
),
),
// ── City, State, Country — auto-filled but still editable ──────────
_buildInputField(
label: "City *",
hint: "Enter the name of your city",
@@ -169,36 +298,21 @@ class _EditYourdetailsState extends State<EditYourdetails> {
maxLength: 50,
onlyLetters: true,
),
_buildDropdownField(
label: "Country *",
hint: "Select your country",
value: _selectedCountry,
items: countries,
onChanged: (val) {
setState(() {
_selectedCountry = val;
});
widget.selectCountry(val!);
},
),
_buildDropdownField(
_buildInputField(
label: "State *",
hint: "Select your state",
value: _selectedState,
items: states,
onChanged: (val) {
setState(() {
_selectedState = val;
});
widget.selectState(val!);
},
hint: "Enter your state",
controller: _stateController,
maxLength: 50,
onlyLetters: true,
onChanged: (val) => widget.selectState(val),
),
_buildInputField(
label: "Zip Code *",
hint: "Enter the Zip Code you reside in",
controller: widget.zipCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
label: "Country *",
hint: "Enter your country",
controller: _countryController,
maxLength: 50,
onlyLetters: true,
onChanged: (val) => widget.selectCountry(val),
),
],
);
@@ -217,7 +331,8 @@ class _EditYourdetailsState extends State<EditYourdetails> {
bool onlyLetters = false,
bool noSpace = false,
bool isFirstLetterCapital = false,
bool noSpecialCharacters = false, // ✅ NEW
bool noSpecialCharacters = false,
ValueChanged<String>? onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
@@ -249,35 +364,20 @@ class _EditYourdetailsState extends State<EditYourdetails> {
const SizedBox(height: 6),
TextFormField(
controller: controller,
onChanged: onChanged,
keyboardType: keyboardType ??
(isMobileNumber
? TextInputType.phone
: TextInputType.text),
(isMobileNumber ? TextInputType.phone : TextInputType.text),
maxLength: maxLength ?? (isMobileNumber ? mobileLength : null),
textCapitalization: isFirstLetterCapital
? TextCapitalization.words
: TextCapitalization.none,
inputFormatters: [
if (isMobileNumber)
FilteringTextInputFormatter.digitsOnly,
if (isMobileNumber) FilteringTextInputFormatter.digitsOnly,
if (onlyLetters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z ]'),
),
if (noSpace)
FilteringTextInputFormatter.deny(
RegExp(r'\s'),
),
// ✅ NO SPECIAL CHARACTERS
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z ]')),
if (noSpace) FilteringTextInputFormatter.deny(RegExp(r'\s')),
if (noSpecialCharacters)
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 ]'),
),
// ✅ Capitalize first letter of each word
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 ]')),
if (isFirstLetterCapital)
TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.isEmpty) return newValue;
@@ -327,7 +427,6 @@ class _EditYourdetailsState extends State<EditYourdetails> {
if (value == null || value.trim().isEmpty) {
return 'Please enter $label';
}
if (isEmail) {
final emailRegex =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
@@ -335,7 +434,6 @@ class _EditYourdetailsState extends State<EditYourdetails> {
return 'Please enter a valid email address';
}
}
if (isMobileNumber) {
if (!RegExp(r'^\d+$').hasMatch(value)) {
return 'Only numbers are allowed';
@@ -344,24 +442,19 @@ class _EditYourdetailsState extends State<EditYourdetails> {
return 'Mobile number must be $mobileLength digits';
}
}
if (onlyLetters) {
if (!RegExp(r'^[a-zA-Z ]+$').hasMatch(value)) {
return 'Only letters are allowed';
}
}
if (noSpace && value.contains(' ')) {
return 'Spaces are not allowed';
}
// ✅ VALIDATION FOR SPECIAL CHARACTERS
if (noSpecialCharacters) {
if (!RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(value)) {
return 'Special characters are not allowed';
}
}
return null;
},
),
@@ -369,92 +462,4 @@ class _EditYourdetailsState extends State<EditYourdetails> {
),
);
}
Widget _buildDropdownField({
required String label,
required String hint,
required String? value,
required List<String> items,
required Function(String?) onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: label.replaceAll(' *', ''),
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
children: label.contains('*')
? [
TextSpan(
text: ' *',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.red,
),
),
]
: [],
),
),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 12,
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xffFDCDCE),
),
hint: Text(
hint,
style: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
),
items: items.map((String item) {
return DropdownMenuItem<String>(value: item, child: Text(item));
}).toList(),
onChanged: onChanged,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select $label';
}
return null;
},
),
],
),
);
}
}
}

View File

@@ -231,6 +231,7 @@ class PurchaseDetailsBottomSheet {
state: profile.stateName,
zipCode: profile.zipCode,
country: profile.country,
isdCode: profile.isdCode,
));
}

View File

@@ -21,6 +21,7 @@ class ContactUsBloc extends Bloc<ContactUsEvent, ContactUsState> {
final response = await repository.submitTicket(
firstName: event.firstName,
lastName: event.lastName,
isdCode: event.isdCode,
emailAddress: event.emailAddress,
mobileNumber: event.mobileNumber,
description: event.description,

View File

@@ -11,6 +11,7 @@ abstract class ContactUsEvent extends Equatable {
class SubmitContactUsEvent extends ContactUsEvent {
final String firstName;
final String lastName;
final String isdCode;
final String emailAddress;
final String mobileNumber;
final String description;
@@ -18,6 +19,7 @@ class SubmitContactUsEvent extends ContactUsEvent {
const SubmitContactUsEvent({
required this.firstName,
required this.lastName,
required this.isdCode,
required this.emailAddress,
required this.mobileNumber,
required this.description,
@@ -27,6 +29,7 @@ class SubmitContactUsEvent extends ContactUsEvent {
List<Object?> get props => [
firstName,
lastName,
isdCode,
emailAddress,
mobileNumber,
description,

View File

@@ -25,6 +25,7 @@ class UpdateProfileEvent extends ProfileEvent {
final String firstName;
final String lastName;
final String mobileNumber;
final String? isdCode;
final String? address1;
final String? address2;
final String? city; // ⭐ NEW
@@ -38,6 +39,7 @@ class UpdateProfileEvent extends ProfileEvent {
required this.firstName,
required this.lastName,
required this.mobileNumber,
this.isdCode,
this.address1,
this.address2,
this.city, // ⭐ NEW
@@ -53,6 +55,7 @@ class UpdateProfileEvent extends ProfileEvent {
firstName,
lastName,
mobileNumber,
isdCode,
address1,
address2,
city, // ⭐ NEW
@@ -67,6 +70,7 @@ class UpdateProfileEvent extends ProfileEvent {
'firstName': firstName,
'lastName': lastName,
'mobileNumber': mobileNumber,
if (isdCode != null && isdCode!.isNotEmpty) 'isdCode': isdCode,
if (address1 != null && address1!.isNotEmpty) 'address1': address1,
if (address2 != null && address2!.isNotEmpty) 'address2': address2,
if (city != null && city!.isNotEmpty) 'city': city, // ⭐ NEW

View File

@@ -4,9 +4,9 @@ class ProfileModel {
final String lastName;
final int roleXid;
final String emailAddress;
final String isdCode;
final String? isdCode;
final String mobileNumber;
final String? profileImage; // ✅ NEW
final String? profileImage;
final String? address1;
final String? address2;
final String? cityName;
@@ -26,7 +26,7 @@ class ProfileModel {
required this.lastName,
required this.roleXid,
required this.emailAddress,
required this.isdCode,
this.isdCode,
required this.mobileNumber,
this.profileImage,
this.address1,
@@ -50,9 +50,9 @@ class ProfileModel {
lastName: json['lastName'] ?? 'N/A',
roleXid: json['roleXid'] ?? 0,
emailAddress: json['emailAddress'] ?? 'N/A',
isdCode: json['isdCode'] ?? 'N/A',
isdCode: json['isdCode'],
mobileNumber: json['mobileNumber'] ?? 'N/A',
profileImage: json['profileImage'], // ✅ added
profileImage: json['profileImage'],
address1: json['address1'],
address2: json['address2'],
cityName: json['cityName'],

View File

@@ -8,6 +8,7 @@ class ContactUsRepository {
Future<Map<String, dynamic>> submitTicket({
required String firstName,
required String lastName,
required String isdCode,
required String emailAddress,
required String mobileNumber,
required String description,
@@ -18,6 +19,7 @@ class ContactUsRepository {
data: {
"firstName": firstName,
"lastName": lastName,
"isdCode": isdCode,
"emailAddress": emailAddress,
"mobileNumber": mobileNumber,
"description": description,

View File

@@ -60,6 +60,9 @@ class ProfileRepository {
if (data['address1'] != null && data['address1'].toString().isNotEmpty)
MapEntry('address1', data['address1']),
if (data['isdCode'] != null && data['isdCode'].toString().isNotEmpty)
MapEntry('isdCode', data['isdCode']),
if (data['address2'] != null && data['address2'].toString().isNotEmpty)
MapEntry('address2', data['address2']),

View File

@@ -2,9 +2,11 @@ import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/back_widget.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:country_code_picker/country_code_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:phone_numbers_parser/phone_numbers_parser.dart';
import '../../bloc/contactUs/contact_us_bloc.dart';
import '../../bloc/contactUs/contact_us_event.dart';
@@ -23,19 +25,36 @@ class ContactUsPage extends StatelessWidget {
}
}
class _ContactUsView extends StatelessWidget {
// ✅ Changed to StatefulWidget to hold _selectedIsdCode state
class _ContactUsView extends StatefulWidget {
const _ContactUsView();
@override
State<_ContactUsView> createState() => _ContactUsViewState();
}
class _ContactUsViewState extends State<_ContactUsView> {
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final messageController = TextEditingController();
final formKey = GlobalKey<FormState>();
String _selectedIsdCode = '+61'; // ✅ tracks selected dial code
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final messageController = TextEditingController();
final formKey = GlobalKey<FormState>();
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
@@ -48,7 +67,6 @@ class _ContactUsView extends StatelessWidget {
backgroundColor: Colors.green,
),
);
firstNameController.clear();
lastNameController.clear();
emailController.clear();
@@ -155,8 +173,6 @@ class _ContactUsView extends StatelessWidget {
isFirstLetterCapital: true,
keyboardType: TextInputType.name,
),
/// EMAIL VALIDATION ADDED
CustomTextField(
label: "Email *",
hint: "Enter your email address",
@@ -175,22 +191,48 @@ class _ContactUsView extends StatelessWidget {
},
),
/// PHONE NUMBER VALIDATION ADDED
// ✅ Phone field with CountryCodePicker via prefixWidget
CustomTextField(
label: "Phone Number *",
hint: "Enter your phone number",
controller: phoneController,
keyboardType: TextInputType.number,
maxLength: 10,
keyboardType: TextInputType.phone,
maxLength: 12,
numbersOnly: true,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Phone number is required";
}
if (value.trim().length != 10) {
return "Enter a valid 10-digit phone number";
try {
final parsed = PhoneNumber.parse(
'$_selectedIsdCode${value.trim()}');
if (!parsed.isValid()) throw Exception();
} catch (_) {
return "Enter a valid phone number for $_selectedIsdCode";
}
return null;
},
prefixWidget: CountryCodePicker(
onChanged: (country) {
setState(() => _selectedIsdCode = country.dialCode!);
},
initialSelection: 'AU',
favorite: const ['+61', '+1', '+44', '+91'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
flagWidth: 24.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
textStyle: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF2D3134),
),
dialogTextStyle: TextStyle(fontSize: 14.sp),
searchDecoration: const InputDecoration(
hintText: 'Search country...',
prefixIcon: Icon(Icons.search),
),
),
),
CustomTextField(
@@ -221,22 +263,17 @@ class _ContactUsView extends StatelessWidget {
onPressed: isLoading
? null
: () {
if (!formKey.currentState!.validate()) {
return;
}
if (!formKey.currentState!.validate()) return;
context.read<ContactUsBloc>().add(
SubmitContactUsEvent(
firstName:
firstNameController.text.trim(),
lastName:
lastNameController.text.trim(),
emailAddress:
emailController.text.trim(),
mobileNumber:
phoneController.text.trim(),
description:
messageController.text.trim(),
lastName: lastNameController.text.trim(),
isdCode: _selectedIsdCode,
emailAddress: emailController.text.trim(),
mobileNumber: phoneController.text.trim(),
description: messageController.text.trim(),
),
);
},
@@ -269,7 +306,6 @@ class _ContactUsView extends StatelessWidget {
);
}
/// Support Box Widget
static Widget _supportBox({
required IconData icon,
required String title,

View File

@@ -2,12 +2,15 @@ import 'dart:io';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/back_widget.dart';
import 'package:citycards_customer/common_packages/custom_textfield.dart';
import 'package:country_code_picker/country_code_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:geocoding/geocoding.dart';
import 'package:image_picker/image_picker.dart';
import 'package:phone_numbers_parser/phone_numbers_parser.dart';
import '../../../localPreference/local_preference.dart';
import '../../../networkApiServices/api_urls.dart';
@@ -33,10 +36,14 @@ class _EditProfilePageState extends State<EditProfilePage> {
final TextEditingController address2Controller = TextEditingController();
final TextEditingController cityController = TextEditingController();
final TextEditingController zipCodeController = TextEditingController();
final TextEditingController stateController = TextEditingController();
final TextEditingController countryController = TextEditingController();
// Dropdown values
String? selectedState;
String? selectedCountry;
String _selectedIsdCode = '';
Key _countryPickerKey = UniqueKey(); // ADD
String _selectedCountryCode = 'AU';
bool _isZipLoading = false;
final _formKey = GlobalKey<FormState>();
final ImagePicker _picker = ImagePicker();
@@ -47,6 +54,30 @@ class _EditProfilePageState extends State<EditProfilePage> {
_fetchProfile();
}
Future<void> fetchLocationFromZip(String zip) async {
if (zip.trim().length < 4) return;
setState(() => _isZipLoading = true);
try {
List<Location> locations = await locationFromAddress(zip);
if (locations.isNotEmpty) {
List<Placemark> placemarks = await placemarkFromCoordinates(
locations.first.latitude,
locations.first.longitude,
);
final place = placemarks.first;
setState(() {
cityController.text = place.locality ?? '';
stateController.text = place.administrativeArea ?? '';
countryController.text = place.country ?? '';
});
}
} catch (e) {
debugPrint("Zip lookup failed: $e");
} finally {
if (mounted) setState(() => _isZipLoading = false);
}
}
Future<void> _fetchProfile() async {
if (kDebugMode) {
print('🔵 [EDIT PROFILE] Fetching profile...');
@@ -74,11 +105,13 @@ class _EditProfilePageState extends State<EditProfilePage> {
address2Controller.text = profile.address2 ?? '';
cityController.text = profile.cityName ?? '';
zipCodeController.text = profile.zipCode ?? '';
stateController.text = profile.stateName ?? '';
countryController.text = profile.country ?? '';
// Set dropdown values from fetched data
setState(() {
selectedState = profile.stateName;
selectedCountry = profile.country;
_selectedIsdCode = profile.isdCode??"";
_selectedCountryCode = profile.isdCode ?? '+61'; // ADD
_countryPickerKey = UniqueKey();
});
// ⭐ REMOVED setState - image is now managed by BLoC state
@@ -313,6 +346,28 @@ class _EditProfilePageState extends State<EditProfilePage> {
if (!mounted) return;
// Phone validation
final phone = phoneController.text.trim();
bool isValidPhone = false;
try {
final fullNumber = '$_selectedIsdCode$phone';
final parsed = PhoneNumber.parse(fullNumber);
isValidPhone = parsed.isValid();
} catch (_) {
isValidPhone = false;
}
if (!isValidPhone) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Enter a valid phone number for $_selectedIsdCode'),
backgroundColor: Colors.red,
),
);
return;
}
// ⭐ Get selectedImageFile from current BLoC state
File? imageFileToSend;
final currentState = context.read<ProfileBloc>().state;
@@ -330,7 +385,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
userId: userId,
firstName: firstNameController.text.trim(),
lastName: lastNameController.text.trim(),
mobileNumber: phoneController.text.trim(),
mobileNumber: phone,
isdCode: _selectedIsdCode,
address1: address1Controller.text.trim().isEmpty
? null
: address1Controller.text.trim(),
@@ -341,8 +397,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
city: cityController.text.trim().isEmpty
? null
: cityController.text.trim(),
state: selectedState,
country: selectedCountry,
state: stateController.text.trim().isEmpty ? null : stateController.text.trim(),
country: countryController.text.trim().isEmpty ? null : countryController.text.trim(),
postalCode: zipCodeController.text.trim().isEmpty
? null
: zipCodeController.text.trim(),
@@ -360,6 +416,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
address2Controller.dispose();
cityController.dispose();
zipCodeController.dispose();
stateController.dispose();
countryController.dispose();
super.dispose();
}
@@ -516,18 +574,36 @@ class _EditProfilePageState extends State<EditProfilePage> {
label: "Phone Number *",
hint: "Enter your phone number",
controller: phoneController,
keyboardType: TextInputType.phone,
maxLength: 12,
numbersOnly: true,
enabled: !isLoading,
keyboardType: TextInputType.number,
maxLength: 10,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Phone number is required";
}
if (value.trim().length != 10) {
return "Enter a valid 10-digit phone number";
}
return null;
},
prefixWidget: CountryCodePicker(
key: _countryPickerKey,
onChanged: isLoading
? null
: (country) {
setState(() => _selectedIsdCode = country.dialCode!);
},
initialSelection: _selectedCountryCode,
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: isLoading
? const Color(0xFF8E8E8E)
: const Color(0xFF2D3134),
),
dialogTextStyle: TextStyle(fontSize: 14.sp),
searchDecoration: const InputDecoration(
hintText: 'Search country...',
prefixIcon: Icon(Icons.search),
),
),
),
),
@@ -568,129 +644,45 @@ class _EditProfilePageState extends State<EditProfilePage> {
),
Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "State *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedState,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
Row(
children: [
Expanded(
child: CustomTextField(
controller: zipCodeController,
enabled: !isLoading,
keyboardType: TextInputType.number,
maxLength: 6,
onChanged: fetchLocationFromZip,
label: 'Zip Code *',
hint: 'Enter the ZIP code you reside in',
),
hint: Text(
"Select state",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
if (_isZipLoading)
Padding(
padding: EdgeInsets.only(right: 12.w),
child: SizedBox(
width: 18.w,
height: 18.h,
child: const CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFF95F62),
),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: isLoading ? null : (value) {
setState(() {
selectedState = value;
});
},
items: [
"New South Wales",
"Victoria",
"Queensland",
"South Australia",
"Western Australia",
"Tasmania",
"Northern Territory",
"Australian Capital Territory"
].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
],
),
// Text(
// "City, State & Country will auto-fill from zip",
// style: TextStyle(fontSize: 10.sp, color: const Color(0xFF8E8E8E)),
// ),
],
),
),
Padding(
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country *", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select country",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: isLoading ? null : (value) {
setState(() {
selectedCountry = value;
});
},
items: ["Australia"].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
@@ -700,22 +692,38 @@ class _EditProfilePageState extends State<EditProfilePage> {
controller: cityController,
enabled: !isLoading,
maxLength: 50,
onlyLetters: true,
// onlyLetters: true,
isPreview: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "ZIP Code *",
hint: "Enter the ZIP code you reside in",
controller: zipCodeController,
label: "State *",
hint: "Enter your state",
controller: stateController,
enabled: !isLoading,
keyboardType: TextInputType.number,
maxLength: 6,
maxLength: 50,
isFirstLetterCapital: true,
isPreview: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Country *",
hint: "Enter your country",
controller: countryController,
enabled: !isLoading,
maxLength: 50,
isPreview: true,
isFirstLetterCapital: true,
),
),
SizedBox(height: 26.h),
// Buttons

View File

@@ -1,5 +1,7 @@
class YourItineraryDetailsModel {
final int id;
final String userFirstName;
final String validUpto;
final String title;
final String city;
final String cityBanner;
@@ -11,6 +13,8 @@ class YourItineraryDetailsModel {
YourItineraryDetailsModel({
required this.id,
required this.userFirstName,
required this.validUpto,
required this.title,
required this.city,
required this.cityBanner,
@@ -21,17 +25,19 @@ class YourItineraryDetailsModel {
required this.days,
});
factory YourItineraryDetailsModel.fromJson(Map<String, dynamic>? json) {
factory YourItineraryDetailsModel.fromJson(Map<String, dynamic> json) {
return YourItineraryDetailsModel(
id: json?['id'] ?? 0,
title: json?['title'] ?? "",
city: json?['city'] ?? "",
cityBanner: json?['cityBanner'] ?? "",
totalDays: json?['totalDays'] ?? 0,
totalStops: json?['totalStops'] ?? 0,
adults: json?['adults'] ?? 0,
children: json?['children'] ?? 0,
days: (json?['days'] as List?)
id: json['id'] ?? 0,
userFirstName: json['userFirstName'] ?? "",
validUpto: json['validUpto'] ?? "",
title: json['title'] ?? "",
city: json['city'] ?? "",
cityBanner: json['cityBanner'] ?? "",
totalDays: json['totalDays'] ?? 0,
totalStops: json['totalStops'] ?? 0,
adults: json['adults'] ?? 0,
children: json['children'] ?? 0,
days: (json['days'] as List?)
?.map((e) => ItineraryDay.fromJson(e))
.toList() ??
[],
@@ -52,12 +58,12 @@ class ItineraryDay {
required this.items,
});
factory ItineraryDay.fromJson(Map<String, dynamic>? json) {
factory ItineraryDay.fromJson(Map<String, dynamic> json) {
return ItineraryDay(
dayNumber: json?['dayNumber'] ?? 0,
title: json?['title'] ?? "",
date: json?['date'] ?? "",
items: (json?['items'] as List?)
dayNumber: json['dayNumber'] ?? 0,
title: json['title'] ?? "",
date: json['date'] ?? "",
items: (json['items'] as List?)
?.map((e) => ItineraryItem.fromJson(e))
.toList() ??
[],
@@ -92,21 +98,20 @@ class ItineraryItem {
required this.attractionXid,
});
factory ItineraryItem.fromJson(Map<String, dynamic>? json) {
factory ItineraryItem.fromJson(Map<String, dynamic> json) {
return ItineraryItem(
id: json?['id'] ?? 0,
itineraryDayXid: json?['itineraryDayXid'] ?? 0,
timeSlot: json?['timeSlot'] ?? "",
title: json?['title'] ?? "",
description: json?['description'] ?? "",
locationName: json?['locationName'] ?? "",
id: json['id'] ?? 0,
itineraryDayXid: json['itineraryDayXid'] ?? 0,
timeSlot: json['timeSlot'] ?? "",
title: json['title'] ?? "",
description: json['description'] ?? "",
locationName: json['locationName'] ?? "",
categories:
(json?['categories'] as List?)?.map((e) => e.toString()).toList() ??
[],
imageUrl: json?['imageUrl'] ?? "",
latitude: (json?['latitude'] ?? 0).toDouble(),
longitude: (json?['longitude'] ?? 0).toDouble(),
attractionXid: json?['attractionXid'],
(json['categories'] as List?)?.map((e) => e.toString()).toList() ?? [],
imageUrl: json['imageUrl'] ?? "",
latitude: (json['latitude'] ?? 0).toDouble(),
longitude: (json['longitude'] ?? 0).toDouble(),
attractionXid: json['attractionXid'],
);
}
}

View File

@@ -68,6 +68,9 @@ class _YourItineraryViewState extends State<YourItineraryView> {
final file = File('${dir.path}/itinerary_${widget.itineraryId}.pdf');
await file.writeAsBytes(state.pdfBytes);
await OpenFilex.open(file.path);
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('PDF downloaded successfully!')),
// );
} else if (state is DownloadItineraryPdfFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
@@ -165,7 +168,7 @@ class _YourItineraryViewState extends State<YourItineraryView> {
// Title
Text(
'Your',
"${itinerary.userFirstName}'s",
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.w700,
@@ -256,9 +259,7 @@ class _YourItineraryViewState extends State<YourItineraryView> {
),
SizedBox(width: 2.w),
Text(
itinerary.days.isNotEmpty
? itinerary.days.first.date
: 'N/A',
itinerary.validUpto,
style: TextStyle(
fontSize: 10.5.sp,
color: Color(0xFF6B7280)),

View File

@@ -75,11 +75,23 @@ class ItineraryVisitingPlaceCard extends StatelessWidget {
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
errorWidget: (context, url, error) => CachedNetworkImage(
imageUrl: image, // ← fallback: try image URL directly
width: 350.w,
height: 200.h,
color: Colors.grey[200],
child: const Icon(Icons.error),
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 350.w,
height: 200.h,
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
width: 350.w,
height: 200.h,
color: Colors.grey[200],
child: const Icon(Icons.error), // both failed
),
),
),
Positioned(

View File

@@ -60,25 +60,25 @@ class SummaryCard extends StatelessWidget {
weight: FontWeight.w500,
color: const Color(0xFF212121),
),
SizedBox(width: 16.w),
Spacer(), // 👈 Add this
Row(
children: [
Image.asset(
"assets/icons/calender_filled.png",
color: const Color(0xFFF95F62),
width: 20.sp,
width: 16.sp, // 👈 slightly smaller (was 20.sp)
),
SizedBox(width: 4.w),
CustomText(
text: date,
color: const Color(0xFFF95F62),
size: 16.sp,
size: 14.sp, // 👈 slightly smaller (was 16.sp)
weight: FontWeight.w500,
),
],
),
],
),
),
SizedBox(height: 15.h),

View File

@@ -9,6 +9,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
url: "https://pub.dev"
source: hosted
version: "1.0.3"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
url: "https://pub.dev"
source: hosted
version: "1.0.4"
archive:
dependency: transitive
description:
@@ -77,10 +109,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -113,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
country_code_picker:
dependency: "direct main"
description:
name: country_code_picker
sha256: f0411f4833b6f98e8b7215f4fa3813bcc88e50f13925f70a170dbd36e3e447f5
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cross_file:
dependency: transitive
description:
@@ -177,6 +217,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
diacritic:
dependency: transitive
description:
name: diacritic
sha256: "12981945ec38931748836cd76f2b38773118d0baef3c68404bdfde9566147876"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
dio:
dependency: "direct main"
description:
@@ -581,6 +629,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.8"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
html:
dependency: transitive
description:
@@ -761,18 +817,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -909,6 +965,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
phone_numbers_parser:
dependency: "direct main"
description:
name: phone_numbers_parser
sha256: c30ec1a8ee216da8631eb32d6c3ce0fec85c9accb221c8868bb0aa90c0ce5e95
url: "https://pub.dev"
source: hosted
version: "9.0.20"
platform:
dependency: transitive
description:
@@ -949,6 +1013,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: transitive
description:
@@ -1206,10 +1286,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.8"
three_js:
dependency: "direct main"
description:
@@ -1516,4 +1596,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"
flutter: ">=3.38.1"

View File

@@ -67,6 +67,10 @@ dependencies:
google_mlkit_translation: ^0.13.1
url_launcher: ^6.3.2
open_filex: ^4.7.0
country_code_picker: ^3.4.1
phone_numbers_parser: ^9.0.20
qr_flutter: ^4.1.0
app_links: ^7.0.0
dev_dependencies:
flutter_test: