Add Local Network Checker

This commit is contained in:
Leon Böttger committed 2025-12-20 15:24:57 +01:00
1 parent b61aecf315
commit fb13405247
5 files changed
+6078 -6173

No files matched your search

+15 -11
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 70;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
@@ -124,6 +124,7 @@
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
EB7654C02E534A8E006B4AA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
EB7654C12E534A8E006B4AA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
EB7A95F42EF6E1BE0021111B /* IntroductionSplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7A95F22EF6E1BE0021111B /* IntroductionSplashView.swift */; };
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54112D78E95C00989564 /* TutorialView.swift */; };
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
@@ -355,6 +356,7 @@
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
EB721F232E75AFA900F2099F /* QuickDropIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = QuickDropIcon.icon; sourceTree = "<group>"; };
EB7654772E525ACC006B4AA3 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
EB7A95F22EF6E1BE0021111B /* IntroductionSplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSplashView.swift; sourceTree = "<group>"; };
EB7F54112D78E95C00989564 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
EB7F54132D78E9E400989564 /* LicenseWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseWindow.swift; sourceTree = "<group>"; };
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropToastView.swift; sourceTree = "<group>"; };
@@ -393,10 +395,6 @@
EBE5FF4A2EDB8D4B0019B380 /* Config.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Config.storekit; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EB7A95582EF67C860021111B /* Introduction */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Introduction; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
699DEB9E2AB0573200115D22 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -630,6 +628,14 @@
path = Helpers;
sourceTree = "<group>";
};
EB7A95F32EF6E1BE0021111B /* Introduction */ = {
isa = PBXGroup;
children = (
EB7A95F22EF6E1BE0021111B /* IntroductionSplashView.swift */,
);
path = Introduction;
sourceTree = "<group>";
};
EB950B6C2E5206E2002D68FA /* iOSShareExtension */ = {
isa = PBXGroup;
children = (
@@ -661,7 +667,7 @@
EB7F54112D78E95C00989564 /* TutorialView.swift */,
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
EB7A95582EF67C860021111B /* Introduction */,
EB7A95F32EF6E1BE0021111B /* Introduction */,
EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */,
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
@@ -741,9 +747,6 @@
dependencies = (
699DEBAD2AB0573200115D22 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EB7A95582EF67C860021111B /* Introduction */,
);
name = QuickDrop;
packageProductDependencies = (
EB2F9BFD2DA05F48004010A2 /* BezelNotification */,
@@ -1043,6 +1046,7 @@
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
EB7A95F42EF6E1BE0021111B /* IntroductionSplashView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1332,7 +1336,7 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SHARED_BUILD = 99;
SHARED_BUILD = 101;
SHARED_VERSION = 2.10;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@@ -1395,7 +1399,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SHARED_BUILD = 99;
SHARED_BUILD = 101;
SHARED_VERSION = 2.10;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
-667
View File
@@ -5,233 +5,11 @@
"comment" : "Bundle display name",
"extractionState" : "extracted_with_value",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ca" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"cs" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"da" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"de" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"el" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "QuickDrop"
}
},
"en-AU" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-CA" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-GB" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"es" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"es-419" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"he" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hu" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"id" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"it" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ja" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ms" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"nb" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"nl" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pl" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ro" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ru" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"sv" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"th" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"tr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"uk" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"vi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
}
}
},
@@ -239,233 +17,11 @@
"comment" : "Bundle name",
"extractionState" : "extracted_with_value",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ca" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"cs" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"da" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"de" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"el" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "QuickDrop"
}
},
"en-AU" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-CA" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-GB" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"es" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"es-419" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"he" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hu" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"id" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"it" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ja" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ms" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"nb" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"nl" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pl" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ro" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ru" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"sv" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"th" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"tr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"uk" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"vi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
}
}
},
@@ -473,239 +29,16 @@
"comment" : "Copyright (human-readable)",
"extractionState" : "extracted_with_value",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ca" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"cs" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"da" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"de" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"el" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-AU" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-CA" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"en-GB" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"es" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"es-419" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"he" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"hu" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"id" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"it" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ja" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ms" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"nb" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"nl" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pl" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ro" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"ru" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"sv" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"th" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"tr" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"uk" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"vi" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
}
}
},
"NSLocalNetworkUsageDescription" : {
"comment" : "Privacy - Local Network Usage Description",
"extractionState" : "extracted_with_value",
"localizations" : {
"ar" : {
"stringUnit" : {
File diff suppressed because it is too large. Load diff
+113 -19
View File
@@ -13,30 +13,86 @@ final class DeviceToDeviceHeuristicScanner {
static let shared = DeviceToDeviceHeuristicScanner()
private init() {}
private let scanQueue = DispatchQueue(label: "DeviceToDeviceScanQueue")
private var reachableIPs: [String] = []
private var completion: ((Bool) -> Void)?
private var finished = false
var success: Bool {
self.reachableIPs.count > 0
private enum ScanMode {
case peerReachability // true only if some peer is reachable
case localNetworkAccess // true if EHOSTDOWN seen (or ready)
}
private let scanQueue = DispatchQueue(label: "DeviceToDeviceScanQueue")
private var reachableIPs: [String] = []
private var activeConnections: [NWConnection] = []
private var completion: ((Bool) -> Void)?
private var finished = false
private var currentMode: ScanMode = .peerReachability
// Cache: once true, stays true for subsequent calls
private var cachedLocalNetworkAccess = false
private var peerReachableCached: Bool { !reachableIPs.isEmpty }
// MARK: - Public API
/// Start scanning the subnet for reachable peers.
/// - Parameters:
/// - baseSubnet: e.g. "192.168.1" — if nil, will auto-detect
/// - completion: true if any peer device was reachable
/// - Returns: true if any peer device was reachable (same semantics as before).
func scan(subnet baseSubnet: String? = nil,
port: UInt16 = 80,
timeout: TimeInterval = 10.0,
completion: @escaping (Bool) -> Void) {
if self.success {
if peerReachableCached {
completion(true)
return
}
startScan(
mode: .peerReachability,
subnet: baseSubnet,
port: port,
timeout: timeout,
completion: completion
)
}
/// Returns true if:
/// - cached value is true, OR
/// - during a scan at least one host produces `.waiting(.posix(.EHOSTDOWN))`.
///
/// As soon as EHOSTDOWN is observed, returns true immediately.
/// Otherwise waits until timeout (default 10s) and returns false.
func hasLocalNetworkAccess(subnet baseSubnet: String? = nil,
port: UInt16 = 80,
timeout: TimeInterval = 10.0,
completion: @escaping (Bool) -> Void) -> Bool {
if cachedLocalNetworkAccess {
return true
}
startScan(
mode: .localNetworkAccess,
subnet: baseSubnet,
port: port,
timeout: timeout,
completion: completion
)
return false
}
// MARK: - Core scan logic
private func startScan(mode: ScanMode,
subnet baseSubnet: String?,
port: UInt16,
timeout: TimeInterval,
completion: @escaping (Bool) -> Void) {
scanQueue.async {
// Cancel any in-flight scan to keep behavior deterministic.
self.cancelActiveConnections()
self.currentMode = mode
self.completion = completion
self.finished = false
@@ -47,16 +103,17 @@ final class DeviceToDeviceHeuristicScanner {
self.testConnection(to: ip, port: port)
}
// Timeout fallback
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
self.finish()
self.finish(result: self.resultForCurrentModeOnTimeout())
}
}
}
private func testConnection(to ip: String, port: UInt16) {
guard let nwPort = NWEndpoint.Port(rawValue: port) else { return }
let connection = NWConnection(host: NWEndpoint.Host(ip), port: nwPort, using: .tcp)
activeConnections.append(connection)
connection.stateUpdateHandler = { [weak self] state in
guard let self else { return }
@@ -65,12 +122,31 @@ final class DeviceToDeviceHeuristicScanner {
case .ready:
self.scanQueue.async {
self.reachableIPs.append(ip)
// If a TCP connection succeeds, local network access is effectively confirmed as well.
self.cachedLocalNetworkAccess = true
// For scan(): success means peer reachable.
// For hasLocalNetworkAccess(): success also means local network access.
self.finish(result: true)
}
self.finish()
connection.cancel()
case .waiting(let error):
// "Host is down" is treated as a strong signal that local-network traffic is possible,
// even if the specific host doesn't respond.
if error == .posix(.EHOSTDOWN) {
self.scanQueue.async {
self.cachedLocalNetworkAccess = true
if self.currentMode == .localNetworkAccess {
self.finish(result: true)
}
}
}
case .failed, .cancelled:
connection.cancel()
// Ignore; scan completes via early success or timeout.
break
default:
break
@@ -80,20 +156,38 @@ final class DeviceToDeviceHeuristicScanner {
connection.start(queue: scanQueue)
}
private func finish() {
private func resultForCurrentModeOnTimeout() -> Bool {
switch currentMode {
case .peerReachability:
return peerReachableCached
case .localNetworkAccess:
return cachedLocalNetworkAccess
}
}
private func finish(result: Bool) {
scanQueue.async {
guard !self.finished else { return }
self.finished = true
let completion = self.completion
self.completion = nil
self.cancelActiveConnections()
DispatchQueue.main.async {
completion?(self.success)
completion?(result)
}
}
}
private func cancelActiveConnections() {
for c in activeConnections { c.cancel() }
activeConnections.removeAll()
}
// MARK: - Subnet detection
/// Attempts to detect the local subnet prefix (e.g., "192.168.0")
private static func getLocalSubnetPrefix() -> String? {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
@@ -67,7 +67,9 @@ struct IntroductionSplashView: View {
Divider()
HStack {
if let skipAction = currentPage.skipAction {
if let skipAction = currentPage.skipAction,
(currentPage.needRequestBeforeSkipAvailable == false || pollingPage != nil) {
Button("introduction_skip") {
self.skipAction = skipAction
}
@@ -75,7 +77,7 @@ struct IntroductionSplashView: View {
.buttonStyle(.borderless)
.alert(item: $skipAction) { action in
.init(title: Text(action.warningTitle.localized()), message: Text(action.warningMessage.localized()), primaryButton: .default(Text("introduction_skip".localized()), action: {
.init(title: Text(action.warningTitle.localized()), message: Text(action.warningMessage.localized()), primaryButton: .default(Text("introduction_continue".localized()), action: {
stopPolling()
self.skipAction = nil
@@ -181,11 +183,6 @@ struct IntroductionSplashView: View {
private func triggerLocalNetworkPermission() {
self.startReceiving()
// just to be sure it is triggered
runAfter(seconds: 1) {
DeviceToDeviceHeuristicScanner.shared.scan(completion: { _ in })
}
}
private func advance() {
@@ -251,16 +248,25 @@ enum IntroductionPage: CaseIterable {
case .splash:
return nil
case .noWifi:
return .init(action: {presentNextPage()}, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message", id: "noWifiSkip")
return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message", id: "noWifiSkip")
case .localNetworkAccess:
return nil
return .init(action: presentNextPage, warningTitle: "introduction_local_network_access_skip_title", warningMessage: "introduction_local_network_access_skip_message", id: "localNetworkAccessSkip")
case .enableShareExtension:
return .init(action: {presentNextPage()}, warningTitle: "introduction_enable_share_extension_skip_title", warningMessage: "introduction_enable_share_extension_skip_message", id: "enableShareExtensionSkip")
return .init(action: presentNextPage, warningTitle: "introduction_enable_share_extension_skip_title", warningMessage: "introduction_enable_share_extension_skip_message", id: "enableShareExtensionSkip")
case .finished:
return nil
}
}
var needRequestBeforeSkipAvailable: Bool {
switch self {
case .enableShareExtension:
return true
default:
return false
}
}
var canPerformAction: Bool {
switch self {
case .noWifi:
@@ -291,6 +297,8 @@ enum IntroductionPage: CaseIterable {
return NearbyConnectionManager.shared.isConnectedToLocalNetwork
case .enableShareExtension:
return NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension")) != nil
case .localNetworkAccess:
return DeviceToDeviceHeuristicScanner.shared.hasLocalNetworkAccess(completion: {_ in })
default:
return true
}