Manos Chorianopoulos

add retrieveCoupon functionality

......@@ -7,7 +7,7 @@
<key>Pods-SwiftWarplyFramework.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
</dict>
</dict>
......
......@@ -7,7 +7,7 @@
<key>SwiftWarplyFramework.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
</dict>
</dict>
......
......@@ -1675,6 +1675,77 @@ public final class WarplySDK {
}
}
/// Retrieve a coupon from a coupon set (offer)
/// - Parameters:
/// - couponSetUuid: The UUID of the coupon set (offer) to retrieve a coupon from
/// - completion: Completion handler with response dictionary containing coupon code and expiration
/// - failureCallback: Failure callback with error code
///
/// Response structure on success:
/// ```json
/// {
/// "msg": "Retrieved",
/// "result": {
/// "coupon": "FNMRE2E5FXHH",
/// "expiration": "2026-10-31 23:59:00"
/// },
/// "status": 1
/// }
/// ```
public func retrieveCoupon(couponSetUuid: String, completion: @escaping ([String: Any]?) -> Void, failureCallback: @escaping (Int) -> Void) {
Task {
do {
let response = try await networkService.retrieveCoupon(couponSetUuid: couponSetUuid)
await MainActor.run {
if response["status"] as? Int == 1 {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_retrieve_coupon_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
print("✅ [WarplySDK] Coupon retrieved successfully")
if let result = response["result"] as? [String: Any] {
print(" Coupon: \(result["coupon"] as? String ?? "unknown")")
print(" Expiration: \(result["expiration"] as? String ?? "unknown")")
}
completion(response)
} else {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_error_retrieve_coupon_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
failureCallback(-1)
}
}
} catch {
await MainActor.run {
self.handleError(error, context: "retrieveCoupon", endpoint: "retrieveCoupon", failureCallback: failureCallback)
}
}
}
}
/// Retrieve a coupon from a coupon set (async/await variant)
/// - Parameter couponSetUuid: The UUID of the coupon set (offer) to retrieve a coupon from
/// - Returns: Response dictionary containing coupon code and expiration
/// - Throws: WarplyError if the request fails
public func retrieveCoupon(couponSetUuid: String) async throws -> [String: Any] {
return try await withCheckedThrowingContinuation { continuation in
retrieveCoupon(couponSetUuid: couponSetUuid, completion: { response in
if let response = response {
continuation.resume(returning: response)
} else {
continuation.resume(throwing: WarplyError.networkError)
}
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
/// Verify ticket for user authentication
public func verifyTicket(guid: String, ticket: String, completion: @escaping (VerifyTicketResponseModel?) -> Void) {
// Clear previous state
......
......@@ -89,6 +89,7 @@ public enum Endpoint {
// Coupon Operations
case validateCoupon(coupon: [String: Any])
case redeemCoupon(productId: String, productUuid: String, merchantId: String)
case retrieveCoupon(couponSetUuid: String)
// Profile
case getProfile
......@@ -137,7 +138,7 @@ public enum Endpoint {
return "/api/mobile/v2/{appUUID}/context/"
// Authenticated Context endpoints - /oauth/{appUUID}/context
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon:
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon:
return "/oauth/{appUUID}/context"
// Session endpoints - /api/session/{sessionUuid}
......@@ -166,7 +167,7 @@ public enum Endpoint {
switch self {
case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized,
.getCoupons, .getCouponSets, .getAvailableCoupons,
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin:
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin:
return .POST
case .getSingleCampaign, .getNetworkStatus:
return .GET
......@@ -383,6 +384,14 @@ public enum Endpoint {
]
]
case .retrieveCoupon(let couponSetUuid):
return [
"coupon": [
"action": "retrieve_coupon",
"coupon_set": couponSetUuid
]
]
// Merchants - using correct shops structure for DEI API
case .getMerchants(let language, let categories, let defaultShown, let center, let tags, let uuid, let distance, let parentUuids):
return [
......@@ -473,7 +482,7 @@ public enum Endpoint {
return .standardContext
// Authenticated Context - /oauth/{appUUID}/context
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon:
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon:
return .authenticatedContext
// Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token
......@@ -515,7 +524,7 @@ public enum Endpoint {
return .standard
// Bearer Token Authentication (loyalty headers + Authorization: Bearer)
case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon:
case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon:
return .bearerToken
// Basic Authentication (loyalty headers + Authorization: Basic)
......
......@@ -1084,6 +1084,20 @@ extension NetworkService {
return response
}
/// Retrieve a coupon from a coupon set
/// - Parameter couponSetUuid: The UUID of the coupon set (offer) to retrieve a coupon from
/// - Returns: Response dictionary containing the retrieved coupon code and expiration
/// - Throws: NetworkError if request fails
public func retrieveCoupon(couponSetUuid: String) async throws -> [String: Any] {
print("🔄 [NetworkService] Retrieving coupon for coupon set: \(couponSetUuid)")
let endpoint = Endpoint.retrieveCoupon(couponSetUuid: couponSetUuid)
let response = try await requestRaw(endpoint)
print("✅ [NetworkService] Coupon retrieval request completed")
return response
}
// MARK: - Card Security Utilities
/// Mask card number for secure logging
......
......@@ -42,9 +42,7 @@ import UIKit
@IBOutlet weak var termsLabel: UILabel!
@IBOutlet weak var termsLabelHeight: NSLayoutConstraint!
@IBOutlet weak var mapButton: UIButton!
@IBOutlet weak var websiteButton: UIButton!
@IBOutlet weak var redeemButton: UIButton!
var couponset: CouponSetItemModel?
private var isDetailsExpanded = false
......@@ -52,6 +50,9 @@ import UIKit
private var isCouponQRExpanded = false
private var isTermsExpanded = false
// Loader overlay
private var loaderOverlay: UIView?
var postImageURL: String? {
didSet {
if let url = postImageURL {
......@@ -97,21 +98,13 @@ import UIKit
termsButton.addTarget(self, action: #selector(toggleTerms), for: .touchUpInside)
termsLabelHeight.constant = 0
mapButton.titleLabel?.font = UIFont(name: "PingLCG-Bold", size: 16)
mapButton.setTitle("Καταστήματα κοντά μου", for: .normal)
mapButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .normal)
mapButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .highlighted)
mapButton.layer.cornerRadius = 4.0
mapButton.backgroundColor = UIColor(rgb: 0x000F1E)
websiteButton.titleLabel?.font = UIFont(name: "PingLCG-Bold", size: 16)
websiteButton.setTitle("Δες το website", for: .normal)
websiteButton.setTitleColor(UIColor(rgb: 0x000F1E), for: .normal)
websiteButton.setTitleColor(UIColor(rgb: 0x000F1E), for: .highlighted)
websiteButton.backgroundColor = .clear
websiteButton.layer.borderWidth = 1
websiteButton.layer.borderColor = UIColor(rgb: 0x000F1E).cgColor
websiteButton.layer.cornerRadius = 4.0
redeemButton.titleLabel?.font = UIFont(name: "PingLCG-Bold", size: 16)
redeemButton.setTitle("Απόκτησε το κουπόνι", for: .normal)
redeemButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .normal)
redeemButton.setTitleColor(UIColor(rgb: 0xFFFFFF), for: .highlighted)
redeemButton.layer.cornerRadius = 4.0
redeemButton.backgroundColor = UIColor(rgb: 0x000F1E)
redeemButton.addTarget(self, action: #selector(redeemButtonTapped), for: .touchUpInside)
// Configure the view with offer data
if let offer = couponset {
......@@ -119,6 +112,79 @@ import UIKit
}
}
@objc private func redeemButtonTapped() {
guard let couponSetUuid = couponset?._uuid, !couponSetUuid.isEmpty else {
showErrorAlert(message: "Δεν βρέθηκε το αναγνωριστικό της προσφοράς.")
return
}
// Disable button to prevent double taps
redeemButton.isEnabled = false
showLoader()
WarplySDK.shared.retrieveCoupon(couponSetUuid: couponSetUuid, completion: { [weak self] response in
guard let self = self else { return }
self.hideLoader()
self.redeemButton.isEnabled = true
if let response = response,
let result = response["result"] as? [String: Any],
let couponCode = result["coupon"] as? String {
let expiration = result["expiration"] as? String ?? ""
self.showSuccessAlert(couponCode: couponCode, expiration: expiration)
} else {
self.showErrorAlert(message: "Η ενεργοποίηση του κουπονιού απέτυχε. Παρακαλώ δοκίμασε ξανά.")
}
}, failureCallback: { [weak self] errorCode in
guard let self = self else { return }
self.hideLoader()
self.redeemButton.isEnabled = true
self.showErrorAlert(message: "Η ενεργοποίηση του κουπονιού απέτυχε. Παρακαλώ δοκίμασε ξανά.")
})
}
// MARK: - Loader
private func showLoader() {
let overlay = UIView(frame: self.view.bounds)
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.3)
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let spinner = UIActivityIndicatorView(style: .large)
spinner.color = .white
spinner.center = overlay.center
spinner.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin]
spinner.startAnimating()
overlay.addSubview(spinner)
self.view.addSubview(overlay)
self.loaderOverlay = overlay
}
private func hideLoader() {
loaderOverlay?.removeFromSuperview()
loaderOverlay = nil
}
// MARK: - Alerts
private func showSuccessAlert(couponCode: String, expiration: String) {
var message = "Ο κωδικός κουπονιού σου:\n\(couponCode)"
if !expiration.isEmpty {
message += "\n\nΛήξη: \(expiration)"
}
let alert = UIAlertController(title: "Επιτυχία", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
private func showErrorAlert(message: String) {
let alert = UIAlertController(title: "Σφάλμα", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
private func setupUI(with couponset: CouponSetItemModel) {
// couponImage.image = UIImage(named: couponset._img_preview, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
self.postImageURL = couponset._img_preview
......