Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions PayForMe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
48E7F75B2403FD6B000CE4E6 /* FancyLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E7F75A2403FD6B000CE4E6 /* FancyLoadingButton.swift */; };
56E7DB46FDCDC5B46C475020 /* BalanceCalculationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143F41EE78329905894C8A29 /* BalanceCalculationTests.swift */; };
57BC8D25B91CE40F4669650C /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0865D2DAF541B24078D1DC41 /* TestHelpers.swift */; };
62475CF1301F718C0097B7C9 /* CospendTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62475CF0301F718C0097B7C9 /* CospendTag.swift */; };
62A13D412FA3AB760036EE34 /* ShareProjectQRCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A13D402FA3AB700036EE34 /* ShareProjectQRCodeViewModel.swift */; };
62C73597878BA5AD4572FE90 /* BillSortingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92ABB02BEA28BEA7C8C64841 /* BillSortingTests.swift */; };
650059FC23DDE1C300D1D599 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650059FB23DDE1C300D1D599 /* Person.swift */; };
Expand Down Expand Up @@ -60,14 +61,15 @@
6591C8E524052C240054BA24 /* CommunicationIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6591C8E424052C240054BA24 /* CommunicationIndicator.swift */; };
6591C8E7240542250054BA24 /* LoadingDots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6591C8E6240542250054BA24 /* LoadingDots.swift */; };
65A65E3F2527529D00C86533 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A65E3E2527529D00C86533 /* Combine.swift */; };
65AF00022F1A000100000002 /* StorageMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AF00012F1A000100000001 /* StorageMigrationTests.swift */; };
65AF00042F1A000100000004 /* BillDetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AF00032F1A000100000003 /* BillDetailViewModelTests.swift */; };
65C9E2CE23E1B8F500814B37 /* BalanceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C9E2CD23E1B8F500814B37 /* BalanceList.swift */; };
65C9E2D023E1B96B00814B37 /* BalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C9E2CF23E1B96B00814B37 /* BalanceViewModel.swift */; };
65D225F32428038500C3F1EC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 65D225F52428038500C3F1EC /* Localizable.strings */; };
65D9094025581BE100351F4B /* AddFromURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D9093F25581BE100351F4B /* AddFromURLView.swift */; };
BF4D7AF265FC4A90B1AC5AEB /* JSONDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F4D55C3EF5EFCD925428041 /* JSONDecodingTests.swift */; };
D279295CFA25D9CC69C6F27C /* BillTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EED3194A2342518B0E74079 /* BillTests.swift */; };
DBBD86EF399BCF232C021935 /* NetworkRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316FBA7C0CE4ABB51EC2A121 /* NetworkRequestTests.swift */; };
65AF00022F1A000100000002 /* StorageMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AF00012F1A000100000001 /* StorageMigrationTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -98,6 +100,7 @@
48CC8F7323F5C16D00DCE3D8 /* AddBillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBillView.swift; sourceTree = "<group>"; };
48CF709A240FF82000770B97 /* AddMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMemberView.swift; sourceTree = "<group>"; };
48E7F75A2403FD6B000CE4E6 /* FancyLoadingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyLoadingButton.swift; sourceTree = "<group>"; };
62475CF0301F718C0097B7C9 /* CospendTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CospendTag.swift; sourceTree = "<group>"; };
62A13D402FA3AB700036EE34 /* ShareProjectQRCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProjectQRCodeViewModel.swift; sourceTree = "<group>"; };
650059FB23DDE1C300D1D599 /* Person.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = "<group>"; };
65005A0123DDFB1A00D1D599 /* WhoPaidView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhoPaidView.swift; sourceTree = "<group>"; };
Expand All @@ -106,7 +109,6 @@
6514934B26CFC71000F00851 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/LaunchScreen.strings; sourceTree = "<group>"; };
6514934C26CFC71000F00851 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
6523FC4B25580EEF00BCD843 /* UrlExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlExtensionsTests.swift; sourceTree = "<group>"; };
65AF00012F1A000100000001 /* StorageMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMigrationTests.swift; sourceTree = "<group>"; };
6523FC502558153600BCD843 /* AddPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPasswordView.swift; sourceTree = "<group>"; };
6533A02A23D888B100FF822B /* BillCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillCell.swift; sourceTree = "<group>"; };
654754B72528736F00A82EB6 /* Views+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Views+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -148,6 +150,8 @@
6591C8E6240542250054BA24 /* LoadingDots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoadingDots.swift; path = PayForMe/Views/LoadingDots.swift; sourceTree = SOURCE_ROOT; };
65A65E3E2527529D00C86533 /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = "<group>"; };
65AA00A6252C881E00C5DEE8 /* PayForMe.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PayForMe.entitlements; sourceTree = "<group>"; };
65AF00012F1A000100000001 /* StorageMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMigrationTests.swift; sourceTree = "<group>"; };
65AF00032F1A000100000003 /* BillDetailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillDetailViewModelTests.swift; sourceTree = "<group>"; };
65B848F9243337DC0096F51C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
65C9E2CD23E1B8F500814B37 /* BalanceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceList.swift; sourceTree = "<group>"; };
65C9E2CF23E1B96B00814B37 /* BalanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -237,6 +241,7 @@
92ABB02BEA28BEA7C8C64841 /* BillSortingTests.swift */,
316FBA7C0CE4ABB51EC2A121 /* NetworkRequestTests.swift */,
65AF00012F1A000100000001 /* StorageMigrationTests.swift */,
65AF00032F1A000100000003 /* BillDetailViewModelTests.swift */,
);
path = PayForMeTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -294,6 +299,7 @@
children = (
656E8CB523D9C88A00B3ED10 /* Project.swift */,
65662A1823D7322B00303207 /* Bill.swift */,
62475CF0301F718C0097B7C9 /* CospendTag.swift */,
65662A1C23D7353400303207 /* Server.swift */,
650059FB23DDE1C300D1D599 /* Person.swift */,
);
Expand Down Expand Up @@ -552,6 +558,7 @@
62C73597878BA5AD4572FE90 /* BillSortingTests.swift in Sources */,
DBBD86EF399BCF232C021935 /* NetworkRequestTests.swift in Sources */,
65AF00022F1A000100000002 /* StorageMigrationTests.swift in Sources */,
65AF00042F1A000100000004 /* BillDetailViewModelTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -578,6 +585,7 @@
658FC55925273E4D004A49AA /* AddProjectQRViewModel.swift in Sources */,
489D35FA23E081120029F59F /* Util.swift in Sources */,
65A65E3F2527529D00C86533 /* Combine.swift in Sources */,
62475CF1301F718C0097B7C9 /* CospendTag.swift in Sources */,
489995DA23F6EC6F008B7E38 /* OnboardingView.swift in Sources */,
6591C8E524052C240054BA24 /* CommunicationIndicator.swift in Sources */,
65662A0523D7320000303207 /* SceneDelegate.swift in Sources */,
Expand Down
5 changes: 4 additions & 1 deletion PayForMe/Model/Bill.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct Bill: Codable, Identifiable, Hashable {
var payer_id: Int
var owers: [Person]
var `repeat`: String?
var categoryid: Int?
var paymentmodeid: Int?
var lastchanged: Int?

func paramsFor(_ backend: ProjectBackend) -> [String: Any] {
Expand All @@ -26,8 +28,9 @@ struct Bill: Codable, Identifiable, Hashable {
]
if backend == .cospend {
dict["payed_for"] = owers.map { $0.id.description }.joined(separator: ",")
dict["categoryid"] = (categoryid ?? 0).description
dict["paymentmodeid"] = (paymentmodeid ?? 0).description
dict["paymentmode"] = "n"
dict["categoryid"] = "0"

if let rep = self.repeat {
dict["repeat"] = rep
Expand Down
75 changes: 75 additions & 0 deletions PayForMe/Model/CospendTag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// CospendTag.swift
// PayForMe
//

import Foundation

struct CospendTag: Codable, Identifiable, Hashable {
let id: Int
let name: String?
let color: String?
let icon: String?
let order: Int?

var displayName: String {
guard let name = name, !name.isEmpty else { return "—" }
return name
}

var label: String {
guard let icon = icon, !icon.isEmpty else { return displayName }
return "\(icon) \(displayName)"
}
}

struct CospendProjectTags: Decodable {
let categories: [CospendTag]
let paymentModes: [CospendTag]

static let empty = CospendProjectTags(categories: [], paymentModes: [])

init(categories: [CospendTag], paymentModes: [CospendTag]) {
self.categories = categories
self.paymentModes = paymentModes
}

private enum CodingKeys: String, CodingKey {
case categories
case paymentmodes
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
categories = Self.decodeList(from: container, forKey: .categories)
paymentModes = Self.decodeList(from: container, forKey: .paymentmodes)
}

private static func decodeList(from container: KeyedDecodingContainer<CodingKeys>, forKey key: CodingKeys) -> [CospendTag] {
if let keyed = try? container.decode([String: CospendTag].self, forKey: key) {
return sorted(Array(keyed.values))
}
if let list = try? container.decode([CospendTag].self, forKey: key) {
return sorted(list)
}
return []
}

private static func sorted(_ tags: [CospendTag]) -> [CospendTag] {
tags.sorted { lhs, rhs in
let lhsOrder = lhs.order ?? Int.max
let rhsOrder = rhs.order ?? Int.max
guard lhsOrder == rhsOrder else { return lhsOrder < rhsOrder }
return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending
}
}
}

let previewCategories = [
CospendTag(id: 122, name: "Grocery", color: "#ffaa00", icon: "🛒", order: 0),
CospendTag(id: 126, name: "Excursion/Culture", color: "#0055ff", icon: "🚸", order: 0),
]
let previewPaymentModes = [
CospendTag(id: 37, name: "Cash", color: "#556B2F", icon: "💵", order: 0),
CospendTag(id: 40, name: "Online service", color: "#9932CC", icon: "🌎", order: 0),
]
45 changes: 41 additions & 4 deletions PayForMe/Model/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class Project: Codable, Identifiable {

var members: [Int: Person]
var bills: [Bill]
var categories: [CospendTag]
var paymentModes: [CospendTag]
var me: Int?

let projectId: String
Expand All @@ -25,15 +27,17 @@ class Project: Codable, Identifiable {
self.init(name: name, password: password, token: token, backend: backend, url: url, id: nil, projectId: projectId)
}

fileprivate init(name: String, password: String, token: String, backend: ProjectBackend, url: URL, id: Int?, me: Int? = nil, projectId: String) {
fileprivate init(name: String, password: String, token: String, backend: ProjectBackend, url: URL, id: Int?, me: Int? = nil, projectId: String, categories: [CospendTag] = [], paymentModes: [CospendTag] = []) {
self.name = name
self.password = password
self.token = token
self.backend = backend
self.url = url
self.id = id
members = [:]
bills = []
self.members = [:]
self.bills = []
self.categories = categories
self.paymentModes = paymentModes
self.me = me
self.projectId = projectId
}
Expand Down Expand Up @@ -81,6 +85,29 @@ struct StoredProject: Codable {
}
}

extension Project {
func category(for bill: Bill) -> CospendTag? {
tag(id: bill.categoryid, in: categories)
}

func paymentMode(for bill: Bill) -> CospendTag? {
tag(id: bill.paymentmodeid, in: paymentModes)
}

private func tag(id: Int?, in tags: [CospendTag]) -> CospendTag? {
guard let id = id, id != 0 else { return nil }
return tags.first { $0.id == id }
}

func listedCategoryId(_ id: Int) -> Int { listedId(id, in: categories) }

func listedPaymentModeId(_ id: Int) -> Int { listedId(id, in: paymentModes) }

private func listedId(_ id: Int, in tags: [CospendTag]) -> Int {
tags.contains { $0.id == id } ? id : 0
}
}

extension Project: Equatable {
static func == (lhs: Project, rhs: Project) -> Bool {
return lhs.url == rhs.url && lhs.name == rhs.name && lhs.backend == rhs.backend && lhs.password == rhs.password
Expand All @@ -107,7 +134,17 @@ enum ProjectBackend: Int, Codable {
}
}

let previewProject = Project(name: "TestProject", password: "TestPassword", token: "asdasdas", backend: .cospend, url: URL(string: "https://testserver.de")!, id: 0, projectId: "TestProject")
let previewProject = Project(
name: "TestProject",
password: "TestPassword",
token: "asdasdas",
backend: .cospend,
url: URL(string: "https://testserver.de")!,
id: 0,
projectId: "TestProject",
categories: [CospendTag(id: 0, name: "Food", color: "FFFEEE", icon: "", order: 0)],
paymentModes: [CospendTag(id: 0, name: "Cash", color: "010010", icon: "", order: 0)]
)
let previewProjects = [
previewProject,
Project(name: "test1", password: "test23", token: "dasdasa", backend: .cospend, url: URL(string: "https://testserver.de")!, id: 1, projectId: "test1"),
Expand Down
24 changes: 24 additions & 0 deletions PayForMe/Services/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ class NetworkService {
.eraseToAnyPublisher()
}


func loadTagsPublisher(_ project: Project) -> AnyPublisher<CospendProjectTags, Never> {
guard project.backend == .cospend else {
return Just(CospendProjectTags.empty).eraseToAnyPublisher()
}
let request = buildURLRequest("", params: [:], project: project)
return URLSession.shared.dataTaskPublisher(for: request)
.compactMap { data, response -> Data? in
guard let httpResponse = response as? HTTPURLResponse else {
print("Network Error")
return nil
}
guard httpResponse.statusCode == 200 else {
print("Network Error: Status code: \(httpResponse.statusCode) \(httpResponse.description)")
return nil
}
return data
}
.decode(type: CospendProjectTags.self, decoder: decoder)
.replaceError(with: .empty)
.replaceEmpty(with: .empty)
.eraseToAnyPublisher()
}

func testProject(_ project: Project) -> AnyPublisher<(Project, Int), Never> {
let request = buildURLRequest("dummy", params: [:], project: project)
let requestPub = URLSession.shared.dataTaskPublisher(for: request)
Expand Down
19 changes: 19 additions & 0 deletions PayForMe/Services/ProjectManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ProjectManager: ObservableObject {

private var cancellable: Cancellable?
private var loadCancellable: AnyCancellable?
private var tagsCancellable: AnyCancellable?

@Published
private(set) var projects = [Project]()
Expand Down Expand Up @@ -61,6 +62,8 @@ class ProjectManager: ObservableObject {
func loadBillsAndMembers(completion: (() -> Void)? = nil) {
let project = currentProject

loadTags(for: project)

let billsPublisher = NetworkService.shared.loadBillsPublisher(project)
let membersPublisher = NetworkService.shared.loadMembersPublisher(project)

Expand Down Expand Up @@ -88,6 +91,21 @@ class ProjectManager: ObservableObject {
)
}


private func loadTags(for project: Project) {
tagsCancellable = NetworkService.shared.loadTagsPublisher(project)
.receive(on: DispatchQueue.main)
.sink { [weak self] tags in
project.categories = tags.categories
project.paymentModes = tags.paymentModes
// `Project` is a class, so the assignments above are already visible everywhere —
// but only re-publishing makes SwiftUI redraw. Compared by identity, because
// `Project: Equatable` ignores the id and would accept a different project.
guard let self = self, self.currentProject === project else { return }
self.currentProject = project
}
}

private func sendBillToServer(bill: Bill, update: Bool, completion: @escaping () -> Void) {
cancellable?.cancel()
cancellable = nil
Expand Down Expand Up @@ -274,6 +292,7 @@ extension ProjectManager {
return
}
loadCancellable?.cancel()
tagsCancellable?.cancel()
currentProject = project
loadBillsAndMembers()
defaults.set(project.id, forKey: "projectID")
Expand Down
12 changes: 12 additions & 0 deletions PayForMe/Strings/cs.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"Bills" = "Účty";

/* */
"Category" = "Kategorie";


"Changed date" = "Změněné datum";


Expand Down Expand Up @@ -81,6 +84,12 @@
"Members" = "Členové";


"No category" = "Bez kategorie";


"No payment mode" = "Bez způsobu platby";


"None" = "Žádní";


Expand All @@ -92,6 +101,9 @@
/* BillDetail.swift */
"Payer" = "Plátce";


"Payment mode" = "Způsob platby";

/* ProjectQRPermissionCheckerView.swift */
"Please allow us to use the camera in order to scan the Cospend QR code" = "Pokud chcete naskenovat QR kód z Cospend, povolte použití kamery zařízení";

Expand Down
Loading
Loading