Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IOS-995 Show exit constraints in connect view #7495

Merged
merged 1 commit into from
Jan 24, 2025
Merged
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
5 changes: 4 additions & 1 deletion ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ extension RelaySelector {
filter: RelayConstraint<RelayFilter>,
in relaysResponse: REST.ServerRelaysResponse
) -> REST.BridgeRelay? {
let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
let mappedBridges = RelayWithLocation.locateRelays(
relays: relaysResponse.bridge.relays,
locations: relaysResponse.locations
)
let filteredRelays = (try? applyConstraints(
location,
filterConstraint: filter,
Expand Down
5 changes: 4 additions & 1 deletion ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ extension RelaySelector {
filterConstraint: RelayConstraint<RelayFilter>,
daitaEnabled: Bool
) throws -> [RelayWithLocation<REST.ServerRelay>] {
let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)
let mappedRelays = RelayWithLocation.locateRelays(
relays: relays.wireguard.relays,
locations: relays.locations
)

return try applyConstraints(
relayConstraint,
Expand Down
10 changes: 0 additions & 10 deletions ios/MullvadREST/Relay/RelaySelector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,6 @@ public enum RelaySelector {
return randomRelay
}

static func mapRelays<T: AnyRelay>(
relays: [T],
locations: [String: REST.ServerLocation]
) -> [RelayWithLocation<T>] {
relays.compactMap { relay in
guard let serverLocation = locations[relay.location] else { return nil }
return makeRelayWithLocationFrom(serverLocation, relay: relay)
}
}

/// Produce a list of `RelayWithLocation` items satisfying the given constraints
static func applyConstraints<T: AnyRelay>(
_ relayConstraint: RelayConstraint<UserSelectedRelays>,
Expand Down
34 changes: 33 additions & 1 deletion ios/MullvadREST/Relay/RelayWithLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public struct RelayWithLocation<T: AnyRelay> {
let relay: T
public let serverLocation: Location

func matches(location: RelayLocation) -> Bool {
public func matches(location: RelayLocation) -> Bool {
return switch location {
case let .country(countryCode):
serverLocation.countryCode == countryCode
Expand All @@ -28,6 +28,38 @@ public struct RelayWithLocation<T: AnyRelay> {
relay.hostname == hostname
}
}

init(relay: T, serverLocation: Location) {
self.relay = relay
self.serverLocation = serverLocation
}

init?(_ relay: T, locations: [String: REST.ServerLocation]) {
let locationComponents = relay.location.split(separator: "-")
guard
locationComponents.count > 1,
let serverLocation = locations[relay.location]
else { return nil }

self.relay = relay
self.serverLocation = Location(
country: serverLocation.country,
countryCode: String(locationComponents[0]),
city: serverLocation.city,
cityCode: String(locationComponents[1]),
latitude: serverLocation.latitude,
longitude: serverLocation.longitude
)
}

/// given a list of `AnyRelay` values and a name to location mapping, produce a list of
/// `RelayWithLocation`values for those whose locations have successfully been found.
public static func locateRelays(
relays: [T],
locations: [String: REST.ServerLocation]
) -> [RelayWithLocation<T>] {
relays.compactMap { RelayWithLocation($0, locations: locations) }
}
}

extension RelayWithLocation: Equatable {
Expand Down
10 changes: 10 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; };
44DD7D2D2B74E44A0005F67F /* QuantumResistanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */; };
44DF8AC42BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */; };
44E1F7582D3EA83A003A60FF /* DestinationDescriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E1F7572D3EA82C003A60FF /* DestinationDescriber.swift */; };
44E1F75A2D3FDCCA003A60FF /* DestinationDescriberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E1F7592D3FDCBA003A60FF /* DestinationDescriberTests.swift */; };
44E1F75B2D3FEC81003A60FF /* DestinationDescriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E1F7572D3EA82C003A60FF /* DestinationDescriber.swift */; };
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; };
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; };
5807E2C02432038B00F5FF30 /* String+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Helpers.swift */; };
Expand Down Expand Up @@ -1483,6 +1486,8 @@
44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; };
44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantumResistanceSettings.swift; sourceTree = "<group>"; };
44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+PostQuantum.swift"; sourceTree = "<group>"; };
44E1F7572D3EA82C003A60FF /* DestinationDescriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationDescriber.swift; sourceTree = "<group>"; };
44E1F7592D3FDCBA003A60FF /* DestinationDescriberTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationDescriberTests.swift; sourceTree = "<group>"; };
5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteProtocol.swift; sourceTree = "<group>"; };
5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2664,6 +2669,7 @@
440E9EFD2BDA982A00B1FD11 /* Tunnel */ = {
isa = PBXGroup;
children = (
44E1F7592D3FDCBA003A60FF /* DestinationDescriberTests.swift */,
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */,
);
path = Tunnel;
Expand Down Expand Up @@ -2712,6 +2718,7 @@
4419AA862D28264D001B13C9 /* ConnectionView */ = {
isa = PBXGroup;
children = (
44E1F7572D3EA82C003A60FF /* DestinationDescriber.swift */,
F0ADF1CF2D01B50B00299F09 /* ChipView */,
7AA130972CFF364F00640DF9 /* FeatureIndicators */,
449E9A6E2D283C7400F8574A /* ButtonPanel.swift */,
Expand Down Expand Up @@ -5623,6 +5630,7 @@
F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */,
A9A5F9F12ACB05160083449F /* String+Helpers.swift in Sources */,
A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */,
44E1F75B2D3FEC81003A60FF /* DestinationDescriber.swift in Sources */,
A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */,
A9A5F9F52ACB05160083449F /* NewDeviceNotificationProvider.swift in Sources */,
F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */,
Expand Down Expand Up @@ -5730,6 +5738,7 @@
F0ACE3362BE517D6006D5333 /* ServerRelaysResponse+Stubs.swift in Sources */,
7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */,
A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */,
44E1F75A2D3FDCCA003A60FF /* DestinationDescriberTests.swift in Sources */,
A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */,
A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */,
A9BD4D552CA58C3700C8A0E6 /* RESTTransportStub.swift in Sources */,
Expand Down Expand Up @@ -6229,6 +6238,7 @@
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
F09D04B32AE919AC003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
7A5869BF2B57D0A100640D27 /* IPOverrideStatus.swift in Sources */,
44E1F7582D3EA83A003A60FF /* DestinationDescriber.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
7AF10EB22ADE859200C090B9 /* AlertViewController.swift in Sources */,
587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import MullvadMockData
import MullvadREST
import MullvadSettings
import MullvadTypes
import PacketTunnelCore
Expand Down Expand Up @@ -37,7 +38,10 @@ struct ConnectionViewComponentPreview<Content: View>: View {
isDaitaEnabled: true
)),
state: .connected(SelectedRelaysStub.selectedRelays, isPostQuantum: true, isDaita: true)
)
),
relayConstraints: RelayConstraints(),
relayCache: RelayCache(cacheDirectory: ApplicationConfiguration.containerURL),
customListRepository: CustomListRepository()
)

var content: (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
//

import Combine
import MullvadREST
import MullvadSettings
import MullvadTypes
import SwiftUI

class ConnectionViewViewModel: ObservableObject {
Expand All @@ -26,6 +29,17 @@ class ConnectionViewViewModel: ObservableObject {

@Published private(set) var tunnelStatus: TunnelStatus
@Published var outgoingConnectionInfo: OutgoingConnectionInfo?
@Published var showsActivityIndicator = false

@Published var relayConstraints: RelayConstraints
let destinationDescriber: DestinationDescribing

var combinedState: Publishers.CombineLatest<
Published<TunnelStatus>.Publisher,
Published<Bool>.Publisher
> {
$tunnelStatus.combineLatest($showsActivityIndicator)
}

var tunnelIsConnected: Bool {
if case .connected = tunnelStatus.state {
Expand All @@ -35,8 +49,25 @@ class ConnectionViewViewModel: ObservableObject {
}
}

init(tunnelStatus: TunnelStatus) {
var connectionName: String? {
if case let .only(loc) = relayConstraints.exitLocations {
return destinationDescriber.describe(loc)
}
return nil
}

init(
tunnelStatus: TunnelStatus,
relayConstraints: RelayConstraints,
relayCache: RelayCacheProtocol,
customListRepository: CustomListRepositoryProtocol
) {
self.tunnelStatus = tunnelStatus
self.relayConstraints = relayConstraints
self.destinationDescriber = DestinationDescriber(
relayCache: relayCache,
customListRepository: customListRepository
)
}

func update(tunnelStatus: TunnelStatus) {
Expand Down Expand Up @@ -131,7 +162,7 @@ extension ConnectionViewViewModel {
var localizedTitleForSelectLocationButton: LocalizedStringKey {
switch tunnelStatus.state {
case .disconnecting, .pendingReconnect, .disconnected, .waitingForConnectivity(.noNetwork):
LocalizedStringKey("Select location")
LocalizedStringKey(connectionName ?? "Select location")
case .connecting, .connected, .reconnecting, .waitingForConnectivity(.noConnection),
.negotiatingEphemeralPeer, .error:
LocalizedStringKey("Switch location")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// DestinationDescriber.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2025-01-20.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
// A source of truth for converting an exit relay destination (i.e., a relay or list) into a name

import MullvadREST
import MullvadSettings
import MullvadTypes

protocol DestinationDescribing {
func describe(_ destination: UserSelectedRelays) -> String?
}

struct DestinationDescriber: DestinationDescribing {
let relayCache: RelayCacheProtocol
let customListRepository: CustomListRepositoryProtocol

public init(
relayCache: RelayCacheProtocol,
customListRepository: CustomListRepositoryProtocol
) {
self.relayCache = relayCache
self.customListRepository = customListRepository
}

private func customListDescription(_ destination: UserSelectedRelays) -> String? {
// We only return a description for the list if the user has selected the
// entire list. If they have only selected relays/locations from it,
// we show those as if they selected them from elsewhere.
guard
let customListSelection = destination.customListSelection,
customListSelection.isList,
let customList = customListRepository.fetch(by: customListSelection.listId)
else { return nil }
return customList.name
}

private func describeRelayLocation(
_ locationSpec: RelayLocation,
usingRelayWithLocation serverLocation: Location
) -> String {
switch locationSpec {
case .country: serverLocation.country
case .city: serverLocation.city
case let .hostname(_, _, hostname):
"\(serverLocation.city) (\(hostname))"
}
}

private func relayDescription(_ destination: UserSelectedRelays) -> String? {
guard
let location = destination.locations.first,
let cachedRelays = try? relayCache.read().relays
else { return nil }
let locatedRelays = RelayWithLocation.locateRelays(
relays: cachedRelays.wireguard.relays,
locations: cachedRelays.locations
)

guard let matchingRelay = (locatedRelays.first { $0.matches(location: location)
}) else { return nil }

return describeRelayLocation(location, usingRelayWithLocation: matchingRelay.serverLocation)
}

func describe(_ destination: UserSelectedRelays) -> String? {
customListDescription(destination) ?? relayDescription(destination)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Combine
import MapKit
import MullvadLogging
import MullvadREST
import MullvadSettings
import MullvadTypes
import SwiftUI
Expand Down Expand Up @@ -60,7 +61,12 @@ class TunnelViewController: UIViewController, RootContainment {
self.interactor = interactor

tunnelState = interactor.tunnelStatus.state
connectionViewViewModel = ConnectionViewViewModel(tunnelStatus: interactor.tunnelStatus)
connectionViewViewModel = ConnectionViewViewModel(
tunnelStatus: interactor.tunnelStatus,
relayConstraints: interactor.tunnelSettings.relayConstraints,
relayCache: RelayCache(cacheDirectory: ApplicationConfiguration.containerURL),
customListRepository: CustomListRepository()
)
indicatorsViewViewModel = FeatureIndicatorsViewModel(
tunnelSettings: interactor.tunnelSettings,
ipOverrides: interactor.ipOverrides
Expand Down Expand Up @@ -97,6 +103,7 @@ class TunnelViewController: UIViewController, RootContainment {

interactor.didUpdateTunnelSettings = { [weak self] tunnelSettings in
self?.indicatorsViewViewModel.tunnelSettings = tunnelSettings
self?.connectionViewViewModel.relayConstraints = tunnelSettings.relayConstraints
}

interactor.didUpdateIpOverrides = { [weak self] overrides in
Expand Down
Loading
Loading