Showing
9 changed files
with
184 additions
and
24 deletions
| ... | @@ -7,7 +7,7 @@ | ... | @@ -7,7 +7,7 @@ |
| 7 | <key>Pods-SwiftWarplyFramework.xcscheme_^#shared#^_</key> | 7 | <key>Pods-SwiftWarplyFramework.xcscheme_^#shared#^_</key> |
| 8 | <dict> | 8 | <dict> |
| 9 | <key>orderHint</key> | 9 | <key>orderHint</key> |
| 10 | - <integer>1</integer> | 10 | + <integer>0</integer> |
| 11 | </dict> | 11 | </dict> |
| 12 | </dict> | 12 | </dict> |
| 13 | </dict> | 13 | </dict> | ... | ... |
| ... | @@ -7,7 +7,7 @@ | ... | @@ -7,7 +7,7 @@ |
| 7 | <key>SwiftWarplyFramework.xcscheme_^#shared#^_</key> | 7 | <key>SwiftWarplyFramework.xcscheme_^#shared#^_</key> |
| 8 | <dict> | 8 | <dict> |
| 9 | <key>orderHint</key> | 9 | <key>orderHint</key> |
| 10 | - <integer>0</integer> | 10 | + <integer>1</integer> |
| 11 | </dict> | 11 | </dict> |
| 12 | </dict> | 12 | </dict> |
| 13 | </dict> | 13 | </dict> | ... | ... |
No preview for this file type
| ... | @@ -1675,6 +1675,77 @@ public final class WarplySDK { | ... | @@ -1675,6 +1675,77 @@ public final class WarplySDK { |
| 1675 | } | 1675 | } |
| 1676 | } | 1676 | } |
| 1677 | 1677 | ||
| 1678 | + /// Retrieve a coupon from a coupon set (offer) | ||
| 1679 | + /// - Parameters: | ||
| 1680 | + /// - couponSetUuid: The UUID of the coupon set (offer) to retrieve a coupon from | ||
| 1681 | + /// - completion: Completion handler with response dictionary containing coupon code and expiration | ||
| 1682 | + /// - failureCallback: Failure callback with error code | ||
| 1683 | + /// | ||
| 1684 | + /// Response structure on success: | ||
| 1685 | + /// ```json | ||
| 1686 | + /// { | ||
| 1687 | + /// "msg": "Retrieved", | ||
| 1688 | + /// "result": { | ||
| 1689 | + /// "coupon": "FNMRE2E5FXHH", | ||
| 1690 | + /// "expiration": "2026-10-31 23:59:00" | ||
| 1691 | + /// }, | ||
| 1692 | + /// "status": 1 | ||
| 1693 | + /// } | ||
| 1694 | + /// ``` | ||
| 1695 | + public func retrieveCoupon(couponSetUuid: String, completion: @escaping ([String: Any]?) -> Void, failureCallback: @escaping (Int) -> Void) { | ||
| 1696 | + Task { | ||
| 1697 | + do { | ||
| 1698 | + let response = try await networkService.retrieveCoupon(couponSetUuid: couponSetUuid) | ||
| 1699 | + | ||
| 1700 | + await MainActor.run { | ||
| 1701 | + if response["status"] as? Int == 1 { | ||
| 1702 | + let dynatraceEvent = LoyaltySDKDynatraceEventModel() | ||
| 1703 | + dynatraceEvent._eventName = "custom_success_retrieve_coupon_loyalty" | ||
| 1704 | + dynatraceEvent._parameters = nil | ||
| 1705 | + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent) | ||
| 1706 | + | ||
| 1707 | + print("✅ [WarplySDK] Coupon retrieved successfully") | ||
| 1708 | + if let result = response["result"] as? [String: Any] { | ||
| 1709 | + print(" Coupon: \(result["coupon"] as? String ?? "unknown")") | ||
| 1710 | + print(" Expiration: \(result["expiration"] as? String ?? "unknown")") | ||
| 1711 | + } | ||
| 1712 | + | ||
| 1713 | + completion(response) | ||
| 1714 | + } else { | ||
| 1715 | + let dynatraceEvent = LoyaltySDKDynatraceEventModel() | ||
| 1716 | + dynatraceEvent._eventName = "custom_error_retrieve_coupon_loyalty" | ||
| 1717 | + dynatraceEvent._parameters = nil | ||
| 1718 | + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent) | ||
| 1719 | + | ||
| 1720 | + failureCallback(-1) | ||
| 1721 | + } | ||
| 1722 | + } | ||
| 1723 | + } catch { | ||
| 1724 | + await MainActor.run { | ||
| 1725 | + self.handleError(error, context: "retrieveCoupon", endpoint: "retrieveCoupon", failureCallback: failureCallback) | ||
| 1726 | + } | ||
| 1727 | + } | ||
| 1728 | + } | ||
| 1729 | + } | ||
| 1730 | + | ||
| 1731 | + /// Retrieve a coupon from a coupon set (async/await variant) | ||
| 1732 | + /// - Parameter couponSetUuid: The UUID of the coupon set (offer) to retrieve a coupon from | ||
| 1733 | + /// - Returns: Response dictionary containing coupon code and expiration | ||
| 1734 | + /// - Throws: WarplyError if the request fails | ||
| 1735 | + public func retrieveCoupon(couponSetUuid: String) async throws -> [String: Any] { | ||
| 1736 | + return try await withCheckedThrowingContinuation { continuation in | ||
| 1737 | + retrieveCoupon(couponSetUuid: couponSetUuid, completion: { response in | ||
| 1738 | + if let response = response { | ||
| 1739 | + continuation.resume(returning: response) | ||
| 1740 | + } else { | ||
| 1741 | + continuation.resume(throwing: WarplyError.networkError) | ||
| 1742 | + } | ||
| 1743 | + }, failureCallback: { errorCode in | ||
| 1744 | + continuation.resume(throwing: WarplyError.unknownError(errorCode)) | ||
| 1745 | + }) | ||
| 1746 | + } | ||
| 1747 | + } | ||
| 1748 | + | ||
| 1678 | /// Verify ticket for user authentication | 1749 | /// Verify ticket for user authentication |
| 1679 | public func verifyTicket(guid: String, ticket: String, completion: @escaping (VerifyTicketResponseModel?) -> Void) { | 1750 | public func verifyTicket(guid: String, ticket: String, completion: @escaping (VerifyTicketResponseModel?) -> Void) { |
| 1680 | // Clear previous state | 1751 | // Clear previous state | ... | ... |
| ... | @@ -89,6 +89,7 @@ public enum Endpoint { | ... | @@ -89,6 +89,7 @@ public enum Endpoint { |
| 89 | // Coupon Operations | 89 | // Coupon Operations |
| 90 | case validateCoupon(coupon: [String: Any]) | 90 | case validateCoupon(coupon: [String: Any]) |
| 91 | case redeemCoupon(productId: String, productUuid: String, merchantId: String) | 91 | case redeemCoupon(productId: String, productUuid: String, merchantId: String) |
| 92 | + case retrieveCoupon(couponSetUuid: String) | ||
| 92 | 93 | ||
| 93 | // Profile | 94 | // Profile |
| 94 | case getProfile | 95 | case getProfile |
| ... | @@ -137,7 +138,7 @@ public enum Endpoint { | ... | @@ -137,7 +138,7 @@ public enum Endpoint { |
| 137 | return "/api/mobile/v2/{appUUID}/context/" | 138 | return "/api/mobile/v2/{appUUID}/context/" |
| 138 | 139 | ||
| 139 | // Authenticated Context endpoints - /oauth/{appUUID}/context | 140 | // Authenticated Context endpoints - /oauth/{appUUID}/context |
| 140 | - case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon: | 141 | + case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon: |
| 141 | return "/oauth/{appUUID}/context" | 142 | return "/oauth/{appUUID}/context" |
| 142 | 143 | ||
| 143 | // Session endpoints - /api/session/{sessionUuid} | 144 | // Session endpoints - /api/session/{sessionUuid} |
| ... | @@ -166,7 +167,7 @@ public enum Endpoint { | ... | @@ -166,7 +167,7 @@ public enum Endpoint { |
| 166 | switch self { | 167 | switch self { |
| 167 | case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, | 168 | case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, |
| 168 | .getCoupons, .getCouponSets, .getAvailableCoupons, | 169 | .getCoupons, .getCouponSets, .getAvailableCoupons, |
| 169 | - .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin: | 170 | + .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin: |
| 170 | return .POST | 171 | return .POST |
| 171 | case .getSingleCampaign, .getNetworkStatus: | 172 | case .getSingleCampaign, .getNetworkStatus: |
| 172 | return .GET | 173 | return .GET |
| ... | @@ -383,6 +384,14 @@ public enum Endpoint { | ... | @@ -383,6 +384,14 @@ public enum Endpoint { |
| 383 | ] | 384 | ] |
| 384 | ] | 385 | ] |
| 385 | 386 | ||
| 387 | + case .retrieveCoupon(let couponSetUuid): | ||
| 388 | + return [ | ||
| 389 | + "coupon": [ | ||
| 390 | + "action": "retrieve_coupon", | ||
| 391 | + "coupon_set": couponSetUuid | ||
| 392 | + ] | ||
| 393 | + ] | ||
| 394 | + | ||
| 386 | // Merchants - using correct shops structure for DEI API | 395 | // Merchants - using correct shops structure for DEI API |
| 387 | case .getMerchants(let language, let categories, let defaultShown, let center, let tags, let uuid, let distance, let parentUuids): | 396 | case .getMerchants(let language, let categories, let defaultShown, let center, let tags, let uuid, let distance, let parentUuids): |
| 388 | return [ | 397 | return [ |
| ... | @@ -473,7 +482,7 @@ public enum Endpoint { | ... | @@ -473,7 +482,7 @@ public enum Endpoint { |
| 473 | return .standardContext | 482 | return .standardContext |
| 474 | 483 | ||
| 475 | // Authenticated Context - /oauth/{appUUID}/context | 484 | // Authenticated Context - /oauth/{appUUID}/context |
| 476 | - case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon: | 485 | + case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon: |
| 477 | return .authenticatedContext | 486 | return .authenticatedContext |
| 478 | 487 | ||
| 479 | // Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token | 488 | // Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token |
| ... | @@ -515,7 +524,7 @@ public enum Endpoint { | ... | @@ -515,7 +524,7 @@ public enum Endpoint { |
| 515 | return .standard | 524 | return .standard |
| 516 | 525 | ||
| 517 | // Bearer Token Authentication (loyalty headers + Authorization: Bearer) | 526 | // Bearer Token Authentication (loyalty headers + Authorization: Bearer) |
| 518 | - case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon: | 527 | + case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon: |
| 519 | return .bearerToken | 528 | return .bearerToken |
| 520 | 529 | ||
| 521 | // Basic Authentication (loyalty headers + Authorization: Basic) | 530 | // Basic Authentication (loyalty headers + Authorization: Basic) | ... | ... |
| ... | @@ -1084,6 +1084,20 @@ extension NetworkService { | ... | @@ -1084,6 +1084,20 @@ extension NetworkService { |
| 1084 | return response | 1084 | return response |
| 1085 | } | 1085 | } |
| 1086 | 1086 | ||
| 1087 | + /// Retrieve a coupon from a coupon set | ||
| 1088 | + /// - Parameter couponSetUuid: The UUID of the coupon set (offer) to retrieve a coupon from | ||
| 1089 | + /// - Returns: Response dictionary containing the retrieved coupon code and expiration | ||
| 1090 | + /// - Throws: NetworkError if request fails | ||
| 1091 | + public func retrieveCoupon(couponSetUuid: String) async throws -> [String: Any] { | ||
| 1092 | + print("🔄 [NetworkService] Retrieving coupon for coupon set: \(couponSetUuid)") | ||
| 1093 | + let endpoint = Endpoint.retrieveCoupon(couponSetUuid: couponSetUuid) | ||
| 1094 | + let response = try await requestRaw(endpoint) | ||
| 1095 | + | ||
| 1096 | + print("✅ [NetworkService] Coupon retrieval request completed") | ||
| 1097 | + | ||
| 1098 | + return response | ||
| 1099 | + } | ||
| 1100 | + | ||
| 1087 | // MARK: - Card Security Utilities | 1101 | // MARK: - Card Security Utilities |
| 1088 | 1102 | ||
| 1089 | /// Mask card number for secure logging | 1103 | /// Mask card number for secure logging | ... | ... |
This diff is collapsed. Click to expand it.
| ... | @@ -42,9 +42,7 @@ import UIKit | ... | @@ -42,9 +42,7 @@ import UIKit |
| 42 | @IBOutlet weak var termsLabel: UILabel! | 42 | @IBOutlet weak var termsLabel: UILabel! |
| 43 | @IBOutlet weak var termsLabelHeight: NSLayoutConstraint! | 43 | @IBOutlet weak var termsLabelHeight: NSLayoutConstraint! |
| 44 | 44 | ||
| 45 | - @IBOutlet weak var mapButton: UIButton! | 45 | + @IBOutlet weak var redeemButton: UIButton! |
| 46 | - | ||
| 47 | - @IBOutlet weak var websiteButton: UIButton! | ||
| 48 | 46 | ||
| 49 | var couponset: CouponSetItemModel? | 47 | var couponset: CouponSetItemModel? |
| 50 | private var isDetailsExpanded = false | 48 | private var isDetailsExpanded = false |
| ... | @@ -52,6 +50,9 @@ import UIKit | ... | @@ -52,6 +50,9 @@ import UIKit |
| 52 | private var isCouponQRExpanded = false | 50 | private var isCouponQRExpanded = false |
| 53 | private var isTermsExpanded = false | 51 | private var isTermsExpanded = false |
| 54 | 52 | ||
| 53 | + // Loader overlay | ||
| 54 | + private var loaderOverlay: UIView? | ||
| 55 | + | ||
| 55 | var postImageURL: String? { | 56 | var postImageURL: String? { |
| 56 | didSet { | 57 | didSet { |
| 57 | if let url = postImageURL { | 58 | if let url = postImageURL { |
| ... | @@ -97,21 +98,13 @@ import UIKit | ... | @@ -97,21 +98,13 @@ import UIKit |
| 97 | termsButton.addTarget(self, action: #selector(toggleTerms), for: .touchUpInside) | 98 | termsButton.addTarget(self, action: #selector(toggleTerms), for: .touchUpInside) |
| 98 | termsLabelHeight.constant = 0 | 99 | termsLabelHeight.constant = 0 |
| 99 | 100 | ||
| 100 | - mapButton.titleLabel?.font = UIFont(name: "PingLCG-Bold", size: 16) | 101 | + redeemButton.titleLabel?.font = UIFont(name: "PingLCG-Bold", size: 16) |
| 101 | - mapButton.setTitle("Καταστήματα κοντά μου", for: .normal) | 102 | + redeemButton.setTitle("Απόκτησε το κουπόνι", for: .normal) |
| 102 | - mapButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .normal) | 103 | + redeemButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .normal) |
| 103 | - mapButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .highlighted) | 104 | + redeemButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .highlighted) |
| 104 | - mapButton.layer.cornerRadius = 4.0 | 105 | + redeemButton.layer.cornerRadius = 4.0 |
| 105 | - mapButton.backgroundColor = UIColor(rgb: 0x000F1E) | 106 | + redeemButton.backgroundColor = UIColor(rgb: 0x000F1E) |
| 106 | - | 107 | + redeemButton.addTarget(self, action: #selector(redeemButtonTapped), for: .touchUpInside) |
| 107 | - websiteButton.titleLabel?.font = UIFont(name: "PingLCG-Bold", size: 16) | ||
| 108 | - websiteButton.setTitle("Δες το website", for: .normal) | ||
| 109 | - websiteButton.setTitleColor(UIColor(rgb: 0x000F1E), for: .normal) | ||
| 110 | - websiteButton.setTitleColor(UIColor(rgb: 0x000F1E), for: .highlighted) | ||
| 111 | - websiteButton.backgroundColor = .clear | ||
| 112 | - websiteButton.layer.borderWidth = 1 | ||
| 113 | - websiteButton.layer.borderColor = UIColor(rgb: 0x000F1E).cgColor | ||
| 114 | - websiteButton.layer.cornerRadius = 4.0 | ||
| 115 | 108 | ||
| 116 | // Configure the view with offer data | 109 | // Configure the view with offer data |
| 117 | if let offer = couponset { | 110 | if let offer = couponset { |
| ... | @@ -119,6 +112,79 @@ import UIKit | ... | @@ -119,6 +112,79 @@ import UIKit |
| 119 | } | 112 | } |
| 120 | } | 113 | } |
| 121 | 114 | ||
| 115 | + @objc private func redeemButtonTapped() { | ||
| 116 | + guard let couponSetUuid = couponset?._uuid, !couponSetUuid.isEmpty else { | ||
| 117 | + showErrorAlert(message: "Δεν βρέθηκε το αναγνωριστικό της προσφοράς.") | ||
| 118 | + return | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + // Disable button to prevent double taps | ||
| 122 | + redeemButton.isEnabled = false | ||
| 123 | + showLoader() | ||
| 124 | + | ||
| 125 | + WarplySDK.shared.retrieveCoupon(couponSetUuid: couponSetUuid, completion: { [weak self] response in | ||
| 126 | + guard let self = self else { return } | ||
| 127 | + self.hideLoader() | ||
| 128 | + self.redeemButton.isEnabled = true | ||
| 129 | + | ||
| 130 | + if let response = response, | ||
| 131 | + let result = response["result"] as? [String: Any], | ||
| 132 | + let couponCode = result["coupon"] as? String { | ||
| 133 | + let expiration = result["expiration"] as? String ?? "" | ||
| 134 | + self.showSuccessAlert(couponCode: couponCode, expiration: expiration) | ||
| 135 | + } else { | ||
| 136 | + self.showErrorAlert(message: "Η ενεργοποίηση του κουπονιού απέτυχε. Παρακαλώ δοκίμασε ξανά.") | ||
| 137 | + } | ||
| 138 | + }, failureCallback: { [weak self] errorCode in | ||
| 139 | + guard let self = self else { return } | ||
| 140 | + self.hideLoader() | ||
| 141 | + self.redeemButton.isEnabled = true | ||
| 142 | + self.showErrorAlert(message: "Η ενεργοποίηση του κουπονιού απέτυχε. Παρακαλώ δοκίμασε ξανά.") | ||
| 143 | + }) | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + // MARK: - Loader | ||
| 147 | + | ||
| 148 | + private func showLoader() { | ||
| 149 | + let overlay = UIView(frame: self.view.bounds) | ||
| 150 | + overlay.backgroundColor = UIColor.black.withAlphaComponent(0.3) | ||
| 151 | + overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight] | ||
| 152 | + | ||
| 153 | + let spinner = UIActivityIndicatorView(style: .large) | ||
| 154 | + spinner.color = .white | ||
| 155 | + spinner.center = overlay.center | ||
| 156 | + spinner.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin] | ||
| 157 | + spinner.startAnimating() | ||
| 158 | + | ||
| 159 | + overlay.addSubview(spinner) | ||
| 160 | + self.view.addSubview(overlay) | ||
| 161 | + self.loaderOverlay = overlay | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + private func hideLoader() { | ||
| 165 | + loaderOverlay?.removeFromSuperview() | ||
| 166 | + loaderOverlay = nil | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + // MARK: - Alerts | ||
| 170 | + | ||
| 171 | + private func showSuccessAlert(couponCode: String, expiration: String) { | ||
| 172 | + var message = "Ο κωδικός κουπονιού σου:\n\(couponCode)" | ||
| 173 | + if !expiration.isEmpty { | ||
| 174 | + message += "\n\nΛήξη: \(expiration)" | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + let alert = UIAlertController(title: "Επιτυχία", message: message, preferredStyle: .alert) | ||
| 178 | + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) | ||
| 179 | + present(alert, animated: true, completion: nil) | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + private func showErrorAlert(message: String) { | ||
| 183 | + let alert = UIAlertController(title: "Σφάλμα", message: message, preferredStyle: .alert) | ||
| 184 | + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) | ||
| 185 | + present(alert, animated: true, completion: nil) | ||
| 186 | + } | ||
| 187 | + | ||
| 122 | private func setupUI(with couponset: CouponSetItemModel) { | 188 | private func setupUI(with couponset: CouponSetItemModel) { |
| 123 | // couponImage.image = UIImage(named: couponset._img_preview, in: Bundle.frameworkResourceBundle, compatibleWith: nil) | 189 | // couponImage.image = UIImage(named: couponset._img_preview, in: Bundle.frameworkResourceBundle, compatibleWith: nil) |
| 124 | self.postImageURL = couponset._img_preview | 190 | self.postImageURL = couponset._img_preview | ... | ... |
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment