Manos Chorianopoulos

add retrieveCoupon functionality

...@@ -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>
......
...@@ -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
......
...@@ -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
......