Skip to content

Commit e10b40a

Browse files
feat: Handle downloads from WKWebView (#54)
2 parents d2cb40d + 2c3b09a commit e10b40a

File tree

3 files changed

+137
-14
lines changed

3 files changed

+137
-14
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Infomaniak Euria - iOS App
3+
Copyright (C) 2025 Infomaniak Network SA
4+
5+
This program is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import Foundation
20+
21+
public extension URL {
22+
static func temporaryDownloadsDirectory() throws -> URL {
23+
let temporaryFolder = URL.temporaryDirectory.appending(path: "/euria_downloads", directoryHint: .isDirectory)
24+
try FileManager.default.createDirectory(at: temporaryFolder, withIntermediateDirectories: true)
25+
return temporaryFolder
26+
}
27+
}

EuriaFeatures/MainView/EuriaWebViewDelegate.swift

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import EuriaCore
2020
import InfomaniakCore
2121
import InfomaniakDI
2222
import InfomaniakLogin
23+
import OSLog
2324
import Sentry
2425
import SwiftUI
2526
import UIKit
@@ -29,19 +30,54 @@ import WebKit
2930
class EuriaWebViewDelegate: NSObject, ObservableObject {
3031
@Published var isLoaded = false
3132

33+
@Published var isPresentingDocument: URL?
34+
@Published var error: ErrorDomain?
35+
3236
let webConfiguration: WKWebViewConfiguration
37+
var downloads = [WKDownload: URL]()
3338

3439
enum Cookie: String {
3540
case userToken = "USER-TOKEN"
3641
case userLanguage = "USER-LANGUAGE"
3742
}
3843

44+
enum ErrorDomain: LocalizedError, Equatable {
45+
case urlGenerationFailed(error: Error)
46+
case downloadFailed(error: Error)
47+
48+
var errorDescription: String? {
49+
switch self {
50+
case .urlGenerationFailed(let error):
51+
return error.localizedDescription
52+
case .downloadFailed(let error):
53+
return error.localizedDescription
54+
}
55+
}
56+
57+
static func == (lhs: ErrorDomain, rhs: ErrorDomain) -> Bool {
58+
switch (lhs, rhs) {
59+
case (.urlGenerationFailed, .urlGenerationFailed):
60+
return true
61+
case (.downloadFailed, .downloadFailed):
62+
return true
63+
default:
64+
return false
65+
}
66+
}
67+
}
68+
3969
init(session: any UserSessionable) {
4070
webConfiguration = WKWebViewConfiguration()
4171
super.init()
4272
setupWebViewConfiguration(token: session.apiFetcher.currentToken)
4373
}
4474

75+
deinit {
76+
Task {
77+
await EuriaWebViewDelegate.cleanTemporaryFolder()
78+
}
79+
}
80+
4581
private func setupWebViewConfiguration(token: ApiToken?) {
4682
addCookies(token: token)
4783
addUserContentControllers()
@@ -77,35 +113,82 @@ class EuriaWebViewDelegate: NSObject, ObservableObject {
77113
]
78114
)
79115
}
116+
117+
private nonisolated static func cleanTemporaryFolder() async {
118+
do {
119+
try FileManager.default.removeItem(at: URL.temporaryDownloadsDirectory())
120+
} catch {
121+
Logger.general.error("Error while cleaning temporary folder: \(error)")
122+
}
123+
}
80124
}
81125

82126
// MARK: - WKNavigationDelegate
83127

84128
extension EuriaWebViewDelegate: WKNavigationDelegate {
85-
func webView(
86-
_ webView: WKWebView,
87-
decidePolicyFor navigationAction: WKNavigationAction,
88-
decisionHandler: @MainActor (WKNavigationActionPolicy) -> Void
89-
) {
129+
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
130+
guard !navigationAction.shouldPerformDownload else {
131+
return .download
132+
}
133+
90134
guard let navigationHost = navigationAction.request.url?.host() else {
91-
decisionHandler(.allow)
92-
return
135+
return .allow
93136
}
94137

95138
if navigationHost == ApiEnvironment.current.euriaHost {
96-
decisionHandler(.allow)
97-
} else {
98-
if navigationAction.navigationType == .linkActivated,
99-
let url = navigationAction.request.url {
100-
UIApplication.shared.open(url)
101-
}
102-
decisionHandler(.cancel)
139+
return .allow
103140
}
141+
142+
if navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url {
143+
await UIApplication.shared.open(url)
144+
}
145+
return .cancel
104146
}
105147

106148
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
107149
isLoaded = true
108150
}
151+
152+
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
153+
download.delegate = self
154+
}
155+
}
156+
157+
// MARK: - WKDownloadDelegate
158+
159+
extension EuriaWebViewDelegate: WKDownloadDelegate {
160+
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? {
161+
do {
162+
let fileDestinationURL = try URL.temporaryDownloadsDirectory().appending(path: suggestedFilename)
163+
guard !FileManager.default.fileExists(atPath: fileDestinationURL.path(percentEncoded: false)) else {
164+
isPresentingDocument = fileDestinationURL
165+
return nil
166+
}
167+
168+
downloads[download] = fileDestinationURL
169+
return fileDestinationURL
170+
} catch {
171+
self.error = .urlGenerationFailed(error: error)
172+
Logger.general.error("Error while generating the destination URL for a download: \(error)")
173+
return nil
174+
}
175+
}
176+
177+
func downloadDidFinish(_ download: WKDownload) {
178+
guard let fileURL = downloads[download] else {
179+
return
180+
}
181+
182+
isPresentingDocument = fileURL
183+
downloads[download] = nil
184+
}
185+
186+
func download(_ download: WKDownload, didFailWithError error: any Error, resumeData: Data?) {
187+
self.error = .downloadFailed(error: error)
188+
Logger.general.error("Error while downloading a file: \(error)")
189+
190+
downloads[download] = nil
191+
}
109192
}
110193

111194
// MARK: - WKScriptMessageHandler

EuriaFeatures/MainView/MainView.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ import InAppTwoFactorAuthentication
2323
import InfomaniakConcurrency
2424
import InfomaniakCore
2525
import InfomaniakCoreCommonUI
26+
import InfomaniakCoreUIResources
2627
import InfomaniakDI
2728
import InfomaniakLogin
2829
import SwiftUI
2930
import WebKit
3031

3132
public struct MainView: View {
3233
@StateObject private var webViewDelegate: EuriaWebViewDelegate
34+
3335
@State private var isShowingWebView = true
36+
@State private var isShowingErrorAlert = false
3437

3538
@ObservedObject var networkMonitor = NetworkMonitor.shared
3639

@@ -55,6 +58,16 @@ public struct MainView: View {
5558
delegate: webViewDelegate
5659
)
5760
.ignoresSafeArea()
61+
.quickLookPreview($webViewDelegate.isPresentingDocument)
62+
.onChange(of: webViewDelegate.error) { newValue in
63+
guard newValue != nil else { return }
64+
isShowingErrorAlert = true
65+
}
66+
.alert(isPresented: $isShowingErrorAlert, error: webViewDelegate.error) {
67+
Button(InfomaniakCoreUIResources.CoreUILocalizable.buttonClose) {
68+
webViewDelegate.error = nil
69+
}
70+
}
5871
}
5972

6073
if isShowingOfflineView {

0 commit comments

Comments
 (0)