49 Commits
dinesh ... raj

Author SHA1 Message Date
Raj.Ghag
496287716a bugs solved and more fixes 2026-04-27 13:35:17 +05:30
Raj.Ghag
d0ecd48407 added pinput package for better otp enter experience 2026-04-27 12:35:46 +05:30
Raj.Ghag
092fa1215f chnages and bug fixes 2026-04-24 10:35:13 +05:30
Raj.Ghag
3ca76d0c26 send otp snackbar updated 2026-04-20 15:26:13 +05:30
Raj.Ghag
54f9a4b2ad sprint three chnagees done 2026-04-15 19:06:30 +05:30
Raj.Ghag
b37bb3bf2b apply filte delay solved and refeesh token logic updated 2026-04-01 11:50:00 +05:30
b78c83cc4a translator removed 2026-03-25 18:03:50 +05:30
c4e28decb9 horizonal rotate disable and progressbar added in create intinerary and more fixes 2026-03-25 18:00:42 +05:30
6038d450e4 horizonal rotate disable and progressbar added in create intinerary and more fixes 2026-03-25 17:59:22 +05:30
c06c844210 bug fixes done 2026-03-24 13:21:37 +05:30
9d27389bf2 bug fixes done 2026-03-23 19:25:32 +05:30
d1038e846e bug fixes done 2026-03-20 18:08:52 +05:30
177f891a31 added check in and Qr scaner and bookng with api and more fixes and changes 2026-03-17 17:07:14 +05:30
adc737a6af magic itinerary api intigration and more fixes 2026-03-13 11:11:53 +05:30
265bddc784 added create magic itinerary with api and more and bug fixes 2026-03-05 19:02:22 +05:30
60486e737a bug fixes 2026-02-26 15:54:57 +05:30
77aba2f1a0 added fixes of city catds word 2026-02-26 11:29:06 +05:30
06e60cfd57 my cart with postcads added and more fixes. 2026-02-26 10:20:34 +05:30
f59b14bec7 bug fixes and ui updates and my passses cart updated. 2026-02-20 18:50:28 +05:30
cbe03f21b4 pull taken from shreeyash 2026-02-17 15:24:10 +05:30
a80a0ac790 bug fixes and more 2026-02-17 15:18:45 +05:30
Shreeyash Thorat
cdfb9c74ca upload, edit postcard image with filter 2026-02-17 15:15:21 +05:30
80b724d6d4 bug fixes and more 2026-02-16 19:13:08 +05:30
0abdd2b796 validations added 2026-02-16 13:43:24 +05:30
dd1991da09 search added in my drafts and y orders 2026-02-16 12:58:17 +05:30
mystery012728
48fd7037ea snack bar bug solved 2026-02-13 18:34:00 +05:30
mystery012728
40f0ed3a52 pull taken from shree branch and conflict fixes 2026-02-13 17:13:22 +05:30
mystery012728
b08e2699e9 added my passes and more chnages 2026-02-13 15:27:14 +05:30
Shreeyash Thorat
53264619a8 postcard edit 2026-02-13 15:25:05 +05:30
mystery012728
5d08e07de3 added pass details screen new and updated create account page and more changes... 2026-02-10 19:05:42 +05:30
Shreeyash Thorat
68c3f28d76 itnerary 2026-02-10 15:05:38 +05:30
mystery012728
3a08830cce updated iternary api and updated buy pass flow 2026-02-10 13:58:58 +05:30
mystery012728
0c663bdec7 pull taken of shreeyash and conflict solved 2026-02-10 10:44:19 +05:30
mystery012728
e91d24becc pull taken of shreeyash and conflict solved 2026-02-09 10:55:36 +05:30
Shreeyash Thorat
09726eb4e6 API Integration 2026-02-06 19:34:34 +05:30
mystery012728
10eae3577f added payment api for passes and more 2026-02-06 19:01:49 +05:30
mystery012728
460f553aee added payment api for passes and more 2026-02-06 18:58:58 +05:30
mystery012728
a7548ccebd added apply coupon with api intigration and more fixes 2026-02-05 19:35:01 +05:30
mystery012728
c2ffc9d9a7 my Post Cards added with get api and more changes 2026-02-05 12:07:33 +05:30
mystery012728
082bb9b74a razer pay added and more chnages added 2026-01-30 19:27:06 +05:30
mystery012728
fa4f78bceb added offers , offer details and pass details api and more chnages 2026-01-29 19:32:11 +05:30
mystery012728
0434b16bde added userdetails api get and put and more changes 2026-01-28 19:28:37 +05:30
mystery012728
1cb344738e refresh token api integreted and isLogin created in local storages 2026-01-27 18:47:15 +05:30
mystery012728
f5782f6da1 api integrtaion send Otp ,verify otp and faq , Terms ,Policy 2026-01-23 19:00:55 +05:30
mystery012728
bbb96512d1 added local preferance for selectCityID and more fixes 2026-01-21 19:02:23 +05:30
mystery012728
a55510a482 Api Integrated in regitsered home page and in attarction and attraction details pages and there are chnages are there from backend they are pending. 2026-01-19 19:10:14 +05:30
mystery012728
d3abf4053a Added api of upcoming cities and cities and selection cities 2026-01-16 19:18:42 +05:30
mystery012728
aac65c57be city_list_model added 2026-01-16 12:32:24 +05:30
mystery012728
c62c725410 added retry hit api after timeouts 2026-01-12 12:48:59 +05:30
412 changed files with 59800 additions and 10241 deletions

View File

@@ -12,7 +12,7 @@ A few resources to get you started if this is your first Flutter project:
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
[online documentation](https://docs.flutter.dev/),which offers tutorials,
samples, guidance on mobile development, and a full API reference.
<h1>Figma Link</h1>

View File

@@ -35,10 +35,16 @@ android {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
flutter {
source = "../.."
}
}

15
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,15 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# Keep Stripe Push Provisioning classes
-keep class com.stripe.android.pushProvisioning.** { *; }
-dontwarn com.stripe.android.pushProvisioning.**
# Keep Stripe SDK
-keep class com.stripe.android.** { *; }
-dontwarn com.stripe.android.**
# Keep React Native Stripe SDK
-keep class com.reactnativestripesdk.** { *; }
-dontwarn com.reactnativestripesdk.**

View File

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

View File

@@ -1,5 +1,5 @@
package com.citycards_customer.citycards_customer
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterActivity()
class MainActivity : FlutterFragmentActivity()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
assets/icons/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/downlaod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/icons/love_them.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/maybe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
assets/icons/no_kids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/icons/person.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/icons/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
assets/icons/time.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/card_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
assets/images/not_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

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

5
l10n.yaml Normal file
View File

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

View File

@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import '../repository/stripe_service.dart';
import 'stripe_payment_event.dart';
import 'stripe_payment_state.dart';
class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
final StripeService _stripeService;
// 🔒 Flag to prevent re-initialization after success
bool _paymentCompleted = false;
StripePaymentBloc({
StripeService? stripeService,
}) : _stripeService = stripeService ?? StripeService(),
super(const StripePaymentInitial()) {
on<InitiatePayment>(_onInitiatePayment);
on<InitiatePaymentWithClientSecret>(_onInitiatePaymentWithClientSecret);
on<CancelPaymentEvent>(_onCancelPayment);
on<ResetPaymentState>(_onResetPaymentState);
on<RetryPaymentEvent>(_onRetryPayment);
}
Future<void> _onInitiatePayment(
InitiatePayment event,
Emitter<StripePaymentState> emit,
) async {
// 🛑 Prevent re-initialization if payment already completed
if (_paymentCompleted) {
debugPrint('⚠️ Payment already completed. Ignoring re-initialization.');
return;
}
try {
emit(const StripePaymentLoading(
message: 'Creating payment intent...',
));
/// Stripe expects smallest currency unit
/// USD → cents, INR → paise
final int stripeAmount = (event.amount * 100).toInt();
// 1⃣ Create PaymentIntent from backend
final clientSecret = await _stripeService.createPaymentIntent(
amount: stripeAmount,
currency: event.currency,
);
emit(const StripePaymentLoading(
message: 'Initializing payment sheet...',
));
// 2⃣ Init Payment Sheet
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: clientSecret,
merchantDisplayName: "CityCards",
style: ThemeMode.light,
allowsDelayedPaymentMethods: true,
),
);
await Stripe.instance.presentPaymentSheet();
emit(const StripePaymentSheetReady());
emit(const StripePaymentLoading(
message: 'Processing payment...',
));
// 3⃣ Show Payment Sheet
await Stripe.instance.presentPaymentSheet();
// ✅ SUCCESS - Mark as completed
_paymentCompleted = true;
emit(const StripePaymentSuccess());
} on StripeException catch (e) {
_handleStripeException(e, emit);
} catch (e) {
emit(StripePaymentFailure(
error: 'An unexpected error occurred: ${e.toString()}',
isRetryable: true,
));
}
}
/// Handle payment with clientSecret directly from backend
Future<void> _onInitiatePaymentWithClientSecret(
InitiatePaymentWithClientSecret event,
Emitter<StripePaymentState> emit,
) async {
// 🛑 Prevent re-initialization if payment already completed
if (_paymentCompleted) {
debugPrint('⚠️ Payment already completed. Ignoring re-initialization.');
return;
}
try {
emit(const StripePaymentLoading(
message: 'Initializing payment...',
));
// 1⃣ Init Payment Sheet with clientSecret from backend
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: event.clientSecret,
merchantDisplayName: "CityCards",
style: ThemeMode.light,
allowsDelayedPaymentMethods: true,
),
);
emit(const StripePaymentSheetReady());
emit(const StripePaymentLoading(
message: 'Processing payment...',
));
// 2⃣ Show Payment Sheet
await Stripe.instance.presentPaymentSheet();
// ✅ SUCCESS - Mark as completed
_paymentCompleted = true;
emit(const StripePaymentSuccess());
} on StripeException catch (e) {
_handleStripeException(e, emit);
} catch (e) {
emit(StripePaymentFailure(
error: 'An unexpected error occurred: ${e.toString()}',
isRetryable: true,
));
}
}
/// Handle payment cancellation
void _onCancelPayment(
CancelPaymentEvent event,
Emitter<StripePaymentState> emit,
) {
// Only emit cancelled if not already completed
if (!_paymentCompleted) {
emit(const StripePaymentCancelled(
message: 'Payment cancelled by user',
));
}
}
/// Handle payment retry
Future<void> _onRetryPayment(
RetryPaymentEvent event,
Emitter<StripePaymentState> emit,
) async {
// 🔄 Reset completion flag for retry
_paymentCompleted = false;
// Reset state first
emit(const StripePaymentInitial());
// Then initiate payment again
add(InitiatePaymentWithClientSecret(
clientSecret: event.clientSecret,
));
}
/// Reset payment state back to initial
void _onResetPaymentState(
ResetPaymentState event,
Emitter<StripePaymentState> emit,
) {
// 🔄 Reset completion flag
_paymentCompleted = false;
emit(const StripePaymentInitial());
}
/// Centralized Stripe exception handling
void _handleStripeException(
StripeException e,
Emitter<StripePaymentState> emit,
) {
final errorCode = e.error.code;
final errorMessage = e.error.localizedMessage ?? 'Payment failed';
// Handle cancellation separately
if (errorCode == FailureCode.Canceled) {
emit(StripePaymentCancelled(
message: errorMessage,
));
return;
}
// Handle different error types
switch (errorCode) {
case FailureCode.Failed:
emit(StripePaymentFailure(
error: errorMessage,
errorCode: errorCode.toString(),
isRetryable: true,
));
break;
case FailureCode.Timeout:
emit(const StripePaymentFailure(
error: 'Payment timed out. Please try again.',
errorCode: 'timeout',
isRetryable: true,
));
break;
default:
emit(StripePaymentFailure(
error: errorMessage,
errorCode: errorCode?.toString(),
isRetryable: _isRetryableError(errorCode),
));
}
}
/// Determine if an error is retryable
bool _isRetryableError(FailureCode? errorCode) {
if (errorCode == null) return true;
// Non-retryable errors
const nonRetryableErrors = [
// Add specific non-retryable error codes here if needed
];
return !nonRetryableErrors.contains(errorCode);
}
@override
Future<void> close() {
// Reset flag on bloc disposal
_paymentCompleted = false;
return super.close();
}
}

View File

@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
abstract class StripePaymentEvent extends Equatable {
const StripePaymentEvent();
@override
List<Object?> get props => [];
}
class InitiatePayment extends StripePaymentEvent {
final double amount;
final String currency;
const InitiatePayment({
required this.amount,
required this.currency,
});
@override
List<Object?> get props => [amount, currency];
}
/// Event to initiate payment with clientSecret from backend
class InitiatePaymentWithClientSecret extends StripePaymentEvent {
final String clientSecret;
const InitiatePaymentWithClientSecret({
required this.clientSecret,
});
@override
List<Object?> get props => [clientSecret];
}
/// Event to cancel ongoing payment
class CancelPaymentEvent extends StripePaymentEvent {
const CancelPaymentEvent();
}
/// Event to reset payment state back to initial
class ResetPaymentState extends StripePaymentEvent {
const ResetPaymentState();
}
/// Event to retry failed payment
class RetryPaymentEvent extends StripePaymentEvent {
final String clientSecret;
const RetryPaymentEvent({
required this.clientSecret,
});
@override
List<Object?> get props => [clientSecret];
}

View File

@@ -0,0 +1,96 @@
import 'package:equatable/equatable.dart';
abstract class StripePaymentState extends Equatable {
const StripePaymentState();
@override
List<Object?> get props => [];
}
/// Initial state before any payment action
class StripePaymentInitial extends StripePaymentState {
const StripePaymentInitial();
}
/// Payment is being processed
class StripePaymentLoading extends StripePaymentState {
final String? message;
const StripePaymentLoading({
this.message,
});
@override
List<Object?> get props => [message];
}
/// Payment sheet is initialized and ready to be presented
class StripePaymentSheetReady extends StripePaymentState {
const StripePaymentSheetReady();
}
/// Payment was successful
class StripePaymentSuccess extends StripePaymentState {
final String message;
final String? paymentIntentId;
const StripePaymentSuccess({
this.message = 'Payment Successful',
this.paymentIntentId,
});
@override
List<Object?> get props => [message, paymentIntentId];
}
/// Payment failed
class StripePaymentFailure extends StripePaymentState {
final String error;
final String? errorCode;
final bool isRetryable;
const StripePaymentFailure({
required this.error,
this.errorCode,
this.isRetryable = true,
});
@override
List<Object?> get props => [error, errorCode, isRetryable];
}
/// Payment was cancelled by user
class StripePaymentCancelled extends StripePaymentState {
final String message;
const StripePaymentCancelled({
this.message = 'Payment Cancelled',
});
@override
List<Object?> get props => [message];
}
/// Payment requires additional authentication (3D Secure, etc.)
class StripePaymentRequiresAction extends StripePaymentState {
final String message;
const StripePaymentRequiresAction({
this.message = 'Additional authentication required',
});
@override
List<Object?> get props => [message];
}
/// Payment is processing on the backend
class StripePaymentProcessing extends StripePaymentState {
final String message;
const StripePaymentProcessing({
this.message = 'Payment is being processed...',
});
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,97 @@
import 'package:dio/dio.dart';
class StripeService {
final Dio _dio = Dio(
BaseOptions(
headers: {
"Content-Type": "application/json",
},
),
);
// ⚠️ TEMPORARY FALLBACK - Use secret key directly
// TODO: Remove this and use backend when ready!
final String _stripeSecretKey = ''; // ← ADD YOUR SECRET KEY
Future<String> createPaymentIntent({
required int amount,
required String currency,
}) async {
try {
// 🔥 DIRECT STRIPE API CALL (Temporary fallback)
final response = await _dio.post(
'https://api.stripe.com/v1/payment_intents',
data: {
'amount': amount.toString(),
'currency': currency,
'automatic_payment_methods[enabled]': 'true',
},
options: Options(
headers: {
'Authorization': 'Bearer $_stripeSecretKey',
'Content-Type': 'application/x-www-form-urlencoded',
},
contentType: Headers.formUrlEncodedContentType,
),
);
if (response.data == null || response.data['client_secret'] == null) {
throw Exception('Invalid response from Stripe');
}
return response.data['client_secret'];
} on DioException catch (e) {
if (e.response != null) {
print('Stripe API Error: ${e.response?.data}');
throw Exception('Stripe error: ${e.response?.data['error']?['message'] ?? e.message}');
}
throw Exception('Network error: ${e.message}');
} catch (e) {
print('Payment Intent Error: $e');
throw Exception('Failed to create payment intent: $e');
}
}
}
/*
🔒 PRODUCTION VERSION (Use this when backend is ready):
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:dio/dio.dart';
class StripeService {
final Dio _dio = Dio(
BaseOptions(
baseUrl: ApiUrls.baseUrl,
headers: {
"Content-Type": "application/json",
},
),
);
Future<String> createPaymentIntent({
required int amount,
required String currency,
}) async {
try {
final response = await _dio.post(
"/create-payment-intent",
data: {
"amount": amount,
"currency": currency,
},
);
if (response.data == null || response.data['clientSecret'] == null) {
throw Exception('Invalid response from server');
}
return response.data['clientSecret'];
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Failed to create payment intent: $e');
}
}
}
*/

View File

@@ -0,0 +1,475 @@
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';
import '../repository/stripe_service.dart';
/// 🎯 Reusable Stripe Payment Screen
///
/// This widget handles Stripe payment flow and can be used across different features
/// like postcards, subscriptions, bookings, etc.
class StripePaymentScreen extends StatelessWidget {
/// Client secret from your backend payment intent
final String clientSecret;
/// Amount to display (optional)
final double? amount;
/// Currency symbol (default: \$)
final String currencySymbol;
/// Custom title for the payment screen
final String? title;
/// Custom loading message
final String loadingMessage;
/// Custom success message
final String successMessage;
/// Custom failure message prefix
final String failureMessage;
/// Callback when payment succeeds
final VoidCallback? onPaymentSuccess;
/// Callback when payment fails
final void Function(String error)? onPaymentFailure;
/// Callback when payment is cancelled
final VoidCallback? onPaymentCancelled;
/// Primary color for the UI
final Color primaryColor;
/// Success icon color
final Color successColor;
/// Error icon color
final Color errorColor;
/// Custom height ratio (0.0 to 1.0)
final double heightRatio;
/// Whether to show close button during loading
final bool showCloseButtonDuringLoading;
/// Custom widget to show above the status (optional)
final Widget? headerWidget;
/// Custom widget to show below the status (optional)
final Widget? footerWidget;
const StripePaymentScreen({
super.key,
required this.clientSecret,
this.amount,
this.currencySymbol = '\$',
this.title,
this.loadingMessage = 'Processing payment...',
this.successMessage = 'Payment Successful!',
this.failureMessage = 'Payment Failed',
this.onPaymentSuccess,
this.onPaymentFailure,
this.onPaymentCancelled,
this.primaryColor = const Color(0xFFF95F62),
this.successColor = Colors.green,
this.errorColor = Colors.red,
this.heightRatio = 0.5,
this.showCloseButtonDuringLoading = false,
this.headerWidget,
this.footerWidget,
});
/// 🚀 Static method to show as bottom sheet
static Future<bool?> showAsBottomSheet({
required BuildContext context,
required String clientSecret,
double? amount,
String currencySymbol = '\$',
String? title,
String loadingMessage = 'Processing payment...',
String successMessage = 'Payment Successful!',
String failureMessage = 'Payment Failed',
VoidCallback? onPaymentSuccess,
void Function(String error)? onPaymentFailure,
VoidCallback? onPaymentCancelled,
Color primaryColor = const Color(0xFFF95F62),
Color successColor = Colors.green,
Color errorColor = Colors.red,
double heightRatio = 0.5,
bool isDismissible = false,
bool enableDrag = false,
bool showCloseButtonDuringLoading = false,
Widget? headerWidget,
Widget? footerWidget,
}) async {
return await showModalBottomSheet<bool>(
context: context,
isDismissible: isDismissible,
enableDrag: enableDrag,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (bottomSheetContext) {
return BlocProvider(
create: (_) => StripePaymentBloc(stripeService: StripeService())
..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)),
child: StripePaymentScreen(
clientSecret: clientSecret,
amount: amount,
currencySymbol: currencySymbol,
title: title,
loadingMessage: loadingMessage,
successMessage: successMessage,
failureMessage: failureMessage,
onPaymentSuccess: onPaymentSuccess,
onPaymentFailure: onPaymentFailure,
onPaymentCancelled: onPaymentCancelled,
primaryColor: primaryColor,
successColor: successColor,
errorColor: errorColor,
heightRatio: heightRatio,
showCloseButtonDuringLoading: showCloseButtonDuringLoading,
headerWidget: headerWidget,
footerWidget: footerWidget,
),
);
},
);
}
/// 🚀 Static method to show as full screen dialog
static Future<bool?> showAsDialog({
required BuildContext context,
required String clientSecret,
double? amount,
String currencySymbol = '\$',
String? title,
String loadingMessage = 'Processing payment...',
String successMessage = 'Payment Successful!',
String failureMessage = 'Payment Failed',
VoidCallback? onPaymentSuccess,
void Function(String error)? onPaymentFailure,
VoidCallback? onPaymentCancelled,
Color primaryColor = const Color(0xFFF95F62),
Color successColor = Colors.green,
Color errorColor = Colors.red,
bool barrierDismissible = false,
bool showCloseButtonDuringLoading = false,
Widget? headerWidget,
Widget? footerWidget,
}) async {
return await showDialog<bool>(
context: context,
barrierDismissible: barrierDismissible,
builder: (dialogContext) {
return BlocProvider(
create: (_) => StripePaymentBloc(stripeService: StripeService())
..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)),
child: Dialog(
backgroundColor: Colors.transparent,
child: StripePaymentScreen(
clientSecret: clientSecret,
amount: amount,
currencySymbol: currencySymbol,
title: title,
loadingMessage: loadingMessage,
successMessage: successMessage,
failureMessage: failureMessage,
onPaymentSuccess: onPaymentSuccess,
onPaymentFailure: onPaymentFailure,
onPaymentCancelled: onPaymentCancelled,
primaryColor: primaryColor,
successColor: successColor,
errorColor: errorColor,
heightRatio: 1.0,
showCloseButtonDuringLoading: showCloseButtonDuringLoading,
headerWidget: headerWidget,
footerWidget: footerWidget,
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<StripePaymentBloc, StripePaymentState>(
// 🔒 CRITICAL: Only listen when state actually changes to prevent duplicate triggers
listenWhen: (previous, current) {
// Don't re-trigger if both states are the same success state
if (previous is StripePaymentSuccess && current is StripePaymentSuccess) {
debugPrint('⚠️ Preventing duplicate success listener');
return false;
}
return true;
},
listener: (context, state) {
if (state is StripePaymentSuccess) {
debugPrint('✅ Payment Success - Calling callback');
// ✅ Call the callback first
onPaymentSuccess?.call();
// ✅ Then auto-close and return true after 1.5 seconds
Future.delayed(const Duration(milliseconds: 1500), () {
if (context.mounted) {
Navigator.of(context).pop(true);
}
});
} else if (state is StripePaymentFailure) {
debugPrint('❌ Payment Failure - ${state.error}');
onPaymentFailure?.call(state.error);
// Auto-close after 2 seconds on failure
Future.delayed(const Duration(seconds: 2), () {
if (context.mounted) {
Navigator.of(context).pop(false);
}
});
} else if (state is StripePaymentCancelled) {
debugPrint('🚫 Payment Cancelled');
onPaymentCancelled?.call();
Navigator.of(context).pop(false);
}
},
buildWhen: (previous, current) {
// 🔒 Prevent unnecessary rebuilds on duplicate success states
if (previous is StripePaymentSuccess && current is StripePaymentSuccess) {
return false;
}
return true;
},
builder: (context, state) {
return Container(
height: heightRatio == 1.0
? MediaQuery.of(context).size.height
: MediaQuery.of(context).size.height * heightRatio,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: heightRatio == 1.0
? null
: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Stack(
children: [
// Main content
Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Custom header widget
if (headerWidget != null) ...[
headerWidget!,
const SizedBox(height: 16),
],
// Title
if (title != null) ...[
Text(
title!,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF333333),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
],
// Amount display
if (amount != null) ...[
Text(
'$currencySymbol${amount!.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
const SizedBox(height: 24),
],
// Payment status
_buildPaymentStatus(context, state),
// Custom footer widget
if (footerWidget != null) ...[
const SizedBox(height: 16),
footerWidget!,
],
],
),
),
),
// Close button (only show when allowed)
if (_shouldShowCloseButton(state))
Positioned(
top: 16,
right: 16,
child: IconButton(
onPressed: () {
if (state is StripePaymentLoading) {
// Cancel payment if loading
context
.read<StripePaymentBloc>()
.add(CancelPaymentEvent());
} else {
Navigator.of(context).pop(false);
}
},
icon: Icon(
Icons.close,
color: Colors.grey[600],
size: 24,
),
),
),
],
),
);
},
);
}
/// Build payment status widget based on state
Widget _buildPaymentStatus(BuildContext context, StripePaymentState state) {
if (state is StripePaymentLoading) {
return Column(
children: [
CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
),
const SizedBox(height: 24),
Text(
loadingMessage,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
textAlign: TextAlign.center,
),
],
);
} else if (state is StripePaymentSuccess) {
return Column(
children: [
Icon(
Icons.check_circle,
color: successColor,
size: 64,
),
const SizedBox(height: 16),
Text(
successMessage,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
textAlign: TextAlign.center,
),
],
);
} else if (state is StripePaymentFailure) {
return Column(
children: [
Icon(
Icons.error,
color: errorColor,
size: 64,
),
const SizedBox(height: 16),
Text(
failureMessage,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
state.error,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
// Retry payment
context.read<StripePaymentBloc>().add(
RetryPaymentEvent(
clientSecret: clientSecret,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Retry Payment',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
);
} else if (state is StripePaymentCancelled) {
return Column(
children: [
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),
),
textAlign: TextAlign.center,
),
],
);
}
return const SizedBox.shrink();
}
/// Determine if close button should be shown
bool _shouldShowCloseButton(StripePaymentState state) {
if (state is StripePaymentLoading) {
return showCloseButtonDuringLoading;
}
// Show for failure and cancelled states
return state is StripePaymentFailure || state is StripePaymentCancelled;
}
}

View File

@@ -2,184 +2,294 @@ 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:citycards_customer/core/route_constants.dart';
import 'package:country_code_picker/country_code_picker.dart'; // ✅ NEW IMPORT
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:phone_numbers_parser/phone_numbers_parser.dart'; // ✅ NEW IMPORT
import '../l10n/app_localizations.dart';
class AddDetailsView extends StatelessWidget {
AddDetailsView({super.key});
import '../checkout/bloc/pass_purchase_details_bloc.dart';
import '../checkout/bloc/pass_purchase_details_event.dart';
import '../checkout/bloc/pass_purchase_details_state.dart';
class AddDetailsView extends StatefulWidget {
final int bookingId;
const AddDetailsView({super.key, required this.bookingId});
@override
State<AddDetailsView> createState() => _AddDetailsViewState();
}
class _AddDetailsViewState extends State<AddDetailsView> {
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController addressController = TextEditingController();
final TextEditingController cityController = TextEditingController();
final TextEditingController countryController = TextEditingController();
String _selectedIsdCode = '+61'; // ✅ NEW: tracks selected country dial code
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
Row(
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Icon(Icons.arrow_back, size: 24.sp),
),
SizedBox(width: 8.w),
Text(
"Add details",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 42.h),
Align(
alignment: Alignment.centerLeft,
child: CustomText(
text: "Tell us about yourself",
size: 18.sp,
weight: FontWeight.w500,
),
),
SizedBox(height: 12.h),
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
countryController.dispose();
phoneController.dispose();
cityController.dispose();
super.dispose();
}
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "First Name",
hint: "Enter your first name",
controller: firstNameController,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Last Name",
hint: "Enter your last name",
controller: lastNameController,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Email",
hint: "Enter your email address",
controller: emailController,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Phone Number",
hint: "Enter your phone number",
controller: phoneController,
),
),
bool _isValidEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return emailRegex.hasMatch(email);
}
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "City",
hint: "Enter the name of your city",
controller: phoneController,
),
),
// ✅ UPDATED: now validates phone using phone_numbers_parser against the selected ISD code
bool _isValidPhone(String phone) {
try {
final fullNumber = '$_selectedIsdCode$phone';
final parsed = PhoneNumber.parse(fullNumber);
return parsed.isValid();
} catch (_) {
return false;
}
}
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: StatefulBuilder(
builder: (context, setState) {
String? selectedCountry;
return DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select your country",
style: TextStyle(
fontSize: 12.sp,
color: Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedCountry = value;
});
},
items: ["India", "USA", "UK", "Canada"].map((
value,
) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
);
},
),
),
),
],
),
),
void _handleSubmit(BuildContext context, bool isSubmitting) {
if (isSubmitting) return;
const Spacer(),
CustomFilledButton(
onTap: () {
},
label: "Continue",
width: double.infinity,
),
SizedBox(height: 50.h),
],
),
if (firstNameController.text.isEmpty ||
lastNameController.text.isEmpty ||
emailController.text.isEmpty ||
phoneController.text.isEmpty ||
cityController.text.isEmpty ||
countryController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.pleaseFillAllFields),
backgroundColor: Colors.red,
),
);
return;
}
if (!_isValidEmail(emailController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.enterValidEmail),
backgroundColor: Colors.red,
),
);
return;
}
// ✅ UPDATED: error message now shows the selected ISD code
if (!_isValidPhone(phoneController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.enterValidPhoneForIsd(_selectedIsdCode)),
backgroundColor: Colors.red,
),
);
return;
}
context.read<PurchaseDetailsBloc>().add(
SubmitUserDetailsEvent(
bookingId: widget.bookingId,
isForSelf: false,
recipientFirstName: firstNameController.text,
recipientLastName: lastNameController.text,
isdCode: _selectedIsdCode,
recipientEmail: emailController.text,
recipientPhone: phoneController.text,
city: cityController.text,
country: countryController.text,
),
);
}
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => PurchaseDetailsBloc(),
child: BlocConsumer<PurchaseDetailsBloc, PurchaseDetailsState>(
listener: (context, state) {
if (state is PurchaseDetailsSubmitted) {
Navigator.of(context).pop('success');
}
if (state is PurchaseDetailsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? AppLocalizations.of(context)!.failedToSubmitDetails),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
final isSubmitting = state.isSubmittingDetails;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
Row(
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Icon(Icons.arrow_back, size: 24.sp),
),
SizedBox(width: 8.w),
Text(
AppLocalizations.of(context)!.addDetailsTitle,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 42.h),
Align(
alignment: Alignment.centerLeft,
child: CustomText(
text: AppLocalizations.of(context)!.aboutRecipient,
size: 18.sp,
weight: FontWeight.w500,
),
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.firstNameLabelWithStar,
hint: AppLocalizations.of(context)!.firstNameHint,
controller: firstNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.lastNameLabelWithStar,
hint: AppLocalizations.of(context)!.lastNameHint,
controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.emailLabelWithStar,
hint: AppLocalizations.of(context)!.emailHint,
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
),
// ✅ NEW: Phone field with CountryCodePicker (replaces plain CustomTextField)
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.phoneNumberLabelWithStar,
hint: AppLocalizations.of(context)!.phoneNumberHint,
controller: phoneController,
keyboardType: TextInputType.phone,
maxLength: 12,
numbersOnly: true,
prefixWidget: CountryCodePicker(
onChanged: (country) {
setState(() => _selectedIsdCode = country.dialCode!);
},
initialSelection: 'AU',
favorite: const ['+61', '+1', '+44', '+91'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
flagWidth: 24.w,
padding: EdgeInsets.symmetric(horizontal: 8.w),
textStyle: TextStyle(
fontSize: 13.sp,
color: const Color(0xFF2D3134),
),
dialogTextStyle: TextStyle(fontSize: 14.sp),
searchDecoration: InputDecoration(
hintText: AppLocalizations.of(context)!.searchCountryHint,
prefixIcon: const Icon(Icons.search),
),
),
),
),
// ✅ END of new phone field
SizedBox(height: 8.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.cityLabelWithStar,
hint: AppLocalizations.of(context)!.cityHint,
controller: cityController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: AppLocalizations.of(context)!.countryLabelWithStar,
hint: AppLocalizations.of(context)!.countryHint,
controller: countryController,
maxLength: 50,
onlyLetters: true,
isFirstLetterCapital: true,
),
),
SizedBox(height: 24.h),
CustomFilledButton(
onTap: () => _handleSubmit(context, isSubmitting),
label: isSubmitting ? AppLocalizations.of(context)!.submittingLabel : AppLocalizations.of(context)!.continueTitle,
width: double.infinity,
),
SizedBox(height: 50.h),
],
),
),
),
),
);
},
),
);
}
}

View File

@@ -1,484 +0,0 @@
import 'package:citycards_customer/attraction_details/share_bottomsheet.dart';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../core/route_constants.dart';
class AttractionDetailsView extends StatelessWidget {
const AttractionDetailsView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Image.asset(
'assets/images/koh_rong_samloem_banner.png',
height: 377.h,
width: double.infinity,
fit: BoxFit.cover,
),
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(isWhiteLogo: true, isProfilePage: false, showDivider: true,),
SizedBox(height: 10.h),
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.arrow_back,
size: 24.sp,
color: Colors.white,
),
),
SizedBox(width: 8.w),
Text(
"Koh Rong Samloem",
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
],
),
),
),
),
Positioned(
bottom: 31.h,
left: 12.w,
child: Text(
"Koh Rong\nSamloem",
style: TextStyle(
color: Colors.white,
fontSize: 44.sp,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
),
Positioned(
bottom: 31.h,
right: 17.w,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const ShareBottomSheet(),
);
},
child: Container(
height: 36.h,
width: 36.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
),
child: Center(
child: Icon(
Icons.share_sharp,
color: Colors.black,
size: 18.sp,
),
),
),
),
),
],
),
// About Section
Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 30.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"About",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 12.32.h),
Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis. Proin sed aliquet rhoncus ut pellentesque ullamcorper sit eget ac.Sit nisi, cras amet varius eget egestas pellentesque. Cursus gravida euismod non...",
style: TextStyle(
color: Color(0xFF262626),
fontWeight: FontWeight.w400,
fontSize: 14.sp,
height: 1.5,
),
),
],
),
),
SizedBox(height: 41.h),
// Booking Section
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"How to make a booking?",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Color(0xFFF95F62)),
),
child: Row(
children: [
Icon(
Icons.call,
color: Color(0xFFF95F62),
size: 32.w,
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Contact Number",
color: Colors.black.withOpacity(.6),
size: 12.sp,
weight: FontWeight.w500,
),
SizedBox(height: 6.h),
CustomText(
text: "+1012 3456 789",
color: Colors.black,
size: 14.sp,
weight: FontWeight.w600,
),
SizedBox(height: 6.h),
CustomText(
text: "Tap to call",
color: Colors.black.withOpacity(.4),
size: 12.sp,
weight: FontWeight.w400,
),
],
),
),
],
),
),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Color(0xFFF95F62)),
),
child: Row(
children: [
Icon(
Icons.email_sharp,
color: Color(0xFFF95F62),
size: 32.w,
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Email",
color: Colors.black.withOpacity(.6),
size: 12.sp,
weight: FontWeight.w500,
),
SizedBox(height: 6.h),
CustomText(
text: "CityCards24@gmail.com",
color: Colors.black,
size: 14.sp,
weight: FontWeight.w600,
),
SizedBox(height: 6.h),
CustomText(
text: "Tap to email",
color: Colors.black.withOpacity(.4),
size: 12.sp,
weight: FontWeight.w400,
),
],
),
),
],
),
),
SizedBox(height: 16.h),
InkWell(
onTap: (){
Navigator.of(context).pushNamed(RouteConstants.makeBooking);
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 18.h,
),
decoration: BoxDecoration(
color: Color(0xFFF95F62),
borderRadius: BorderRadius.circular(10.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: "Via CityCards",
size: 16.sp,
weight: FontWeight.w500,
color: Colors.white,
),
SizedBox(height: 8.h),
CustomText(
text: "Create a booking via app",
size: 11.sp,
weight: FontWeight.w400,
color: Colors.white,
),
],
),
),
Icon(
Icons.arrow_forward_ios_outlined,
color: Colors.white,
),
],
),
),
),
SizedBox(height: 30.h),
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
"What is included",
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.h),
Wrap(
runSpacing: 16.h,
spacing: 16.w,
children: [
includedBox(
"assets/icons/bus.png",
"Bus",
"Transportation",
),
includedBox(
"assets/icons/clock.png",
"2 day 1 night",
"Duration",
),
includedBox(
"assets/icons/bx_qr.png",
"TAC200812695",
"Product code",
),
],
),
SizedBox(height: 30.h),
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
"Exact Location",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 8.h),
CustomText(
text: "View the location on map",
size: 12.sp,
color: Colors.black.withOpacity(.6),
),
SizedBox(height: 17.h),
ClipRRect(
borderRadius: BorderRadius.circular(13.54.r),
child: Image.asset(
height: 178.7.h,
width: double.infinity,
"assets/images/attra_detail_map.png",
fit: BoxFit.cover,
),
),
SizedBox(height: 17.h),
CustomText(
text:
"Angkor Mails Hotel \nNR6, Krong Siem Reap Cambodia",
size: 12.sp,
color: Colors.black.withOpacity(0.6),
),
SizedBox(height: 30.h),
Divider(color: Colors.black.withOpacity(0.2)),
SizedBox(height: 30.h),
Text(
"People frequently ask",
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 15.h),
faqBox(
"About this place",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...",
),
SizedBox(height: 15.h),
faqBox(
"Term and condition",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...",
),
SizedBox(height: 15.h),
faqBox(
"Cancellation Policy",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. A id diam nisl, non justo, in odio...",
),
],
),
),
SizedBox(height: 24.h),
],
),
),
),
);
}
Widget includedBox(String icon, String title, String disc) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
decoration: BoxDecoration(
color: Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(10.r),
border: Border.all(color: Color(0xFFFDCDCE)),
),
child: IntrinsicWidth(
child: Row(
children: [
Image.asset(icon, scale: 4),
SizedBox(width: 16.w),
Column(
children: [
CustomText(
text: title,
size: 16.sp,
weight: FontWeight.w500,
color: Color(0xFF212121),
),
SizedBox(height: 4.h),
CustomText(
text: disc,
size: 11.sp,
weight: FontWeight.w400,
color: Color(0xFF666666),
),
],
),
],
),
),
);
}
Widget faqBox(String title, String desc) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
decoration: BoxDecoration(
color: Color(0xFFFFF5F5),
border: Border.all(color: Color(0xFFFDCDCE)),
borderRadius: BorderRadius.circular(10.r),
),
child: Column(
children: [
Row(
children: [
CustomText(
text: title,
size: 16.sp,
weight: FontWeight.w500,
color: Color(0xFF212121),
),
SizedBox(width: 20.w),
Icon(Icons.arrow_forward_ios_outlined, size: 18.sp),
],
),
SizedBox(height: 9.h),
CustomText(text: desc, size: 11.sp, color: Color(0xFF7D7D7D)),
],
),
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'attraction_details_event.dart';
import 'attraction_details_state.dart';
import '../repository/attraction_details_repository.dart';
class AttractionDetailsBloc
extends Bloc<AttractionDetailsEvent, AttractionDetailsState> {
final AttractionDetailsRepository repository;
AttractionDetailsBloc({
required this.repository,
}) : super(AttractionDetailsInitial()) {
on<FetchAttractionDetails>(_onFetchAttractionDetails);
on<ToggleDescriptionExpanded>(_onToggleDescriptionExpanded);
on<UpdateGalleryIndex>(_onUpdateGalleryIndex);
on<UpdateFullScreenGalleryIndex>(_onUpdateFullScreenGalleryIndex);
}
void _onToggleDescriptionExpanded(
ToggleDescriptionExpanded event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(isExpanded: !currentState.isExpanded));
}
}
void _onUpdateGalleryIndex(
UpdateGalleryIndex event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(galleryIndex: event.index));
}
}
void _onUpdateFullScreenGalleryIndex(
UpdateFullScreenGalleryIndex event,
Emitter<AttractionDetailsState> emit,
) {
if (state is AttractionDetailsLoaded) {
final currentState = state as AttractionDetailsLoaded;
emit(currentState.copyWith(fullScreenGalleryIndex: event.index));
}
}
Future<void> _onFetchAttractionDetails(
FetchAttractionDetails event,
Emitter<AttractionDetailsState> emit,
) async {
emit(AttractionDetailsLoading());
try {
final response = await repository.fetchAttractionDetails(
attractionId: event.attractionId,
);
emit(
AttractionDetailsLoaded(
attractionDetails: response,
),
);
} catch (e) {
emit(
AttractionDetailsError(
message: e.toString(),
),
);
}
}
}

View File

@@ -0,0 +1,35 @@
import 'package:equatable/equatable.dart';
abstract class AttractionDetailsEvent extends Equatable {
const AttractionDetailsEvent();
@override
List<Object?> get props => [];
}
class FetchAttractionDetails extends AttractionDetailsEvent {
final int attractionId;
const FetchAttractionDetails({
required this.attractionId,
});
@override
List<Object?> get props => [attractionId];
}
class ToggleDescriptionExpanded extends AttractionDetailsEvent {}
class UpdateGalleryIndex extends AttractionDetailsEvent {
final int index;
const UpdateGalleryIndex({required this.index});
@override
List<Object?> get props => [index];
}
class UpdateFullScreenGalleryIndex extends AttractionDetailsEvent {
final int index;
const UpdateFullScreenGalleryIndex({required this.index});
@override
List<Object?> get props => [index];
}

View File

@@ -0,0 +1,56 @@
import 'package:equatable/equatable.dart';
import '../models/attraction_details_model.dart';
abstract class AttractionDetailsState extends Equatable {
const AttractionDetailsState();
@override
List<Object?> get props => [];
}
class AttractionDetailsInitial extends AttractionDetailsState {}
class AttractionDetailsLoading extends AttractionDetailsState {}
class AttractionDetailsLoaded extends AttractionDetailsState {
final AttractionDetailsModel attractionDetails;
final bool isExpanded;
final int galleryIndex;
final int fullScreenGalleryIndex;
const AttractionDetailsLoaded({
required this.attractionDetails,
this.isExpanded = false,
this.galleryIndex = 0,
this.fullScreenGalleryIndex = 0,
});
AttractionDetailsLoaded copyWith({
AttractionDetailsModel? attractionDetails,
bool? isExpanded,
int? galleryIndex,
int? fullScreenGalleryIndex,
}) {
return AttractionDetailsLoaded(
attractionDetails: attractionDetails ?? this.attractionDetails,
isExpanded: isExpanded ?? this.isExpanded,
galleryIndex: galleryIndex ?? this.galleryIndex,
fullScreenGalleryIndex: fullScreenGalleryIndex ?? this.fullScreenGalleryIndex,
);
}
@override
List<Object?> get props => [attractionDetails, isExpanded, galleryIndex, fullScreenGalleryIndex];
}
class AttractionDetailsError extends AttractionDetailsState {
final String message;
const AttractionDetailsError({
required this.message,
});
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,246 @@
class AttractionDetailsModel {
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 DateTime createdAt;
final DateTime updatedAt;
final List<AttractionGallery> attractionGalleries;
final List<AttractionInclusion> attractionInclusions;
final List<AttractionFaq> attractionFaqs;
AttractionDetailsModel({
required this.id,
required this.title,
required this.description,
required this.cityXid,
this.cardTypeXid,
required this.partnerXid,
required this.productCode,
required this.subTitle,
required this.urlSlug,
required this.isBookingRequired,
required this.isPartnerAccess,
required this.bookingEmail,
required 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,
});
factory AttractionDetailsModel.fromJson(Map<String, dynamic> json) {
return AttractionDetailsModel(
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'] ?? 'N/A',
subTitle: json['subTitle'] ?? 'N/A',
urlSlug: json['urlSlug'] ?? 'N/A',
isBookingRequired: json['isBookingRequired'] ?? false,
isPartnerAccess: json['isPartnerAccess'] ?? false,
bookingEmail: json['bookingEmail'] ?? 'N/A',
bookingPhoneNumber: json['bookingPhoneNumber'] ?? 'N/A',
address: json['address'] ?? 'N/A',
latitudeCoordinate: json['latitudeCoordinate'] != null
? (json['latitudeCoordinate'] as num).toDouble()
: 0.0,
longitudeCoordinate: json['longitudeCoordinate'] != null
? (json['longitudeCoordinate'] as num).toDouble()
: 0.0,
ticketPriceAdult: json['ticketPriceAdult'] != null
? (json['ticketPriceAdult'] as num).toDouble()
: 0.0,
ticketPriceChild: json['ticketPriceChild'] != null
? (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'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'])
: DateTime.now(),
attractionGalleries: json['attractionGalleries'] != null
? (json['attractionGalleries'] as List)
.map((e) => AttractionGallery.fromJson(e))
.toList()
: [],
attractionInclusions: json['attractionInclusions'] != null
? (json['attractionInclusions'] as List)
.map((e) => AttractionInclusion.fromJson(e))
.toList()
: [],
attractionFaqs: json['attractionFaqs'] != null
? (json['attractionFaqs'] as List)
.map((e) => AttractionFaq.fromJson(e))
.toList()
: [],
);
}
}
/// =======================
/// Attraction Gallery
/// =======================
class AttractionGallery {
final int id;
final int attractionXid;
final String fileType;
final String filePathUrl;
final String altText;
final bool isCoverImage;
final bool isActive;
final DateTime createdAt;
final DateTime 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'] ?? 'N/A',
altText: json['altText'] ?? 'N/A',
isCoverImage: json['isCoverImage'] ?? false,
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'])
: DateTime.now(),
);
}
}
/// =======================
/// Attraction Inclusion
/// =======================
class AttractionInclusion {
final int id;
final int attractionXid;
final String title;
final String description;
final int? iconXid;
final bool isInclusion;
final bool isActive;
final DateTime createdAt;
final DateTime 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'] ?? 'N/A',
description: json['description'] ?? 'N/A',
iconXid: json['iconXid'],
isInclusion: json['isInclusion'] ?? false,
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'])
: DateTime.now(),
);
}
}
/// =======================
/// Attraction FAQ
/// =======================
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'] ?? 'N/A',
faqAnswer: json['faqAnswer'] ?? 'N/A',
displayOrder: json['displayOrder'] ?? 0,
isActive: json['isActive'] ?? false,
);
}
}

View File

@@ -0,0 +1,17 @@
import '../models/attraction_details_model.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
class AttractionDetailsRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch attraction details by attractionId
Future<AttractionDetailsModel> fetchAttractionDetails({
required int attractionId,
}) async {
final response = await _apiService.getApi(
url: '${ApiUrls.attractionDetails}/$attractionId',
);
return AttractionDetailsModel.fromJson(response.data);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,15 +26,18 @@ class ShareBottomSheet extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handle
Container(
height: 4.h,
width: 47.w,
margin: EdgeInsets.only(bottom: 16),
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: Color(0xFF222222),
color: const Color(0xFF222222),
borderRadius: BorderRadius.circular(8),
),
),
// link field
TextField(
readOnly: true,
decoration: InputDecoration(
@@ -51,7 +54,10 @@ class ShareBottomSheet extends StatelessWidget {
),
),
),
SizedBox(height: 20.h),
// grid
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -67,7 +73,16 @@ class ShareBottomSheet extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(item['icon']!, width: 55.w),
// FIXED SIZE ICON CONTAINER
Container(
width: 55.w,
height: 55.w,
alignment: Alignment.center,
child: Image.asset(
item['icon']!,
fit: BoxFit.contain,
),
),
SizedBox(height: 8.h),
Text(
item['title']!,
@@ -78,26 +93,32 @@ class ShareBottomSheet extends StatelessWidget {
);
},
),
const SizedBox(height: 20),
// page indicator
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4,
(index) => Container(
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
width: 8.w,
height: 8.h,
decoration: BoxDecoration(
color: index == 0 ? Color(0xFF676363) : Colors.white,
border: Border.all(color: Color(0xFF676363)),
color: index == 0
? const Color(0xFF676363)
: Colors.white,
border: Border.all(color: const Color(0xFF676363)),
shape: BoxShape.circle,
),
),
),
),
SizedBox(height: 10.h),
],
),
);
}
}
}

View File

@@ -1,34 +1,70 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/attraction_model.dart';
import '../repository/attractions_repository.dart';
part 'attractions_event.dart';
part 'attractions_state.dart';
import 'attractions_event.dart';
import 'attractions_state.dart';
class AttractionsBloc extends Bloc<AttractionsEvent, AttractionsState> {
final AttractionsRepository repository;
AttractionsBloc(this.repository) : super(AttractionsInitial()) {
on<LoadAttractions>((event, emit) {
final attractions = repository.fetchAttractions();
emit(AttractionsLoaded(attractions));
});
on<LoadMyPassAttraction>((event, emit) {
final attractions = repository.fetchMyPassAttraction();
emit(AttractionsLoaded(attractions));
});
on<SearchAttractions>((event, emit) {
if (state is AttractionsLoaded) {
final currentState = state as AttractionsLoaded;
final filtered = currentState.attractions
.where((a) =>
a.title.toLowerCase().contains(event.query.toLowerCase()) ||
a.location.toLowerCase().contains(event.query.toLowerCase()))
.toList();
emit(AttractionsLoaded(filtered));
}
});
AttractionsBloc({required this.repository}) : super(AttractionsInitial()) {
on<FetchAttractionsByCategory>(_onFetchAttractionsByCategory);
on<SearchAttractions>(_onSearchAttractions);
}
}
Future<void> _onFetchAttractionsByCategory(
FetchAttractionsByCategory event,
Emitter<AttractionsState> emit,
) async {
emit(AttractionsLoading());
try {
final AttractionsResponse response =
await repository.fetchAttractionsByCategory(
categoryXid: event.categoryXid,
);
final allAttractions = response.attractions ?? [];
emit(
AttractionsLoaded(
attractions: allAttractions,
allAttractions: allAttractions,
categories: response.categories ?? [],
selectedCategoryId: event.categoryXid,
searchQuery: '',
),
);
} catch (e) {
emit(AttractionsError(e.toString()));
}
}
void _onSearchAttractions(
SearchAttractions event,
Emitter<AttractionsState> emit,
) {
final currentState = state;
if (currentState is! AttractionsLoaded) return;
final query = event.query.trim().toLowerCase();
final filtered = query.isEmpty
? currentState.allAttractions
: currentState.allAttractions.where((attraction) {
final name = (attraction.title ?? '').toLowerCase();
final description = (attraction.description ?? '').toLowerCase();
return name.contains(query) || description.contains(query);
}).toList();
emit(
AttractionsLoaded(
attractions: filtered,
allAttractions: currentState.allAttractions,
categories: currentState.categories,
selectedCategoryId: currentState.selectedCategoryId,
searchQuery: event.query,
),
);
}
}

View File

@@ -1,12 +1,26 @@
part of 'attractions_bloc.dart';
import 'package:equatable/equatable.dart';
abstract class AttractionsEvent {}
abstract class AttractionsEvent extends Equatable {
const AttractionsEvent();
class LoadAttractions extends AttractionsEvent {}
@override
List<Object?> get props => [];
}
class LoadMyPassAttraction extends AttractionsEvent {}
class FetchAttractionsByCategory extends AttractionsEvent {
final int? categoryXid;
const FetchAttractionsByCategory({this.categoryXid});
@override
List<Object?> get props => [categoryXid];
}
class SearchAttractions extends AttractionsEvent {
final String query;
SearchAttractions(this.query);
}
const SearchAttractions(this.query);
@override
List<Object?> get props => [query];
}

View File

@@ -1,10 +1,47 @@
part of 'attractions_bloc.dart';
import 'package:equatable/equatable.dart';
import '../models/attraction_model.dart';
abstract class AttractionsState {}
abstract class AttractionsState extends Equatable {
const AttractionsState();
@override
List<Object?> get props => [];
}
class AttractionsInitial extends AttractionsState {}
class AttractionsLoading extends AttractionsState {}
class AttractionsLoaded extends AttractionsState {
final List<Attraction> attractions;
AttractionsLoaded(this.attractions);
final List<Attraction> allAttractions; // Keep full list for local filtering
final List<Category> categories;
final int? selectedCategoryId;
final String searchQuery;
const AttractionsLoaded({
required this.attractions,
required this.allAttractions,
required this.categories,
this.selectedCategoryId,
this.searchQuery = '',
});
@override
List<Object?> get props => [
attractions,
allAttractions,
categories,
selectedCategoryId,
searchQuery,
];
}
class AttractionsError extends AttractionsState {
final String message;
const AttractionsError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -1,19 +1,299 @@
/* -------------------- RESPONSE -------------------- */
class AttractionsResponse {
final List<Attraction> attractions;
final List<Category> categories;
AttractionsResponse({
required this.attractions,
required this.categories,
});
factory AttractionsResponse.fromJson(Map<String, dynamic> json) {
return AttractionsResponse(
attractions: (json['attractions'] as List<dynamic>?)
?.map((e) => Attraction.fromJson(e))
.toList() ??
[],
categories: (json['categories'] as List<dynamic>?)
?.map((e) => Category.fromJson(e))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'attractions': attractions.map((e) => e.toJson()).toList(),
'categories': categories.map((e) => e.toJson()).toList(),
};
}
}
/* -------------------- ATTRACTION -------------------- */
class Attraction {
final int id;
final String title;
final String location;
final String price;
final String image;
final List<String> tags;
final bool isBookingRequired;
final String description;
final String urlSlug;
final num cityXid;
final num cardTypeXid;
final num partnerXid;
final String productCode;
final bool isBookingRequired;
final bool isPartnerAccess;
final String bookingEmail;
final String bookingPhoneNumber;
final num latitudeCoordinate;
final num longitudeCoordinate;
final String address;
final num? ticketPriceAdult;
final num? ticketPriceChild;
final num durations;
final num groupSize;
final String ageRange;
final String seoTitle;
final String seoDescription;
final String attractionStatus;
final bool isActive;
final String createdAt;
final String updatedAt;
final List<CardModel> cards;
final List<Category> categories;
final List<Gallery> galleries;
Attraction({
required this.id,
required this.title,
required this.location,
required this.price,
required this.image,
required this.tags,
required this.description,
required this.urlSlug,
required this.cityXid,
required this.cardTypeXid,
required this.partnerXid,
required this.productCode,
required this.isBookingRequired,
required this.description
required this.isPartnerAccess,
required this.bookingEmail,
required this.bookingPhoneNumber,
required this.latitudeCoordinate,
required this.longitudeCoordinate,
required this.address,
this.ticketPriceAdult,
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.cards,
required this.categories,
required this.galleries,
});
factory Attraction.fromJson(Map<String, dynamic> json) {
return Attraction(
id: json['id'] ?? 0,
title: json['title'] ?? '',
description: json['description'] ?? '',
urlSlug: json['urlSlug'] ?? '',
cityXid: json['cityXid'] ?? 0,
cardTypeXid: json['cardTypeXid'] ?? 0,
partnerXid: json['partnerXid'] ?? 0,
productCode: json['productCode'] ?? '',
isBookingRequired: json['isBookingRequired'] ?? false,
isPartnerAccess: json['isPartnerAccess'] ?? false,
bookingEmail: json['bookingEmail'] ?? '',
bookingPhoneNumber: json['bookingPhonenumber'] ?? '',
latitudeCoordinate: (json['latitudeCoordinate'] as num?) ?? 0,
longitudeCoordinate: (json['longitudeCoordinate'] as num?) ?? 0,
address: json['address'] ?? '',
ticketPriceAdult: json['ticketPriceAdult'] as num?,
ticketPriceChild: json['ticketPriceChild'] as num?,
durations: json['durations'] ?? 0,
groupSize: json['groupSize'] ?? 0,
ageRange: json['ageRange'] ?? '',
seoTitle: json['seoTitle'] ?? '',
seoDescription: json['seoDescription'] ?? '',
attractionStatus: json['attractionStatus'] ?? '',
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] ?? '',
updatedAt: json['updatedAt'] ?? '',
cards: (json['cards'] as List<dynamic>?)
?.map((e) => CardModel.fromJson(e))
.toList() ??
[],
categories: (json['categories'] as List<dynamic>?)
?.map((e) => Category.fromJson(e))
.toList() ??
[],
galleries: (json['galleries'] as List<dynamic>?)
?.map((e) => Gallery.fromJson(e))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'urlSlug': urlSlug,
'cityXid': cityXid,
'cardTypeXid': cardTypeXid,
'partnerXid': partnerXid,
'productCode': productCode,
'isBookingRequired': isBookingRequired,
'isPartnerAccess': isPartnerAccess,
'bookingEmail': bookingEmail,
'bookingPhonenumber': bookingPhoneNumber,
'latitudeCoordinate': latitudeCoordinate,
'longitudeCoordinate': longitudeCoordinate,
'address': address,
'ticketPriceAdult': ticketPriceAdult,
'ticketPriceChild': ticketPriceChild,
'durations': durations,
'groupSize': groupSize,
'ageRange': ageRange,
'seoTitle': seoTitle,
'seoDescription': seoDescription,
'attractionStatus': attractionStatus,
'isActive': isActive,
'createdAt': createdAt,
'updatedAt': updatedAt,
'cards': cards.map((e) => e.toJson()).toList(),
'categories': categories.map((e) => e.toJson()).toList(),
'galleries': galleries.map((e) => e.toJson()).toList(),
};
}
/// 🟢 Helper: Cover image URL (UI-safe)
String get coverImageUrl {
if (galleries.isEmpty) return '';
return galleries
.firstWhere(
(g) => g.isCoverImage,
orElse: () => galleries.first,
)
.filePathUrl;
}
}
/* -------------------- CARD -------------------- */
class CardModel {
final int id;
final String title;
final num cardTypeXid;
final num adultPrice;
final num childPrice;
final String cardStatus;
CardModel({
required this.id,
required this.title,
required this.cardTypeXid,
required this.adultPrice,
required this.childPrice,
required this.cardStatus,
});
factory CardModel.fromJson(Map<String, dynamic> json) {
return CardModel(
id: json['id'] ?? 0,
title: json['title'] ?? '',
cardTypeXid: json['cardTypeXid'] ?? 0,
adultPrice: json['adultPrice'] ?? 0,
childPrice: json['childPrice'] ?? 0,
cardStatus: json['cardStatus'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'cardTypeXid': cardTypeXid,
'adultPrice': adultPrice,
'childPrice': childPrice,
'cardStatus': cardStatus,
};
}
}
/* -------------------- GALLERY -------------------- */
class Gallery {
final int id;
final String fileType;
final String filePathUrl;
final String altText;
final bool isCoverImage;
Gallery({
required this.id,
required this.fileType,
required this.filePathUrl,
required this.altText,
required this.isCoverImage,
});
factory Gallery.fromJson(Map<String, dynamic> json) {
return Gallery(
id: json['id'] ?? 0,
fileType: json['fileType'] ?? '',
filePathUrl: json['filePathUrl'] ?? '',
altText: json['altText'] ?? '',
isCoverImage: json['isCoverImage'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'fileType': fileType,
'filePathUrl': filePathUrl,
'altText': altText,
'isCoverImage': isCoverImage,
};
}
bool get hasImage => filePathUrl.isNotEmpty;
}
/* -------------------- CATEGORY -------------------- */
class Category {
final int id;
final String categoryName;
Category({
required this.id,
required this.categoryName,
});
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json['id'] ?? 0,
categoryName: json['categoryName'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'categoryName': categoryName,
};
}
}

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