Manos Chorianopoulos

add new CarouselContent at MyRewardsViewController

......@@ -7,7 +7,7 @@
<key>Pods-SwiftWarplyFramework.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
</dict>
</dict>
......
......@@ -76,6 +76,7 @@
626AF6DB2F698FF1008BCA08 /* MerchantAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626AF6DA2F698FF1008BCA08 /* MerchantAnnotation.swift */; };
626AF6DE2F699081008BCA08 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626AF6DC2F699081008BCA08 /* MapViewController.swift */; };
626AF6DF2F699081008BCA08 /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 626AF6DD2F699081008BCA08 /* MapViewController.xib */; };
626DC8012F6ACA3B00CFC8C2 /* CarouselItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626DC8002F6ACA3B00CFC8C2 /* CarouselItemModel.swift */; };
62A0A6D32F67FEDC00508534 /* MyCouponsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A0A6D12F67FEDC00508534 /* MyCouponsViewController.swift */; };
62A0A6D42F67FEDC00508534 /* MyCouponsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 62A0A6D22F67FEDC00508534 /* MyCouponsViewController.xib */; };
62A0A6D82F680C6A00508534 /* MyCouponsHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A0A6D62F680C6A00508534 /* MyCouponsHeaderTableViewCell.swift */; };
......@@ -160,6 +161,7 @@
626AF6DA2F698FF1008BCA08 /* MerchantAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MerchantAnnotation.swift; sourceTree = "<group>"; };
626AF6DC2F699081008BCA08 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; };
626AF6DD2F699081008BCA08 /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = "<group>"; };
626DC8002F6ACA3B00CFC8C2 /* CarouselItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselItemModel.swift; sourceTree = "<group>"; };
62A0A6D12F67FEDC00508534 /* MyCouponsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCouponsViewController.swift; sourceTree = "<group>"; };
62A0A6D22F67FEDC00508534 /* MyCouponsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MyCouponsViewController.xib; sourceTree = "<group>"; };
62A0A6D62F680C6A00508534 /* MyCouponsHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCouponsHeaderTableViewCell.swift; sourceTree = "<group>"; };
......@@ -212,6 +214,7 @@
1E089DF42DF87C39007459F1 /* Response.swift */,
1E089DF52DF87C39007459F1 /* SectionModel.swift */,
1E116F6A2DE86CAD009AE791 /* Models.swift */,
626DC8002F6ACA3B00CFC8C2 /* CarouselItemModel.swift */,
);
path = models;
sourceTree = "<group>";
......@@ -733,6 +736,7 @@
1E089E0A2DF87D16007459F1 /* EventDispatcher.swift in Sources */,
1E116F692DE845B1009AE791 /* ProfileFilterCollectionViewCell.swift in Sources */,
1E0E72472E0C3B1200BC926F /* DatabaseManager.swift in Sources */,
626DC8012F6ACA3B00CFC8C2 /* CarouselItemModel.swift in Sources */,
1EDBAF0D2DE8441000911E79 /* ProfileQuestionnaireTableViewCell.swift in Sources */,
1E64E1842DE48E0600543217 /* MyRewardsOfferCollectionViewCell.swift in Sources */,
E6A77955282933E70045BBA8 /* ViewControllerExtensions.swift in Sources */,
......
......@@ -7,7 +7,7 @@
<key>SwiftWarplyFramework.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
</dict>
</dict>
......
......@@ -4335,3 +4335,66 @@ public final class WarplySDK {
setCarouselList(carouselArray)
}
}
// MARK: - Carousel Content
extension WarplySDK {
/// Get carousel content
/// - Parameters:
/// - completion: Completion handler with array of CarouselItemModel
/// - failureCallback: Failure callback with error code
public func getCarouselContent(completion: @escaping ([CarouselItemModel]?) -> Void, failureCallback: @escaping (Int) -> Void) {
Task {
do {
let endpoint = Endpoint.getCarouselContent
let response = try await networkService.requestRaw(endpoint)
await MainActor.run {
if response["status"] as? Int == 1 {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_get_carousel_content_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
var items: [CarouselItemModel] = []
if let resultArray = response["result"] as? [[String: Any]] {
for itemDict in resultArray {
items.append(CarouselItemModel(dictionary: itemDict))
}
}
print("✅ [getCarouselContent] Parsed \(items.count) carousel items")
completion(items)
} else {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_error_get_carousel_content_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
failureCallback(-1)
}
}
} catch {
await MainActor.run {
self.handleError(error, context: "getCarouselContent", endpoint: "getCarouselContent", failureCallback: failureCallback)
}
}
}
}
/// Get carousel content (async/await variant)
/// - Returns: Array of CarouselItemModel
/// - Throws: WarplyError if the request fails
public func getCarouselContent() async throws -> [CarouselItemModel] {
return try await withCheckedThrowingContinuation { continuation in
getCarouselContent(completion: { result in
if let result = result {
continuation.resume(returning: result)
} else {
continuation.resume(throwing: WarplyError.networkError)
}
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
}
......
......@@ -78,6 +78,9 @@ public enum Endpoint {
// Articles
case getArticles(language: String, categories: [String]?)
// Carousel Content
case getCarouselContent
// Card Management
case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String)
case getCards
......@@ -139,7 +142,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, .retrieveCoupon:
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent:
return "/oauth/{appUUID}/context"
// Session endpoints - /api/session/{sessionUuid}
......@@ -168,7 +171,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, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getStores, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin:
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getStores, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin, .getCarouselContent:
return .POST
case .getSingleCampaign, .getNetworkStatus:
return .GET
......@@ -438,6 +441,14 @@ public enum Endpoint {
"content": contentParams
]
// Carousel Content - using content structure with get_carousel action
case .getCarouselContent:
return [
"content": [
"action": "get_carousel"
]
]
// Analytics endpoints - events structure
case .sendEvent(let eventName, let priority):
return [
......@@ -494,7 +505,7 @@ public enum Endpoint {
return .standardContext
// Authenticated Context - /oauth/{appUUID}/context
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon:
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent:
return .authenticatedContext
// Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token
......@@ -536,7 +547,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, .retrieveCoupon:
case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent:
return .bearerToken
// Basic Authentication (loyalty headers + Authorization: Basic)
......
......@@ -38,13 +38,17 @@ public class MyRewardsBannerOfferCollectionViewCell: UICollectionViewCell {
contentView.layer.masksToBounds = true
}
func configureCell(data: CampaignItemModel) {
// Use campaign's banner image - no hardcoded defaults
self.postImageURL = data._banner_img_mobile ?? ""
}
// func configureCell(data: CampaignItemModel) {
// // Use campaign's banner image - no hardcoded defaults
// self.postImageURL = data._banner_img_mobile ?? ""
// }
func configureCell(data: ArticleModel) {
// Use article's preview image - same visual treatment as campaigns
self.postImageURL = data._img_preview.isEmpty ? nil : data._img_preview
// func configureCell(data: ArticleModel) {
// // Use article's preview image - same visual treatment as campaigns
// self.postImageURL = data._img_preview.isEmpty ? nil : data._img_preview
// }
func configureCell(data: CarouselItemModel) {
self.postImageURL = data._app_img
}
}
......
......@@ -10,6 +10,7 @@ import UIKit
protocol MyRewardsBannerOffersScrollTableViewCellDelegate: AnyObject {
func didSelectBannerOffer(_ index: Int)
func didSelectBannerArticle(_ index: Int)
func didSelectBannerCarouselItem(_ index: Int)
}
@objc(MyRewardsBannerOffersScrollTableViewCell)
......@@ -96,7 +97,7 @@ extension MyRewardsBannerOffersScrollTableViewCell: UICollectionViewDataSource,
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyRewardsBannerOfferCollectionViewCell", for: indexPath) as! MyRewardsBannerOfferCollectionViewCell
// Handle both CampaignItemModel and ArticleModel
// Handle CarouselItemModel
guard let data = self.data,
let items = data.items,
indexPath.row < items.count else {
......@@ -105,10 +106,13 @@ extension MyRewardsBannerOffersScrollTableViewCell: UICollectionViewDataSource,
let item = items[indexPath.row]
if let campaign = item as? CampaignItemModel {
cell.configureCell(data: campaign)
} else if let article = item as? ArticleModel {
cell.configureCell(data: article)
// if let campaign = item as? CampaignItemModel {
// cell.configureCell(data: campaign)
// } else if let article = item as? ArticleModel {
// cell.configureCell(data: article)
// }
if let carouselItem = item as? CarouselItemModel {
cell.configureCell(data: carouselItem)
}
return cell
......@@ -124,10 +128,13 @@ extension MyRewardsBannerOffersScrollTableViewCell: UICollectionViewDataSource,
let item = items[indexPath.row]
if item is CampaignItemModel {
delegate?.didSelectBannerOffer(indexPath.row)
} else if item is ArticleModel {
delegate?.didSelectBannerArticle(indexPath.row)
// if item is CampaignItemModel {
// delegate?.didSelectBannerOffer(indexPath.row)
// } else if item is ArticleModel {
// delegate?.didSelectBannerArticle(indexPath.row)
// }
if item is CarouselItemModel {
delegate?.didSelectBannerCarouselItem(indexPath.row)
}
}
......
//
// CarouselItemModel.swift
// SwiftWarplyFramework
//
// Created by Manos Chorianopoulos on 18/3/26.
//
import Foundation
public class CarouselItemModel: NSObject {
private var uuid: String?
private var name: String?
private var entity: String?
private var app_img: String?
private var app_url: String?
private var url: String?
private var web_img: String?
private var web_img_responsive: String?
public override init() {
super.init()
}
public init(dictionary: [String: Any]) {
self.uuid = dictionary["uuid"] as? String ?? ""
self.name = dictionary["name"] as? String ?? ""
self.entity = dictionary["entity"] as? String ?? ""
self.app_img = dictionary["app_img"] as? String
self.app_url = dictionary["app_url"] as? String
self.url = dictionary["url"] as? String
self.web_img = dictionary["web_img"] as? String
self.web_img_responsive = dictionary["web_img_responsive"] as? String
}
public var _uuid: String { get { return self.uuid ?? "" } }
public var _name: String { get { return self.name ?? "" } }
public var _entity: String { get { return self.entity ?? "" } }
public var _app_img: String? { get { return self.app_img } }
public var _app_url: String? { get { return self.app_url } }
public var _url: String? { get { return self.url } }
public var _web_img: String? { get { return self.web_img } }
public var _web_img_responsive: String? { get { return self.web_img_responsive } }
}
......@@ -42,6 +42,9 @@ import UIKit
// Dynamic sections array - populated by API calls
var sections: [SectionModel] = []
// carouselItems data for banners
var carouselItems: [CarouselItemModel] = []
// Campaign data for banners
var bannerCampaigns: [CampaignItemModel] = []
......@@ -87,7 +90,8 @@ import UIKit
// Load data
loadProfile() // Load Profile
loadCampaigns() // Load campaigns
loadCarouselContent()
// loadCampaigns() // Load campaigns
loadCouponSets() // Load couponsets
loadCoupons()
......@@ -160,6 +164,23 @@ import UIKit
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
// MARK: - CarouselContent Loading
private func loadCarouselContent() {
// Load CarouselContent from WarplySDK
WarplySDK.shared.getCarouselContent { [weak self] carouselContent in
guard let self = self, let carouselContent = carouselContent else {
return
}
self.carouselItems = carouselContent
self.createBannerSection()
print("=== getCarouselContent ✅ [MyRewardsViewController] Loaded \(carouselContent.count) carouselContent")
} failureCallback: { [weak self] errorCode in
print("=== getCarouselContent Failed to load carouselContent: \(errorCode)")
}
}
// MARK: - Campaign Loading
private func loadCampaigns() {
......@@ -220,11 +241,14 @@ import UIKit
// Combine campaigns and articles for banner section
var bannerItems: [Any] = []
// Add campaigns first
bannerItems.append(contentsOf: self.bannerCampaigns)
// // Add campaigns first
// bannerItems.append(contentsOf: self.bannerCampaigns)
// Add articles after campaigns
bannerItems.append(contentsOf: self.articles)
// // Add articles after campaigns
// bannerItems.append(contentsOf: self.articles)
// // Add articles after campaigns
bannerItems.append(contentsOf: self.carouselItems)
// Create banner section if we have any items
if !bannerItems.isEmpty {
......@@ -779,6 +803,10 @@ extension MyRewardsViewController: MyRewardsBannerOffersScrollTableViewCellDeleg
openArticleViewController(with: index)
}
func didSelectBannerCarouselItem(_ index: Int) {
// TODO: handle carousel item tap
}
// func didTapProfileButton() {
// // Navigate to ProfileViewController
// openProfileViewController()
......