Skip to content
Merged
4 changes: 2 additions & 2 deletions PayForMe/Model/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ enum ProjectBackend: Int, Codable {
var staticPath: String {
switch self {
case .cospend:
return "/index.php/apps/cospend/api/projects"
return "index.php/apps/cospend/api/projects"
case .iHateMoney:
return "/api/projects"
return "api/projects"
}
}
}
Expand Down
36 changes: 33 additions & 3 deletions PayForMe/Services/ProjectManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ProjectManager: ObservableObject {
private let defaults = UserDefaults.standard

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

@Published
private(set) var projects = [Project]()
Expand Down Expand Up @@ -54,20 +55,42 @@ class ProjectManager: ObservableObject {

// MARK: Server Communication

func loadBillsAndMembers() {
func refresh() async {
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
loadBillsAndMembers {
continuation.resume()
}
}
}

func loadBillsAndMembers(completion: (() -> Void)? = nil) {
let project = currentProject

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

Publishers.Zip(billsPublisher, membersPublisher)
var completionInvoked = false
let invokeCompletionOnce: () -> Void = {
guard !completionInvoked else { return }
completionInvoked = true
completion?()
}

loadCancellable = Publishers.Zip(billsPublisher, membersPublisher)
.map { bills, members in
project.bills = bills
project.members = members
return project
}
.receive(on: DispatchQueue.main)
.assign(to: &$currentProject)
.handleEvents(receiveCancel: invokeCompletionOnce)
.sink(
receiveCompletion: { _ in invokeCompletionOnce() },
receiveValue: { [weak self] project in
self?.currentProject = project
invokeCompletionOnce()
}
)
}

private func sendBillToServer(bill: Bill, update: Bool, completion: @escaping () -> Void) {
Expand All @@ -76,6 +99,7 @@ class ProjectManager: ObservableObject {

if update {
cancellable = NetworkService.shared.updateBillPublisher(bill: bill)
.receive(on: DispatchQueue.main)
.sink { success in
if success {
print("Bill id\(bill.id) updated")
Expand All @@ -86,6 +110,7 @@ class ProjectManager: ObservableObject {
}
} else {
cancellable = NetworkService.shared.postBillPublisher(bill: bill)
.receive(on: DispatchQueue.main)
.sink { success in
if success {
print("Bill posted")
Expand All @@ -102,6 +127,7 @@ class ProjectManager: ObservableObject {
cancellable = nil

cancellable = NetworkService.shared.deleteBillPublisher(bill: bill)
.receive(on: DispatchQueue.main)
.sink { success in
if success {
print("Bill successfully deleted")
Expand All @@ -118,6 +144,7 @@ class ProjectManager: ObservableObject {

if update {
cancellable = NetworkService.shared.updateMemberPublisher(member: member)
.receive(on: DispatchQueue.main)
.sink { success in
if success {
print("Member id\(member.id) updated")
Expand All @@ -128,6 +155,7 @@ class ProjectManager: ObservableObject {
}
} else {
cancellable = NetworkService.shared.createMemberPublisher(name: member.name)
.receive(on: DispatchQueue.main)
.sink { success in
if success {
print("Member successfully created")
Expand All @@ -144,6 +172,7 @@ class ProjectManager: ObservableObject {
cancellable = nil

cancellable = NetworkService.shared.deleteMemberPublisher(member: member)
.receive(on: DispatchQueue.main)
.sink { success in
if success {
print("Member id\(member.id) successfully deleted")
Expand Down Expand Up @@ -235,6 +264,7 @@ extension ProjectManager {
}) else {
return
}
loadCancellable?.cancel()
currentProject = project
loadBillsAndMembers()
defaults.set(project.id, forKey: "projectID")
Expand Down
9 changes: 4 additions & 5 deletions PayForMe/Views/Balance/BalanceList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ struct BalanceList: View {
NavigationView {
mainView
.navigationBarTitle("Members")
.onAppear {
ProjectManager.shared.loadBillsAndMembers()
}
}.navigationViewStyle(StackNavigationViewStyle())
}

Expand Down Expand Up @@ -52,6 +49,9 @@ struct BalanceList: View {
}
}
}
.refreshable {
await ProjectManager.shared.refresh()
}
}

func balanceSort(_ a: Balance, _ b: Balance) -> Bool {
Expand Down Expand Up @@ -94,8 +94,7 @@ struct BalanceList_Previews: PreviewProvider {
}

struct BalanceCell: View {
@State
var balance: Balance
let balance: Balance

var body: some View {
HStack {
Expand Down
3 changes: 1 addition & 2 deletions PayForMe/Views/BillDetail/WhoPaidView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import SwiftUI

struct WhoPaidView: View {
@State
var members: [Person]
let members: [Person]

@Binding
var selectedPayer: Int
Expand Down
5 changes: 2 additions & 3 deletions PayForMe/Views/BillList/BillCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ struct BillCell: View {
@ObservedObject
var viewModel: BillListViewModel

@State
var bill: Bill
let bill: Bill

var body: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 10) {
Text(bill.what).font(.headline)
PersonsView(bill: $bill, members: $viewModel.currentProject.members)
PersonsView(bill: bill, members: viewModel.currentProject.members)
}
Spacer()
VStack(alignment: .trailing, spacing: 10) {
Expand Down
7 changes: 3 additions & 4 deletions PayForMe/Views/BillList/BillList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ struct BillList: View {
iOS15ListContent
}
.addFloatingAddButton()
.id(viewModel.currentProject.bills)
.refreshable {
await ProjectManager.shared.refresh()
}
.navigationBarTitle("Bills")
.alert(item: $deleteAlert) { index in
Alert(title: Text("Delete Bill"),
Expand All @@ -32,9 +34,6 @@ struct BillList: View {
}
.listStyle(InsetGroupedListStyle())
}
.onAppear {
ProjectManager.shared.loadBillsAndMembers()
}
}

@ViewBuilder
Expand Down
6 changes: 3 additions & 3 deletions PayForMe/Views/BillList/BillListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ class BillListViewModel: ObservableObject {
init() {
currentProject = manager.currentProject
cancellable = currentProjectChanged
$sortBy
.map {
$0.sort(bills: self.currentProject.bills)
Publishers.CombineLatest($sortBy, $currentProject)
.map { sortBy, project in
sortBy.sort(bills: project.bills)
}
.assign(to: &$sortedBills)
}
Expand Down
7 changes: 2 additions & 5 deletions PayForMe/Views/BillList/PersonsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
import SwiftUI

struct PersonsView: View {
@Binding
var bill: Bill

@Binding
var members: [Int: Person]
let bill: Bill
let members: [Int: Person]

var body: some View {
HStack(spacing: 5) {
Expand Down
18 changes: 16 additions & 2 deletions PayForMe/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ struct ContentView: View {
@ObservedObject
var manager = ProjectManager.shared

@StateObject
private var billListViewModel = BillListViewModel()

@StateObject
private var balanceViewModel = BalanceViewModel()

@Environment(\.scenePhase)
private var scenePhase

@State
var tabBarIndex = tabBarItems.BillList

Expand All @@ -32,16 +41,21 @@ struct ContentView: View {
.sheet(item: $manager.openedByURL) { url in
AddFromURLView(viewmodel: AddProjectQRViewModel(openedByURL: url))
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .active && !manager.projects.isEmpty {
manager.loadBillsAndMembers()
}
}
}

var tabBar: some View {
TabView(selection: $tabBarIndex) {
BillList(viewModel: BillListViewModel())
BillList(viewModel: billListViewModel)
.tabItem {
Image(systemName: "rectangle.stack")
}
.tag(tabBarItems.BillList)
BalanceList(viewModel: BalanceViewModel())
BalanceList(viewModel: balanceViewModel)
.tabItem {
Image(systemName: "arrow.right.arrow.left")
}
Expand Down
3 changes: 1 addition & 2 deletions PayForMe/Views/PersonText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import SwiftUI

struct PersonText: View {
@State
var person: Person
let person: Person

var body: some View {
Text(person.name)
Expand Down
9 changes: 7 additions & 2 deletions PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ class AddProjectQRViewModel: ObservableObject {

init() {
foundCodeSink.store(in: &subscriptions)
passwordCorrect.assign(to: &$isProject)
isTestingSubject.assign(to: &$isProject)
passwordCorrect
.receive(on: DispatchQueue.main)
.assign(to: &$isProject)
isTestingSubject
.receive(on: DispatchQueue.main)
.assign(to: &$isProject)
}

convenience init(openedByURL: URL?) {
Expand Down Expand Up @@ -90,6 +94,7 @@ class AddProjectQRViewModel: ObservableObject {

var foundCodeSink: AnyCancellable {
foundCode
.receive(on: DispatchQueue.main)
.sink { codedUrl in
let projectData = codedUrl.decodeQRCode()
guard let url = projectData.server, let token = projectData.project else { return }
Expand Down
18 changes: 18 additions & 0 deletions PayForMeTests/NetworkRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ class NetworkRequestTests: XCTestCase {
"Cospend must embed token and password. Got: \(url)")
}

func testCospend_loadBills_trailingSlashInServerURL_doesNotProduceDoubleSlash() {
let project = Project.makeCospend(token: "tok", password: "pass",
url: "https://cloud.example.com/")
MockURLProtocol.requestHandler = jsonHandler()

let exp = expectation(description: "request intercepted")
NetworkService.shared.loadBillsPublisher(project)
.sink { _ in exp.fulfill() }
.store(in: &subscriptions)
waitForExpectations(timeout: 2)

let url = MockURLProtocol.lastCapturedRequest?.url?.absoluteString ?? ""
XCTAssertFalse(url.contains("com//"),
"Server URL with trailing slash must not produce a double slash. Got: \(url)")
XCTAssertTrue(url.contains("/index.php/apps/cospend/api/projects/tok/pass/bills"),
"Cospend path must remain correct with trailing-slash server URL. Got: \(url)")
}

// MARK: - Cospend: no auth header

func testCospend_loadBills_noAuthorizationHeader() {
Expand Down
Loading