diff --git a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift index 30fead0..5ce3fcb 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/JoinGroup.swift @@ -191,7 +191,7 @@ struct JoinGroup: View { print("Scanned code: \(code)") - if code.contains("vitty.app/join") { + if code.contains("vitty://join") { if let url = URL(string: code) { handleDeepLink(url) } diff --git a/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift b/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift index 58b7a2c..143ac82 100644 --- a/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift +++ b/VITTY/VITTY/Connect/View/Circles/Components/QrCode.swift @@ -38,7 +38,7 @@ struct QRCodeModalView: View { .foregroundColor(.white) } - // QR Code Display + if isGeneratingCode { Rectangle() .fill(Color.gray.opacity(0.3)) @@ -54,7 +54,7 @@ struct QRCodeModalView: View { } ) } else if !joinCode.isEmpty { - if let qrImage = generateQRCode(from: createInvitationLink()) { + if let qrImage = generateQRCode(from: createDeepLink()) { Image(uiImage: qrImage) .interpolation(.none) .resizable() @@ -134,28 +134,29 @@ struct QRCodeModalView: View { .multilineTextAlignment(.center) .padding(.horizontal) - // Action Buttons + HStack(spacing: 12) { - - Button(action: { - if joinCode.isEmpty { - generateJoinCode() - } else { - showingShareSheet = true - } - }) { - HStack { - Image(systemName: joinCode.isEmpty ? "qrcode" : "square.and.arrow.up") - Text(joinCode.isEmpty ? "Generate QR Code" : "Share Invitation") + if joinCode.isEmpty{ + Button(action: { + if joinCode.isEmpty { + generateJoinCode() + } + }) { + HStack { + Image(systemName:"qrcode" ) + Text( "Generate QR Code") + } + .font(.custom("Poppins-SemiBold", size: 14)) + .foregroundColor(Color("Background")) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color("Accent")) + .cornerRadius(8) } - .font(.custom("Poppins-SemiBold", size: 14)) - .foregroundColor(Color("Background")) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(Color("Accent")) - .cornerRadius(8) + .disabled(isGeneratingCode) } - .disabled(isGeneratingCode) + + } } .frame(maxWidth: 300) @@ -167,12 +168,7 @@ struct QRCodeModalView: View { Spacer() } .background(Color.black.opacity(0.5).edgesIgnoringSafeArea(.all)) - .sheet(isPresented: $showingShareSheet) { - ShareSheetQr(items: [ - createInvitationLink(), - "Join my circle '\(circleName)' on VITTY! Use code: \(joinCode)" - ]) - } + .alert("Error", isPresented: $showError) { Button("OK") { } } message: { @@ -225,14 +221,13 @@ struct QRCodeModalView: View { print("Join code copied to clipboard") } - private func createInvitationLink() -> String { - let baseURL = "https://vitty.app/join" - + + private func createDeepLink() -> String { guard let encodedCircleName = circleName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { - return "\(baseURL)?code=\(joinCode)" + return "vitty://join?code=\(joinCode)" } - - return "\(baseURL)?code=\(joinCode)&circleName=\(encodedCircleName)" + //vitty://join?code=Ow2tWaHExs&circleName=newircircle + return "vitty://join?code=\(joinCode)&circleName=\(encodedCircleName)" } private func generateQRCode(from string: String) -> UIImage? { @@ -254,13 +249,3 @@ struct QRCodeModalView: View { } } -struct ShareSheetQr: UIViewControllerRepresentable { - let items: [Any] - - func makeUIViewController(context: Context) -> UIActivityViewController { - let controller = UIActivityViewController(activityItems: items, applicationActivities: nil) - return controller - } - - func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} -} diff --git a/VITTY/VITTY/Connect/View/Circles/View/Circles.swift b/VITTY/VITTY/Connect/View/Circles/View/Circles.swift index 118874c..8215cb2 100644 --- a/VITTY/VITTY/Connect/View/Circles/View/Circles.swift +++ b/VITTY/VITTY/Connect/View/Circles/View/Circles.swift @@ -6,12 +6,17 @@ // import SwiftUI +import SwiftUI + struct CirclesView: View { @Binding var isCreatingGroup: Bool @State private var searchText = "" @Environment(CommunityPageViewModel.self) private var communityPageViewModel @Environment(AuthViewModel.self) private var authViewModel - + + + @EnvironmentObject private var navigationCoordinator: NavigationCoordinator + var body: some View { NavigationStack { VStack(spacing: 12) { @@ -52,7 +57,7 @@ struct CirclesView: View { VStack(spacing: 10) { ForEach(filteredCircles, id: \.circleID) { circle in - NavigationLink(destination: InsideCircle(circleName: circle.circleName, circle_id:circle.circleID, circle_join_code: circle.circleJoinCode,circle_role: circle.circleRole)) { + NavigationLink(destination: InsideCircle(circleName: circle.circleName, circle_id: circle.circleID, circle_join_code: circle.circleJoinCode, circle_role: circle.circleRole)) { CirclesRow(circle: circle) } .buttonStyle(PlainButtonStyle()) @@ -71,6 +76,24 @@ struct CirclesView: View { loading: true ) } + .onReceive(NotificationCenter.default.publisher(for: Notification.Name("CircleJoinedSuccessfully"))) { _ in + + communityPageViewModel.fetchCircleData( + from: "\(APIConstants.base_url)circles", + token: authViewModel.loggedInBackendUser?.token ?? "", + loading: true + ) + } + + .onAppear { + + if let pendingInvite = navigationCoordinator.pendingCircleInvite { + + print("CirclesView appeared with pending invite: \(pendingInvite.code)") + + + } + } } } } diff --git a/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift b/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift index 482eadc..7ec11e2 100644 --- a/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift +++ b/VITTY/VITTY/Connect/View/Circles/View/InsideCircle.swift @@ -509,12 +509,13 @@ struct InsideCircle: View { LeaveCircleAlert(circleName: "\(circleName)", onCancel: { showLeaveAlert = false }, onLeave: { - let url = "\(APIConstants.base_url)circles/\(circle_id)/leave" + let url = "\(APIConstants.base_url)circles/leave/\(circle_id)" let token = authViewModel.loggedInBackendUser?.token ?? "" communityPageViewModel.leaveCircle(from: url, token: token) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + communityPageViewModel.fetchCircleData(from:"\(APIConstants.base_url)circles" , token: token) showLeaveAlert = false presentationMode.wrappedValue.dismiss() } @@ -532,6 +533,7 @@ struct InsideCircle: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { showDeleteAlert = false + presentationMode.wrappedValue.dismiss() } }) diff --git a/VITTY/VITTY/Connect/View/ConnectPage.swift b/VITTY/VITTY/Connect/View/ConnectPage.swift index d04ac36..4e23323 100644 --- a/VITTY/VITTY/Connect/View/ConnectPage.swift +++ b/VITTY/VITTY/Connect/View/ConnectPage.swift @@ -35,6 +35,7 @@ struct ConnectPage: View { @State private var showCircleMenu = false @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var navigationCoordinator: NavigationCoordinator @Binding var isCreatingGroup : Bool @State private var isAddFriendsViewPresented = false @@ -143,8 +144,20 @@ struct ConnectPage: View { case .groupRequests: CircleRequestsView() } + }.onChange(of: navigationCoordinator.shouldNavigateToCircles) { _, shouldNavigate in + if shouldNavigate { + selectedTab = 0 + } + communityPageViewModel.fetchCircleData( + from: "\(APIConstants.base_url)circles", + token: authViewModel.loggedInBackendUser?.token ?? "", + loading: true + ) } .onAppear { + if navigationCoordinator.shouldNavigateToCircles { + selectedTab = 0 + } let shouldShowLoading = !hasLoadedInitialData @@ -153,6 +166,7 @@ struct ConnectPage: View { loading: shouldShowLoading ) + if communityPageViewModel.friends.isEmpty || !hasLoadedInitialData { communityPageViewModel.fetchFriendsData( from: "\(APIConstants.base_url)friends/\(authViewModel.loggedInBackendUser?.username ?? "")/", diff --git a/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift b/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift index f76b7c8..045b66f 100644 --- a/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift +++ b/VITTY/VITTY/Connect/ViewModel/CommunityPageViewModel.swift @@ -265,9 +265,6 @@ class CommunityPageViewModel { } } } - } - } - } //MARK : Circle Leave func fetchCircleLeave(from url: String, token: String, loading: Bool = false) { @@ -310,13 +307,17 @@ class CommunityPageViewModel { switch response.result { case .success: self.logger.info("Successfully left circle") + case .failure(let error): self.logger.error("Error leaving circle: \(error)") self.errorCircleMembers = true } } + + } + } //MARK: Delete Circle diff --git a/VITTY/VITTY/Home/View/HomeView.swift b/VITTY/VITTY/Home/View/HomeView.swift index 8c48c1a..5a19d32 100644 --- a/VITTY/VITTY/Home/View/HomeView.swift +++ b/VITTY/VITTY/Home/View/HomeView.swift @@ -7,6 +7,9 @@ struct HomeView: View { @State private var showProfileSidebar: Bool = false @State private var isCreatingGroup = false @StateObject private var tipManager = CustomTipManager() + + + @EnvironmentObject private var navigationCoordinator: NavigationCoordinator var body: some View { NavigationStack { @@ -39,6 +42,16 @@ struct HomeView: View { .onChange(of: selectedPage) { _, newValue in handleTabChange(newValue) } + // NEW: Listen for deep link navigation + .onReceive(NotificationCenter.default.publisher(for: Notification.Name("NavigateToCircles"))) { _ in + selectedPage = 2 // Navigate to Connects tab + } + // NEW: Handle navigation coordinator changes + .onChange(of: navigationCoordinator.shouldNavigateToCircles) { _, shouldNavigate in + if shouldNavigate { + selectedPage = 2 + } + } } } @@ -138,7 +151,6 @@ struct HomeView: View { } private func handleTabChange(_ newTab: Int) { - print("Switched to tab: \(newTab)") } } diff --git a/VITTY/VITTY/Settings/View/SettingsView.swift b/VITTY/VITTY/Settings/View/SettingsView.swift index 2392331..d6a3bfc 100644 --- a/VITTY/VITTY/Settings/View/SettingsView.swift +++ b/VITTY/VITTY/Settings/View/SettingsView.swift @@ -202,10 +202,13 @@ struct SettingsView: View { try await deleteUserFromServer(username: username) await MainActor.run { - cleanupLocalData() - authViewModel.signOut() - showDeleteUserAlert = false - isDeletingUser = false + + Task { + await cleanupLocalData() + authViewModel.signOut() + showDeleteUserAlert = false + isDeletingUser = false + } } } catch { await MainActor.run { @@ -237,18 +240,26 @@ struct SettingsView: View { } } - private func cleanupLocalData() { + private func cleanupLocalData() async { do { - try modelContext.delete(model: TimeTable.self) - try modelContext.delete(model: Remainder.self) - try modelContext.delete(model: CreateNoteModel.self) - try modelContext.delete(model: UploadedFile.self) - try modelContext.save() - print("Successfully cleaned up local data") + + await Task.detached { [modelContext] in + do { + try modelContext.delete(model: TimeTable.self) + try modelContext.delete(model: Remainder.self) + try modelContext.delete(model: CreateNoteModel.self) + try modelContext.delete(model: UploadedFile.self) + try modelContext.save() + print("Successfully cleaned up local data") + } catch { + print("Failed to clean up local data: \(error)") + } + }.value } catch { - print("Failed to clean up local data: \(error)") + print("Failed to clear local data: \(error)") } } + private func copyLecturesToSaturday(from day: String) { diff --git a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift index 5b8d181..eb75e5f 100644 --- a/VITTY/VITTY/TimeTable/Views/TimeTableView.swift +++ b/VITTY/VITTY/TimeTable/Views/TimeTableView.swift @@ -89,31 +89,50 @@ struct TimeTableView: View { } case .data: VStack(spacing: 0) { - // Day selector - ScrollView(.horizontal) { - HStack { - ForEach(daysOfWeek, id: \.self) { day in - Text(day) - .foregroundStyle(daysOfWeek[viewModel.dayNo] == day - ? Color("Background") : Color("Accent")) - .frame(width: 60, height: 54) - .background( - daysOfWeek[viewModel.dayNo] == day - ? Color("Accent") : Color.clear - ) - .onTapGesture { - withAnimation(.easeInOut(duration: 0.2)) { - viewModel.dayNo = daysOfWeek.firstIndex( - of: day - )! - viewModel.changeDay() + + ScrollViewReader { proxy in + ScrollView(.horizontal) { + HStack { + ForEach(daysOfWeek, id: \.self) { day in + Text(day) + .foregroundStyle(daysOfWeek[viewModel.dayNo] == day + ? Color("Background") : Color("Accent")) + .frame(width: 60, height: 54) + .background( + daysOfWeek[viewModel.dayNo] == day + ? Color("Accent") : Color.clear + ) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.dayNo = daysOfWeek.firstIndex( + of: day + )! + viewModel.changeDay() + + + proxy.scrollTo(day, anchor: .center) + } } - } - .clipShape(RoundedRectangle(cornerRadius: 10)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .id(day) + } + } + .padding(.horizontal, 8) + } + .scrollIndicators(.hidden) + .onAppear { + + let currentDay = daysOfWeek[viewModel.dayNo] + proxy.scrollTo(currentDay, anchor: .center) + } + .onChange(of: viewModel.dayNo) { oldValue, newValue in + + let selectedDay = daysOfWeek[newValue] + withAnimation(.easeInOut(duration: 0.3)) { + proxy.scrollTo(selectedDay, anchor: .center) } } } - .scrollIndicators(.hidden) .background(Color("Secondary")) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) @@ -123,7 +142,7 @@ struct TimeTableView: View { VStack(spacing: 16) { Image(systemName: "calendar.badge.exclamationmark") .font(.system(size: 50)) - .foregroundColor(.secondary) + .foregroundColor(.secondary) Text("No classes today!") .font(Font.custom("Poppins-Bold", size: 24)) @@ -135,6 +154,7 @@ struct TimeTableView: View { .padding(.horizontal) } Spacer() + } else { ScrollView { VStack(spacing: 12) { diff --git a/VITTY/VITTY/UserProfileSideBar/SideBar.swift b/VITTY/VITTY/UserProfileSideBar/SideBar.swift index 2e974ad..77acbfd 100644 --- a/VITTY/VITTY/UserProfileSideBar/SideBar.swift +++ b/VITTY/VITTY/UserProfileSideBar/SideBar.swift @@ -89,7 +89,6 @@ struct UserProfileSidebar: View { } } } - } Spacer() diff --git a/VITTY/VITTYApp.swift b/VITTY/VITTYApp.swift index b29b191..d5ac2b3 100644 --- a/VITTY/VITTYApp.swift +++ b/VITTY/VITTYApp.swift @@ -11,36 +11,6 @@ import SwiftUI import SwiftData import TipKit -/** - `NOTE FOR FUTURE/NEW DEVS:` - - - always use the latest and greatest apple tools, don't use something that's been replaced by apple (start watching WWDC to stay updated) - for eg: use SwiftData and not CoreData. use @Observable and not ObservableObject and u don't want to use UIKit instead of SwiftUI. trust me on this. - reason: it makes the code more future proof and incase there's no activity on the development for a year, the app wont be outdated. - downside: minimum deployment target has to be raised which hurts adoption but apple doesn't care about this either so we don't too. - - `personal experience, we have had issues when this app would just crash for iOS 16+ because the code were not updated.` - `we lost a lot of users and our ratings dropped to 3.` - - - continuation to the first point, pls replace parts of the app that uses these old tech as soon as you can. - - - focus on keeping the package dependencies on latest versions - - - use `swift-format` to format the code before pushing. it's already configured for the project. double click on VITTY on left panel and click on format code. - - - use `tabs` and `not` spaces pls. - - - try to focus on subtle animations and transitions. it makes the app feel more polished. `withAnimation{ }` is the greatest tool ever made by apple. - - - try to use haptics wherever possible. users love to feel those and apple makes it easier for us to implement - - - try to stick to Apple HIG as much as possible. ik it's difficult considering the UI we have now but it's worth it. - - - use // MARK: when u create a function, it helps to navigate. - */ - - - @main struct VITTYApp: App { @@ -54,6 +24,15 @@ struct VITTYApp: App { @State private var deepLinkURL: URL? @State private var showJoinCircleAlert = false @State private var pendingCircleInvite: (code: String, circleName: String?)? + + + @State private var isProcessingDeepLink = false + + + @StateObject private var navigationCoordinator = NavigationCoordinator() + + + @StateObject private var toastManager = ToastManager() init() { setupFirebase() @@ -62,28 +41,44 @@ struct VITTYApp: App { var body: some Scene { WindowGroup { - ContentView() - .preferredColorScheme(.dark) - .task { - try? Tips.configure([.displayFrequency(.immediate), .datastoreLocation(.applicationDefault)]) - } - .onOpenURL { url in - handleDeepLink(url) - } - .alert("Join Circle", isPresented: $showJoinCircleAlert) { - Button("Cancel", role: .cancel) { - pendingCircleInvite = nil + ZStack { + ContentView() + .preferredColorScheme(.dark) + .environmentObject(navigationCoordinator) + .task { + try? Tips.configure([.displayFrequency(.immediate), .datastoreLocation(.applicationDefault)]) + } + .onOpenURL { url in + handleDeepLink(url) } - Button("Join") { + .alert("Join Circle", isPresented: $showJoinCircleAlert) { + Button("Cancel", role: .cancel) { + pendingCircleInvite = nil + isProcessingDeepLink = false + } + Button("Join") { + if let invite = pendingCircleInvite { + handleCircleInvite(invite) + } + } + } message: { if let invite = pendingCircleInvite { - handleCircleInvite(invite) + Text("Do you want to join the circle with code '\(invite.code)'?") } } - } message: { - if let invite = pendingCircleInvite { - Text("Do you want to join the circle with code '\(invite.code)'?") - } + + + if toastManager.isShowing { + CircleToastView( + message: toastManager.message, + isError: toastManager.isError, + isShowing: $toastManager.isShowing + ) + .animation(.easeInOut(duration: 0.3), value: toastManager.isShowing) + .zIndex(1000) } + } + .environmentObject(toastManager) } .modelContainer(sharedModelContainer) } @@ -97,6 +92,116 @@ struct VITTYApp: App { } } +// MARK: - Toast Manager +class ToastManager: ObservableObject { + @Published var isShowing = false + @Published var message = "" + @Published var isError = false + + private var hideTimer: Timer? + + init() { + + NotificationCenter.default.addObserver( + self, + selector: #selector(showToastNotification), + name: Notification.Name("ShowToast"), + object: nil + ) + } + + @objc private func showToastNotification(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let message = userInfo["message"] as? String, + let isError = userInfo["isError"] as? Bool else { + return + } + + showToast(message: message, isError: isError) + } + + func showToast(message: String, isError: Bool) { + DispatchQueue.main.async { + self.message = message + self.isError = isError + self.isShowing = true + + + self.hideTimer?.invalidate() + self.hideTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in + self.hideToast() + } + } + } + + func hideToast() { + DispatchQueue.main.async { + self.isShowing = false + self.hideTimer?.invalidate() + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + hideTimer?.invalidate() + } +} + +// MARK: - Toast View +struct CircleToastView: View { + let message: String + let isError: Bool + @Binding var isShowing: Bool + + var body: some View { + VStack { + Spacer() + + HStack { + Image(systemName: isError ? "xmark.circle.fill" : "checkmark.circle.fill") + .foregroundColor(isError ? .red : .green) + .font(.system(size: 20)) + + Text(message) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + .multilineTextAlignment(.leading) + + Spacer() + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.black.opacity(0.9)) + .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 5) + ) + .padding(.horizontal, 20) + .padding(.bottom, 100) + } + .transition(.move(edge: .bottom).combined(with: .opacity)) + .onTapGesture { + isShowing = false + } + } +} + +// MARK: - Navigation Coordinator +class NavigationCoordinator: ObservableObject { + @Published var shouldNavigateToCircles = false + @Published var pendingCircleInvite: (code: String, circleName: String?)? + + func navigateToCirclesForInvite(code: String, circleName: String?) { + pendingCircleInvite = (code: code, circleName: circleName) + shouldNavigateToCircles = true + } + + func resetNavigation() { + shouldNavigateToCircles = false + pendingCircleInvite = nil + } +} + // MARK: - Deep Link Handling extension VITTYApp { @@ -104,51 +209,182 @@ extension VITTYApp { logger.info("Deep link received: \(url.absoluteString)") - if url.absoluteString.contains("vitty.app/join") { + guard !isProcessingDeepLink else { + logger.info("Already processing a deep link, ignoring") + return + } + + isProcessingDeepLink = true + + if url.absoluteString.contains("vitty://join") { handleJoinCircleURL(url) } else { - logger.info("Unhandled deep link type: \(url.absoluteString)") + isProcessingDeepLink = false } } - + private func handleJoinCircleURL(_ url: URL) { + logger.info("Handling join circle URL") + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { logger.error("Failed to parse URL components") + isProcessingDeepLink = false return } - guard let code = components.queryItems?.first(where: { $0.name == "code" })?.value else { logger.error("No code found in URL") + showToast(message: "Error: Invalid invitation link", isError: true) + isProcessingDeepLink = false return } - let circleName = components.queryItems?.first(where: { $0.name == "circleName" })?.value - // Store the invite and show alert - pendingCircleInvite = (code: code, circleName: circleName) - showJoinCircleAlert = true + logger.info("Parsed circle code: \(code)") + if let name = circleName { + logger.info("Parsed circle name: \(name)") + } - logger.info("Circle join code prepared: \(code)") + + navigationCoordinator.navigateToCirclesForInvite(code: code, circleName: circleName) + + let invite = (code: code, circleName: circleName) + + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.pendingCircleInvite = invite + self.showJoinCircleAlert = true + } + + logger.info("Circle join alert prepared for code: \(code)") } private func handleCircleInvite(_ invite: (code: String, circleName: String?)) { - - NotificationCenter.default.post( - name: Notification.Name("JoinCircleFromDeepLink"), - object: nil, - userInfo: [ - "code": invite.code, - "circleName": invite.circleName ?? "Unknown Circle" - ] - ) - + guard let token = UserDefaults.standard.string(forKey: UserDefaultKeys.tokenKey), + !token.isEmpty else { + logger.error("No token found in UserDefaults") + showToast(message: "Error: Unable to get user information", isError: true) + cleanup() + return + } + + if invite.code.count < 3 { + showToast(message: "Error: Circle code must be at least 3 characters", isError: true) + cleanup() + return + } + + let urlString = "\(APIConstants.base_url)circles/join?code=\(invite.code)" + guard let url = URL(string: urlString) else { + logger.error("Invalid URL: \(urlString)") + showToast(message: "Error: Invalid URL", isError: true) + cleanup() + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Token \(token)", forHTTPHeaderField: "Authorization") + + logger.info("Joining circle with code: \(invite.code)") + logger.info("Request URL: \(urlString)") + + URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { + + if let error = error { + self.logger.error("Network error: \(error.localizedDescription)") + self.showToast(message: "Network error: \(error.localizedDescription)", isError: true) + self.cleanup() + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + self.showToast(message: "Error: Invalid response", isError: true) + self.cleanup() + return + } + + self.logger.info("Response status code: \(httpResponse.statusCode)") + + if httpResponse.statusCode == 200 || httpResponse.statusCode == 201 { + self.showToast(message: "Successfully joined the circle! 🎉", isError: false) + + let impactFeedback = UIImpactFeedbackGenerator(style: .medium) + impactFeedback.impactOccurred() + + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + NotificationCenter.default.post( + name: Notification.Name("CircleJoinedSuccessfully"), + object: nil, + userInfo: ["code": invite.code] + ) + } + + self.logger.info("Successfully joined circle with code: \(invite.code)") + } else { + + if let data = data { + self.logger.error("Error response data: \(String(data: data, encoding: .utf8) ?? "No data")") + + if let errorResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let message = errorResponse["message"] as? String { + self.showToast(message: "Error: \(message)", isError: true) + } else { + self.handleHTTPError(statusCode: httpResponse.statusCode) + } + } else { + self.handleHTTPError(statusCode: httpResponse.statusCode) + } + } + + self.cleanup() + } + }.resume() + } + + // MARK: - Helper Methods + + + private func cleanup() { pendingCircleInvite = nil + isProcessingDeepLink = false + navigationCoordinator.resetNavigation() + } + + private func handleHTTPError(statusCode: Int) { + switch statusCode { + case 400: + showToast(message: "Error: Bad request", isError: true) + case 401: + showToast(message: "Error: Unauthorized", isError: true) + case 403: + showToast(message: "Error: Forbidden", isError: true) + case 404: + showToast(message: "Error: Circle not found", isError: true) + case 409: + showToast(message: "Error: Already a member of this circle", isError: true) + case 500: + showToast(message: "Error: Server error", isError: true) + default: + showToast(message: "Error: Something went wrong", isError: true) + } + } + + private func showToast(message: String, isError: Bool) { + // NEW: Use ToastManager directly + toastManager.showToast(message: message, isError: isError) - logger.info("Circle join notification posted for code: \(invite.code)") + if isError { + logger.error("Toast Error: \(message)") + } else { + logger.info("Toast Success: \(message)") + } } }