Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
496287716a | ||
|
|
d0ecd48407 | ||
|
|
092fa1215f | ||
|
|
3ca76d0c26 | ||
|
|
54f9a4b2ad | ||
|
|
b37bb3bf2b | ||
| b78c83cc4a | |||
| c4e28decb9 | |||
| 6038d450e4 | |||
| c06c844210 | |||
| 9d27389bf2 | |||
| d1038e846e | |||
| 177f891a31 | |||
| adc737a6af | |||
| 265bddc784 | |||
| 60486e737a | |||
| 77aba2f1a0 | |||
| 06e60cfd57 | |||
| f59b14bec7 | |||
| cbe03f21b4 | |||
| a80a0ac790 | |||
|
|
cdfb9c74ca | ||
| 80b724d6d4 | |||
| 0abdd2b796 | |||
| dd1991da09 | |||
|
|
48fd7037ea | ||
|
|
40f0ed3a52 | ||
|
|
b08e2699e9 | ||
|
|
53264619a8 | ||
|
|
5d08e07de3 | ||
|
|
68c3f28d76 | ||
|
|
3a08830cce | ||
|
|
0c663bdec7 | ||
|
|
e91d24becc | ||
|
|
09726eb4e6 | ||
|
|
10eae3577f | ||
|
|
460f553aee | ||
|
|
a7548ccebd | ||
|
|
c2ffc9d9a7 | ||
|
|
082bb9b74a | ||
|
|
fa4f78bceb | ||
|
|
0434b16bde | ||
|
|
1cb344738e | ||
|
|
f5782f6da1 | ||
|
|
bbb96512d1 | ||
|
|
a55510a482 | ||
|
|
d3abf4053a | ||
|
|
aac65c57be | ||
|
|
c62c725410 |
@@ -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>
|
||||
|
||||
@@ -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
@@ -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.**
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 6.3 KiB |
663
android/build/reports/problems/problems-report.html
Normal file
BIN
assets/font/Poppins-Regular.ttf
Normal file
BIN
assets/gif/citycards customer app.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.0 KiB |
BIN
assets/icons/calendar.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/citycards_customer_logo.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/icons/citycards_main_logo.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/icons/compass_outlined.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/icons/downlaod.png
Normal file
|
After Width: | Height: | Size: 991 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/location_outlined.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/icons/love_them.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/maybe.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/icons/no_kids.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/not_interested.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/icons/payment_summary_outlined.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/icons/person.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
BIN
assets/icons/refresh.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
assets/icons/sounds_good.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icons/time.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/icons/traveling_with_kids.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
BIN
assets/images/card_bg.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
assets/images/empty_postcard_drafts.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
assets/images/empty_postcard_orders.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 95 KiB |
BIN
assets/images/guest_illustration.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/images/hotel_offers_bg.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
assets/images/no_itinerary.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/images/not_login.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/postcard_stamp_logo.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/images/unlimited_card_details.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
141
assets/intro/city_cards_splash_screen.json
Normal file
@@ -0,0 +1,141 @@
|
||||
[{
|
||||
"version": "1.0",
|
||||
"image": {
|
||||
"name": "frames/frame002.png",
|
||||
"baseName": "frame002.png",
|
||||
"permissions": 664,
|
||||
"format": "PNG",
|
||||
"formatDescription": "Portable Network Graphics",
|
||||
"mimeType": "image/png",
|
||||
"class": "DirectClass",
|
||||
"geometry": {
|
||||
"width": 1868,
|
||||
"height": 3840,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"resolution": {
|
||||
"x": 370753,
|
||||
"y": 370798
|
||||
},
|
||||
"printSize": {
|
||||
"x": 0.00503839,
|
||||
"y": 0.010356
|
||||
},
|
||||
"units": "Undefined",
|
||||
"type": "TrueColor",
|
||||
"endianness": "Undefined",
|
||||
"colorspace": "sRGB",
|
||||
"depth": 8,
|
||||
"baseDepth": 8,
|
||||
"channelDepth": {
|
||||
"red": 8,
|
||||
"green": 8,
|
||||
"blue": 1
|
||||
},
|
||||
"pixels": 7173120,
|
||||
"imageStatistics": {
|
||||
"Overall": {
|
||||
"min": 67,
|
||||
"max": 255,
|
||||
"mean": 142.829,
|
||||
"median": 140,
|
||||
"standardDeviation": 17.1849,
|
||||
"kurtosis": 37.2771,
|
||||
"skewness": 4.24387,
|
||||
"entropy": 0.291301
|
||||
}
|
||||
},
|
||||
"channelStatistics": {
|
||||
"red": {
|
||||
"min": 174,
|
||||
"max": 255,
|
||||
"mean": 237.888,
|
||||
"median": 238,
|
||||
"standardDeviation": 2.65253,
|
||||
"kurtosis": 41.5763,
|
||||
"skewness": 0.61346,
|
||||
"entropy": 0.338084
|
||||
},
|
||||
"green": {
|
||||
"min": 73,
|
||||
"max": 255,
|
||||
"mean": 94.2729,
|
||||
"median": 90,
|
||||
"standardDeviation": 24.5069,
|
||||
"kurtosis": 35.19,
|
||||
"skewness": 6.06676,
|
||||
"entropy": 0.237928
|
||||
},
|
||||
"blue": {
|
||||
"min": 67,
|
||||
"max": 255,
|
||||
"mean": 96.325,
|
||||
"median": 92,
|
||||
"standardDeviation": 24.3954,
|
||||
"kurtosis": 35.0649,
|
||||
"skewness": 6.05138,
|
||||
"entropy": 0.297891
|
||||
}
|
||||
},
|
||||
"renderingIntent": "Perceptual",
|
||||
"gamma": 0.454545,
|
||||
"chromaticity": {
|
||||
"redPrimary": {
|
||||
"x": 0.64,
|
||||
"y": 0.33
|
||||
},
|
||||
"greenPrimary": {
|
||||
"x": 0.3,
|
||||
"y": 0.6
|
||||
},
|
||||
"bluePrimary": {
|
||||
"x": 0.15,
|
||||
"y": 0.06
|
||||
},
|
||||
"whitePrimary": {
|
||||
"x": 0.3127,
|
||||
"y": 0.329
|
||||
}
|
||||
},
|
||||
"matteColor": "#BDBDBDBDBDBD",
|
||||
"backgroundColor": "#FFFFFFFFFFFF",
|
||||
"borderColor": "#DFDFDFDFDFDF",
|
||||
"transparentColor": "#000000000000",
|
||||
"interlace": "None",
|
||||
"intensity": "Undefined",
|
||||
"compose": "Over",
|
||||
"pageGeometry": {
|
||||
"width": 1868,
|
||||
"height": 3840,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"dispose": "Undefined",
|
||||
"iterations": 0,
|
||||
"scene": 1,
|
||||
"scenes": 2,
|
||||
"compression": "Zip",
|
||||
"orientation": "Undefined",
|
||||
"properties": {
|
||||
"date:create": "2026-02-18T13:36:29+00:00",
|
||||
"date:modify": "2026-02-18T13:36:29+00:00",
|
||||
"date:timestamp": "2026-02-18T13:36:29+00:00",
|
||||
"png:IHDR.bit-depth-orig": "8",
|
||||
"png:IHDR.bit_depth": "8",
|
||||
"png:IHDR.color-type-orig": "2",
|
||||
"png:IHDR.color_type": "2 (Truecolor)",
|
||||
"png:IHDR.interlace_method": "0 (Not interlaced)",
|
||||
"png:IHDR.width,height": "1868, 3840",
|
||||
"png:pHYs": "x_res=370753, y_res=370798, units=0",
|
||||
"signature": "7fb181e6439aa51f6eb134a4991711167b5850e80e40ae5cb0c67cf29c118dfe"
|
||||
},
|
||||
"tainted": false,
|
||||
"filesize": "3422B",
|
||||
"numberPixels": "7.17312M",
|
||||
"pixelsPerSecond": "2.74974MB",
|
||||
"userTime": "2.880u",
|
||||
"elapsedTime": "0:03.608",
|
||||
"version": "ImageMagick 7.1.1-41 Q16-HDRI x86_64 22504 https://imagemagick.org"
|
||||
}
|
||||
}]
|
||||
1
assets/intro/citycards_splash_screen.json
Normal file
1
assets/intro/itinerary_animation.json
Normal file
1
assets/intro/itinerary_creating.json
Normal file
@@ -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"
|
||||
|
||||
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
132
ios/Podfile.lock
@@ -1,4 +1,6 @@
|
||||
PODS:
|
||||
- app_links (7.0.0):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_angle (0.3.8):
|
||||
- Flutter
|
||||
@@ -7,6 +9,8 @@ PODS:
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- FlutterAngle (0.0.8)
|
||||
- geocoding_ios (1.0.5):
|
||||
- Flutter
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -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
@@ -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
|
||||
237
lib/StripePayment/bloc/stripe_payment_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
55
lib/StripePayment/bloc/stripe_payment_event.dart
Normal 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];
|
||||
}
|
||||
96
lib/StripePayment/bloc/stripe_payment_state.dart
Normal 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];
|
||||
}
|
||||
97
lib/StripePayment/repository/stripe_service.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
475
lib/StripePayment/view/stripe_payment.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/attraction_details/bloc/attraction_details_bloc.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
lib/attraction_details/bloc/attraction_details_event.dart
Normal 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];
|
||||
}
|
||||
56
lib/attraction_details/bloc/attraction_details_state.dart
Normal 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];
|
||||
}
|
||||
246
lib/attraction_details/models/attraction_details_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
1003
lib/attraction_details/views/attraction_details_view.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||