Manos Chorianopoulos

added getArticles request, intergrated articles in MyRewardsVC

This diff is collapsed. Click to expand it.
...@@ -2273,6 +2273,91 @@ public final class WarplySDK { ...@@ -2273,6 +2273,91 @@ public final class WarplySDK {
2273 }) 2273 })
2274 } 2274 }
2275 } 2275 }
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 + }
2276 2361
2277 // MARK: - Profile 2362 // MARK: - Profile
2278 2363
......
...@@ -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 - cell.configureCell(data: campaign) 106 + let item = items[indexPath.row]
107 +
108 + if let campaign = item as? CampaignItemModel {
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
113 - delegate?.didSelectBannerOffer(indexPath.row) 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 {
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,33 +122,81 @@ import UIKit ...@@ -119,33 +122,81 @@ 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 - // }
130 -
131 - // Create banner section with real campaign data
132 - if !self.bannerCampaigns.isEmpty {
133 - let bannerSection = SectionModel(
134 - sectionType: .myRewardsBannerOffers,
135 - title: "Διαγωνισμός",
136 - items: self.bannerCampaigns,
137 - itemType: .campaigns
138 - )
139 - self.sections.append(bannerSection)
140 } 136 }
141 137
142 - // Reload table view with new sections 138 + // Load articles after campaigns are loaded
143 - DispatchQueue.main.async { 139 + self.loadArticles()
144 - self.tableView.reloadData() 140 +
145 - }
146 } failureCallback: { [weak self] errorCode in 141 } failureCallback: { [weak self] errorCode in
147 print("Failed to load campaigns: \(errorCode)") 142 print("Failed to load campaigns: \(errorCode)")
148 - // No sections added on failure - table will be empty 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)
180 +
181 + // Add articles after campaigns
182 + bannerItems.append(contentsOf: self.articles)
183 +
184 + // Create banner section if we have any items
185 + if !bannerItems.isEmpty {
186 + let bannerSection = SectionModel(
187 + sectionType: .myRewardsBannerOffers,
188 + title: "Διαγωνισμός",
189 + items: bannerItems,
190 + itemType: .mixed
191 + )
192 + self.sections.append(bannerSection)
193 +
194 + print("✅ [MyRewardsViewController] Created banner section with \(self.bannerCampaigns.count) campaigns and \(self.articles.count) articles")
195 + }
196 +
197 + // Reload table view with new sections
198 + DispatchQueue.main.async {
199 + self.tableView.reloadData()
149 } 200 }
150 } 201 }
151 202
...@@ -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)")
452 return 508 return
453 } 509 }
454 510
455 - let campaign = bannerCampaigns[index] 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")
516 + return
517 + }
518 +
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()
......