Manos Chorianopoulos

added getArticles request, intergrated articles in MyRewardsVC

This diff is collapsed. Click to expand it.
......@@ -2274,6 +2274,91 @@ public final class WarplySDK {
}
}
// MARK: - Articles
/// Get articles (carousel content)
/// - Parameters:
/// - language: Language for the articles (optional, defaults to applicationLocale)
/// - categories: Categories to retrieve (optional, defaults to nil for all categories)
/// - completion: Completion handler with articles array
/// - failureCallback: Failure callback with error code
public func getArticles(
language: String? = nil,
categories: [String]? = nil,
completion: @escaping ([ArticleModel]?) -> Void,
failureCallback: @escaping (Int) -> Void
) {
let finalLanguage = language ?? self.applicationLocale
Task {
do {
let response = try await networkService.getArticles(language: finalLanguage, categories: categories)
await MainActor.run {
if response["status"] as? Int == 1 {
// Success analytics
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_get_articles_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
var articles: [ArticleModel] = []
// Parse from context.CONTENT structure
if let content = response["CONTENT"] as? [[String: Any]] {
for articleDict in content {
let article = ArticleModel(dictionary: articleDict)
articles.append(article)
}
let categoriesDesc = categories?.joined(separator: ", ") ?? "all categories"
print("✅ [WarplySDK] Retrieved \(articles.count) articles for \(categoriesDesc)")
completion(articles)
} else {
print("⚠️ [WarplySDK] No articles found in response")
completion([])
}
} else {
// Error analytics
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_error_get_articles_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
failureCallback(-1)
}
}
} catch {
await MainActor.run {
self.handleError(error, context: "getArticles", endpoint: "getArticles", failureCallback: failureCallback)
}
}
}
}
/// Get articles (async/await variant)
/// - Parameters:
/// - language: Language for the articles (optional, defaults to applicationLocale)
/// - categories: Categories to retrieve (optional, defaults to nil for all categories)
/// - Returns: Array of articles
/// - Throws: WarplyError if the request fails
public func getArticles(
language: String? = nil,
categories: [String]? = nil
) async throws -> [ArticleModel] {
return try await withCheckedThrowingContinuation { continuation in
getArticles(language: language, categories: categories, completion: { articles in
if let articles = articles {
continuation.resume(returning: articles)
} else {
continuation.resume(throwing: WarplyError.networkError)
}
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
// MARK: - Profile
/// Get user profile details
......
......@@ -73,6 +73,9 @@ public enum Endpoint {
case getMerchants(language: String, categories: [String], defaultShown: Bool, center: Double, tags: [String], uuid: String, distance: Int, parentUuids: [String])
case getMerchantCategories(language: String)
// Articles
case getArticles(language: String, categories: [String]?)
// Card Management
case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String)
case getCards
......@@ -127,7 +130,7 @@ public enum Endpoint {
return "/user/v5/{appUUID}/logout"
// Standard Context endpoints - /api/mobile/v2/{appUUID}/context/
case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories:
case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories, .getArticles:
return "/api/mobile/v2/{appUUID}/context/"
// Authenticated Context endpoints - /oauth/{appUUID}/context
......@@ -160,7 +163,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, .sendEvent, .sendDeviceInfo, .getCosmoteUser:
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser:
return .POST
case .getSingleCampaign, .getNetworkStatus:
return .GET
......@@ -389,6 +392,22 @@ public enum Endpoint {
]
]
// Articles - using content structure for DEI API
case .getArticles(let language, let categories):
var contentParams: [String: Any] = [
"language": language,
"action": "retrieve_multilingual"
]
// Only add category field if categories are provided
if let categories = categories, !categories.isEmpty {
contentParams["category"] = categories
}
return [
"content": contentParams
]
// Analytics endpoints - events structure
case .sendEvent(let eventName, let priority):
return [
......@@ -444,7 +463,7 @@ public enum Endpoint {
return .userManagement
// Standard Context - /api/mobile/v2/{appUUID}/context/
case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories:
case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories, .getArticles:
return .standardContext
// Authenticated Context - /oauth/{appUUID}/context
......@@ -486,7 +505,7 @@ public enum Endpoint {
// Standard Authentication (loyalty headers only)
case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout,
.verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo,
.getMerchants, .getMerchantCategories, .getNetworkStatus:
.getMerchants, .getMerchantCategories, .getArticles, .getNetworkStatus:
return .standard
// Bearer Token Authentication (loyalty headers + Authorization: Bearer)
......
......@@ -978,6 +978,26 @@ extension NetworkService {
return response
}
// MARK: - Articles Methods
/// Get articles (carousel content)
/// - Parameters:
/// - language: Language for the articles
/// - categories: Categories to retrieve (optional)
/// - Returns: Response dictionary containing articles
/// - Throws: NetworkError if request fails
public func getArticles(language: String, categories: [String]?) async throws -> [String: Any] {
let categoriesDesc = categories?.joined(separator: ", ") ?? "all categories"
print("🔄 [NetworkService] Getting articles for language: \(language), categories: \(categoriesDesc)")
let endpoint = Endpoint.getArticles(language: language, categories: categories)
let response = try await requestRaw(endpoint)
print("✅ [NetworkService] Get articles request completed")
return response
}
// MARK: - Coupon Operations Methods
/// Validate a coupon for the user
......
......@@ -40,4 +40,9 @@ public class MyRewardsBannerOfferCollectionViewCell: UICollectionViewCell {
// 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 ?? ""
}
}
......
......@@ -9,6 +9,7 @@ import UIKit
protocol MyRewardsBannerOffersScrollTableViewCellDelegate: AnyObject {
func didSelectBannerOffer(_ index: Int)
func didSelectBannerArticle(_ index: Int)
}
@objc(MyRewardsBannerOffersScrollTableViewCell)
......@@ -95,22 +96,39 @@ extension MyRewardsBannerOffersScrollTableViewCell: UICollectionViewDataSource,
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyRewardsBannerOfferCollectionViewCell", for: indexPath) as! MyRewardsBannerOfferCollectionViewCell
// Handle only CampaignItemModel - banner cells are campaign-specific
// Handle both CampaignItemModel and ArticleModel
guard let data = self.data,
let items = data.items,
indexPath.row < items.count,
let campaign = items[indexPath.row] as? CampaignItemModel else {
indexPath.row < items.count else {
return cell
}
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)
}
return cell
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Call the delegate method to notify the parent
// Determine item type and call appropriate delegate method
guard let data = self.data,
let items = data.items,
indexPath.row < items.count else {
return
}
let item = items[indexPath.row]
if item is CampaignItemModel {
delegate?.didSelectBannerOffer(indexPath.row)
} else if item is ArticleModel {
delegate?.didSelectBannerArticle(indexPath.row)
}
}
// MARK: - UICollectionViewDelegateFlowLayout
......
......@@ -113,5 +113,7 @@ public class MyRewardsProfileInfoTableViewCell: UITableViewCell {
private func loadProfileImage(from urlString: String) {
// For now, use default image - can be enhanced later with URL loading
profileImage.image = UIImage(named: "profile_pic_default", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
// TODO: Add dynamic profile picture and tags
}
}
......
......@@ -24,6 +24,8 @@ public enum SectionType {
public enum ItemType {
case profile // ProfileModel
case campaigns // [CampaignItemModel]
case articles // [ArticleModel]
case mixed // Mixed content (campaigns + articles)
case couponSets // [CouponSetItemModel]
case coupons // [CouponItemModel]
case offers // [OfferModel] - temporary, will migrate to dynamic coupons later
......
......@@ -40,6 +40,9 @@ import UIKit
// Campaign data for banners
var bannerCampaigns: [CampaignItemModel] = []
// Articles data for banners
var articles: [ArticleModel] = []
// Coupon sets data
var couponSets: [CouponSetItemModel] = []
......@@ -119,34 +122,82 @@ import UIKit
private func loadCampaigns() {
// Load campaigns from WarplySDK
WarplySDK.shared.getCampaigns { [weak self] campaigns in
guard let self = self, let campaigns = campaigns else { return }
guard let self = self, let campaigns = campaigns else {
// Even if campaigns fail, try to load articles
self?.loadArticles()
return
}
// Filter campaigns for banner display (contest campaigns) if needed
self.bannerCampaigns = campaigns
// .filter { campaign in
// // Filter by category "contest" or campaign_type "contest"
// return campaign._category == "contest" || campaign._campaign_type == "contest"
// }
.filter { campaign in
// Filter by category "contest" or campaign_type "contest"
return campaign._category == "contest" || campaign._campaign_type == "contest"
}
// Load articles after campaigns are loaded
self.loadArticles()
} failureCallback: { [weak self] errorCode in
print("Failed to load campaigns: \(errorCode)")
// Even if campaigns fail, try to load articles
self?.loadArticles()
}
}
// MARK: - Articles Loading
private func loadArticles() {
// Load articles from WarplySDK with "Carousel" category filter
WarplySDK.shared.getArticles(categories: ["Carousel"]) { [weak self] articles in
guard let self = self, let articles = articles else {
// Create banner section with only campaigns if articles fail
self?.createBannerSection()
return
}
self.articles = articles
print("✅ [MyRewardsViewController] Loaded \(articles.count) carousel articles")
// Create banner section with both campaigns and articles
self.createBannerSection()
// TODO: Add Couponsets here
} failureCallback: { [weak self] errorCode in
print("Failed to load carousel articles: \(errorCode)")
// Create banner section with only campaigns if articles fail
self?.createBannerSection()
}
}
// MARK: - Banner Section Creation
private func createBannerSection() {
// Combine campaigns and articles for banner section
var bannerItems: [Any] = []
// Add campaigns first
bannerItems.append(contentsOf: self.bannerCampaigns)
// Create banner section with real campaign data
if !self.bannerCampaigns.isEmpty {
// Add articles after campaigns
bannerItems.append(contentsOf: self.articles)
// Create banner section if we have any items
if !bannerItems.isEmpty {
let bannerSection = SectionModel(
sectionType: .myRewardsBannerOffers,
title: "Διαγωνισμός",
items: self.bannerCampaigns,
itemType: .campaigns
items: bannerItems,
itemType: .mixed
)
self.sections.append(bannerSection)
print("✅ [MyRewardsViewController] Created banner section with \(self.bannerCampaigns.count) campaigns and \(self.articles.count) articles")
}
// Reload table view with new sections
DispatchQueue.main.async {
self.tableView.reloadData()
}
} failureCallback: { [weak self] errorCode in
print("Failed to load campaigns: \(errorCode)")
// No sections added on failure - table will be empty
}
}
// MARK: - Coupon Sets Loading
......@@ -446,13 +497,25 @@ import UIKit
}
private func openCampaignViewController(with index: Int) {
// Validate index bounds
guard index < bannerCampaigns.count else {
print("Invalid campaign index: \(index)")
// Get the combined banner items (campaigns + articles)
var bannerItems: [Any] = []
bannerItems.append(contentsOf: self.bannerCampaigns)
bannerItems.append(contentsOf: self.articles)
// Validate index bounds for combined items
guard index < bannerItems.count else {
print("Invalid banner item index: \(index)")
return
}
let item = bannerItems[index]
// Handle only campaigns - articles will be handled by didSelectBannerArticle
guard let campaign = item as? CampaignItemModel else {
print("Item at index \(index) is not a campaign")
return
}
let campaign = bannerCampaigns[index]
let campaignUrl = campaign._campaign_url ?? campaign.index_url
// Check if URL is not empty before proceeding
......@@ -468,6 +531,32 @@ import UIKit
self.navigationController?.pushViewController(vc, animated: true)
}
private func openArticleViewController(with index: Int) {
// Get the combined banner items (campaigns + articles)
var bannerItems: [Any] = []
bannerItems.append(contentsOf: self.bannerCampaigns)
bannerItems.append(contentsOf: self.articles)
// Validate index bounds for combined items
guard index < bannerItems.count else {
print("Invalid banner item index: \(index)")
return
}
let item = bannerItems[index]
// Handle only articles
guard let article = item as? ArticleModel else {
print("Item at index \(index) is not an article")
return
}
// TODO: Implement article navigation
// This could navigate to a web view with article content,
// or a dedicated article detail screen
print("TODO: Navigate to article: \(article.title ?? "Unknown") - \(article.uuid ?? "No UUID")")
}
private func openCouponViewController(with offer: OfferModel) {
// let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle)
// vc.coupon = offer
......@@ -570,6 +659,11 @@ extension MyRewardsViewController: MyRewardsBannerOffersScrollTableViewCellDeleg
openCampaignViewController(with: index)
}
func didSelectBannerArticle(_ index: Int) {
// Navigate to Article detail (TODO implementation)
openArticleViewController(with: index)
}
// func didTapProfileButton() {
// // Navigate to ProfileViewController
// openProfileViewController()
......