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 { ...@@ -2274,6 +2274,91 @@ public final class WarplySDK {
2274 } 2274 }
2275 } 2275 }
2276 2276
2277 + // MARK: - Articles
2278 +
2279 + /// Get articles (carousel content)
2280 + /// - Parameters:
2281 + /// - language: Language for the articles (optional, defaults to applicationLocale)
2282 + /// - categories: Categories to retrieve (optional, defaults to nil for all categories)
2283 + /// - completion: Completion handler with articles array
2284 + /// - failureCallback: Failure callback with error code
2285 + public func getArticles(
2286 + language: String? = nil,
2287 + categories: [String]? = nil,
2288 + completion: @escaping ([ArticleModel]?) -> Void,
2289 + failureCallback: @escaping (Int) -> Void
2290 + ) {
2291 + let finalLanguage = language ?? self.applicationLocale
2292 +
2293 + Task {
2294 + do {
2295 + let response = try await networkService.getArticles(language: finalLanguage, categories: categories)
2296 +
2297 + await MainActor.run {
2298 + if response["status"] as? Int == 1 {
2299 + // Success analytics
2300 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
2301 + dynatraceEvent._eventName = "custom_success_get_articles_loyalty"
2302 + dynatraceEvent._parameters = nil
2303 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
2304 +
2305 + var articles: [ArticleModel] = []
2306 +
2307 + // Parse from context.CONTENT structure
2308 + if let content = response["CONTENT"] as? [[String: Any]] {
2309 + for articleDict in content {
2310 + let article = ArticleModel(dictionary: articleDict)
2311 + articles.append(article)
2312 + }
2313 +
2314 + let categoriesDesc = categories?.joined(separator: ", ") ?? "all categories"
2315 + print("✅ [WarplySDK] Retrieved \(articles.count) articles for \(categoriesDesc)")
2316 + completion(articles)
2317 + } else {
2318 + print("⚠️ [WarplySDK] No articles found in response")
2319 + completion([])
2320 + }
2321 + } else {
2322 + // Error analytics
2323 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
2324 + dynatraceEvent._eventName = "custom_error_get_articles_loyalty"
2325 + dynatraceEvent._parameters = nil
2326 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
2327 +
2328 + failureCallback(-1)
2329 + }
2330 + }
2331 + } catch {
2332 + await MainActor.run {
2333 + self.handleError(error, context: "getArticles", endpoint: "getArticles", failureCallback: failureCallback)
2334 + }
2335 + }
2336 + }
2337 + }
2338 +
2339 + /// Get articles (async/await variant)
2340 + /// - Parameters:
2341 + /// - language: Language for the articles (optional, defaults to applicationLocale)
2342 + /// - categories: Categories to retrieve (optional, defaults to nil for all categories)
2343 + /// - Returns: Array of articles
2344 + /// - Throws: WarplyError if the request fails
2345 + public func getArticles(
2346 + language: String? = nil,
2347 + categories: [String]? = nil
2348 + ) async throws -> [ArticleModel] {
2349 + return try await withCheckedThrowingContinuation { continuation in
2350 + getArticles(language: language, categories: categories, completion: { articles in
2351 + if let articles = articles {
2352 + continuation.resume(returning: articles)
2353 + } else {
2354 + continuation.resume(throwing: WarplyError.networkError)
2355 + }
2356 + }, failureCallback: { errorCode in
2357 + continuation.resume(throwing: WarplyError.unknownError(errorCode))
2358 + })
2359 + }
2360 + }
2361 +
2277 // MARK: - Profile 2362 // MARK: - Profile
2278 2363
2279 /// Get user profile details 2364 /// Get user profile details
......
...@@ -73,6 +73,9 @@ public enum Endpoint { ...@@ -73,6 +73,9 @@ public enum Endpoint {
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 case getMerchantCategories(language: String)
75 75
76 + // Articles
77 + case getArticles(language: String, categories: [String]?)
78 +
76 // Card Management 79 // Card Management
77 case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String) 80 case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String)
78 case getCards 81 case getCards
...@@ -127,7 +130,7 @@ public enum Endpoint { ...@@ -127,7 +130,7 @@ public enum Endpoint {
127 return "/user/v5/{appUUID}/logout" 130 return "/user/v5/{appUUID}/logout"
128 131
129 // Standard Context endpoints - /api/mobile/v2/{appUUID}/context/ 132 // Standard Context endpoints - /api/mobile/v2/{appUUID}/context/
130 - case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories: 133 + case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories, .getArticles:
131 return "/api/mobile/v2/{appUUID}/context/" 134 return "/api/mobile/v2/{appUUID}/context/"
132 135
133 // Authenticated Context endpoints - /oauth/{appUUID}/context 136 // Authenticated Context endpoints - /oauth/{appUUID}/context
...@@ -160,7 +163,7 @@ public enum Endpoint { ...@@ -160,7 +163,7 @@ public enum Endpoint {
160 switch self { 163 switch self {
161 case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, 164 case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized,
162 .getCoupons, .getCouponSets, .getAvailableCoupons, 165 .getCoupons, .getCouponSets, .getAvailableCoupons,
163 - .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .sendEvent, .sendDeviceInfo, .getCosmoteUser: 166 + .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser:
164 return .POST 167 return .POST
165 case .getSingleCampaign, .getNetworkStatus: 168 case .getSingleCampaign, .getNetworkStatus:
166 return .GET 169 return .GET
...@@ -389,6 +392,22 @@ public enum Endpoint { ...@@ -389,6 +392,22 @@ public enum Endpoint {
389 ] 392 ]
390 ] 393 ]
391 394
395 + // Articles - using content structure for DEI API
396 + case .getArticles(let language, let categories):
397 + var contentParams: [String: Any] = [
398 + "language": language,
399 + "action": "retrieve_multilingual"
400 + ]
401 +
402 + // Only add category field if categories are provided
403 + if let categories = categories, !categories.isEmpty {
404 + contentParams["category"] = categories
405 + }
406 +
407 + return [
408 + "content": contentParams
409 + ]
410 +
392 // Analytics endpoints - events structure 411 // Analytics endpoints - events structure
393 case .sendEvent(let eventName, let priority): 412 case .sendEvent(let eventName, let priority):
394 return [ 413 return [
...@@ -444,7 +463,7 @@ public enum Endpoint { ...@@ -444,7 +463,7 @@ public enum Endpoint {
444 return .userManagement 463 return .userManagement
445 464
446 // Standard Context - /api/mobile/v2/{appUUID}/context/ 465 // Standard Context - /api/mobile/v2/{appUUID}/context/
447 - case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories: 466 + case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories, .getArticles:
448 return .standardContext 467 return .standardContext
449 468
450 // Authenticated Context - /oauth/{appUUID}/context 469 // Authenticated Context - /oauth/{appUUID}/context
...@@ -486,7 +505,7 @@ public enum Endpoint { ...@@ -486,7 +505,7 @@ public enum Endpoint {
486 // Standard Authentication (loyalty headers only) 505 // Standard Authentication (loyalty headers only)
487 case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout, 506 case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout,
488 .verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo, 507 .verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo,
489 - .getMerchants, .getMerchantCategories, .getNetworkStatus: 508 + .getMerchants, .getMerchantCategories, .getArticles, .getNetworkStatus:
490 return .standard 509 return .standard
491 510
492 // Bearer Token Authentication (loyalty headers + Authorization: Bearer) 511 // Bearer Token Authentication (loyalty headers + Authorization: Bearer)
......
...@@ -978,6 +978,26 @@ extension NetworkService { ...@@ -978,6 +978,26 @@ extension NetworkService {
978 return response 978 return response
979 } 979 }
980 980
981 + // MARK: - Articles Methods
982 +
983 + /// Get articles (carousel content)
984 + /// - Parameters:
985 + /// - language: Language for the articles
986 + /// - categories: Categories to retrieve (optional)
987 + /// - Returns: Response dictionary containing articles
988 + /// - Throws: NetworkError if request fails
989 + public func getArticles(language: String, categories: [String]?) async throws -> [String: Any] {
990 + let categoriesDesc = categories?.joined(separator: ", ") ?? "all categories"
991 + print("🔄 [NetworkService] Getting articles for language: \(language), categories: \(categoriesDesc)")
992 +
993 + let endpoint = Endpoint.getArticles(language: language, categories: categories)
994 + let response = try await requestRaw(endpoint)
995 +
996 + print("✅ [NetworkService] Get articles request completed")
997 +
998 + return response
999 + }
1000 +
981 // MARK: - Coupon Operations Methods 1001 // MARK: - Coupon Operations Methods
982 1002
983 /// Validate a coupon for the user 1003 /// Validate a coupon for the user
......
...@@ -40,4 +40,9 @@ public class MyRewardsBannerOfferCollectionViewCell: UICollectionViewCell { ...@@ -40,4 +40,9 @@ public class MyRewardsBannerOfferCollectionViewCell: UICollectionViewCell {
40 // Use campaign's banner image - no hardcoded defaults 40 // Use campaign's banner image - no hardcoded defaults
41 self.postImageURL = data._banner_img_mobile ?? "" 41 self.postImageURL = data._banner_img_mobile ?? ""
42 } 42 }
43 +
44 + func configureCell(data: ArticleModel) {
45 + // Use article's preview image - same visual treatment as campaigns
46 + self.postImageURL = data.img_preview ?? ""
47 + }
43 } 48 }
......
...@@ -9,6 +9,7 @@ import UIKit ...@@ -9,6 +9,7 @@ import UIKit
9 9
10 protocol MyRewardsBannerOffersScrollTableViewCellDelegate: AnyObject { 10 protocol MyRewardsBannerOffersScrollTableViewCellDelegate: AnyObject {
11 func didSelectBannerOffer(_ index: Int) 11 func didSelectBannerOffer(_ index: Int)
12 + func didSelectBannerArticle(_ index: Int)
12 } 13 }
13 14
14 @objc(MyRewardsBannerOffersScrollTableViewCell) 15 @objc(MyRewardsBannerOffersScrollTableViewCell)
...@@ -95,22 +96,39 @@ extension MyRewardsBannerOffersScrollTableViewCell: UICollectionViewDataSource, ...@@ -95,22 +96,39 @@ extension MyRewardsBannerOffersScrollTableViewCell: UICollectionViewDataSource,
95 public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 96 public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
96 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyRewardsBannerOfferCollectionViewCell", for: indexPath) as! MyRewardsBannerOfferCollectionViewCell 97 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyRewardsBannerOfferCollectionViewCell", for: indexPath) as! MyRewardsBannerOfferCollectionViewCell
97 98
98 - // Handle only CampaignItemModel - banner cells are campaign-specific 99 + // Handle both CampaignItemModel and ArticleModel
99 guard let data = self.data, 100 guard let data = self.data,
100 let items = data.items, 101 let items = data.items,
101 - indexPath.row < items.count, 102 + indexPath.row < items.count else {
102 - let campaign = items[indexPath.row] as? CampaignItemModel else {
103 return cell 103 return cell
104 } 104 }
105 105
106 + let item = items[indexPath.row]
107 +
108 + if let campaign = item as? CampaignItemModel {
106 cell.configureCell(data: campaign) 109 cell.configureCell(data: campaign)
110 + } else if let article = item as? ArticleModel {
111 + cell.configureCell(data: article)
112 + }
107 113
108 return cell 114 return cell
109 } 115 }
110 116
111 public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 117 public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
112 - // Call the delegate method to notify the parent 118 + // Determine item type and call appropriate delegate method
119 + guard let data = self.data,
120 + let items = data.items,
121 + indexPath.row < items.count else {
122 + return
123 + }
124 +
125 + let item = items[indexPath.row]
126 +
127 + if item is CampaignItemModel {
113 delegate?.didSelectBannerOffer(indexPath.row) 128 delegate?.didSelectBannerOffer(indexPath.row)
129 + } else if item is ArticleModel {
130 + delegate?.didSelectBannerArticle(indexPath.row)
131 + }
114 } 132 }
115 133
116 // MARK: - UICollectionViewDelegateFlowLayout 134 // MARK: - UICollectionViewDelegateFlowLayout
......
...@@ -113,5 +113,7 @@ public class MyRewardsProfileInfoTableViewCell: UITableViewCell { ...@@ -113,5 +113,7 @@ public class MyRewardsProfileInfoTableViewCell: UITableViewCell {
113 private func loadProfileImage(from urlString: String) { 113 private func loadProfileImage(from urlString: String) {
114 // For now, use default image - can be enhanced later with URL loading 114 // For now, use default image - can be enhanced later with URL loading
115 profileImage.image = UIImage(named: "profile_pic_default", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 115 profileImage.image = UIImage(named: "profile_pic_default", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
116 +
117 + // TODO: Add dynamic profile picture and tags
116 } 118 }
117 } 119 }
......
...@@ -24,6 +24,8 @@ public enum SectionType { ...@@ -24,6 +24,8 @@ public enum SectionType {
24 public enum ItemType { 24 public enum ItemType {
25 case profile // ProfileModel 25 case profile // ProfileModel
26 case campaigns // [CampaignItemModel] 26 case campaigns // [CampaignItemModel]
27 + case articles // [ArticleModel]
28 + case mixed // Mixed content (campaigns + articles)
27 case couponSets // [CouponSetItemModel] 29 case couponSets // [CouponSetItemModel]
28 case coupons // [CouponItemModel] 30 case coupons // [CouponItemModel]
29 case offers // [OfferModel] - temporary, will migrate to dynamic coupons later 31 case offers // [OfferModel] - temporary, will migrate to dynamic coupons later
......
...@@ -40,6 +40,9 @@ import UIKit ...@@ -40,6 +40,9 @@ import UIKit
40 // Campaign data for banners 40 // Campaign data for banners
41 var bannerCampaigns: [CampaignItemModel] = [] 41 var bannerCampaigns: [CampaignItemModel] = []
42 42
43 + // Articles data for banners
44 + var articles: [ArticleModel] = []
45 +
43 // Coupon sets data 46 // Coupon sets data
44 var couponSets: [CouponSetItemModel] = [] 47 var couponSets: [CouponSetItemModel] = []
45 48
...@@ -119,34 +122,82 @@ import UIKit ...@@ -119,34 +122,82 @@ import UIKit
119 private func loadCampaigns() { 122 private func loadCampaigns() {
120 // Load campaigns from WarplySDK 123 // Load campaigns from WarplySDK
121 WarplySDK.shared.getCampaigns { [weak self] campaigns in 124 WarplySDK.shared.getCampaigns { [weak self] campaigns in
122 - guard let self = self, let campaigns = campaigns else { return } 125 + guard let self = self, let campaigns = campaigns else {
126 + // Even if campaigns fail, try to load articles
127 + self?.loadArticles()
128 + return
129 + }
123 130
124 // Filter campaigns for banner display (contest campaigns) if needed 131 // Filter campaigns for banner display (contest campaigns) if needed
125 self.bannerCampaigns = campaigns 132 self.bannerCampaigns = campaigns
126 - // .filter { campaign in 133 + .filter { campaign in
127 - // // Filter by category "contest" or campaign_type "contest" 134 + // Filter by category "contest" or campaign_type "contest"
128 - // return campaign._category == "contest" || campaign._campaign_type == "contest" 135 + return campaign._category == "contest" || campaign._campaign_type == "contest"
129 - // } 136 + }
137 +
138 + // Load articles after campaigns are loaded
139 + self.loadArticles()
140 +
141 + } failureCallback: { [weak self] errorCode in
142 + print("Failed to load campaigns: \(errorCode)")
143 + // Even if campaigns fail, try to load articles
144 + self?.loadArticles()
145 + }
146 + }
147 +
148 + // MARK: - Articles Loading
149 + private func loadArticles() {
150 + // Load articles from WarplySDK with "Carousel" category filter
151 + WarplySDK.shared.getArticles(categories: ["Carousel"]) { [weak self] articles in
152 + guard let self = self, let articles = articles else {
153 + // Create banner section with only campaigns if articles fail
154 + self?.createBannerSection()
155 + return
156 + }
157 +
158 + self.articles = articles
159 + print("✅ [MyRewardsViewController] Loaded \(articles.count) carousel articles")
160 +
161 + // Create banner section with both campaigns and articles
162 + self.createBannerSection()
163 +
164 + // TODO: Add Couponsets here
165 +
166 + } failureCallback: { [weak self] errorCode in
167 + print("Failed to load carousel articles: \(errorCode)")
168 + // Create banner section with only campaigns if articles fail
169 + self?.createBannerSection()
170 + }
171 + }
172 +
173 + // MARK: - Banner Section Creation
174 + private func createBannerSection() {
175 + // Combine campaigns and articles for banner section
176 + var bannerItems: [Any] = []
177 +
178 + // Add campaigns first
179 + bannerItems.append(contentsOf: self.bannerCampaigns)
130 180
131 - // Create banner section with real campaign data 181 + // Add articles after campaigns
132 - if !self.bannerCampaigns.isEmpty { 182 + bannerItems.append(contentsOf: self.articles)
183 +
184 + // Create banner section if we have any items
185 + if !bannerItems.isEmpty {
133 let bannerSection = SectionModel( 186 let bannerSection = SectionModel(
134 sectionType: .myRewardsBannerOffers, 187 sectionType: .myRewardsBannerOffers,
135 title: "Διαγωνισμός", 188 title: "Διαγωνισμός",
136 - items: self.bannerCampaigns, 189 + items: bannerItems,
137 - itemType: .campaigns 190 + itemType: .mixed
138 ) 191 )
139 self.sections.append(bannerSection) 192 self.sections.append(bannerSection)
193 +
194 + print("✅ [MyRewardsViewController] Created banner section with \(self.bannerCampaigns.count) campaigns and \(self.articles.count) articles")
140 } 195 }
141 196
142 // Reload table view with new sections 197 // Reload table view with new sections
143 DispatchQueue.main.async { 198 DispatchQueue.main.async {
144 self.tableView.reloadData() 199 self.tableView.reloadData()
145 } 200 }
146 - } failureCallback: { [weak self] errorCode in
147 - print("Failed to load campaigns: \(errorCode)")
148 - // No sections added on failure - table will be empty
149 - }
150 } 201 }
151 202
152 // MARK: - Coupon Sets Loading 203 // MARK: - Coupon Sets Loading
...@@ -446,13 +497,25 @@ import UIKit ...@@ -446,13 +497,25 @@ import UIKit
446 } 497 }
447 498
448 private func openCampaignViewController(with index: Int) { 499 private func openCampaignViewController(with index: Int) {
449 - // Validate index bounds 500 + // Get the combined banner items (campaigns + articles)
450 - guard index < bannerCampaigns.count else { 501 + var bannerItems: [Any] = []
451 - print("Invalid campaign index: \(index)") 502 + bannerItems.append(contentsOf: self.bannerCampaigns)
503 + bannerItems.append(contentsOf: self.articles)
504 +
505 + // Validate index bounds for combined items
506 + guard index < bannerItems.count else {
507 + print("Invalid banner item index: \(index)")
508 + return
509 + }
510 +
511 + let item = bannerItems[index]
512 +
513 + // Handle only campaigns - articles will be handled by didSelectBannerArticle
514 + guard let campaign = item as? CampaignItemModel else {
515 + print("Item at index \(index) is not a campaign")
452 return 516 return
453 } 517 }
454 518
455 - let campaign = bannerCampaigns[index]
456 let campaignUrl = campaign._campaign_url ?? campaign.index_url 519 let campaignUrl = campaign._campaign_url ?? campaign.index_url
457 520
458 // Check if URL is not empty before proceeding 521 // Check if URL is not empty before proceeding
...@@ -468,6 +531,32 @@ import UIKit ...@@ -468,6 +531,32 @@ import UIKit
468 self.navigationController?.pushViewController(vc, animated: true) 531 self.navigationController?.pushViewController(vc, animated: true)
469 } 532 }
470 533
534 + private func openArticleViewController(with index: Int) {
535 + // Get the combined banner items (campaigns + articles)
536 + var bannerItems: [Any] = []
537 + bannerItems.append(contentsOf: self.bannerCampaigns)
538 + bannerItems.append(contentsOf: self.articles)
539 +
540 + // Validate index bounds for combined items
541 + guard index < bannerItems.count else {
542 + print("Invalid banner item index: \(index)")
543 + return
544 + }
545 +
546 + let item = bannerItems[index]
547 +
548 + // Handle only articles
549 + guard let article = item as? ArticleModel else {
550 + print("Item at index \(index) is not an article")
551 + return
552 + }
553 +
554 + // TODO: Implement article navigation
555 + // This could navigate to a web view with article content,
556 + // or a dedicated article detail screen
557 + print("TODO: Navigate to article: \(article.title ?? "Unknown") - \(article.uuid ?? "No UUID")")
558 + }
559 +
471 private func openCouponViewController(with offer: OfferModel) { 560 private func openCouponViewController(with offer: OfferModel) {
472 // let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle) 561 // let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle)
473 // vc.coupon = offer 562 // vc.coupon = offer
...@@ -570,6 +659,11 @@ extension MyRewardsViewController: MyRewardsBannerOffersScrollTableViewCellDeleg ...@@ -570,6 +659,11 @@ extension MyRewardsViewController: MyRewardsBannerOffersScrollTableViewCellDeleg
570 openCampaignViewController(with: index) 659 openCampaignViewController(with: index)
571 } 660 }
572 661
662 + func didSelectBannerArticle(_ index: Int) {
663 + // Navigate to Article detail (TODO implementation)
664 + openArticleViewController(with: index)
665 + }
666 +
573 // func didTapProfileButton() { 667 // func didTapProfileButton() {
574 // // Navigate to ProfileViewController 668 // // Navigate to ProfileViewController
575 // openProfileViewController() 669 // openProfileViewController()
......