diff --git a/README.md b/README.md index dba8c76..3d2efae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ +## What is our app +Our App is a simple bill splitter app that intents to use AI so that the user can take a picture of their reciept and split each item with all other members. + +## Use Flow +Users will first login to their account and from there, they are able to create or join a group. + +Groups will have all the users who are planning on splitting a bill. + +The creator of the group will be able to take a picture of the reciept and automatically display a listing of all items, and their costs. +All members of the group will be assigned to each item which will be their split that they pay to the owner of the group. + +Users will use Strip to make their payments. ## Specifications Our code is structured where, we have a 2 files that help handle our database interaction, and our User specific data. @@ -15,4 +27,27 @@ In the home view, the user can view their account that allow them to update thei ## Special Instructions -Our app uses the phone camera in order to function and as a result will need to be tested with an actual phone. +### App Testing Requirements +- **Camera Usage**: Our application requires access to the phone's camera to operate correctly. Ensure testing is on an actual mobile device with camera capabilities. + +### Stripe Onboarding Test Data +Use the following dummy information for testing the Stripe onboarding process: + +- **Test Phone Number**: Enter `000-000-0000` for any phone number fields. +- **SMS Code**: Use `000-000` when prompted for an SMS verification code. +- **Personal ID Numbers**: + - For successful individual verification, use `000000000` for the `individual.id_number` or the `id_number` attribute on the `Person` object. For SSN's last 4 digits, `0000` will work. +- **Business Tax ID Numbers**: + - Input `000000000` in the `company.tax_id` field for successful company verification. +- **Website Information**: Use `https://accessible.stripe.com` for website-related fields. +- **Address Validation**: + - Input legitimate values for `city`, `state`, and `postal_code` in the `address_full_match`. + +### Payment Method Simulation +For payment testing, use the following mock credit card details: +- **Card Number**: `4242424242424242` +- **CVC**: Any 3-digit number +- **Expiration Date**: Any future date + +Note: These values are for testing purposes only. Switch to real data for production. + diff --git a/reciept bill splitter.xcodeproj/project.pbxproj b/reciept bill splitter.xcodeproj/project.pbxproj index d173bd0..2c031e6 100644 --- a/reciept bill splitter.xcodeproj/project.pbxproj +++ b/reciept bill splitter.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 32847F2A2B8D96C60061449F /* ScanReciept.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32847F292B8D96C60061449F /* ScanReciept.swift */; }; 32847F2C2B8D96D50061449F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32847F2B2B8D96D50061449F /* ContentView.swift */; }; 328EEB5C2B9199530021F461 /* DatabaseAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 328EEB5B2B9199520021F461 /* DatabaseAPI.swift */; }; + 329A95DE2BADC39F0028FDEF /* AssignedTransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 329A95DD2BADC39F0028FDEF /* AssignedTransactionDetails.swift */; }; 32A6DA972BA13C81005FAA2C /* JoinGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6DA962BA13C81005FAA2C /* JoinGroupView.swift */; }; 32B1A4BF2B828F9400A20FDD /* reciept_bill_splitterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B1A4BE2B828F9400A20FDD /* reciept_bill_splitterApp.swift */; }; 32B1A4C32B828F9D00A20FDD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 32B1A4C22B828F9D00A20FDD /* Assets.xcassets */; }; @@ -26,8 +27,7 @@ 36788E3F2BA50F22006925A6 /* GroupDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36788E3E2BA50F22006925A6 /* GroupDetailView.swift */; }; 36E9BE642B86ACE00020BE12 /* SignUpLogInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE632B86ACE00020BE12 /* SignUpLogInView.swift */; }; 36E9BE662B86AD1C0020BE12 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE652B86AD1C0020BE12 /* HomeView.swift */; }; - 36E9BE682B86AD3F0020BE12 /* FriendsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE672B86AD3F0020BE12 /* FriendsView.swift */; }; - 36E9BE6C2B86AD5D0020BE12 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE6B2B86AD5D0020BE12 /* HistoryView.swift */; }; + 36E9BE6C2B86AD5D0020BE12 /* AllAssignedTransactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE6B2B86AD5D0020BE12 /* AllAssignedTransactions.swift */; }; 36E9BE6E2B86AD6E0020BE12 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE6D2B86AD6E0020BE12 /* SettingsView.swift */; }; 36E9BE702B86AD8D0020BE12 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE6F2B86AD8D0020BE12 /* SplitView.swift */; }; 36E9BE722B86B6240020BE12 /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E9BE712B86B6240020BE12 /* SignUpView.swift */; }; @@ -46,7 +46,6 @@ E591A68D2B9E8AD600AC1F5F /* StripePaymentSheet in Frameworks */ = {isa = PBXBuildFile; productRef = E591A68C2B9E8AD600AC1F5F /* StripePaymentSheet */; }; E591A68F2B9E8AD600AC1F5F /* StripePayments in Frameworks */ = {isa = PBXBuildFile; productRef = E591A68E2B9E8AD600AC1F5F /* StripePayments */; }; E591A6912B9E8AD600AC1F5F /* StripePaymentsUI in Frameworks */ = {isa = PBXBuildFile; productRef = E591A6902B9E8AD600AC1F5F /* StripePaymentsUI */; }; - E5AB946B2BAAA75B00836C2E /* ManualTransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AB946A2BAAA75B00836C2E /* ManualTransactionView.swift */; }; E5E6F0D82BAD36A0004A0697 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E6F0D72BAD36A0004A0697 /* CreateGroupView.swift */; }; /* End PBXBuildFile section */ @@ -55,6 +54,7 @@ 32847F292B8D96C60061449F /* ScanReciept.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanReciept.swift; sourceTree = ""; }; 32847F2B2B8D96D50061449F /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 328EEB5B2B9199520021F461 /* DatabaseAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseAPI.swift; sourceTree = ""; }; + 329A95DD2BADC39F0028FDEF /* AssignedTransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignedTransactionDetails.swift; sourceTree = ""; }; 32A6DA962BA13C81005FAA2C /* JoinGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupView.swift; sourceTree = ""; }; 32B1A4BB2B828F9400A20FDD /* reciept bill splitter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "reciept bill splitter.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 32B1A4BE2B828F9400A20FDD /* reciept_bill_splitterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reciept_bill_splitterApp.swift; sourceTree = ""; }; @@ -68,8 +68,7 @@ 36788E422BA7EEB3006925A6 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = ""; }; 36E9BE632B86ACE00020BE12 /* SignUpLogInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpLogInView.swift; sourceTree = ""; }; 36E9BE652B86AD1C0020BE12 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - 36E9BE672B86AD3F0020BE12 /* FriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsView.swift; sourceTree = ""; }; - 36E9BE6B2B86AD5D0020BE12 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; + 36E9BE6B2B86AD5D0020BE12 /* AllAssignedTransactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllAssignedTransactions.swift; sourceTree = ""; }; 36E9BE6D2B86AD6E0020BE12 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 36E9BE6F2B86AD8D0020BE12 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; 36E9BE712B86B6240020BE12 /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; @@ -77,10 +76,8 @@ 36E9BE752B86CAEA0020BE12 /* AddFriendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFriendView.swift; sourceTree = ""; }; 36E9BE852BA007F10020BE12 /* TransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionView.swift; sourceTree = ""; }; E50B498E2BAA0A640080CEAB /* PaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentManager.swift; sourceTree = ""; }; - E52146C82B9FE060007571A2 /* reciept-bill-splitter-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "reciept-bill-splitter-Info.plist"; sourceTree = SOURCE_ROOT; }; E52146C92B9FE12C007571A2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; E5AB94682BAAA47F00836C2E /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = ""; }; - E5AB946A2BAAA75B00836C2E /* ManualTransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualTransactionView.swift; sourceTree = ""; }; E5E6F0D72BAD36A0004A0697 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -145,17 +142,14 @@ 36E9BE752B86CAEA0020BE12 /* AddFriendView.swift */, 32B1A4C22B828F9D00A20FDD /* Assets.xcassets */, 32847F2B2B8D96D50061449F /* ContentView.swift */, - 36E9BE672B86AD3F0020BE12 /* FriendsView.swift */, 32E5E1882B8DA66C005BF4F4 /* GoogleService-Info.plist */, 36788E3E2BA50F22006925A6 /* GroupDetailView.swift */, - 36E9BE6B2B86AD5D0020BE12 /* HistoryView.swift */, + 36E9BE6B2B86AD5D0020BE12 /* AllAssignedTransactions.swift */, 36E9BE652B86AD1C0020BE12 /* HomeView.swift */, 32A6DA962BA13C81005FAA2C /* JoinGroupView.swift */, 32B1A4C02B828F9400A20FDD /* LaunchScreenView.swift */, - E5AB946A2BAAA75B00836C2E /* ManualTransactionView.swift */, 32B1A4C42B828F9D00A20FDD /* Preview Content */, 32B1A4BE2B828F9400A20FDD /* reciept_bill_splitterApp.swift */, - E52146C82B9FE060007571A2 /* reciept-bill-splitter-Info.plist */, 32847F292B8D96C60061449F /* ScanReciept.swift */, E52146C92B9FE12C007571A2 /* SceneDelegate.swift */, 36E9BE6D2B86AD6E0020BE12 /* SettingsView.swift */, @@ -166,6 +160,7 @@ 32847F272B8D96B70061449F /* UIImageExtension.swift */, 32C3CD472B8711A700FB2BF1 /* ViewModels */, E5E6F0D72BAD36A0004A0697 /* CreateGroupView.swift */, + 329A95DD2BADC39F0028FDEF /* AssignedTransactionDetails.swift */, ); path = "reciept bill splitter"; sourceTree = ""; @@ -292,16 +287,15 @@ 328EEB5C2B9199530021F461 /* DatabaseAPI.swift in Sources */, E5E6F0D82BAD36A0004A0697 /* CreateGroupView.swift in Sources */, 32847F282B8D96B70061449F /* UIImageExtension.swift in Sources */, - 36E9BE6C2B86AD5D0020BE12 /* HistoryView.swift in Sources */, + 36E9BE6C2B86AD5D0020BE12 /* AllAssignedTransactions.swift in Sources */, 36E9BE6E2B86AD6E0020BE12 /* SettingsView.swift in Sources */, - 36E9BE682B86AD3F0020BE12 /* FriendsView.swift in Sources */, 36788E3F2BA50F22006925A6 /* GroupDetailView.swift in Sources */, 32847F2A2B8D96C60061449F /* ScanReciept.swift in Sources */, 32847F2C2B8D96D50061449F /* ContentView.swift in Sources */, 36E9BE742B86C18D0020BE12 /* AccountView.swift in Sources */, 36E9BE762B86CAEA0020BE12 /* AddFriendView.swift in Sources */, - E5AB946B2BAAA75B00836C2E /* ManualTransactionView.swift in Sources */, E52146CA2B9FE12C007571A2 /* SceneDelegate.swift in Sources */, + 329A95DE2BADC39F0028FDEF /* AssignedTransactionDetails.swift in Sources */, 36E9BE702B86AD8D0020BE12 /* SplitView.swift in Sources */, E50B498F2BAA0A640080CEAB /* PaymentManager.swift in Sources */, 36E9BE722B86B6240020BE12 /* SignUpView.swift in Sources */, @@ -446,7 +440,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"reciept bill splitter/Preview Content\""; - DEVELOPMENT_TEAM = 3Y46278W32; + DEVELOPMENT_TEAM = S3PTQ97NTN; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -463,7 +457,7 @@ ); MARKETING_VERSION = 1.0; NEW_SETTING = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.jlvu; + PRODUCT_BUNDLE_IDENTIFIER = com.martinezdiego; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -481,7 +475,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"reciept bill splitter/Preview Content\""; - DEVELOPMENT_TEAM = 3Y46278W32; + DEVELOPMENT_TEAM = S3PTQ97NTN; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -498,7 +492,7 @@ ); MARKETING_VERSION = 1.0; NEW_SETTING = ""; - PRODUCT_BUNDLE_IDENTIFIER = "com.jlvu-ucdavis.edu"; + PRODUCT_BUNDLE_IDENTIFIER = com.martinezdiego; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/reciept bill splitter/.DS_Store b/reciept bill splitter/.DS_Store index 1b58d38..6bb6028 100644 Binary files a/reciept bill splitter/.DS_Store and b/reciept bill splitter/.DS_Store differ diff --git a/reciept bill splitter/AccountView.swift b/reciept bill splitter/AccountView.swift index a72a5ec..520f672 100644 --- a/reciept bill splitter/AccountView.swift +++ b/reciept bill splitter/AccountView.swift @@ -1,183 +1,3 @@ -/*import SwiftUI -import FirebaseAuth - -struct AccountView: View { - @State private var isEditing = false - @State private var newUsername = "" - @State private var userEmail = "" // Add state to store user email - @State private var userCanGetPaid = false // Add state to store user email - @State private var user_id = "" - - @EnvironmentObject var user: UserViewModel - @EnvironmentObject var paymentManager: PaymentManager - @State private var balanceData: [String: Any]? = nil - @State var isLoggedOut = false - - var body: some View { - NavigationStack{ - VStack { - HStack { - if isEditing { - TextField("Enter new username", text: $newUsername) - .padding() - .textFieldStyle(RoundedBorderTextFieldStyle()) - } else { - Text("Current Username: " + (newUsername.isEmpty ? "N/A" : newUsername)) - .padding() - } - Button(action: { - isEditing.toggle() - }) { - Image(systemName: isEditing ? "checkmark.circle.fill" : "pencil.circle.fill") - .foregroundColor(isEditing ? .green : .blue) - } - } - - if isEditing { - Button("Save") { - Task { - await user.updateUserName(newName: newUsername) - isEditing.toggle() - } - } - .padding() - } - Text("Email: \(userEmail)") // Display user email - .padding() - // Stripe balance section - if user.canGetPaid { - if let balanceData = balanceData { - VStack { - Text("Stripe Balance") - .font(.title) - .padding() - if let availableArray = balanceData["available"] as? [[String: Any]], - let available = availableArray.first, - let availableAmount = available["amount"] as? Int { - Text("Available Balance: \(formatAmount(availableAmount))") - } - if let pendingArray = balanceData["pending"] as? [[String: Any]], - let pending = pendingArray.first, - let pendingAmount = pending["amount"] as? Int { - Text("Pending Balance: \(formatAmount(pendingAmount))") - } - Button("Update payment methods") { - print("creating link") - DatabaseAPI.getStripeConnectAccountId { accountId, error in - if let error = error { - print("Error retrieving account ID: \(error.localizedDescription)") - } else if let accountId = accountId { - print("Retrieved Stripe Connect Account ID: \(accountId)") - // Use the accountId for whatever you need, like creating an account link - paymentManager.createStripeAccountLink(stripeAccountID: accountId) - } else { - print("Stripe Connect Account ID not found") - } - } - } - } - } else { - Text("") - } - } - else { - Button("Setup Payments") { - print("creating account") - paymentManager.createExpressConnectAccountAndOnboardingLink(email: userEmail) - - //SETUP after onboarding it is the only way and have to check for reauth fuckkkkkkkkkkk (setup the cangetpaid of course) - - DatabaseAPI.setCanGetPaid(forUserId: user_id, canGetPaid: true) { error in // Pass the userId here - if let error = error { - // Handle the error - print("Error setting canGetPaid: \(error.localizedDescription)") - } else { - // Update was successful - self.userCanGetPaid = true - user.canGetPaid = true - print("canGetPaid successfully set for the user") - } - } - } - } - } - .navigationTitle("Accounts") - .onAppear { - Task{ - await fetchUserDetails() - await fetchStripeBalance() - await user.updateCanGetPaidStatus() - - } - } - Button(action: { - // Sign out action - do { - try Auth.auth().signOut() - isLoggedOut = true // Set isLoggedIn to false to navigate to SignUpView - - } catch { - print("Error signing out: \(error.localizedDescription)") - } - }) { - Text("Sign Out") - .foregroundColor(.red) - } - .padding() - } - .navigationDestination(isPresented: $isLoggedOut) { - SignUpLogInView(isLoggedIn: $isLoggedOut) - .navigationBarHidden(true) - - } - } - - - - private func fetchUserDetails() async{ - Task { - if let user = await DatabaseAPI.grabUserData() { - self.newUsername = user.userName - self.userEmail = user.email - } - if let user = Auth.auth().currentUser { - self.user_id = user.uid - } - } - } - - private func fetchStripeBalance() async { - // Check if the current user has a Stripe Connect Account ID - DatabaseAPI.getStripeConnectAccountId { accountId, error in - guard let accountId = accountId, error == nil else { - print("Stripe Connect Account ID not found or error occurred: \(error?.localizedDescription ?? "Unknown error")") - // Handle UI update for users without Stripe account here - // For example, show a message or hide the balance section - return - } - - // If the account ID exists, fetch the Stripe balance - paymentManager.checkStripeBalance(accountId: accountId) { result in - switch result { - case .success(let balance): - // Here you can update some state to display the balance in your UI - self.balanceData = balance - print("Retrieved Stripe balance: \(balance)") - case .failure(let error): - print("Error fetching Stripe balance: \(error.localizedDescription)") - } - } - } - } - - private func formatAmount(_ amount: Int) -> String { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .currency - numberFormatter.currencyCode = "USD" - return numberFormatter.string(from: NSNumber(value: Double(amount) / 100)) ?? "$0.00" - } - -}*/ import SwiftUI import FirebaseAuth @@ -187,14 +7,17 @@ struct AccountView: View { @State private var userEmail = "" @State private var userCanGetPaid = false @State private var user_id = "" - + @EnvironmentObject var user: UserViewModel @EnvironmentObject var paymentManager: PaymentManager @State private var balanceData: [String: Any]? = nil - @State private var isLoggedOut = false + @Environment(\.dismiss) var dismiss + + @Binding var isLoggedIn: Bool + var body: some View { - NavigationStack { + NavigationStack{ VStack(spacing: 20) { VStack(alignment: .leading, spacing: 10) { HStack { @@ -225,7 +48,7 @@ struct AccountView: View { .foregroundColor(.white) .padding() .frame(maxWidth: .infinity) - .background(Color.blue) + .background(LinearGradient(gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]), startPoint: .leading, endPoint: .trailing)) .cornerRadius(8) } } @@ -277,7 +100,7 @@ struct AccountView: View { } .foregroundColor(.white) .padding() - .background(Color.blue) + .background(LinearGradient(gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]), startPoint: .leading, endPoint: .trailing)) .cornerRadius(8) } .padding() @@ -288,15 +111,17 @@ struct AccountView: View { print("Creating account") paymentManager.createExpressConnectAccountAndOnboardingLink(email: userEmail) - DatabaseAPI.setCanGetPaid(forUserId: user_id, canGetPaid: true) { error in - if let error = error { - print("Error setting canGetPaid: \(error.localizedDescription)") - } else { - self.userCanGetPaid = true - user.canGetPaid = true - print("canGetPaid successfully set for the user") - } - } + DatabaseAPI.setCanGetPaid(forUserId: user_id, canGetPaid: true) { error in // Pass the userId here + if let error = error { + // Handle the error + print("Error setting canGetPaid: \(error.localizedDescription)") + } else { + // Update was successful + self.userCanGetPaid = true + user.canGetPaid = true + print("canGetPaid successfully set for the user") + } + } } .foregroundColor(.white) .padding() @@ -315,10 +140,11 @@ struct AccountView: View { } Button(action: { - // Sign out action + isLoggedIn = false do { try Auth.auth().signOut() - isLoggedOut = true + isLoggedIn = false + dismiss() } catch { print("Error signing out: \(error.localizedDescription)") } @@ -327,16 +153,11 @@ struct AccountView: View { .foregroundColor(.black) .padding() .frame(maxWidth: .infinity) - .background(Color.gray.opacity(0.2)) - .cornerRadius(8) + .background(LinearGradient(gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]), startPoint: .leading, endPoint: .trailing)) .cornerRadius(8) } .padding() .buttonStyle(PlainButtonStyle()) } - .navigationDestination(isPresented: $isLoggedOut) { - SignUpLogInView(isLoggedIn: $isLoggedOut) - .navigationBarHidden(true) - } } private func fetchUserDetails() async { diff --git a/reciept bill splitter/AllAssignedTransactions.swift b/reciept bill splitter/AllAssignedTransactions.swift new file mode 100644 index 0000000..7caa535 --- /dev/null +++ b/reciept bill splitter/AllAssignedTransactions.swift @@ -0,0 +1,41 @@ +import SwiftUI +import FirebaseFirestore +import FirebaseFirestoreSwift + +struct AllAssignedTransactions: View { + @EnvironmentObject var user: UserViewModel + @State var allAssignedTransactions: [AssignedTransaction] = [] + @EnvironmentObject var paymentManager: PaymentManager + + var body: some View { + NavigationStack { + Text("Assigned Transactions") + List { + ForEach(0 ..< allAssignedTransactions.count, id:\.self) { index in + NavigationLink(destination: AssignedTransactionDetails(assignedTransaction: $allAssignedTransactions[index]).environmentObject(user).environmentObject(paymentManager)) { + VStack(alignment: .leading) { + if allAssignedTransactions[index].isPaid { + Text(allAssignedTransactions[index].transactionName) + .opacity(0.5) + } + else { + Text(allAssignedTransactions[index].transactionName) + } + } + } + } + } + } + .onAppear { + fetchAllTransactions() + } + } + + private func fetchAllTransactions() { + Task { + if let userAssignedTransactions = await DatabaseAPI.grabUserAssignedTransactions() { + allAssignedTransactions = userAssignedTransactions + } + } + } +} diff --git a/reciept bill splitter/AssignedTransactionDetails.swift b/reciept bill splitter/AssignedTransactionDetails.swift new file mode 100644 index 0000000..24c425b --- /dev/null +++ b/reciept bill splitter/AssignedTransactionDetails.swift @@ -0,0 +1,66 @@ +import SwiftUI +import StripePaymentSheet + +struct AssignedTransactionDetails: View { + @EnvironmentObject var paymentManager: PaymentManager + @EnvironmentObject var user: UserViewModel + @Binding var assignedTransaction: AssignedTransaction + + @Environment(\.dismiss) var dismiss // Use the dismiss environment value + + var body: some View { + VStack { + Text("Transaction Name: \(assignedTransaction.transactionName)") + .font(.headline) + + Text("Amount to Pay: $\(String(format: "%.2f", Double(assignedTransaction.amountToPay) / 100))") + .font(.subheadline) + + if !assignedTransaction.isPaid { + if let paymentSheet = paymentManager.paymentSheet { + PaymentSheet.PaymentButton(paymentSheet: paymentSheet) { paymentResult in + // This is the completion handler + paymentManager.onPaymentCompletion(result: paymentResult) + if case .completed = paymentResult { + // Call transferMoney function after successful payment + paymentManager.transferMoney(amount: assignedTransaction.amountToPay, destinationAccountId: assignedTransaction.user_idToPay, assignedTransactionId: assignedTransaction.associatedTransaction_id) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Optional delay to allow users to see the completion message + dismiss() // Dismiss the view + } + } + } content: { + Text("Pay Now") + .padding() + .background(Color.blue) + .foregroundColor(Color.white) + .cornerRadius(10) + } + } else { + Text("Loading…") + } + if let result = paymentManager.paymentResult { + switch result { + case .completed: + Text("Payment complete") + case .failed(let error): + Text("Payment failed: \(error.localizedDescription)") + case .canceled: + Text("Payment canceled.") + } + } + + } else { + Text("Transaction already paid") + .foregroundColor(.green) + } + } + .onAppear(){ + print(assignedTransaction) + paymentManager.fetchPaymentDataAndPrepareSheet(uid: user.user_id, amount: assignedTransaction.amountToPay) + + } + .padding() + .navigationBarTitle("Transaction Details", displayMode: .inline) + } +} diff --git a/reciept bill splitter/FriendsView.swift b/reciept bill splitter/FriendsView.swift deleted file mode 100644 index b5a55ff..0000000 --- a/reciept bill splitter/FriendsView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// FriendsView.swift -// reciept bill splitter -// -// Created by Josh Vu on 2/21/24. -// - -import SwiftUI - -struct FriendsView: View { - @State private var isAddFriendsActive : Bool = false - @State private var searchText: String = "" - @State private var friends: [(String, String)] = [ - ("John", "john_doe"), - ("Jane", "jane_smith"), - ("Alice", "alice_wonder"), - ("Bob", "bob_jones"), - ("Emily", "emily_green"), - ("Michael", "michael_brown"), - ] - @State private var isContextMenuVisible = false // State variable to control visibility of context menu - @State private var friendToDelete: (String, String)? // Track the friend to delete - @State private var showingDeleteAlert = false // State variable to control visibility of delete confirmation alert - - - var body: some View { - NavigationStack{ - VStack { - TextField("Search", text: $searchText) - .autocapitalization(.none) // Disable automatic capitalization - .padding() - .background(Color(.systemGray6)) - .cornerRadius(8) - .padding(.horizontal) - List(friends.filter({ searchText.isEmpty ? true : $0.0.contains(searchText) || $0.1.contains(searchText) }), id: \.0) { friend, username in - HStack { - Image(systemName: "person.circle") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 50, height: 50) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text(friend) - .font(.headline) - Text("\(username)") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() // Add spacer to push button to the right - - Button(action: { - friendToDelete = (friend, username) // Set the friend to delete - showingDeleteAlert = true - }) { - Image(systemName: "trash") - .font(.headline) - .foregroundColor(.black) - } - } - } - .alert(isPresented: $showingDeleteAlert) { - Alert(title: Text("Delete Friend"), message: Text("Are you sure you want to delete \(friendToDelete?.1 ?? "")?"), primaryButton: .destructive(Text("Yes")) { - // Handle delete action here - if let friendToDelete = friendToDelete { - deleteFriend(friendToDelete) - } - }, secondaryButton: .cancel(Text("Cancel"))) - } - Button(action: { - isAddFriendsActive = true // Set isSignUpActive to true when button is tapped - }) { - Text("Add Friends") - .foregroundColor(.blue) - .padding(.horizontal, 20) - } - Rectangle() - .foregroundColor(Color(.systemGray6)) - .frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height/15) - } - .navigationTitle("Friends") - .navigationDestination(isPresented: $isAddFriendsActive){ - AddFriendView() - } - } - } - func deleteFriend(_ friend: (String, String)) { - // Implement your logic to delete the friend here - if let index = friends.firstIndex(where: { $0 == friend }) { - friends.remove(at: index) - } - } -} - -#Preview { - FriendsView() -} diff --git a/reciept bill splitter/GroupDetailView.swift b/reciept bill splitter/GroupDetailView.swift index a467500..4840dc7 100644 --- a/reciept bill splitter/GroupDetailView.swift +++ b/reciept bill splitter/GroupDetailView.swift @@ -12,7 +12,6 @@ import FirebaseFirestoreSwift struct GroupDetailView: View { let db = Firestore.firestore() @State private var isCameraPresented = false - @State private var isTransactionSelected = false @State private var selectedImage: UIImage? @State private var isTaken = false @@ -20,10 +19,7 @@ struct GroupDetailView: View { @State private var totalSpent: Double = 0 @State private var isViewMembersPopoverPresented = false - - @State private var isManualInputPresented = false - @State private var transactionName = "" @State private var transactionPrice = "" @@ -31,7 +27,7 @@ struct GroupDetailView: View { @State private var selectedTransactionID = "" @State var isAlert = false - + @State var scannedItems: [ReceiptItem] = [] @StateObject var scanReceipt = ScanReceipt() @EnvironmentObject var user: UserViewModel @@ -44,57 +40,47 @@ struct GroupDetailView: View { List { ForEach(user.currentSelectedGroupTransactions.indices, id: \.self) { index in + let transactionData = user.currentSelectedGroupTransactions[index] let date = formatter.string(from: transactionData.dateCreated?.dateValue() ?? Date()) - - if transactionData.isCompleted { - HStack { - Text(transactionData.name) - Text(date) - } - .onTapGesture { - user.selectedTransaction = transactionData - selectedTransactionID = transactionData.transaction_id - isTransactionSelected = true - } - .opacity(0.5) - } else { - HStack { - Text(transactionData.name) - Text(date) - } - .onTapGesture { - user.selectedTransaction = transactionData - selectedTransactionID = transactionData.transaction_id - isTransactionSelected = true + NavigationLink(destination: TransactionView(selectedTransactionId: $user.currentSelectedGroupTransactions[index].transaction_id, groupData: $selectedGroup)) { + if transactionData.isCompleted { + HStack { + Text(transactionData.name) + Text(date) + } + .opacity(0.5) + } else { + HStack { + Text(transactionData.name) + Text(date) + } } } } } - Button("Add Transaction") { - isManualInputPresented.toggle() - } } else { Text("No transactions found") } if selectedGroup.owner_id == user.user_id { + Button("Open Camera") { - Task { - await createTransaction() - } - //isCameraPresented = true + + isCameraPresented = true } .sheet(isPresented: $isCameraPresented) { CameraView(isPresented: $isCameraPresented, selectedImage: $selectedImage, isTaken: $isTaken) } .onChange(of: isTaken) { - if let imageToScan = selectedImage { + if isTaken, let imageToScan = selectedImage { Task { - await scanReceipt.scanReceipt(image: imageToScan) + self.scannedItems = await scanReceipt.scanReceipt(image: imageToScan) + await createTransaction() + isTaken = false // Reset the flag after transaction creation + await loadTransactions() } } - isTaken = false // Reset the flag } } @@ -109,21 +95,7 @@ struct GroupDetailView: View { MembersListView(members: selectedGroup.members) } } - .navigationDestination(isPresented: $isTransactionSelected) { - TransactionView(selectedTransactionId: $selectedTransactionID, groupData: $selectedGroup) - //TransactionView() - - } - .navigationDestination(isPresented: $isManualInputPresented) { - ManualTransactionInputView(isPresented: $isManualInputPresented, transactionName: $transactionName, transactionPrice: $transactionPrice, groupID: selectedGroup.groupID) - } - .onChange(of: scanReceipt.isScanning) { - if !scanReceipt.isScanning { - Task { - await createTransaction() - } - } - } + .onAppear { formatter.dateStyle = .short @@ -138,13 +110,12 @@ struct GroupDetailView: View { } private func createTransaction() async { - let scannedItems = scanReceipt.receiptItems // Assume these are the scanned receipt items + let scannedItems = scannedItems// Assume these are the scanned receipt items let transactionItems = scannedItems.map { Item(priceInCents: Int($0.price * 100), name: $0.name) } - let tempTransaction = Transaction(transaction_id: "", itemList: [], itemBidders: [:], name: scanReceipt.title ?? "Untitled Transaction", isCompleted: false, dateCreated: nil) let newTransaction = Transaction(transaction_id: "", itemList: transactionItems, itemBidders: [:], name: scanReceipt.title ?? "Untitled Transaction", isCompleted: false, dateCreated: nil) - await DatabaseAPI.createTransaction(transactionData: tempTransaction, groupID: selectedGroup.groupID) + await DatabaseAPI.createTransaction(transactionData: newTransaction, groupID: selectedGroup.groupID) } private func loadTransactions() async { @@ -163,20 +134,7 @@ struct GroupDetailView: View { } } -/*struct MembersListView: View { - let members: [GroupMember] - - var body: some View { - List { - ForEach(members, id: \.id) { member in - Text(member.id) - } - } - .onAppear { - print("Members: \(members)") - } - } -}*/ + struct MembersListView: View { let members: [GroupMember] diff --git a/reciept bill splitter/HistoryView.swift b/reciept bill splitter/HistoryView.swift deleted file mode 100644 index 8316f54..0000000 --- a/reciept bill splitter/HistoryView.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI -import FirebaseFirestore -import FirebaseFirestoreSwift - -struct HistoryView: View { - @EnvironmentObject var user: UserViewModel - @State private var allTransactions: [Transaction] = [] - - var body: some View { - List { - ForEach(allTransactions.indices, id: \.self) { index in - VStack(alignment: .leading) { - Text(allTransactions[index].name) - } - } - } - .onAppear { - fetchAllTransactions() - } - } - - private func fetchAllTransactions() { - Task { - var transactions: [Transaction] = [] - // Fetch transactions for each group - for group in user.groups { - if let groupTransactions = await DatabaseAPI.grabAllTransactionsForGroup(groupID: group.groupID) { - transactions.append(contentsOf: groupTransactions) - } - } - // Sort transactions by time created - DispatchQueue.main.async { - allTransactions = transactions - } - } - } -} diff --git a/reciept bill splitter/HomeView.swift b/reciept bill splitter/HomeView.swift index 867728f..61242f2 100644 --- a/reciept bill splitter/HomeView.swift +++ b/reciept bill splitter/HomeView.swift @@ -9,7 +9,6 @@ struct HomeView: View { @State private var isJoiningGroup = false @State private var isEmptyDisplayFormat = true @EnvironmentObject var userViewModel: UserViewModel - @State private var isCreatingGroup = false @State private var selectedGroup: Group? @State private var isAlert = false @@ -18,6 +17,7 @@ struct HomeView: View { @StateObject private var paymentManager = PaymentManager() @State private var showPaymentSheet = false + @Binding var isLoggedIn: Bool var body: some View { NavigationStack { VStack { @@ -30,54 +30,47 @@ struct HomeView: View { Text(group.group_name) Text("Invite Code: \(group.invite_code)") } + .onAppear { + Task { + listenToTransactionsForGroup(groupId: group.groupID) + } + } } } } Spacer() - Button("Transfer Money") { - paymentManager.transferMoney(amount: 1000, destinationAccountId: "acct_1Ovoc6QQyo8likZn") - } - - Button("Collect Payment") { - paymentManager.fetchPaymentDataAndPrepareSheet(uid: userViewModel.user_id, amount: 1000) - } + - VStack { - if let paymentSheet = paymentManager.paymentSheet { - PaymentSheet.PaymentButton(paymentSheet: paymentSheet, onCompletion: paymentManager.onPaymentCompletion) { - Text("Buy") - } - } else { - Text("Loading…") - } - - if let result = paymentManager.paymentResult { - switch result { - case .completed: - Text("Payment complete") - case .failed(let error): - Text("Payment failed: \(error.localizedDescription)") - case .canceled: - Text("Payment canceled.") + Menu { + Button("Create Group") { + if userViewModel.canGetPaid { + isCreatingGroup = true + } else { + showInfoAlert = true + } + }.foregroundColor(userViewModel.canGetPaid ? .white : .red) // Text color changes based on `canGetPaid` + Button("Join Group") { + isJoiningGroup = true + print("Join Group tapped") } + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .frame(width: 50, height: 50) + .foregroundColor(.blue) + } } - Button("Create Group") { - if userViewModel.canGetPaid { - isCreatingGroup = true - } else { - showInfoAlert = true - } - } - .foregroundColor(userViewModel.canGetPaid ? .primary : .red) .navigationDestination(isPresented: $isCreatingGroup) { CreateGroupView() } - - BottomToolbar().environmentObject(paymentManager) + .navigationDestination(isPresented: $isJoiningGroup) { + JoinGroupView() + } + BottomToolbar(isLoggedIn: $isLoggedIn).environmentObject(paymentManager).environmentObject(userViewModel) } .navigationTitle("Home") .alert(isPresented: $showInfoAlert) { @@ -90,24 +83,28 @@ struct HomeView: View { .onAppear { Task { await userViewModel.getUserData() + await userViewModel.updateCanGetPaidStatus() } } } + private func assignUsersTransaction() { + Task{ + await userViewModel.getUserData() + } } - private func listenToTransactionsForGroup(groupId: String) { +private func listenToTransactionsForGroup(groupId: String) { let db = Firestore.firestore() db.collection("transactions").whereField("group_id", isEqualTo: groupId) .addSnapshotListener { querySnapshot, error in guard let snapshots = querySnapshot else { - print("Error fetching documents: \(error!)") + print("Error fetching documents: (error!)") return } - + if let error = error { - print("Error retreiving collection: \(error)") + print("Error retreiving collection: (error)") } - // Find Changes where document is a diff snapshots.documentChanges.forEach { diff in if diff.type == .modified { @@ -115,9 +112,12 @@ struct HomeView: View { print("GROUP TRANSACTION HAS BEEN MODIFIED") let data = diff.document.data() let isTransactionCompleted = data["isCompleted"] as? Bool ?? false - + if isTransactionCompleted { - + print("ASSINING PAYMENTRS") + Task { + await DatabaseAPI.assignAllGroupMembersPayment(transaction_id: diff.document.documentID) + } } // Assign Each Member Their Parts to Pay } @@ -127,28 +127,22 @@ struct HomeView: View { } } } - private func assignUsersTransaction() { - Task{ - await userViewModel.getUserData() - await userViewModel.updateCanGetPaidStatus() - } - - } -} - - - + } struct BottomToolbar: View { @EnvironmentObject var paymentManager: PaymentManager // Ensure this is passed down from the parent view + @EnvironmentObject var userViewModel: UserViewModel // Ensure this is passed down from the parent view + + @Binding var isLoggedIn: Bool + var body: some View { HStack(spacing: 0.2) { - ToolbarItem(iconName: "person.2", text: "Friends", destination: AnyView(FriendsView())) + //ToolbarItem(iconName: "person.2", text: "Friends", destination: AnyView(FriendsView())) //ToolbarItem(iconName: "person.3", text: "Home", destination: AnyView(HomeView())) - ToolbarItem(iconName: "bolt", text: "Activities", destination: AnyView(HistoryView())) - ToolbarItem(iconName: "person.crop.circle", text: "Accounts", destination: AnyView(AccountView().environmentObject(paymentManager))) + ToolbarItem(iconName: "dollarsign", text: "Pay Transactions", destination: AnyView(AllAssignedTransactions().environmentObject(userViewModel).environmentObject(paymentManager))) + ToolbarItem(iconName: "person.crop.circle", text: "Accounts", destination: AnyView(AccountView(isLoggedIn: $isLoggedIn).environmentObject(paymentManager))) } .frame(height: 50) .background(Color(UIColor.systemBackground)) diff --git a/reciept bill splitter/LaunchScreenView.swift b/reciept bill splitter/LaunchScreenView.swift index 53a4c5f..9ea38ba 100644 --- a/reciept bill splitter/LaunchScreenView.swift +++ b/reciept bill splitter/LaunchScreenView.swift @@ -25,9 +25,11 @@ struct LaunchScreenView: View { if isActive { NavigationStack { if isLoggedIn || router.currentPage == "onboarding" || router.currentPage == "reauth" { - HomeView() + HomeView(isLoggedIn: $isLoggedIn) } else { SignUpLogInView(isLoggedIn: $isLoggedIn) + .accentColor(.purple) // Match color with logo + } } .environmentObject(user) @@ -36,14 +38,32 @@ struct LaunchScreenView: View { VStack { VStack{ //Displaying Logo - Image(systemName: "wallet.pass.fill") - .font(.system(size: 80)) - .foregroundColor(.red) + ZStack { + Circle() + .fill(LinearGradient(gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]), startPoint: .topLeading, endPoint: .bottomTrailing)) // Gradient circle color + .frame(width: 120, height: 120) // Adjust circle size as needed + .shadow(color: .black.opacity(0.5), radius: 5, x: 0, y: 2) // Add shadow for depth + + Image(systemName: "wallet.pass.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 80, height: 80) + .foregroundColor(.white) // Adjust icon color as needed + } + //Displaying App Name - Text("Bill Split") - .font(Font.custom("Baskerille-Bold", size: 26)) + Text("Wonder Wallet") + .font(.system(size: 26)) .foregroundColor(.black.opacity(0.80)) + .fontWeight(.light) + + // Displaying Slogan + Text("Scan, Split, Simplify.") + .font(.system(size: 16)) + .foregroundColor(.black.opacity(0.60)) + + } .scaleEffect(size) .opacity(opacity) diff --git a/reciept bill splitter/ScanReciept.swift b/reciept bill splitter/ScanReciept.swift index fbb48b5..ffe9c79 100644 --- a/reciept bill splitter/ScanReciept.swift +++ b/reciept bill splitter/ScanReciept.swift @@ -41,7 +41,7 @@ class ScanReceipt: ObservableObject { private var receiptItemsTemp: [ReceiptItem] = [] @Published var uiImage: UIImage? private var tempimage: UIImage? - func scanReceipt(image: UIImage) async { + func scanReceipt(image: UIImage) async -> [ReceiptItem]{ Task { @MainActor in self.isScanning = true self.receiptItems = [] @@ -69,8 +69,13 @@ class ScanReceipt: ObservableObject { self.uiImage = tempimage self.title = titleTemp self.isScanning = false - + } + if let tax = finalTax { + finalItems.append(tax) + } + return finalItems + } private func runModel(image: UIImage) async { diff --git a/reciept bill splitter/SignUpLogInView.swift b/reciept bill splitter/SignUpLogInView.swift index f1b92cf..07fbe75 100644 --- a/reciept bill splitter/SignUpLogInView.swift +++ b/reciept bill splitter/SignUpLogInView.swift @@ -24,7 +24,29 @@ struct SignUpLogInView: View { var body: some View { NavigationStack { - VStack { + VStack { + Text("Wonder Wallet") + .font(.system(size: 30)) + .foregroundColor(.black.opacity(0.80)) + .fontWeight(.light) + .padding() + + ZStack { + Circle() + .fill(LinearGradient(gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]), startPoint: .topLeading, endPoint: .bottomTrailing)) + .frame(width: 80, height: 80) // Adjust circle size as needed + .shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 2) // Adjust shadow radius as needed + + Image(systemName: "wallet.pass.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 50, height: 50) // Adjust icon size as needed + .foregroundColor(.white) // Adjust icon color as needed + } + .frame(width: 100, height: 100) // Adjust total size of the icon + .padding() + + TextField("Email", text: $email) .padding() .background(Color.gray.opacity(0.2)) @@ -87,12 +109,13 @@ struct SignUpLogInView: View { } label: { Text("Login") - .foregroundColor(.white) + .font(.headline) + .foregroundColor(.white) // Set text color to white .padding() .frame(maxWidth: .infinity) - .background(Color.blue) - .cornerRadius(8.0) - .padding(.horizontal, 20) + .background(LinearGradient(gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.purple.opacity(0.8)]), startPoint: .leading, endPoint: .trailing)) // Use gradient background + .cornerRadius(8) // Round the corners of the button + .padding(.horizontal) // Add horizontal padding } Button { @@ -108,6 +131,9 @@ struct SignUpLogInView: View { .navigationDestination(isPresented: $isSignUpActive){ SignUpView(isLoggedIn: $isLoggedIn) } + .navigationDestination(isPresented: $isLoggedIn){ + HomeView(isLoggedIn: $isLoggedIn) + } } } diff --git a/reciept bill splitter/TransactionView.swift b/reciept bill splitter/TransactionView.swift index a1dadf8..04c0d38 100644 --- a/reciept bill splitter/TransactionView.swift +++ b/reciept bill splitter/TransactionView.swift @@ -1,4 +1,7 @@ import SwiftUI +import Firebase +import SwiftUI +import StripePaymentSheet struct TransactionView: View { @@ -8,64 +11,113 @@ struct TransactionView: View { @Binding var groupData: Group @State var transactionData: Transaction? - - var totalSpent: Double { - - if let transaction = user.selectedTransaction { - return transaction.itemList.map { Double($0.priceInCents) / 100 }.reduce(0, +) - } else { - return Double(0.0) - } - } - + @State private var isEditingName = false + @State private var editedName: String = "" + @Environment(\.dismiss) var dismiss // Use the dismiss environment value + var body: some View { VStack { if let transaction = transactionData { + let totalSpent = transaction.itemList.map { Double($0.priceInCents) / 100 }.reduce(0, +) - Text(transaction.name) - .font(.title) + if isEditingName && groupData.owner_id == user.user_id { + TextField("Transaction Name", text: $editedName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Button("Save") { + Task { + await saveTransactionName(transactionId: transaction.transaction_id, newName: editedName) + isEditingName = false + } + } + .padding() + .background(Color.blue) + .foregroundColor(Color.white) + .cornerRadius(10) + } else { + Text(transaction.name) + .font(.title) + if groupData.owner_id == user.user_id { + + Button(action: { + isEditingName = true + editedName = transaction.name + }) { + Image(systemName: isEditingName ? "checkmark.circle.fill" : "pencil.circle.fill") + .foregroundColor(isEditingName ? .green : .blue) + } + } + } // index of the bidding members and get the dictiionary count inside List { ForEach(transaction.itemList.indices, id: \.self) { index in - let bidders = transaction.itemBidders[String(index)] ?? [] - let biddersCount = bidders.count - let isCurrentUserBidding = bidders.contains(user.user_id) + let isCurrentUserBidding = transaction.itemBidders[String(index)]?.contains(user.user_id) ?? false HStack { Text("\(transaction.itemList[index].name): $\(String(format: "%.2f", Double(transaction.itemList[index].priceInCents) / 100))") - .foregroundColor(isCurrentUserBidding ? .blue : .primary) // Change color if the current user is bidding Spacer() - // Display the number of bidders for each item - Text("Bidders: \(biddersCount)") - .foregroundColor(.gray) - .font(.subheadline) + Button(action: { + Task { + await bidOnItem(itemIndex: index, transactionID: transaction.transaction_id, userID: user.user_id) + transactionData = await DatabaseAPI.grabTransaction(transaction_id: selectedTransactionId) + + } + }) { + Text(isCurrentUserBidding ? "Unbid" : "Bid") + .foregroundColor(.white) + .padding() + .background(isCurrentUserBidding ? Color.red : Color.green) + .cornerRadius(10) + } + } } } - Text("Your Total Contribution: $\(String(format: "%.2f", calculateUserTotalContribution(transaction: user.selectedTransaction!, userID: user.user_id)))") + + Text("Your Total Contribution: $\(String(format: "%.2f", calculateUserTotalContribution(transaction: transaction, userID: user.user_id)))") .fontWeight(.bold) Text("Total: $\(String(format: "%.2f", totalSpent))") .fontWeight(.bold) + // ONLY group owner can lock in assigned prices + if groupData.owner_id == user.user_id { + Button("Complete Transaction") { + Task { + await DatabaseAPI.toggleGroupTransactionsCompletion(transactionID: transaction.transaction_id, completion: true) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Optional delay to allow users to see the completion message + dismiss() // Dismiss the view + } + } + } + } + } else { Text("LOADING") } - // ONLY group owner can lock in assigned prices - if groupData.owner_id == user.user_id { - Button("Complete Transaction") { - Task { - await DatabaseAPI.toggleGroupTransactionsCompletion(transactionID: user.selectedTransaction?.transaction_id ?? "", completion: true) - } - } - } + } .onAppear { Task { transactionData = await DatabaseAPI.grabTransaction(transaction_id: selectedTransactionId) } } + .onAppear { + let transactionRef = Firestore.firestore().collection("transactions").document(selectedTransactionId) + transactionRef.addSnapshotListener { documentSnapshot, error in + guard let document = documentSnapshot, error == nil else { + print("Error fetching document: \(error?.localizedDescription ?? "Unknown error")") + return + } + guard let data = document.data() else { + print("Document data was empty.") + return + } + // Parse the data into your Transaction model and update the state + // self.transactionData = self.parseTransactionData(data) + } + } + .navigationTitle("Transaction Details") @@ -83,5 +135,74 @@ struct TransactionView: View { return totalContribution } + func bidOnItem(itemIndex: Int, transactionID: String, userID: String) async { + let transactionRef = Firestore.firestore().collection("transactions").document(transactionID) + + do { + let document = try await transactionRef.getDocument() + if document.exists, var transactionData = document.data() { + var itemBidders = transactionData["itemBidders"] as? [String: [String]] ?? [:] + var bidders = itemBidders[String(itemIndex)] ?? [] + + if let index = bidders.firstIndex(of: userID) { + // User is already bidding, remove their bid + bidders.remove(at: index) + } else { + // Add user's bid + bidders.append(userID) + } + + // Update the bidders list for the item + itemBidders[String(itemIndex)] = bidders + transactionData["itemBidders"] = itemBidders + + // Update the transaction document + try await transactionRef.updateData(transactionData) + } + } catch { + print("Error updating transaction: \(error)") + } + } + func addItemToTransaction(transactionId: String, newItem: Item) { + let transactionRef = Firestore.firestore().collection("transactions").document(transactionId) + + transactionRef.getDocument { (document, error) in + if let document = document, document.exists { + var currentItems = document.get("items") as? [[String: Any]] ?? [] + let newItemDict = ["name": newItem.name, "priceInCents": newItem.priceInCents] + currentItems.append(newItemDict) + + transactionRef.updateData(["items": currentItems]) + } + } + } + func setItemPriceToZero(transactionId: String, itemIndex: Int) { + let transactionRef = Firestore.firestore().collection("transactions").document(transactionId) + + transactionRef.getDocument { (document, error) in + if let document = document, document.exists { + var currentItems = document.get("items") as? [[String: Any]] ?? [] + if itemIndex < currentItems.count { + // Set the item's price to 0 instead of removing it + currentItems[itemIndex]["priceInCents"] = 0 + transactionRef.updateData(["items": currentItems]) + } + } + } + } + func saveTransactionName(transactionId: String, newName: String) async { + let transactionRef = Firestore.firestore().collection("transactions").document(transactionId) + + do { + try await transactionRef.updateData(["name": newName]) + // Update local transaction data to reflect the new name + if var transaction = transactionData { + transaction.name = newName + transactionData = transaction + } + } catch { + print("Error updating transaction name: \(error)") + } + } } diff --git a/reciept bill splitter/ViewModels/DatabaseAPI.swift b/reciept bill splitter/ViewModels/DatabaseAPI.swift index 1a4d9e5..da8b0c0 100644 --- a/reciept bill splitter/ViewModels/DatabaseAPI.swift +++ b/reciept bill splitter/ViewModels/DatabaseAPI.swift @@ -59,6 +59,51 @@ class DatabaseAPI { return nil } + static func grabUserAssignedTransactions() async -> [AssignedTransaction]? { + guard let user = Auth.auth().currentUser else { + print("User Does not exist") + return nil + } + + let userRef = db.collection("users").document(user.uid) + + do { + let document = try await userRef.getDocument() + + var userAssignedTransactions: [AssignedTransaction] = [] + + if document.exists { + let data = document.data() + guard let data = data else { + return nil + } + let assignedTransactions = data["assignedTransaction"] as? [String : [String : Any]] ?? [:] + + // Iterate through each assigned transaction + for (transactionID, dataDict) in assignedTransactions { + if let amountToPay = dataDict["ammountToPay"] as? Double, // Note the spelling 'ammountToPay' matches your Firestore data + let isPaid = dataDict["isPaid"] as? Bool, + let transactionName = dataDict["transactionName"] as? String, + let user_idToPay = dataDict["user_idToPay"] as? String { + + // Using 'transactionID' from the iteration to set 'associatedTransaction_id' + let newAssignment = AssignedTransaction(transactionName: transactionName, associatedTransaction_id: transactionID, user_idToPay: user_idToPay, isPaid: isPaid, amountToPay: Int(amountToPay)) // Convert double to int representing cents + + userAssignedTransactions.append(newAssignment) + } else { + print("Missing data for assigned transaction") + } + } + + + return userAssignedTransactions + } + } catch { + print("Error Grabbing User Assigned Transactions: \(error)") + } + return nil + } + static func createGroup(groupName: String) async -> Void { guard let user = Auth.auth().currentUser else { print("User Does not exist") @@ -335,6 +380,128 @@ class DatabaseAPI { return nil } + static func markTransactionAsPaid(assignedTransactionId: String, completion: @escaping (Error?) -> Void) { + guard let user = Auth.auth().currentUser else { + print("User does not exist") + completion(NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "User not authenticated"])) + return + } + + // Reference to the user's document + let userRef = db.collection("users").document(user.uid) + + // Prepare the update for the specific assigned transaction + let updateField = "assignedTransaction.\(assignedTransactionId).isPaid" + + // Perform the update + userRef.updateData([updateField: true]) { error in + if let error = error { + print("Error marking transaction as paid: \(error.localizedDescription)") + } else { + print("Transaction marked as paid successfully.") + } + completion(error) + } + } + static func assignAllGroupMembersPayment(transaction_id: String) async -> Void { + guard let _ = Auth.auth().currentUser else { + print("User Does not exist") + return + } + + let transactionRef = db.collection("transactions").document(transaction_id) + // Add New AssignedTransaction for transaction in user + do { + let document = try await transactionRef.getDocument() + + guard let transactionData = document.data() else { + return + } + + let groupId = transactionData["group_id"] as? String ?? "" + let groupRef = db.collection("groups").document(groupId) + let groupDocument = try await groupRef.getDocument() + + guard let groupData = groupDocument.data() else { + return + } + + let groupMembers = groupData["members"] as? [String] ?? [] + + // Read document and work + do { + // Firestore Transaction to ensure both documents are written together or both fail + let _ = try await db.runTransaction({ (transaction, errorPointer) -> Any? in + // LOOP through transactions and create a new assigned transaction for each user + let itemBidders = transactionData["itemBidders"] as? [String:[String]] ?? [:] + let items = transactionData["items"] as? [[String : Any]] ?? [[:]] + + var newItemList: [Item] = [] + for item in items { + let newItem = Item(priceInCents: item["priceInCents"] as? Int ?? 0, name: item["name"] as? String ?? "Unknown Item") + newItemList.append(newItem) + } + + var groupMemberReferences: [DocumentReference] = [] + + for (index, groupMember) in groupMembers.enumerated() { + groupMemberReferences.append(db.collection("users").document(groupMember)) + let userDocument: DocumentSnapshot + + do { + try userDocument = transaction.getDocument(groupMemberReferences[index]) + } catch let fetchError as NSError { + errorPointer?.pointee = fetchError + return nil + } + } + + // An Abomination of Code + for (groupMemberIndex, groupMember) in groupMembers.enumerated() { + + var totalCostToPay: Float = 0 + // Check every item for user + for (index, item) in newItemList.enumerated() { + // Seach For Member in item bids + let currentIndex = String(index) + let currentItemBidders = itemBidders[currentIndex] ?? [] + + for userId in currentItemBidders { + if userId == groupMember { + // Add Total cost to pay for user + totalCostToPay += Float(item.priceInCents) / Float(currentItemBidders.count) + } + } + } + // After Adding cost for user for all items + // Create AssignedTransaction for User + var assignedTransactionDict: [String:Any] = [:] + assignedTransactionDict["transactionName"] = transactionData["name"] as? String ?? "" + assignedTransactionDict["associatedTransaction_id"] = transaction_id + assignedTransactionDict["user_idToPay"] = groupData["owner_id"] as? String ?? "" + assignedTransactionDict["isPaid"] = false + assignedTransactionDict["ammountToPay"] = totalCostToPay + + transaction.updateData(["assignedTransaction.\(transaction_id)": assignedTransactionDict], forDocument: groupMemberReferences[groupMemberIndex]) + } + print("FINISHED") + return nil + }) + + return + } catch { + // Handle error during transaction + print("Error Assigning Transaction: \(error)") + return + } + + } catch let error { + print("Error updating transaction: \(error)") + } + + + } + static func retrieveStripeCustomerId(uid: String, completion: @escaping (String?) -> Void) { let db = Firestore.firestore() @@ -413,56 +580,7 @@ class DatabaseAPI { } } - - static func assignAllGroupMembersPayment(transaction_id: String) async -> Void { - guard let _ = Auth.auth().currentUser else { - print("User Does not exist") - return - } - - let transactionRef = db.collection("transactions").document(transaction_id) - // Add New AssignedTransaction for transaction in user - do { - let document = try await transactionRef.getDocument() - - if !document.exists { - return - } - - // Read document and work - do { - // Firestore Transaction to ensure both documents are written together or both fail - let _ = try await db.runTransaction({ (transaction, errorPointer) -> Any? in - // LOOP through transactions and create a new assigned transaction for each user - // let gDoc: DocumentSnapshot - // do { - // try gDoc = transaction.getDocument(docRef) - // } catch let fetchError as NSError { - // errorPointer?.pointee = fetchError - // return nil - // } - // - // // Add user id to group members - // transaction.updateData(["members": FieldValue.arrayUnion([user.uid])], forDocument: gDoc.reference) - // // Add group id to user groups - // transaction.updateData(["groups": FieldValue.arrayUnion([gDoc.documentID])], forDocument: userRef) - return nil - }) - - return - } catch { - // Handle error during transaction - print("Error Assigning Transaction: \(error)") - return - } - - } catch let error { - print("Error updating transaction: \(error)") - } - - - } - + static func fetchUsernames(for documentIDs: [String], completion: @escaping (Result<[String], Error>) -> Void) { let userCollection = db.collection("users") @@ -553,5 +671,5 @@ class DatabaseAPI { } } - } + } diff --git a/reciept bill splitter/ViewModels/PaymentManager.swift b/reciept bill splitter/ViewModels/PaymentManager.swift index 089b209..7f663d2 100644 --- a/reciept bill splitter/ViewModels/PaymentManager.swift +++ b/reciept bill splitter/ViewModels/PaymentManager.swift @@ -2,6 +2,13 @@ import StripePaymentSheet import FirebaseFunctions import SwiftUI +import Foundation +import SwiftUI +import FirebaseAuth +import FirebaseFirestore +import FirebaseFirestoreSwift +import Firebase + class PaymentManager: ObservableObject { @Published var paymentResult: PaymentSheetResult? @Published var paymentSheet: PaymentSheet? @@ -115,7 +122,7 @@ class PaymentManager: ObservableObject { func createStripeAccountLink(stripeAccountID: String) { - + let functions = Functions.functions() functions.httpsCallable("createAccountLink").call(["accountId": stripeAccountID]) { result, error in if let error = error as NSError? { @@ -137,7 +144,7 @@ class PaymentManager: ObservableObject { }else {print("error creating link2")} } } - func transferMoney(amount: Int, destinationAccountId: String) { + func transferMoney(amount: Int, destinationAccountId: String, assignedTransactionId: String) { let functions = Functions.functions() functions.httpsCallable("createTransfer").call(["amount": amount, "destinationAccountId": destinationAccountId]) { result, error in if let error = error { @@ -146,9 +153,45 @@ class PaymentManager: ObservableObject { } if let transferId = (result?.data as? [String: Any])?["transferId"] as? String { print("Transfer successful, transferId: \(transferId)") + // Mark the transaction as paid + DatabaseAPI.markTransactionAsPaid(assignedTransactionId: assignedTransactionId) { error in + if let error = error { + print("Error marking transaction as paid: \(error.localizedDescription)") + } else { + print("Transaction successfully marked as paid") + // Here you can update any UI or state to reflect the payment status + } + } } else { print("Transfer failed") } } } + + func getStripeConnectAccountIdByEmail(email: String, completion: @escaping (String?, Error?) -> Void) { + let customersRef = Firestore.firestore().collection("customers") + customersRef.whereField("email", isEqualTo: email).getDocuments { (querySnapshot, error) in + if let error = error { + print("Error getting documents: \(error)") + completion(nil, error) + return + } + + guard let document = querySnapshot?.documents.first else { + print("No documents found") + completion(nil, NSError(domain: "FirestoreError", code: 404, userInfo: [NSLocalizedDescriptionKey: "Document not found"])) + return + } + + let data = document.data() + if let stripeConnectAccountId = data["stripeConnectAccountId"] as? String { + print("Found Stripe Connect Account ID: \(stripeConnectAccountId)") + completion(stripeConnectAccountId, nil) + } else { + print("Stripe Connect Account ID not found in document") + completion(nil, NSError(domain: "FirestoreError", code: 404, userInfo: [NSLocalizedDescriptionKey: "Stripe Connect Account ID not found"])) + } + } + } + } diff --git a/reciept bill splitter/ViewModels/UserViewModel.swift b/reciept bill splitter/ViewModels/UserViewModel.swift index e4dd006..b411bdb 100644 --- a/reciept bill splitter/ViewModels/UserViewModel.swift +++ b/reciept bill splitter/ViewModels/UserViewModel.swift @@ -109,7 +109,7 @@ class UserViewModel : ObservableObject { "friends": [], // reference document id of other users uid "groups": [], // group collection document ids - "assignedTransaction": [] + "assignedTransaction": [:] ]) print("Document created")