Showing
9 changed files
with
357 additions
and
86 deletions
No preview for this file type
SwiftWarplyFramework/SwiftWarplyFramework/Media.xcassets/barcode_2.imageset/Contents.json
0 → 100644
| 1 | +{ | ||
| 2 | + "images" : [ | ||
| 3 | + { | ||
| 4 | + "filename" : "barcode_2.png", | ||
| 5 | + "idiom" : "universal", | ||
| 6 | + "scale" : "1x" | ||
| 7 | + }, | ||
| 8 | + { | ||
| 9 | + "filename" : "barcode_2 1.png", | ||
| 10 | + "idiom" : "universal", | ||
| 11 | + "scale" : "2x" | ||
| 12 | + }, | ||
| 13 | + { | ||
| 14 | + "filename" : "barcode_2 2.png", | ||
| 15 | + "idiom" : "universal", | ||
| 16 | + "scale" : "3x" | ||
| 17 | + } | ||
| 18 | + ], | ||
| 19 | + "info" : { | ||
| 20 | + "author" : "xcode", | ||
| 21 | + "version" : 1 | ||
| 22 | + } | ||
| 23 | +} |
SwiftWarplyFramework/SwiftWarplyFramework/Media.xcassets/barcode_2.imageset/barcode_2 1.png
0 → 100644
17.6 KB
SwiftWarplyFramework/SwiftWarplyFramework/Media.xcassets/barcode_2.imageset/barcode_2 2.png
0 → 100644
17.6 KB
SwiftWarplyFramework/SwiftWarplyFramework/Media.xcassets/barcode_2.imageset/barcode_2.png
0 → 100644
17.6 KB
| ... | @@ -32,6 +32,40 @@ public class ProfileCouponTableViewCell: UITableViewCell { | ... | @@ -32,6 +32,40 @@ public class ProfileCouponTableViewCell: UITableViewCell { |
| 32 | 32 | ||
| 33 | } | 33 | } |
| 34 | 34 | ||
| 35 | + // MARK: - Image Loading Helpers | ||
| 36 | + | ||
| 37 | + private var bannerImageURL: String? { | ||
| 38 | + didSet { | ||
| 39 | + if let url = bannerImageURL, !url.isEmpty { | ||
| 40 | + self.bannerImage.image = UIImage() | ||
| 41 | + UIImage.loadImageUsingCacheWithUrlString(url) { [weak self] image in | ||
| 42 | + if url == self?.bannerImageURL { | ||
| 43 | + self?.bannerImage.image = image | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + } else { | ||
| 47 | + self.bannerImage.image = nil | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + private var logoImageURL: String? { | ||
| 53 | + didSet { | ||
| 54 | + if let url = logoImageURL, !url.isEmpty { | ||
| 55 | + self.logoImage.image = UIImage() | ||
| 56 | + UIImage.loadImageUsingCacheWithUrlString(url) { [weak self] image in | ||
| 57 | + if url == self?.logoImageURL { | ||
| 58 | + self?.logoImage.image = image | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | + } else { | ||
| 62 | + self.logoImage.image = nil | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + // MARK: - Configure with OfferModel (legacy, kept for backward compatibility) | ||
| 68 | + | ||
| 35 | func configureCell(data: OfferModel) { | 69 | func configureCell(data: OfferModel) { |
| 36 | bannerImage.image = UIImage(named: data.bannerImage, in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 70 | bannerImage.image = UIImage(named: data.bannerImage, in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 37 | favoriteImage.image = UIImage(named: data.isFavorite ? "favorite_filled" : "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 71 | favoriteImage.image = UIImage(named: data.isFavorite ? "favorite_filled" : "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| ... | @@ -64,6 +98,68 @@ public class ProfileCouponTableViewCell: UITableViewCell { | ... | @@ -64,6 +98,68 @@ public class ProfileCouponTableViewCell: UITableViewCell { |
| 64 | 98 | ||
| 65 | logoImage.image = UIImage(named: data.merchantLogo, in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 99 | logoImage.image = UIImage(named: data.merchantLogo, in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 66 | } | 100 | } |
| 101 | + | ||
| 102 | + // MARK: - Configure with CouponItemModel (dynamic data) | ||
| 103 | + | ||
| 104 | + func configureCell(data: CouponItemModel) { | ||
| 105 | + // Banner image — load from couponset_data img_preview (remote URL) | ||
| 106 | + if let imgPreview = data.couponset_data?._img_preview, !imgPreview.isEmpty { | ||
| 107 | + self.bannerImageURL = imgPreview | ||
| 108 | + } else { | ||
| 109 | + bannerImage.image = nil | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + // Favorite — default to not favorite for now | ||
| 113 | + favoriteImage.image = UIImage(named: "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | ||
| 114 | + | ||
| 115 | + // Discount label — use coupon discount or couponset discount | ||
| 116 | + let discountText = data.discount ?? data.couponset_data?._discount ?? "" | ||
| 117 | + discountLabel.text = discountText | ||
| 118 | + discountLabel.font = UIFont(name: "PingLCG-Bold", size: 25) | ||
| 119 | + discountLabel.textColor = UIColor(rgb: 0xF2F2F2) | ||
| 120 | + | ||
| 121 | + // Discount view color based on discount type | ||
| 122 | + let discountType = data.couponset_data?._discount_type ?? "" | ||
| 123 | + let discountColor: UInt = { | ||
| 124 | + switch discountType { | ||
| 125 | + case "percentage": | ||
| 126 | + return 0xFF6B35 | ||
| 127 | + case "value": | ||
| 128 | + return 0x28A745 | ||
| 129 | + case "plus_one": | ||
| 130 | + return 0x007AFF | ||
| 131 | + default: | ||
| 132 | + return 0xEE417D | ||
| 133 | + } | ||
| 134 | + }() | ||
| 135 | + discountView.backgroundColor = UIColor(rgb: discountColor) | ||
| 136 | + | ||
| 137 | + // Title — from couponset_data name | ||
| 138 | + titleLabel.text = data.couponset_data?._name ?? "" | ||
| 139 | + titleLabel.font = UIFont(name: "PingLCG-Bold", size: 22) | ||
| 140 | + titleLabel.textColor = UIColor(rgb: 0x000F1E) | ||
| 141 | + | ||
| 142 | + // Subtitle — from couponset_data short_description | ||
| 143 | + subtitleLabel.text = data.couponset_data?._short_description ?? "" | ||
| 144 | + subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) | ||
| 145 | + subtitleLabel.textColor = UIColor(rgb: 0x00111B) | ||
| 146 | + | ||
| 147 | + // Expiration — already formatted as "dd/MM/yyyy" by CouponItemModel | ||
| 148 | + if let expiration = data.expiration, !expiration.isEmpty { | ||
| 149 | + expirationLabel.text = "έως " + expiration | ||
| 150 | + } else { | ||
| 151 | + expirationLabel.text = "" | ||
| 152 | + } | ||
| 153 | + expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 13) | ||
| 154 | + expirationLabel.textColor = UIColor(rgb: 0x00111B) | ||
| 155 | + | ||
| 156 | + // Logo — load from merchant_details img_preview (remote URL) | ||
| 157 | + if let merchantImgPreview = data.merchant_details?._img_preview, !merchantImgPreview.isEmpty { | ||
| 158 | + self.logoImageURL = merchantImgPreview | ||
| 159 | + } else { | ||
| 160 | + logoImage.image = nil | ||
| 161 | + } | ||
| 162 | + } | ||
| 67 | 163 | ||
| 68 | public override func setSelected(_ selected: Bool, animated: Bool) { | 164 | public override func setSelected(_ selected: Bool, animated: Bool) { |
| 69 | super.setSelected(selected, animated: animated) | 165 | super.setSelected(selected, animated: animated) | ... | ... |
| ... | @@ -81,6 +81,32 @@ public class CouponSetItemModel { | ... | @@ -81,6 +81,32 @@ public class CouponSetItemModel { |
| 81 | // Bound merchant data for performance | 81 | // Bound merchant data for performance |
| 82 | private var merchant: MerchantModel? | 82 | private var merchant: MerchantModel? |
| 83 | 83 | ||
| 84 | + // MARK: - Multi-Format Date Parsing Helper | ||
| 85 | + | ||
| 86 | + /// Supported date input formats for parsing dates from various API responses | ||
| 87 | + private static let supportedDateFormats = [ | ||
| 88 | + "yyyy-MM-dd HH:mm:ss", // "2027-01-01 03:21:00" | ||
| 89 | + "yyyy-MM-dd'T'HH:mm:ss", // "2027-01-01T15:00:00" (ISO 8601) | ||
| 90 | + "yyyy-MM-dd HH:mm", // "2026-06-30 11:59" | ||
| 91 | + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", // "2027-01-01T15:00:00+03:00" (ISO 8601 with timezone) | ||
| 92 | + "yyyy-MM-dd HH:mm:ss.SSSSSS", // "2022-08-04T14:06:31.110522" | ||
| 93 | + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" // "2022-08-04T14:06:31.110522" (ISO 8601 with microseconds) | ||
| 94 | + ] | ||
| 95 | + | ||
| 96 | + /// Try multiple date formats and return the first successful parse | ||
| 97 | + /// - Parameter dateString: The date string to parse | ||
| 98 | + /// - Returns: Parsed Date or nil if no format matched | ||
| 99 | + private static func parseDate(_ dateString: String) -> Date? { | ||
| 100 | + let formatter = DateFormatter() | ||
| 101 | + for format in supportedDateFormats { | ||
| 102 | + formatter.dateFormat = format | ||
| 103 | + if let date = formatter.date(from: dateString) { | ||
| 104 | + return date | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + return nil | ||
| 108 | + } | ||
| 109 | + | ||
| 84 | public init(dictionary: [String: Any]) { | 110 | public init(dictionary: [String: Any]) { |
| 85 | // Existing fields | 111 | // Existing fields |
| 86 | self.uuid = dictionary["uuid"] as? String? ?? "" | 112 | self.uuid = dictionary["uuid"] as? String? ?? "" |
| ... | @@ -253,18 +279,17 @@ public class CouponSetItemModel { | ... | @@ -253,18 +279,17 @@ public class CouponSetItemModel { |
| 253 | return "" | 279 | return "" |
| 254 | } | 280 | } |
| 255 | 281 | ||
| 256 | - let dateFormatter = DateFormatter() | 282 | + if let date = CouponSetItemModel.parseDate(expiration) { |
| 257 | - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" | 283 | + let outputFormatter = DateFormatter() |
| 258 | - | 284 | + outputFormatter.dateFormat = "dd/MM/yyyy" |
| 259 | - if let date = dateFormatter.date(from: expiration) { | 285 | + return outputFormatter.string(from: date) |
| 260 | - dateFormatter.dateFormat = "dd/MM/yyyy" | ||
| 261 | - return dateFormatter.string(from: date) | ||
| 262 | } | 286 | } |
| 263 | 287 | ||
| 264 | return "" | 288 | return "" |
| 265 | } | 289 | } |
| 266 | 290 | ||
| 267 | /// Format expiration date with custom format | 291 | /// Format expiration date with custom format |
| 292 | + /// Supports multiple input formats: "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm", etc. | ||
| 268 | /// - Parameter format: DateFormatter format string (e.g., "dd-MM", "dd/MM/yyyy") | 293 | /// - Parameter format: DateFormatter format string (e.g., "dd-MM", "dd/MM/yyyy") |
| 269 | /// - Returns: Formatted date string or empty string if invalid | 294 | /// - Returns: Formatted date string or empty string if invalid |
| 270 | public func formattedExpiration(format: String) -> String { | 295 | public func formattedExpiration(format: String) -> String { |
| ... | @@ -272,10 +297,7 @@ public class CouponSetItemModel { | ... | @@ -272,10 +297,7 @@ public class CouponSetItemModel { |
| 272 | return "" | 297 | return "" |
| 273 | } | 298 | } |
| 274 | 299 | ||
| 275 | - let inputFormatter = DateFormatter() | 300 | + if let date = CouponSetItemModel.parseDate(expiration) { |
| 276 | - inputFormatter.dateFormat = "yyyy-MM-dd HH:mm" | ||
| 277 | - | ||
| 278 | - if let date = inputFormatter.date(from: expiration) { | ||
| 279 | let outputFormatter = DateFormatter() | 301 | let outputFormatter = DateFormatter() |
| 280 | outputFormatter.dateFormat = format | 302 | outputFormatter.dateFormat = format |
| 281 | return outputFormatter.string(from: date) | 303 | return outputFormatter.string(from: date) |
| ... | @@ -285,6 +307,7 @@ public class CouponSetItemModel { | ... | @@ -285,6 +307,7 @@ public class CouponSetItemModel { |
| 285 | } | 307 | } |
| 286 | 308 | ||
| 287 | /// Format start date with custom format | 309 | /// Format start date with custom format |
| 310 | + /// Supports multiple input formats: "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", etc. | ||
| 288 | /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM") | 311 | /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM") |
| 289 | /// - Returns: Formatted date string or empty string if invalid | 312 | /// - Returns: Formatted date string or empty string if invalid |
| 290 | public func formattedStartDate(format: String) -> String { | 313 | public func formattedStartDate(format: String) -> String { |
| ... | @@ -292,10 +315,7 @@ public class CouponSetItemModel { | ... | @@ -292,10 +315,7 @@ public class CouponSetItemModel { |
| 292 | return "" | 315 | return "" |
| 293 | } | 316 | } |
| 294 | 317 | ||
| 295 | - let inputFormatter = DateFormatter() | 318 | + if let date = CouponSetItemModel.parseDate(startDate) { |
| 296 | - inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" | ||
| 297 | - | ||
| 298 | - if let date = inputFormatter.date(from: startDate) { | ||
| 299 | let outputFormatter = DateFormatter() | 319 | let outputFormatter = DateFormatter() |
| 300 | outputFormatter.dateFormat = format | 320 | outputFormatter.dateFormat = format |
| 301 | return outputFormatter.string(from: date) | 321 | return outputFormatter.string(from: date) |
| ... | @@ -305,6 +325,7 @@ public class CouponSetItemModel { | ... | @@ -305,6 +325,7 @@ public class CouponSetItemModel { |
| 305 | } | 325 | } |
| 306 | 326 | ||
| 307 | /// Format end date with custom format | 327 | /// Format end date with custom format |
| 328 | + /// Supports multiple input formats: "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", etc. | ||
| 308 | /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM") | 329 | /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM") |
| 309 | /// - Returns: Formatted date string or empty string if invalid | 330 | /// - Returns: Formatted date string or empty string if invalid |
| 310 | public func formattedEndDate(format: String) -> String { | 331 | public func formattedEndDate(format: String) -> String { |
| ... | @@ -312,10 +333,7 @@ public class CouponSetItemModel { | ... | @@ -312,10 +333,7 @@ public class CouponSetItemModel { |
| 312 | return "" | 333 | return "" |
| 313 | } | 334 | } |
| 314 | 335 | ||
| 315 | - let inputFormatter = DateFormatter() | 336 | + if let date = CouponSetItemModel.parseDate(endDate) { |
| 316 | - inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" | ||
| 317 | - | ||
| 318 | - if let date = inputFormatter.date(from: endDate) { | ||
| 319 | let outputFormatter = DateFormatter() | 337 | let outputFormatter = DateFormatter() |
| 320 | outputFormatter.dateFormat = format | 338 | outputFormatter.dateFormat = format |
| 321 | return outputFormatter.string(from: date) | 339 | return outputFormatter.string(from: date) |
| ... | @@ -488,14 +506,36 @@ public class CouponItemModel { | ... | @@ -488,14 +506,36 @@ public class CouponItemModel { |
| 488 | self.expiration = "" | 506 | self.expiration = "" |
| 489 | } | 507 | } |
| 490 | 508 | ||
| 491 | - let createdString = dictionary["created"] as? String? ?? "" | 509 | + // Extract created date: try changes_dates.created first (universal coupons), then top-level created |
| 492 | - let dateFormatter2 = DateFormatter() | 510 | + let createdString: String? |
| 493 | - dateFormatter2.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS" | 511 | + if let changes_dates = dictionary["changes_dates"] as? [String: Any], |
| 494 | - if let date = dateFormatter2.date(from: createdString ?? "") { | 512 | + let changesCreated = changes_dates["created"] as? String, !changesCreated.isEmpty { |
| 495 | - dateFormatter2.dateFormat = "dd/MM/yyyy" | 513 | + createdString = changesCreated |
| 496 | - let resultString = dateFormatter2.string(from: date) | ||
| 497 | - self.created = resultString | ||
| 498 | } else { | 514 | } else { |
| 515 | + createdString = dictionary["created"] as? String? ?? "" | ||
| 516 | + } | ||
| 517 | + | ||
| 518 | + let dateFormatter2 = DateFormatter() | ||
| 519 | + // Try multiple date formats for created date | ||
| 520 | + let createdFormats = [ | ||
| 521 | + "yyyy-MM-dd HH:mm:ss.SSSSSS", // "2026-03-02 12:01:08.365179" | ||
| 522 | + "yyyy-MM-dd HH:mm:ss", // "2026-03-02 12:01:08" | ||
| 523 | + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", // ISO 8601 with microseconds | ||
| 524 | + "yyyy-MM-dd'T'HH:mm:ss" // ISO 8601 | ||
| 525 | + ] | ||
| 526 | + | ||
| 527 | + var createdParsed = false | ||
| 528 | + for format in createdFormats { | ||
| 529 | + dateFormatter2.dateFormat = format | ||
| 530 | + if let date = dateFormatter2.date(from: createdString ?? "") { | ||
| 531 | + dateFormatter2.dateFormat = "dd/MM/yyyy" | ||
| 532 | + let resultString = dateFormatter2.string(from: date) | ||
| 533 | + self.created = resultString | ||
| 534 | + createdParsed = true | ||
| 535 | + break | ||
| 536 | + } | ||
| 537 | + } | ||
| 538 | + if !createdParsed { | ||
| 499 | self.created = "" | 539 | self.created = "" |
| 500 | } | 540 | } |
| 501 | 541 | ... | ... |
| ... | @@ -80,7 +80,7 @@ import UIKit | ... | @@ -80,7 +80,7 @@ import UIKit |
| 80 | 80 | ||
| 81 | @IBOutlet weak var websiteButton: UIButton! | 81 | @IBOutlet weak var websiteButton: UIButton! |
| 82 | 82 | ||
| 83 | - var coupon: OfferModel? | 83 | + var coupon: CouponItemModel? |
| 84 | private var isDetailsExpanded = false | 84 | private var isDetailsExpanded = false |
| 85 | private var isCouponCodeExpanded = false | 85 | private var isCouponCodeExpanded = false |
| 86 | private var isCouponQRExpanded = false | 86 | private var isCouponQRExpanded = false |
| ... | @@ -104,7 +104,7 @@ import UIKit | ... | @@ -104,7 +104,7 @@ import UIKit |
| 104 | couponCodeArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 104 | couponCodeArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 105 | copyButtonImage.image = UIImage(named: "copy", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 105 | copyButtonImage.image = UIImage(named: "copy", in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 106 | couponQRArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 106 | couponQRArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 107 | - couponQRImage.image = UIImage(named: "barcode", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 107 | + couponQRImage.image = UIImage(named: "barcode_2", in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 108 | termsButtonArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 108 | termsButtonArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 109 | 109 | ||
| 110 | infoLabel.font = UIFont(name: "PingLCG-Regular", size: 13) | 110 | infoLabel.font = UIFont(name: "PingLCG-Regular", size: 13) |
| ... | @@ -147,54 +147,101 @@ import UIKit | ... | @@ -147,54 +147,101 @@ import UIKit |
| 147 | websiteButton.layer.borderColor = UIColor(rgb: 0x000F1E).cgColor | 147 | websiteButton.layer.borderColor = UIColor(rgb: 0x000F1E).cgColor |
| 148 | websiteButton.layer.cornerRadius = 4.0 | 148 | websiteButton.layer.cornerRadius = 4.0 |
| 149 | 149 | ||
| 150 | - // Configure the view with offer data | 150 | + // Configure the view with coupon data |
| 151 | - if let offer = coupon { | 151 | + if let couponData = coupon { |
| 152 | - setupUI(with: offer) | 152 | + setupUI(with: couponData) |
| 153 | + } | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + // MARK: - Image Loading Helper | ||
| 157 | + | ||
| 158 | + private func loadRemoteImage(_ urlString: String, into imageView: UIImageView) { | ||
| 159 | + guard !urlString.isEmpty else { | ||
| 160 | + imageView.image = nil | ||
| 161 | + return | ||
| 162 | + } | ||
| 163 | + imageView.image = UIImage() | ||
| 164 | + UIImage.loadImageUsingCacheWithUrlString(urlString) { image in | ||
| 165 | + imageView.image = image | ||
| 153 | } | 166 | } |
| 154 | } | 167 | } |
| 155 | 168 | ||
| 156 | - private func setupUI(with coupon: OfferModel) { | 169 | + private func setupUI(with coupon: CouponItemModel) { |
| 157 | - couponImage.image = UIImage(named: coupon.bannerImage, in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 170 | + // Banner image — load from couponset_data img_preview (remote URL) |
| 158 | - favoriteImage.image = UIImage(named: coupon.isFavorite ? "favorite2_filled" : "favorite2_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 171 | + if let imgPreview = coupon.couponset_data?._img_preview, !imgPreview.isEmpty { |
| 172 | + loadRemoteImage(imgPreview, into: couponImage) | ||
| 173 | + } else { | ||
| 174 | + couponImage.image = nil | ||
| 175 | + } | ||
| 159 | 176 | ||
| 177 | + // Favorite — default to not favorite for now | ||
| 178 | + favoriteImage.image = UIImage(named: "favorite2_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) | ||
| 179 | + | ||
| 180 | + // Title — from couponset_data name | ||
| 160 | titleLabel.font = UIFont(name: "PingLCG-Bold", size: 24) | 181 | titleLabel.font = UIFont(name: "PingLCG-Bold", size: 24) |
| 161 | titleLabel.textColor = UIColor(rgb: 0xF2709D) | 182 | titleLabel.textColor = UIColor(rgb: 0xF2709D) |
| 162 | - titleLabel.text = coupon.title | 183 | + titleLabel.text = coupon.couponset_data?._name ?? "" |
| 163 | 184 | ||
| 185 | + // Subtitle — from couponset_data short_description | ||
| 164 | subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 18) | 186 | subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 18) |
| 165 | subtitleLabel.textColor = UIColor(rgb: 0x020E1C) | 187 | subtitleLabel.textColor = UIColor(rgb: 0x020E1C) |
| 166 | - subtitleLabel.text = coupon.description | 188 | + subtitleLabel.text = coupon.couponset_data?._short_description ?? "" |
| 167 | 189 | ||
| 190 | + // Expiration — already formatted as "dd/MM/yyyy" by CouponItemModel | ||
| 168 | expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 14) | 191 | expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 14) |
| 169 | expirationLabel.textColor = UIColor(rgb: 0x020E1C) | 192 | expirationLabel.textColor = UIColor(rgb: 0x020E1C) |
| 170 | -// expirationLabel.text = ("Η προσφορά ισχύει " + coupon.expirationDate) | 193 | + if let expiration = coupon.expiration, !expiration.isEmpty { |
| 171 | - expirationLabel.text = "Η προσφορά ισχύει έως 30-09-2025" | 194 | + expirationLabel.text = "Η προσφορά ισχύει έως " + expiration |
| 195 | + } else { | ||
| 196 | + expirationLabel.text = "" | ||
| 197 | + } | ||
| 172 | 198 | ||
| 173 | - setupExpandableDetails() | 199 | + // Description — from couponset_data description |
| 200 | + setupExpandableDetails(with: coupon) | ||
| 174 | 201 | ||
| 202 | + // Coupon Code section | ||
| 175 | couponCodeTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) | 203 | couponCodeTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) |
| 176 | couponCodeTitleLabel.textColor = UIColor(rgb: 0x000F1E) | 204 | couponCodeTitleLabel.textColor = UIColor(rgb: 0x000F1E) |
| 177 | couponCodeTitleLabel.text = "Κωδικός Κουπονιού" | 205 | couponCodeTitleLabel.text = "Κωδικός Κουπονιού" |
| 178 | 206 | ||
| 179 | couponCodeValueLabel.font = UIFont(name: "PingLCG-Bold", size: 24) | 207 | couponCodeValueLabel.font = UIFont(name: "PingLCG-Bold", size: 24) |
| 180 | couponCodeValueLabel.textColor = UIColor(rgb: 0x000F1E) | 208 | couponCodeValueLabel.textColor = UIColor(rgb: 0x000F1E) |
| 181 | - couponCodeValueLabel.text = "coupons_ab" | 209 | + couponCodeValueLabel.text = coupon.coupon ?? "" |
| 182 | copyButton.addTarget(self, action: #selector(copyButtonTapped), for: .touchUpInside) | 210 | copyButton.addTarget(self, action: #selector(copyButtonTapped), for: .touchUpInside) |
| 183 | 211 | ||
| 212 | + // QR Code section | ||
| 184 | couponQRTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) | 213 | couponQRTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) |
| 185 | couponQRTitleLabel.textColor = UIColor(rgb: 0x000F1E) | 214 | couponQRTitleLabel.textColor = UIColor(rgb: 0x000F1E) |
| 186 | couponQRTitleLabel.text = "QR Κουπονιού" | 215 | couponQRTitleLabel.text = "QR Κουπονιού" |
| 187 | 216 | ||
| 217 | + // Terms — from couponset_data terms | ||
| 188 | termsLabel.font = UIFont(name: "PingLCG-Regular", size: 16) | 218 | termsLabel.font = UIFont(name: "PingLCG-Regular", size: 16) |
| 189 | termsLabel.textColor = UIColor(rgb: 0x020E1C) | 219 | termsLabel.textColor = UIColor(rgb: 0x020E1C) |
| 190 | - termsLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed ex euismod, feugiat justo eu, faucibus urna. Nulla sodales euismod arcu volutpat finibus. Etiam id urna at justo facilisis tempor. Morbi dignissim erat vitae magna sodales dignissim ac in mauris. Mauris tempor convallis tortor, interdum hendrerit turpis eleifend at. Praesent." | 220 | + let termsText = coupon.couponset_data?._terms ?? "" |
| 221 | + termsLabel.text = termsText.isEmpty ? "Δεν υπάρχουν διαθέσιμοι όροι χρήσης." : termsText | ||
| 191 | } | 222 | } |
| 192 | 223 | ||
| 193 | private var fullDetailsText = "" | 224 | private var fullDetailsText = "" |
| 194 | private var shouldTruncaitDetails = false | 225 | private var shouldTruncaitDetails = false |
| 195 | 226 | ||
| 196 | - private func setupExpandableDetails() { | 227 | + private func setupExpandableDetails(with coupon: CouponItemModel? = nil) { |
| 197 | - fullDetailsText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed ex euismod, feugiat justo eu, faucibus urna. Nulla sodales euismod arcu volutpat finibus. Etiam id urna at justo facilisis tempor. Morbi dignissim erat vitae magna sodales dignissim ac in mauris. Mauris tempor convallis tortor, interdum hendrerit turpis eleifend at. Praesent." | 228 | + // Use couponset_data description if available, otherwise merchant body, or fallback |
| 229 | + if let couponData = coupon { | ||
| 230 | + let description = couponData.couponset_data?._description ?? "" | ||
| 231 | + // let merchantBody = couponData.merchant_details?._body ?? "" | ||
| 232 | + | ||
| 233 | + if !description.isEmpty { | ||
| 234 | + fullDetailsText = description | ||
| 235 | + } | ||
| 236 | + // else if !merchantBody.isEmpty { | ||
| 237 | + // fullDetailsText = merchantBody | ||
| 238 | + // } | ||
| 239 | + else { | ||
| 240 | + fullDetailsText = "" | ||
| 241 | + } | ||
| 242 | + } else { | ||
| 243 | + fullDetailsText = "" | ||
| 244 | + } | ||
| 198 | 245 | ||
| 199 | detailsLabel.font = UIFont(name: "PingLCG-Regular", size: 18) | 246 | detailsLabel.font = UIFont(name: "PingLCG-Regular", size: 18) |
| 200 | detailsLabel.textColor = UIColor(rgb: 0x020E1C) | 247 | detailsLabel.textColor = UIColor(rgb: 0x020E1C) | ... | ... |
| ... | @@ -23,7 +23,15 @@ import UIKit | ... | @@ -23,7 +23,15 @@ import UIKit |
| 23 | super.init(coder: coder) | 23 | super.init(coder: coder) |
| 24 | } | 24 | } |
| 25 | 25 | ||
| 26 | - // MARK: - Dummy Data | 26 | + // MARK: - Dynamic Data |
| 27 | + var allCoupons: [CouponItemModel] = [] | ||
| 28 | + | ||
| 29 | + // MARK: - Loading State | ||
| 30 | + private var isLoading: Bool = true | ||
| 31 | + private var loadingIndicator: UIActivityIndicatorView? | ||
| 32 | + | ||
| 33 | + // MARK: - Dummy Data (commented out - replaced by dynamic allCoupons) | ||
| 34 | + /* | ||
| 27 | let allOffers: [OfferModel] = [ | 35 | let allOffers: [OfferModel] = [ |
| 28 | // Προτάσεις για εσένα | 36 | // Προτάσεις για εσένα |
| 29 | OfferModel( | 37 | OfferModel( |
| ... | @@ -184,6 +192,7 @@ import UIKit | ... | @@ -184,6 +192,7 @@ import UIKit |
| 184 | redeemed: true | 192 | redeemed: true |
| 185 | ) | 193 | ) |
| 186 | ] | 194 | ] |
| 195 | + */ | ||
| 187 | 196 | ||
| 188 | let couponFilters: [CouponFilterModel] = [ | 197 | let couponFilters: [CouponFilterModel] = [ |
| 189 | CouponFilterModel(title: "Ενεργά"), | 198 | CouponFilterModel(title: "Ενεργά"), |
| ... | @@ -217,7 +226,11 @@ import UIKit | ... | @@ -217,7 +226,11 @@ import UIKit |
| 217 | tableView.estimatedRowHeight = 200 | 226 | tableView.estimatedRowHeight = 200 |
| 218 | tableView.rowHeight = UITableView.automaticDimension | 227 | tableView.rowHeight = UITableView.automaticDimension |
| 219 | 228 | ||
| 220 | - initializeSections() | 229 | + // Set up loading indicator |
| 230 | + setupLoadingIndicator() | ||
| 231 | + | ||
| 232 | + // Fetch coupons from API | ||
| 233 | + fetchCoupons() | ||
| 221 | } | 234 | } |
| 222 | 235 | ||
| 223 | // NEW: Safe XIB registration method | 236 | // NEW: Safe XIB registration method |
| ... | @@ -240,53 +253,123 @@ import UIKit | ... | @@ -240,53 +253,123 @@ import UIKit |
| 240 | } | 253 | } |
| 241 | 254 | ||
| 242 | } | 255 | } |
| 256 | + | ||
| 257 | + // MARK: - Loading Indicator | ||
| 258 | + | ||
| 259 | + private func setupLoadingIndicator() { | ||
| 260 | + let indicator = UIActivityIndicatorView(style: .large) | ||
| 261 | + indicator.color = UIColor(rgb: 0x000F1E) | ||
| 262 | + indicator.translatesAutoresizingMaskIntoConstraints = false | ||
| 263 | + indicator.hidesWhenStopped = true | ||
| 264 | + view.addSubview(indicator) | ||
| 265 | + | ||
| 266 | + NSLayoutConstraint.activate([ | ||
| 267 | + indicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), | ||
| 268 | + indicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) | ||
| 269 | + ]) | ||
| 270 | + | ||
| 271 | + self.loadingIndicator = indicator | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + private func showLoading() { | ||
| 275 | + isLoading = true | ||
| 276 | + loadingIndicator?.startAnimating() | ||
| 277 | + tableView.alpha = 0.3 | ||
| 278 | + tableView.isUserInteractionEnabled = false | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + private func hideLoading() { | ||
| 282 | + isLoading = false | ||
| 283 | + loadingIndicator?.stopAnimating() | ||
| 284 | + tableView.alpha = 1.0 | ||
| 285 | + tableView.isUserInteractionEnabled = true | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + // MARK: - Data Fetching | ||
| 289 | + | ||
| 290 | + private func fetchCoupons() { | ||
| 291 | + showLoading() | ||
| 292 | + | ||
| 293 | + // Initialize sections with empty data first (so table view can render skeleton) | ||
| 294 | + initializeSections() | ||
| 295 | + | ||
| 296 | + WarplySDK.shared.getCouponsUniversal({ [weak self] couponsData in | ||
| 297 | + guard let self = self else { return } | ||
| 298 | + | ||
| 299 | + if let coupons = couponsData { | ||
| 300 | + self.allCoupons = coupons | ||
| 301 | + print("✅ [ProfileVC] Fetched \(coupons.count) coupons") | ||
| 302 | + | ||
| 303 | + // Debug: Print coupon statuses | ||
| 304 | + let activeCount = coupons.filter { $0.status == 1 }.count | ||
| 305 | + let redeemedCount = coupons.filter { $0.status == 0 }.count | ||
| 306 | + let expiredCount = coupons.filter { $0.status == -1 }.count | ||
| 307 | + print(" Active: \(activeCount), Redeemed: \(redeemedCount), Expired: \(expiredCount)") | ||
| 308 | + } else { | ||
| 309 | + self.allCoupons = [] | ||
| 310 | + print("⚠️ [ProfileVC] No coupons received") | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + self.initializeSections() | ||
| 314 | + self.hideLoading() | ||
| 315 | + }, failureCallback: { [weak self] errorCode in | ||
| 316 | + guard let self = self else { return } | ||
| 317 | + | ||
| 318 | + print("❌ [ProfileVC] Failed to fetch coupons, error: \(errorCode)") | ||
| 319 | + self.allCoupons = [] | ||
| 320 | + self.initializeSections() | ||
| 321 | + self.hideLoading() | ||
| 322 | + }) | ||
| 323 | + } | ||
| 243 | 324 | ||
| 244 | - // MARK: Function | 325 | + // MARK: - Section Initialization |
| 326 | + | ||
| 245 | func initializeSections() { | 327 | func initializeSections() { |
| 246 | - // Προτάσεις για εσένα | 328 | + // Προτάσεις για εσένα — empty for now |
| 247 | - let forYouOffers = allOffers.filter { $0.category == "Προτάσεις για εσένα" } | 329 | + let forYouOffers: [CouponItemModel] = [] |
| 248 | forYouOffersSection = SectionModel( | 330 | forYouOffersSection = SectionModel( |
| 249 | sectionType: .myRewardsHorizontalCouponsets, | 331 | sectionType: .myRewardsHorizontalCouponsets, |
| 250 | title: "Προτάσεις για εσένα", | 332 | title: "Προτάσεις για εσένα", |
| 251 | items: forYouOffers, | 333 | items: forYouOffers, |
| 252 | - itemType: .offers | 334 | + itemType: .coupons |
| 253 | ) | 335 | ) |
| 254 | 336 | ||
| 255 | - // Active Offers | 337 | + // Active Coupons — status == 1 |
| 256 | - let activeOffers = allOffers.filter { $0.active ?? false } | 338 | + let activeCoupons = allCoupons.filter { $0.status == 1 } |
| 257 | activeOffersSection = SectionModel( | 339 | activeOffersSection = SectionModel( |
| 258 | sectionType: .profileCoupon, | 340 | sectionType: .profileCoupon, |
| 259 | title: "Ενεργά", | 341 | title: "Ενεργά", |
| 260 | - items: activeOffers, | 342 | + items: activeCoupons, |
| 261 | - itemType: .offers | 343 | + itemType: .coupons |
| 262 | ) | 344 | ) |
| 263 | 345 | ||
| 346 | + // Set initial filtered section to active | ||
| 264 | filteredOffersSection = activeOffersSection | 347 | filteredOffersSection = activeOffersSection |
| 265 | 348 | ||
| 266 | - // Favorite Offers | 349 | + // Favorite Coupons — empty for now |
| 267 | - let favoriteOffers = allOffers.filter { $0.isFavorite } | 350 | + let favoriteCoupons: [CouponItemModel] = [] |
| 268 | favoriteOffersSection = SectionModel( | 351 | favoriteOffersSection = SectionModel( |
| 269 | sectionType: .profileCoupon, | 352 | sectionType: .profileCoupon, |
| 270 | title: "Αγαπημένα", | 353 | title: "Αγαπημένα", |
| 271 | - items: favoriteOffers, | 354 | + items: favoriteCoupons, |
| 272 | - itemType: .offers | 355 | + itemType: .coupons |
| 273 | ) | 356 | ) |
| 274 | 357 | ||
| 275 | - // Redeemed Offers | 358 | + // Redeemed Coupons — status == 0 |
| 276 | - let redeemedOffers = allOffers.filter { $0.redeemed ?? false } | 359 | + let redeemedCoupons = allCoupons.filter { $0.status == 0 } |
| 277 | redeemedOffersSection = SectionModel( | 360 | redeemedOffersSection = SectionModel( |
| 278 | sectionType: .profileCoupon, | 361 | sectionType: .profileCoupon, |
| 279 | title: "Εξαργυρωμένα", | 362 | title: "Εξαργυρωμένα", |
| 280 | - items: redeemedOffers, | 363 | + items: redeemedCoupons, |
| 281 | - itemType: .offers | 364 | + itemType: .coupons |
| 282 | ) | 365 | ) |
| 283 | 366 | ||
| 284 | self.tableView.reloadData() | 367 | self.tableView.reloadData() |
| 285 | } | 368 | } |
| 286 | 369 | ||
| 287 | - private func openCouponViewController(with offer: OfferModel) { | 370 | + private func openCouponViewController(with coupon: CouponItemModel) { |
| 288 | let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle) | 371 | let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle) |
| 289 | - vc.coupon = offer | 372 | + vc.coupon = coupon |
| 290 | 373 | ||
| 291 | self.navigationController?.pushViewController(vc, animated: true) | 374 | self.navigationController?.pushViewController(vc, animated: true) |
| 292 | } | 375 | } |
| ... | @@ -328,28 +411,13 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { | ... | @@ -328,28 +411,13 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { |
| 328 | 411 | ||
| 329 | public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { | 412 | public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { |
| 330 | return nil | 413 | return nil |
| 331 | -// if (section <= 3) { | ||
| 332 | -// return nil | ||
| 333 | -// } else { | ||
| 334 | -// // Return clear view for spacing | ||
| 335 | -//// let headerView = UIView() | ||
| 336 | -// let headerView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 19)) | ||
| 337 | -// headerView.backgroundColor = UIColor.white | ||
| 338 | -// return headerView | ||
| 339 | -// } | ||
| 340 | } | 414 | } |
| 341 | 415 | ||
| 342 | public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { | 416 | public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { |
| 343 | return 0.0 | 417 | return 0.0 |
| 344 | -// if (section <= 3) { | ||
| 345 | -// return 0.0 | ||
| 346 | -// } else { | ||
| 347 | -// return 19.0 | ||
| 348 | -// } | ||
| 349 | } | 418 | } |
| 350 | 419 | ||
| 351 | public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { | 420 | public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { |
| 352 | -// return CGFloat.leastNormalMagnitude | ||
| 353 | return 0.0 | 421 | return 0.0 |
| 354 | } | 422 | } |
| 355 | 423 | ||
| ... | @@ -360,9 +428,6 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { | ... | @@ -360,9 +428,6 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { |
| 360 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | 428 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
| 361 | if (indexPath.section == 0) { | 429 | if (indexPath.section == 0) { |
| 362 | let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileHeaderTableViewCell", for: indexPath) as! ProfileHeaderTableViewCell | 430 | let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileHeaderTableViewCell", for: indexPath) as! ProfileHeaderTableViewCell |
| 363 | -// cell.delegate = self // Set the banner offers delegate | ||
| 364 | -// cell.configureCell(data: self.bannerOffersSection) | ||
| 365 | -// cell.parent = self | ||
| 366 | return cell | 431 | return cell |
| 367 | 432 | ||
| 368 | } else if (indexPath.section == 1) { | 433 | } else if (indexPath.section == 1) { |
| ... | @@ -389,8 +454,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { | ... | @@ -389,8 +454,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { |
| 389 | let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileCouponTableViewCell", for: indexPath) as! ProfileCouponTableViewCell | 454 | let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileCouponTableViewCell", for: indexPath) as! ProfileCouponTableViewCell |
| 390 | if let items = self.filteredOffersSection?.items, | 455 | if let items = self.filteredOffersSection?.items, |
| 391 | indexPath.row < items.count, | 456 | indexPath.row < items.count, |
| 392 | - let offer = items[indexPath.row] as? OfferModel { | 457 | + let coupon = items[indexPath.row] as? CouponItemModel { |
| 393 | - cell.configureCell(data: offer) | 458 | + cell.configureCell(data: coupon) |
| 394 | } | 459 | } |
| 395 | return cell | 460 | return cell |
| 396 | } | 461 | } |
| ... | @@ -403,8 +468,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { | ... | @@ -403,8 +468,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { |
| 403 | } else { | 468 | } else { |
| 404 | if let items = self.filteredOffersSection?.items, | 469 | if let items = self.filteredOffersSection?.items, |
| 405 | indexPath.row < items.count, | 470 | indexPath.row < items.count, |
| 406 | - let offer = items[indexPath.row] as? OfferModel { | 471 | + let coupon = items[indexPath.row] as? CouponItemModel { |
| 407 | - openCouponViewController(with: offer) | 472 | + openCouponViewController(with: coupon) |
| 408 | } | 473 | } |
| 409 | } | 474 | } |
| 410 | } | 475 | } |
| ... | @@ -413,8 +478,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { | ... | @@ -413,8 +478,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { |
| 413 | // Add delegate conformance | 478 | // Add delegate conformance |
| 414 | extension ProfileViewController: MyRewardsOffersScrollTableViewCellDelegate { | 479 | extension ProfileViewController: MyRewardsOffersScrollTableViewCellDelegate { |
| 415 | func didSelectOffer(_ offer: OfferModel) { | 480 | func didSelectOffer(_ offer: OfferModel) { |
| 416 | - // Navigate to CouponViewController | 481 | + // Legacy OfferModel handling — no longer used but kept for protocol conformance |
| 417 | - openCouponViewController(with: offer) | 482 | + print("⚠️ [ProfileVC] didSelectOffer called with legacy OfferModel — should not happen") |
| 418 | } | 483 | } |
| 419 | 484 | ||
| 420 | func didSelectCouponSet(_ couponSet: CouponSetItemModel) { | 485 | func didSelectCouponSet(_ couponSet: CouponSetItemModel) { | ... | ... |
-
Please register or login to post a comment