Skip to content

Commit

Permalink
Fix restore flow
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-amisha-i authored Dec 23, 2024
1 parent c729353 commit 6c2fe28
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 132 deletions.
36 changes: 0 additions & 36 deletions CONTRIBUTING.md

This file was deleted.

6 changes: 6 additions & 0 deletions Data/Data/Extension/Double+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ public extension Double {
return String(format: "%.2f", self.rounded()) // Fallback to a basic decimal format
}
}

/// Rounds the `Double` value to the specified number of decimal places
func rounded(to decimals: Int) -> Double {
let multiplier = pow(10.0, Double(decimals))
return (self * multiplier).rounded() / multiplier
}
}
26 changes: 25 additions & 1 deletion Data/Data/Model/Expense.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ extension Expense {

switch self.splitType {
case .equally:
return self.amount / Double(self.splitTo.count)
return calculateEqualSplitAmount(for: member)
case .fixedAmount:
return self.splitData?[member] ?? 0
case .percentage:
Expand All @@ -85,6 +85,30 @@ extension Expense {
}
}

/// Returns the equal split amount for the member
private func calculateEqualSplitAmount(for member: String) -> Double {
let totalMembers = Double(self.splitTo.count)
let baseAmount = (self.amount / totalMembers).rounded(to: 2) // Base amount each member owes
let totalSplitAmount = baseAmount * totalMembers // The total split amount after rounding all members base amounts
let remainder = self.amount - totalSplitAmount // The leftover amount due to rounding

// Sort members deterministically to ensure consistent assignment of the remainder.
let sortedMembers = self.splitTo.sorted()

// Assign base amount to each member
var splitAmounts: [String: Double] = [:]
for splitMember in sortedMembers {
splitAmounts[splitMember] = baseAmount
}

// Distribute remainder to the first member in the sorted list
if remainder > 0, let firstMember = sortedMembers.first {
splitAmounts[firstMember]! += remainder
}

return splitAmounts[member] ?? 0
}

/// It will return the owing amount to the member for that expense that he have to get or pay back
public func getCalculatedSplitAmountOf(member: String) -> Double {
let paidAmount = self.paidBy[member] ?? 0
Expand Down
4 changes: 2 additions & 2 deletions Data/Data/Repository/GroupRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public class GroupRepository: ObservableObject {
}

public func removeMemberFrom(group: Groups, removedMember: AppUser) async throws {
guard let user = preference.user else { return }
guard let userId = preference.user?.id else { return }
var group = group

// make group inactive if there are no members
Expand All @@ -107,7 +107,7 @@ public class GroupRepository: ObservableObject {
}
}

let activityType: ActivityType = user.id == removedMember.id ? .groupMemberLeft : .groupMemberRemoved
let activityType: ActivityType = userId == removedMember.id ? .groupMemberLeft : .groupMemberRemoved
try await updateGroup(group: group, type: activityType, removedMember: removedMember)
}

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Splito utilizes the latest iOS technologies and adheres to industry best practic
- CocoaLumberjack for Logging

## Contribution 🤝
The Canopas team enthusiastically welcomes contributions and project participation! There are a bunch of things you can do if you want to contribute! The [Contributor Guide](CONTRIBUTING.md) has all the information you need for everything from reporting bugs to contributing entire new features. Please don't hesitate to jump in if you'd like to, or even ask us questions if something isn't clear.
Splito is an open-source project but currently, we are not accepting any contributions.

## Credits
Splito is owned and maintained by the [Canopas team](https://canopas.com/). You can follow them on X at [@canopas_eng](https://x.com/canopas_eng) for project updates and releases. If you are interested in building apps or designing products, please let us know. We'd love to hear from you!
Expand Down
18 changes: 15 additions & 3 deletions Splito/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@
"An email has been sent to %@ with instructions to reset your password." : {
"extractionState" : "manual"
},
"An unexpected error occurred. Please try again later." : {
"extractionState" : "manual"
},
"Ana borrows $10 from Bob" : {

},
Expand Down Expand Up @@ -445,6 +448,9 @@
"Incorrect email or password" : {
"extractionState" : "manual"
},
"Invalid Credentials" : {
"extractionState" : "manual"
},
"Invalid Email" : {
"extractionState" : "manual"
},
Expand Down Expand Up @@ -607,9 +613,6 @@
"Please enter a cost for your expense first!" : {
"extractionState" : "manual"
},
"Please enter a valid email" : {
"extractionState" : "manual"
},
"Please enter a valid email address." : {
"extractionState" : "manual"
},
Expand Down Expand Up @@ -796,6 +799,9 @@
"The code you've entered is not exists." : {
"extractionState" : "manual"
},
"The credentials provided are invalid. Please try again." : {
"extractionState" : "manual"
},
"The email address is already associated with an existing account. Please use a different email or log in to your existing account." : {
"extractionState" : "manual"
},
Expand Down Expand Up @@ -910,6 +916,9 @@
"Use settle up to divide costs with your friends or colleagues. It's easy and ensures everyone pays or receives their fair share.\\n\\nLet's get started splitting bills easily with friends." : {
"extractionState" : "manual"
},
"User account not found. Please verify your credentials." : {
"extractionState" : "manual"
},
"Warning" : {
"extractionState" : "manual"
},
Expand Down Expand Up @@ -1012,6 +1021,9 @@
"Your requested data not found." : {
"extractionState" : "manual"
},
"Your session has expired. Please log in again." : {
"extractionState" : "manual"
},
"Your total share" : {
"extractionState" : "manual"
}
Expand Down
40 changes: 34 additions & 6 deletions Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,7 @@ extension UserProfileViewModel {
user.reload { [weak self] error in
if let error {
self?.isDeleteInProgress = false
self?.showAlertFor(message: error.localizedDescription)
LogE("UserProfileViewModel: \(#function) Error reloading user: \(error).")
self?.handleFirebaseAuthErrors(error)
} else {
self?.promptForReAuthentication(user)
}
Expand All @@ -253,16 +252,15 @@ extension UserProfileViewModel {
getAuthCredential(user) { [weak self] credential in
guard let credential else {
self?.isDeleteInProgress = false
LogE("UserProfileViewModel: \(#function) Credential are - \(String(describing: credential))")
LogE("UserProfileViewModel: \(#function) Failed to retrieve valid credential.")
return
}

user.reauthenticate(with: credential) { [weak self] _, error in
guard let self else { return }
if let error {
self.isDeleteInProgress = false
self.showAlertFor(message: error.localizedDescription)
LogE("UserProfileViewModel: \(#function) Error re-authenticating user: \(error).")
self.handleFirebaseAuthErrors(error)
} else {
Task {
await self.deleteUser()
Expand Down Expand Up @@ -355,7 +353,7 @@ extension UserProfileViewModel {
guard let password = alert.textFields?.first?.text,
let email = self?.preference.user?.emailId else {
self?.isDeleteInProgress = false
LogE("UserProfileViewModel: \(#function) No email found for email login.")
LogE("UserProfileViewModel: \(#function) Missing email or password for email login.")
return
}
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
Expand All @@ -364,4 +362,34 @@ extension UserProfileViewModel {

TopViewController.shared.topViewController()?.present(alert, animated: true)
}

private func handleFirebaseAuthErrors(_ error: Error) {
guard let authErrorCode = FirebaseAuth.AuthErrorCode(rawValue: (error as NSError).code) else {
showAlertFor(title: "Error", message: "Something went wrong! Please try after some time.")
return
}

switch authErrorCode {
case .webContextCancelled:
showAlertFor(title: "Error", message: "Something went wrong! Please try after some time.")
case .tooManyRequests:
showAlertFor(title: "Error", message: "Too many attempts, please try after some time.")
case .invalidEmail:
showAlertFor(title: "Invalid Email", message: "The email address is not valid. Please check and try again.")
case .userNotFound:
showAlertFor(title: "Error", message: "User account not found. Please verify your credentials.")
case .userDisabled:
showAlertFor(title: "Account Disabled", message: "This account has been disabled. Please contact support.")
case .invalidCredential:
showAlertFor(title: "Invalid Credentials", message: "The credentials provided are invalid. Please try again.")
case .networkError:
showAlertFor(title: "Error", message: "No internet connection!")
case .userTokenExpired:
showAlertFor(title: "Error", message: "Your session has expired. Please log in again.")
default:
showAlertFor(title: "Error", message: "An unexpected error occurred. Please try again later.")
}

LogE("UserProfileViewModel: \(#function) Error re-authenticating user: \(error).")
}
}
4 changes: 2 additions & 2 deletions Splito/UI/Home/ActivityLog/ActivityLogView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct ActivityLogView: View {
var body: some View {
VStack(alignment: .center, spacing: 0) {
if .noInternet == viewModel.viewState || .somethingWentWrong == viewModel.viewState {
ErrorView(isForNoInternet: viewModel.viewState == .noInternet, onClick: viewModel.fetchActivityLogsInitialData)
ErrorView(isForNoInternet: viewModel.viewState == .noInternet, onClick: viewModel.fetchInitialActivityLogs)
} else if case .loading = viewModel.viewState {
LoaderView()
Spacer(minLength: 60)
Expand Down Expand Up @@ -76,7 +76,7 @@ private struct ActivityLogListView: View {
.padding(.bottom, 62)
}
.refreshable {
viewModel.fetchActivityLogsInitialData()
viewModel.fetchInitialActivityLogs()
}
.onAppear {
scrollToActivityLog(proxy)
Expand Down
77 changes: 33 additions & 44 deletions Splito/UI/Home/ActivityLog/ActivityLogViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,48 @@ class ActivityLogViewModel: BaseViewModel, ObservableObject {
self.router = router
super.init()

self.fetchActivityLogsInitialData()
self.fetchInitialActivityLogs()
self.fetchLatestActivityLogs()
}

func fetchActivityLogsInitialData() {
// Listens for real-time updates and returns the latest activity logs for the current user
private func fetchLatestActivityLogs() {
guard let userId = preference.user?.id else { return }

activityLogRepository.fetchLatestActivityLogs(userId: userId) { [weak self] activityLogs in
if let activityLogs {
for activityLog in activityLogs where !(self?.activityLogs.contains(where: { $0.id == activityLog.id }) ?? false) {
self?.activityLogs.append(activityLog)
}
self?.filterActivityLogs()
if self?.activityLogs.count == 1 {
self?.activityLogState = .hasActivity
}
} else {
self?.showToastForError()
}
}
}

func fetchInitialActivityLogs() {
Task {
lastDocument = nil
await fetchActivityLogs()
}
}

// MARK: - Data Loading
private func fetchActivityLogs() async {
guard let userId = preference.user?.id else {
guard let userId = preference.user?.id, hasMoreLogs else {
viewState = .initial
return
}

do {
let result = try await activityLogRepository.fetchActivitiesBy(userId: userId, limit: ACTIVITY_LOG_LIMIT)

activityLogs = result.data
let result = try await activityLogRepository.fetchActivitiesBy(userId: userId,
limit: ACTIVITY_LOG_LIMIT,
lastDocument: lastDocument)
activityLogs = lastDocument == nil ? result.data : (activityLogs + result.data)
lastDocument = result.lastDocument
hasMoreLogs = !(result.data.count < ACTIVITY_LOG_LIMIT)

Expand All @@ -67,33 +88,20 @@ class ActivityLogViewModel: BaseViewModel, ObservableObject {
LogD("ActivityLogViewModel: \(#function) Activity logs fetched successfully.")
} catch {
LogE("ActivityLogViewModel: \(#function) Failed to fetch activity logs: \(error).")
handleServiceError()
handleErrorState()
}
}

func loadMoreActivityLogs() {
Task {
await fetchMoreActivityLogs()
await fetchActivityLogs()
}
}

private func fetchMoreActivityLogs() async {
guard hasMoreLogs, let userId = preference.user?.id else { return }

do {
let result = try await activityLogRepository.fetchActivitiesBy(userId: userId, limit: ACTIVITY_LOG_LIMIT, lastDocument: lastDocument)

activityLogs.append(contentsOf: result.data)
lastDocument = result.lastDocument
hasMoreLogs = !(result.data.count < ACTIVITY_LOG_LIMIT)

filterActivityLogs()
viewState = .initial
activityLogState = activityLogs.isEmpty ? .noActivity : .hasActivity
LogD("ActivityLogViewModel: \(#function) Activity logs fetched successfully.")
} catch {
viewState = .initial
LogE("ActivityLogViewModel: \(#function) Failed to fetch more activity logs: \(error).")
private func handleErrorState() {
if lastDocument == nil {
handleServiceError()
} else {
showToastForError()
}
}
Expand All @@ -109,25 +117,6 @@ class ActivityLogViewModel: BaseViewModel, ObservableObject {
}
}

// Listens for real-time updates and returns the latest activity logs for the current user
private func fetchLatestActivityLogs() {
guard let userId = preference.user?.id else {
viewState = .initial
return
}

activityLogRepository.fetchLatestActivityLogs(userId: userId) { [weak self] activityLogs in
if let activityLogs {
for activityLog in activityLogs where !(self?.activityLogs.contains(where: { $0.id == activityLog.id }) ?? false) {
self?.activityLogs.append(activityLog)
}
self?.filterActivityLogs()
} else {
self?.showToastForError()
}
}
}

// MARK: - User Actions
func handleActivityItemTap(_ activity: ActivityLog) {
switch activity.type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private struct TransactionItemView: View {

Text("\(transactionWithUser.transaction.amount.formattedCurrency)")
.font(.subTitle2())
.foregroundStyle(transactionWithUser.payer?.id == preference.user?.id ? errorColor : successColor)
.foregroundStyle(transactionWithUser.payer?.id == preference.user?.id ? successColor : transactionWithUser.receiver?.id == preference.user?.id ? errorColor : primaryText)
}
}
.padding(.top, 20)
Expand Down
Loading

0 comments on commit 6c2fe28

Please sign in to comment.