2024-09-23 13:02:26 +00:00
|
|
|
//
|
|
|
|
// AppError+L10n.swift
|
|
|
|
// Passepartout
|
|
|
|
//
|
|
|
|
// Created by Davide De Rosa on 8/27/24.
|
2025-01-15 19:22:52 +00:00
|
|
|
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
|
2024-09-23 13:02:26 +00:00
|
|
|
//
|
|
|
|
// https://github.com/passepartoutvpn
|
|
|
|
//
|
|
|
|
// This file is part of Passepartout.
|
|
|
|
//
|
|
|
|
// Passepartout is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// Passepartout is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
|
|
|
|
|
|
import CommonLibrary
|
2024-11-02 09:11:59 +00:00
|
|
|
import CommonUtils
|
2024-09-23 13:02:26 +00:00
|
|
|
import Foundation
|
|
|
|
import PassepartoutKit
|
|
|
|
|
|
|
|
extension AppError: LocalizedError {
|
2024-10-29 13:30:41 +00:00
|
|
|
public var errorDescription: String? {
|
2024-09-23 13:02:26 +00:00
|
|
|
let V = Strings.Errors.App.self
|
|
|
|
switch self {
|
2024-11-10 16:51:28 +00:00
|
|
|
case .couldNotLaunch(let reason):
|
|
|
|
return reason.localizedDescription
|
|
|
|
|
2024-11-07 22:02:10 +00:00
|
|
|
case .emptyProducts:
|
|
|
|
return V.emptyProducts
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
case .emptyProfileName:
|
|
|
|
return V.emptyProfileName
|
|
|
|
|
2024-11-18 16:43:01 +00:00
|
|
|
case .ineligibleProfile:
|
|
|
|
return nil
|
|
|
|
|
2024-12-01 21:34:41 +00:00
|
|
|
case .interactiveLogin:
|
|
|
|
return nil
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
case .malformedModule(let module, let error):
|
2024-11-02 14:23:36 +00:00
|
|
|
return V.malformedModule(module.moduleType.localizedDescription, error.localizedDescription)
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
case .permissionDenied:
|
2025-01-19 10:59:20 +00:00
|
|
|
return V.permissionDenied
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
case .generic(let error):
|
|
|
|
return error.localizedDescription
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Allow graceful period to work around slow receipt validation (#1139)
#1070 is very tricky. When the device boots, StoreKit operations seem to
be severely affected by on-demand VPN profiles. Slowdowns are huge and
unpredictable, as per my [report on the Apple
forums](https://developer.apple.com/forums/thread/773723). I found no
easy way to work around the chicken-and-egg situation where the VPN
requires StoreKit validation to start, but StoreKit requires network
access.
On the other hand, without StoreKit validations, the on-demand tunnel
starts on boot just fine, and so does the app. No eternal activity
indicators. StoreKit is clearly the culprit here.
Therefore, below is the strategy that this PR implements for a decent
trade-off:
- Configure a graceful period for the VPN to start without limitations.
This is initially set to 2 minutes in production, and 10 minutes in
TestFlight. Postpone StoreKit validation until then.
- After the graceful period, StoreKit validation is more likely to
complete fast
- At this point, paying users have their receipts validated and the
connection will silently keep going
- Non-paying users, instead, will see their connection hit the "Purchase
required" message
On the UI side, adjust the app accordingly:
- Drop the "Purchase required" icon from the list/grid of profiles
- The paywall informs that the connection will start, but it will
disconnect after the graceful period if the receipt is not valid
- Add a note that receipt validation may take a while if the device has
just started
This PR also introduces changes in TestFlight behavior:
- Profiles can be saved without limitations
- Profiles using free features work as usual
- Profiles using paid features work for 10 minutes
- Eligibility based on local receipt is ignored (deprecated in iOS 18)
Beta users may therefore test all paid features on iOS/macOS/tvOS for 10
minutes. Until now, paid features were only available to paying iOS
users and unavailable on macOS/tvOS. The tvOS beta was, in fact,
completely useless.
The downside is that paying iOS users will see beta builds restricted
like anybody else. I'll see if I can find a better solution later.
2025-02-05 12:00:42 +00:00
|
|
|
extension TaskTimeoutError: PassepartoutErrorMappable {
|
|
|
|
public var asPassepartoutError: PassepartoutError {
|
|
|
|
PassepartoutError(.timeout)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
// MARK: - App side
|
|
|
|
|
2024-11-13 11:07:30 +00:00
|
|
|
extension PassepartoutError: @retroactive LocalizedError {
|
2024-09-23 13:02:26 +00:00
|
|
|
public var errorDescription: String? {
|
|
|
|
switch code {
|
2024-10-16 07:50:26 +00:00
|
|
|
case .connectionModuleRequired:
|
|
|
|
return Strings.Errors.App.Passepartout.connectionModuleRequired
|
|
|
|
|
|
|
|
case .corruptProviderModule:
|
|
|
|
return Strings.Errors.App.Passepartout.corruptProviderModule(reason?.localizedDescription ?? "")
|
|
|
|
|
|
|
|
case .incompatibleModules:
|
|
|
|
return Strings.Errors.App.Passepartout.incompatibleModules
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
case .invalidFields:
|
2024-10-28 19:07:19 +00:00
|
|
|
let fields = (userInfo as? [String: String?])
|
2024-09-23 13:02:26 +00:00
|
|
|
.map {
|
2024-10-28 19:07:19 +00:00
|
|
|
$0.map {
|
|
|
|
"\($0)=\($1?.description ?? "")"
|
|
|
|
}
|
|
|
|
.joined(separator: ",")
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
|
2024-10-28 19:07:19 +00:00
|
|
|
return [Strings.Errors.App.Passepartout.invalidFields, fields]
|
|
|
|
.compactMap { $0 }
|
|
|
|
.joined(separator: " ")
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-19 19:51:55 +00:00
|
|
|
case .missingProviderEntity:
|
|
|
|
return Strings.Errors.App.Passepartout.missingProviderEntity
|
|
|
|
|
2024-11-01 19:01:18 +00:00
|
|
|
case .noActiveModules:
|
|
|
|
return Strings.Errors.App.Passepartout.noActiveModules
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
case .parsing:
|
2024-12-04 08:11:02 +00:00
|
|
|
let message = userInfo as? String ?? (reason as? LocalizedError)?.localizedDescription
|
2024-10-28 19:07:19 +00:00
|
|
|
|
|
|
|
return [Strings.Errors.App.Passepartout.parsing, message]
|
|
|
|
.compactMap { $0 }
|
|
|
|
.joined(separator: " ")
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-28 19:30:22 +00:00
|
|
|
case .providerRequired:
|
|
|
|
return Strings.Errors.App.Passepartout.providerRequired
|
|
|
|
|
Allow graceful period to work around slow receipt validation (#1139)
#1070 is very tricky. When the device boots, StoreKit operations seem to
be severely affected by on-demand VPN profiles. Slowdowns are huge and
unpredictable, as per my [report on the Apple
forums](https://developer.apple.com/forums/thread/773723). I found no
easy way to work around the chicken-and-egg situation where the VPN
requires StoreKit validation to start, but StoreKit requires network
access.
On the other hand, without StoreKit validations, the on-demand tunnel
starts on boot just fine, and so does the app. No eternal activity
indicators. StoreKit is clearly the culprit here.
Therefore, below is the strategy that this PR implements for a decent
trade-off:
- Configure a graceful period for the VPN to start without limitations.
This is initially set to 2 minutes in production, and 10 minutes in
TestFlight. Postpone StoreKit validation until then.
- After the graceful period, StoreKit validation is more likely to
complete fast
- At this point, paying users have their receipts validated and the
connection will silently keep going
- Non-paying users, instead, will see their connection hit the "Purchase
required" message
On the UI side, adjust the app accordingly:
- Drop the "Purchase required" icon from the list/grid of profiles
- The paywall informs that the connection will start, but it will
disconnect after the graceful period if the receipt is not valid
- Add a note that receipt validation may take a while if the device has
just started
This PR also introduces changes in TestFlight behavior:
- Profiles can be saved without limitations
- Profiles using free features work as usual
- Profiles using paid features work for 10 minutes
- Eligibility based on local receipt is ignored (deprecated in iOS 18)
Beta users may therefore test all paid features on iOS/macOS/tvOS for 10
minutes. Until now, paid features were only available to paying iOS
users and unavailable on macOS/tvOS. The tvOS beta was, in fact,
completely useless.
The downside is that paying iOS users will see beta builds restricted
like anybody else. I'll see if I can find a better solution later.
2025-02-05 12:00:42 +00:00
|
|
|
case .timeout:
|
|
|
|
return Strings.Errors.App.Passepartout.timeout
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
case .unhandled:
|
|
|
|
return reason?.localizedDescription
|
|
|
|
|
|
|
|
default:
|
|
|
|
return Strings.Errors.App.Passepartout.default(code.rawValue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Tunnel side
|
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
extension PassepartoutError.Code: StyledLocalizableEntity {
|
|
|
|
public enum Style {
|
|
|
|
case tunnel
|
|
|
|
}
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
public func localizedDescription(style: Style) -> String {
|
|
|
|
switch style {
|
|
|
|
case .tunnel:
|
|
|
|
let V = Strings.Errors.Tunnel.self
|
|
|
|
switch self {
|
2024-11-09 14:20:59 +00:00
|
|
|
case .App.ineligibleProfile:
|
|
|
|
return V.ineligible
|
2024-11-05 09:03:54 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .authentication:
|
|
|
|
return V.auth
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .crypto:
|
|
|
|
return V.encryption
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .dnsFailure:
|
|
|
|
return V.dns
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .timeout:
|
2024-12-22 09:55:56 +00:00
|
|
|
return Strings.Global.Nouns.timeout
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .OpenVPN.compressionMismatch:
|
|
|
|
return V.compression
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .OpenVPN.noRouting:
|
|
|
|
return V.routing
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .OpenVPN.serverShutdown:
|
|
|
|
return V.shutdown
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-11-04 22:34:22 +00:00
|
|
|
case .OpenVPN.tlsFailure:
|
|
|
|
return V.tls
|
|
|
|
|
|
|
|
default:
|
|
|
|
return V.generic
|
|
|
|
}
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|