Showing
6 changed files
with
332 additions
and
9 deletions
This diff is collapsed. Click to expand it.
... | @@ -2194,8 +2194,88 @@ public final class WarplySDK { | ... | @@ -2194,8 +2194,88 @@ public final class WarplySDK { |
2194 | } | 2194 | } |
2195 | } | 2195 | } |
2196 | 2196 | ||
2197 | - // MARK: - Profile | 2197 | + // MARK: - Merchant Categories |
2198 | + | ||
2199 | + /// Get merchant categories | ||
2200 | + /// - Parameters: | ||
2201 | + /// - language: Language for the categories (optional, defaults to applicationLocale) | ||
2202 | + /// - completion: Completion handler with merchant categories array | ||
2203 | + /// - failureCallback: Failure callback with error code | ||
2204 | + public func getMerchantCategories( | ||
2205 | + language: String? = nil, | ||
2206 | + completion: @escaping ([MerchantCategoryModel]?) -> Void, | ||
2207 | + failureCallback: @escaping (Int) -> Void | ||
2208 | + ) { | ||
2209 | + let finalLanguage = language ?? self.applicationLocale | ||
2210 | + | ||
2211 | + Task { | ||
2212 | + do { | ||
2213 | + let endpoint = Endpoint.getMerchantCategories(language: finalLanguage) | ||
2214 | + let response = try await networkService.requestRaw(endpoint) | ||
2215 | + | ||
2216 | + await MainActor.run { | ||
2217 | + if response["status"] as? Int == 1 { | ||
2218 | + // Success analytics | ||
2219 | + let dynatraceEvent = LoyaltySDKDynatraceEventModel() | ||
2220 | + dynatraceEvent._eventName = "custom_success_get_merchant_categories_loyalty" | ||
2221 | + dynatraceEvent._parameters = nil | ||
2222 | + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent) | ||
2223 | + | ||
2224 | + var categories: [MerchantCategoryModel] = [] | ||
2225 | + | ||
2226 | + // Parse from context.MAPP_SHOPS.result structure | ||
2227 | + if let mappShops = response["MAPP_SHOPS"] as? [String: Any], | ||
2228 | + let result = mappShops["result"] as? [[String: Any]] { | ||
2229 | + | ||
2230 | + for categoryDict in result { | ||
2231 | + let category = MerchantCategoryModel(dictionary: categoryDict) | ||
2232 | + categories.append(category) | ||
2233 | + } | ||
2234 | + | ||
2235 | + print("✅ [WarplySDK] Retrieved \(categories.count) merchant categories") | ||
2236 | + completion(categories) | ||
2237 | + } else { | ||
2238 | + print("⚠️ [WarplySDK] No merchant categories found in response") | ||
2239 | + completion([]) | ||
2240 | + } | ||
2241 | + } else { | ||
2242 | + // Error analytics | ||
2243 | + let dynatraceEvent = LoyaltySDKDynatraceEventModel() | ||
2244 | + dynatraceEvent._eventName = "custom_error_get_merchant_categories_loyalty" | ||
2245 | + dynatraceEvent._parameters = nil | ||
2246 | + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent) | ||
2247 | + | ||
2248 | + failureCallback(-1) | ||
2249 | + } | ||
2250 | + } | ||
2251 | + } catch { | ||
2252 | + await MainActor.run { | ||
2253 | + self.handleError(error, context: "getMerchantCategories", endpoint: "getMerchantCategories", failureCallback: failureCallback) | ||
2254 | + } | ||
2255 | + } | ||
2256 | + } | ||
2257 | + } | ||
2198 | 2258 | ||
2259 | + /// Get merchant categories (async/await variant) | ||
2260 | + /// - Parameter language: Language for the categories (optional, defaults to applicationLocale) | ||
2261 | + /// - Returns: Array of merchant categories | ||
2262 | + /// - Throws: WarplyError if the request fails | ||
2263 | + public func getMerchantCategories(language: String? = nil) async throws -> [MerchantCategoryModel] { | ||
2264 | + return try await withCheckedThrowingContinuation { continuation in | ||
2265 | + getMerchantCategories(language: language, completion: { categories in | ||
2266 | + if let categories = categories { | ||
2267 | + continuation.resume(returning: categories) | ||
2268 | + } else { | ||
2269 | + continuation.resume(throwing: WarplyError.networkError) | ||
2270 | + } | ||
2271 | + }, failureCallback: { errorCode in | ||
2272 | + continuation.resume(throwing: WarplyError.unknownError(errorCode)) | ||
2273 | + }) | ||
2274 | + } | ||
2275 | + } | ||
2276 | + | ||
2277 | + // MARK: - Profile | ||
2278 | + | ||
2199 | /// Get user profile details | 2279 | /// Get user profile details |
2200 | /// - Parameters: | 2280 | /// - Parameters: |
2201 | /// - completion: Completion handler with profile model | 2281 | /// - completion: Completion handler with profile model | ... | ... |
... | @@ -71,6 +71,7 @@ public enum Endpoint { | ... | @@ -71,6 +71,7 @@ public enum Endpoint { |
71 | // Market & Merchants | 71 | // Market & Merchants |
72 | case getMarketPassDetails | 72 | case getMarketPassDetails |
73 | case getMerchants(language: String, categories: [String], defaultShown: Bool, center: Double, tags: [String], uuid: String, distance: Int, parentUuids: [String]) | 73 | case getMerchants(language: String, categories: [String], defaultShown: Bool, center: Double, tags: [String], uuid: String, distance: Int, parentUuids: [String]) |
74 | + case getMerchantCategories(language: String) | ||
74 | 75 | ||
75 | // Card Management | 76 | // Card Management |
76 | case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String) | 77 | case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String) |
... | @@ -126,7 +127,7 @@ public enum Endpoint { | ... | @@ -126,7 +127,7 @@ public enum Endpoint { |
126 | return "/user/v5/{appUUID}/logout" | 127 | return "/user/v5/{appUUID}/logout" |
127 | 128 | ||
128 | // Standard Context endpoints - /api/mobile/v2/{appUUID}/context/ | 129 | // Standard Context endpoints - /api/mobile/v2/{appUUID}/context/ |
129 | - case .getCampaigns, .getAvailableCoupons, .getCouponSets: | 130 | + case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories: |
130 | return "/api/mobile/v2/{appUUID}/context/" | 131 | return "/api/mobile/v2/{appUUID}/context/" |
131 | 132 | ||
132 | // Authenticated Context endpoints - /oauth/{appUUID}/context | 133 | // Authenticated Context endpoints - /oauth/{appUUID}/context |
... | @@ -159,7 +160,7 @@ public enum Endpoint { | ... | @@ -159,7 +160,7 @@ public enum Endpoint { |
159 | switch self { | 160 | switch self { |
160 | case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, | 161 | case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, |
161 | .getCoupons, .getCouponSets, .getAvailableCoupons, | 162 | .getCoupons, .getCouponSets, .getAvailableCoupons, |
162 | - .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .sendEvent, .sendDeviceInfo, .getCosmoteUser: | 163 | + .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .sendEvent, .sendDeviceInfo, .getCosmoteUser: |
163 | return .POST | 164 | return .POST |
164 | case .getSingleCampaign, .getNetworkStatus: | 165 | case .getSingleCampaign, .getNetworkStatus: |
165 | return .GET | 166 | return .GET |
... | @@ -379,6 +380,15 @@ public enum Endpoint { | ... | @@ -379,6 +380,15 @@ public enum Endpoint { |
379 | ] | 380 | ] |
380 | ] | 381 | ] |
381 | 382 | ||
383 | + // Merchant Categories - using shops structure for DEI API | ||
384 | + case .getMerchantCategories(let language): | ||
385 | + return [ | ||
386 | + "shops": [ | ||
387 | + "language": language, | ||
388 | + "action": "retrieve_categories" | ||
389 | + ] | ||
390 | + ] | ||
391 | + | ||
382 | // Analytics endpoints - events structure | 392 | // Analytics endpoints - events structure |
383 | case .sendEvent(let eventName, let priority): | 393 | case .sendEvent(let eventName, let priority): |
384 | return [ | 394 | return [ |
... | @@ -434,7 +444,7 @@ public enum Endpoint { | ... | @@ -434,7 +444,7 @@ public enum Endpoint { |
434 | return .userManagement | 444 | return .userManagement |
435 | 445 | ||
436 | // Standard Context - /api/mobile/v2/{appUUID}/context/ | 446 | // Standard Context - /api/mobile/v2/{appUUID}/context/ |
437 | - case .getCampaigns, .getAvailableCoupons, .getCouponSets: | 447 | + case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories: |
438 | return .standardContext | 448 | return .standardContext |
439 | 449 | ||
440 | // Authenticated Context - /oauth/{appUUID}/context | 450 | // Authenticated Context - /oauth/{appUUID}/context |
... | @@ -476,7 +486,7 @@ public enum Endpoint { | ... | @@ -476,7 +486,7 @@ public enum Endpoint { |
476 | // Standard Authentication (loyalty headers only) | 486 | // Standard Authentication (loyalty headers only) |
477 | case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout, | 487 | case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout, |
478 | .verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo, | 488 | .verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo, |
479 | - .getMerchants, .getNetworkStatus: | 489 | + .getMerchants, .getMerchantCategories, .getNetworkStatus: |
480 | return .standard | 490 | return .standard |
481 | 491 | ||
482 | // Bearer Token Authentication (loyalty headers + Authorization: Bearer) | 492 | // Bearer Token Authentication (loyalty headers + Authorization: Bearer) | ... | ... |
... | @@ -962,6 +962,22 @@ extension NetworkService { | ... | @@ -962,6 +962,22 @@ extension NetworkService { |
962 | return response | 962 | return response |
963 | } | 963 | } |
964 | 964 | ||
965 | + // MARK: - Merchant Categories Methods | ||
966 | + | ||
967 | + /// Get merchant categories | ||
968 | + /// - Parameter language: Language for the categories | ||
969 | + /// - Returns: Response dictionary containing merchant categories | ||
970 | + /// - Throws: NetworkError if request fails | ||
971 | + public func getMerchantCategories(language: String) async throws -> [String: Any] { | ||
972 | + print("🔄 [NetworkService] Getting merchant categories for language: \(language)") | ||
973 | + let endpoint = Endpoint.getMerchantCategories(language: language) | ||
974 | + let response = try await requestRaw(endpoint) | ||
975 | + | ||
976 | + print("✅ [NetworkService] Get merchant categories request completed") | ||
977 | + | ||
978 | + return response | ||
979 | + } | ||
980 | + | ||
965 | // MARK: - Coupon Operations Methods | 981 | // MARK: - Coupon Operations Methods |
966 | 982 | ||
967 | /// Validate a coupon for the user | 983 | /// Validate a coupon for the user | ... | ... |
1 | +// | ||
2 | +// MerchantCategoryModel.swift | ||
3 | +// SwiftWarplyFramework | ||
4 | +// | ||
5 | +// Created by Warply on 28/07/2025. | ||
6 | +// Copyright © 2025 Warply. All rights reserved. | ||
7 | +// | ||
8 | + | ||
9 | +import Foundation | ||
10 | + | ||
11 | +// MARK: - Merchant Category Model | ||
12 | + | ||
13 | +public class MerchantCategoryModel: NSObject { | ||
14 | + private var uuid: String? | ||
15 | + private var admin_name: String? | ||
16 | + private var image: String? | ||
17 | + private var parent: String? | ||
18 | + private var fields: String? | ||
19 | + private var children: [Any]? | ||
20 | + private var count: Int? | ||
21 | + private var name: String? | ||
22 | + | ||
23 | + public init() { | ||
24 | + self.uuid = "" | ||
25 | + self.admin_name = "" | ||
26 | + self.image = "" | ||
27 | + self.parent = "" | ||
28 | + self.fields = "" | ||
29 | + self.children = [] | ||
30 | + self.count = 0 | ||
31 | + self.name = "" | ||
32 | + } | ||
33 | + | ||
34 | + public init(dictionary: [String: Any]) { | ||
35 | + self.uuid = dictionary["uuid"] as? String ?? "" | ||
36 | + self.admin_name = dictionary["admin_name"] as? String ?? "" | ||
37 | + self.image = dictionary["image"] as? String ?? "" | ||
38 | + self.parent = dictionary["parent"] as? String | ||
39 | + self.fields = dictionary["fields"] as? String ?? "" | ||
40 | + self.children = dictionary["children"] as? [Any] ?? [] | ||
41 | + self.count = dictionary["count"] as? Int ?? 0 | ||
42 | + self.name = dictionary["name"] as? String | ||
43 | + } | ||
44 | + | ||
45 | + // MARK: - Public Accessors | ||
46 | + | ||
47 | + public var _uuid: String { | ||
48 | + get { return self.uuid ?? "" } | ||
49 | + set(newValue) { self.uuid = newValue } | ||
50 | + } | ||
51 | + | ||
52 | + public var _admin_name: String { | ||
53 | + get { return self.admin_name ?? "" } | ||
54 | + set(newValue) { self.admin_name = newValue } | ||
55 | + } | ||
56 | + | ||
57 | + public var _image: String { | ||
58 | + get { return self.image ?? "" } | ||
59 | + set(newValue) { self.image = newValue } | ||
60 | + } | ||
61 | + | ||
62 | + public var _parent: String? { | ||
63 | + get { return self.parent } | ||
64 | + set(newValue) { self.parent = newValue } | ||
65 | + } | ||
66 | + | ||
67 | + public var _fields: String { | ||
68 | + get { return self.fields ?? "" } | ||
69 | + set(newValue) { self.fields = newValue } | ||
70 | + } | ||
71 | + | ||
72 | + public var _children: [Any] { | ||
73 | + get { return self.children ?? [] } | ||
74 | + set(newValue) { self.children = newValue } | ||
75 | + } | ||
76 | + | ||
77 | + public var _count: Int { | ||
78 | + get { return self.count ?? 0 } | ||
79 | + set(newValue) { self.count = newValue } | ||
80 | + } | ||
81 | + | ||
82 | + public var _name: String? { | ||
83 | + get { return self.name } | ||
84 | + set(newValue) { self.name = newValue } | ||
85 | + } | ||
86 | + | ||
87 | + // MARK: - Computed Properties | ||
88 | + | ||
89 | + /// Display name for the category - uses name if available, otherwise falls back to admin_name | ||
90 | + public var displayName: String { | ||
91 | + return self.name ?? self.admin_name ?? "" | ||
92 | + } | ||
93 | + | ||
94 | + /// Clean image URL with whitespace trimmed | ||
95 | + public var cleanImageUrl: String { | ||
96 | + return self.image?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" | ||
97 | + } | ||
98 | + | ||
99 | + /// Check if this category has a parent category | ||
100 | + public var hasParent: Bool { | ||
101 | + return self.parent != nil && !(self.parent?.isEmpty ?? true) | ||
102 | + } | ||
103 | + | ||
104 | + /// Check if this category has child categories | ||
105 | + public var hasChildren: Bool { | ||
106 | + return !self.children.isEmpty | ||
107 | + } | ||
108 | +} | ||
109 | + | ||
110 | +// MARK: - Codable Support | ||
111 | + | ||
112 | +extension MerchantCategoryModel: Codable { | ||
113 | + private enum CodingKeys: String, CodingKey { | ||
114 | + case uuid | ||
115 | + case admin_name | ||
116 | + case image | ||
117 | + case parent | ||
118 | + case fields | ||
119 | + case children | ||
120 | + case count | ||
121 | + case name | ||
122 | + } | ||
123 | + | ||
124 | + public func encode(to encoder: Encoder) throws { | ||
125 | + var container = encoder.container(keyedBy: CodingKeys.self) | ||
126 | + try container.encode(uuid, forKey: .uuid) | ||
127 | + try container.encode(admin_name, forKey: .admin_name) | ||
128 | + try container.encode(image, forKey: .image) | ||
129 | + try container.encodeIfPresent(parent, forKey: .parent) | ||
130 | + try container.encode(fields, forKey: .fields) | ||
131 | + try container.encode(count, forKey: .count) | ||
132 | + try container.encodeIfPresent(name, forKey: .name) | ||
133 | + // Note: children is [Any] so we skip encoding it for now | ||
134 | + } | ||
135 | + | ||
136 | + public required init(from decoder: Decoder) throws { | ||
137 | + let container = try decoder.container(keyedBy: CodingKeys.self) | ||
138 | + self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) | ||
139 | + self.admin_name = try container.decodeIfPresent(String.self, forKey: .admin_name) | ||
140 | + self.image = try container.decodeIfPresent(String.self, forKey: .image) | ||
141 | + self.parent = try container.decodeIfPresent(String.self, forKey: .parent) | ||
142 | + self.fields = try container.decodeIfPresent(String.self, forKey: .fields) | ||
143 | + self.count = try container.decodeIfPresent(Int.self, forKey: .count) | ||
144 | + self.name = try container.decodeIfPresent(String.self, forKey: .name) | ||
145 | + self.children = [] // Default empty array for children | ||
146 | + } | ||
147 | +} | ||
148 | + | ||
149 | +// MARK: - Debug Description | ||
150 | + | ||
151 | +extension MerchantCategoryModel { | ||
152 | + public override var description: String { | ||
153 | + return """ | ||
154 | + MerchantCategoryModel { | ||
155 | + uuid: \(_uuid) | ||
156 | + displayName: \(displayName) | ||
157 | + admin_name: \(_admin_name) | ||
158 | + image: \(cleanImageUrl) | ||
159 | + parent: \(_parent ?? "nil") | ||
160 | + count: \(_count) | ||
161 | + hasChildren: \(hasChildren) | ||
162 | + } | ||
163 | + """ | ||
164 | + } | ||
165 | +} |
... | @@ -46,6 +46,9 @@ import UIKit | ... | @@ -46,6 +46,9 @@ import UIKit |
46 | // Merchants data | 46 | // Merchants data |
47 | var merchants: [MerchantModel] = [] | 47 | var merchants: [MerchantModel] = [] |
48 | 48 | ||
49 | + // Merchant categories data | ||
50 | + var merchantCategories: [MerchantCategoryModel] = [] | ||
51 | + | ||
49 | // Profile data | 52 | // Profile data |
50 | var profileModel: ProfileModel? | 53 | var profileModel: ProfileModel? |
51 | var profileSection: SectionModel? | 54 | var profileSection: SectionModel? |
... | @@ -176,9 +179,8 @@ import UIKit | ... | @@ -176,9 +179,8 @@ import UIKit |
176 | self.merchants = merchants | 179 | self.merchants = merchants |
177 | print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants") | 180 | print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants") |
178 | 181 | ||
179 | - // For now, create the coupon sets section without filtering | 182 | + // Load merchant categories after merchants success |
180 | - // Later this will be enhanced to filter by merchant categories | 183 | + self.loadMerchantCategories() |
181 | - self.createCouponSetsSection() | ||
182 | 184 | ||
183 | } failureCallback: { [weak self] errorCode in | 185 | } failureCallback: { [weak self] errorCode in |
184 | print("Failed to load merchants: \(errorCode)") | 186 | print("Failed to load merchants: \(errorCode)") |
... | @@ -187,8 +189,58 @@ import UIKit | ... | @@ -187,8 +189,58 @@ import UIKit |
187 | } | 189 | } |
188 | } | 190 | } |
189 | 191 | ||
192 | + // MARK: - Merchant Categories Loading | ||
193 | + private func loadMerchantCategories() { | ||
194 | + // Load merchant categories from WarplySDK | ||
195 | + WarplySDK.shared.getMerchantCategories { [weak self] categories in | ||
196 | + guard let self = self, let categories = categories else { | ||
197 | + // If categories fail to load, still create coupon sets section without filtering | ||
198 | + self?.createCouponSetsSection() | ||
199 | + return | ||
200 | + } | ||
201 | + | ||
202 | + self.merchantCategories = categories | ||
203 | + print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories") | ||
204 | + | ||
205 | + // TODO: Implement category-based filtering for coupon sets sections | ||
206 | + // For now, create the standard coupon sets section | ||
207 | + self.createCouponSetsSection() | ||
208 | + | ||
209 | + } failureCallback: { [weak self] errorCode in | ||
210 | + print("Failed to load merchant categories: \(errorCode)") | ||
211 | + // If categories fail, still show coupon sets without filtering | ||
212 | + self?.createCouponSetsSection() | ||
213 | + } | ||
214 | + } | ||
215 | + | ||
190 | private func createCouponSetsSection() { | 216 | private func createCouponSetsSection() { |
191 | - // Create coupon sets section with real data | 217 | + // TODO: IMPLEMENT CATEGORY-BASED FILTERING |
218 | + // | ||
219 | + // Current logic: Creates one section with all coupon sets | ||
220 | + // | ||
221 | + // Future enhancement: Filter coupon sets into different sections based on categories | ||
222 | + // Logic: | ||
223 | + // 1. For each couponset, get its merchant_uuid | ||
224 | + // 2. Find the merchant with that merchant_uuid in self.merchants | ||
225 | + // 3. Get the merchant's category_uuid | ||
226 | + // 4. Find the category with that category_uuid in self.merchantCategories | ||
227 | + // 5. Group coupon sets by category | ||
228 | + // 6. Create separate sections for each category | ||
229 | + // | ||
230 | + // Example structure after filtering: | ||
231 | + // - Section: "Εκπαίδευση" (Education) - coupon sets from education merchants | ||
232 | + // - Section: "Ψυχαγωγία" (Entertainment) - coupon sets from entertainment merchants | ||
233 | + // - etc. | ||
234 | + // | ||
235 | + // Implementation steps: | ||
236 | + // 1. Create a dictionary to group coupon sets by category: [String: [CouponSetItemModel]] | ||
237 | + // 2. Iterate through self.couponSets | ||
238 | + // 3. For each coupon set, find its merchant and category | ||
239 | + // 4. Add coupon set to the appropriate category group | ||
240 | + // 5. Create a SectionModel for each category group | ||
241 | + // 6. Sort sections by category name or priority | ||
242 | + | ||
243 | + // Current implementation (temporary): | ||
192 | if !self.couponSets.isEmpty { | 244 | if !self.couponSets.isEmpty { |
193 | let couponSetsSection = SectionModel( | 245 | let couponSetsSection = SectionModel( |
194 | sectionType: .myRewardsHorizontalCouponsets, | 246 | sectionType: .myRewardsHorizontalCouponsets, | ... | ... |
-
Please register or login to post a comment