[iOS] - Add ability to download and install PKPasses (Bundled) (#27258)

This commit is contained in:
Brandon-T
2025-03-05 20:06:24 +01:00
committed by GitHub
parent a6800de279
commit 964cce1e37
7 changed files with 63 additions and 24 deletions
@@ -18,7 +18,7 @@ extension BrowserViewController: WKDownloadDelegate {
) async -> URL? {
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.mimeType != MIMEType.passbook {
if ![MIMEType.passbook, MIMEType.passbookBundle].contains(httpResponse.mimeType) {
let shouldDownload = await downloadAlert(
download,
response: response,
@@ -83,7 +83,9 @@ extension BrowserViewController: WKDownloadDelegate {
textEncodingName: downloadInfo.response.textEncodingName
)
if downloadInfo.response.mimeType == MIMEType.passbook {
if [MIMEType.passbook, MIMEType.passbookBundle].contains(
downloadInfo.response.mimeType
) {
downloadQueue.download(downloadInfo, didFinishDownloadingTo: downloadInfo.fileURL)
if let passbookHelper = OpenPassBookHelper(
request: nil,
@@ -171,7 +171,7 @@ extension BrowserViewController: WKNavigationDelegate {
return .download
}
if response.mimeType == MIMEType.passbook {
if [MIMEType.passbook, MIMEType.passbookBundle].contains(response.mimeType) {
return .download
}
@@ -175,7 +175,8 @@ extension HTTPDownload: URLSessionTaskDelegate, URLSessionDownloadDelegate {
try? FileManager.default.moveItem(at: location, to: temporaryLocation)
Task {
do {
let destination = try await uniqueDownloadPathForFilename(filename)
let downloadsPath = try await AsyncFileManager.default.downloadsPath()
let destination = try await URL.uniqueFileName(filename, in: downloadsPath)
try await AsyncFileManager.default.moveItem(at: temporaryLocation, to: destination)
isComplete = true
delegate?.download(self, didFinishDownloadingTo: destination)
@@ -2,13 +2,16 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
import BraveCore
import BraveShared
import DataImporter
import Foundation
import MobileCoreServices
import PassKit
import Shared
import UniformTypeIdentifiers
import WebKit
import os.log
struct MIMEType {
static let bitmap = "image/bmp"
@@ -19,6 +22,7 @@ struct MIMEType {
static let html = "text/html"
static let octetStream = "application/octet-stream"
static let passbook = "application/vnd.apple.pkpass"
static let passbookBundle = "application/vnd.apple.pkpasses"
static let pdf = "application/pdf"
static let plainText = "text/plain"
static let png = "image/png"
@@ -128,6 +132,7 @@ class DownloadHelper: NSObject {
}
class OpenPassBookHelper: NSObject {
private let mimeType: String
fileprivate var url: URL
fileprivate let browserViewController: BrowserViewController
@@ -139,29 +144,22 @@ class OpenPassBookHelper: NSObject {
forceDownload: Bool,
browserViewController: BrowserViewController
) {
guard let mimeType = response.mimeType, mimeType == MIMEType.passbook,
guard let mimeType = response.mimeType,
[MIMEType.passbook, MIMEType.passbookBundle].contains(mimeType.lowercased()),
PKAddPassesViewController.canAddPasses(),
let responseURL = response.url, !forceDownload
else { return nil }
self.mimeType = mimeType
self.url = responseURL
self.browserViewController = browserViewController
super.init()
}
@MainActor func open() async {
let passData = try? await Task.detached { [url] in
try Data(contentsOf: url)
}.value
guard let passData else { return }
do {
let pass = try PKPass(data: passData)
let passLibrary = PKPassLibrary()
if passLibrary.containsPass(pass) {
await UIApplication.shared.open(pass.passURL!, options: [:])
} else {
if let addController = PKAddPassesViewController(pass: pass) {
browserViewController.present(addController, animated: true, completion: nil)
}
let passes = try await parsePasses()
if let addController = PKAddPassesViewController(passes: passes) {
browserViewController.present(addController, animated: true, completion: nil)
}
} catch {
// display an error
@@ -179,4 +177,46 @@ class OpenPassBookHelper: NSObject {
return
}
}
private func parsePasses() async throws -> [PKPass] {
if mimeType == MIMEType.passbookBundle {
let url = try await ZipImporter.unzip(path: url)
do {
let files = await enumerateFiles(in: url, withExtensions: ["pkpass", "pkpasses"])
let result = try files.map { try PKPass(data: Data(contentsOf: $0)) }
try? await AsyncFileManager.default.removeItem(at: url)
return result
} catch {
try? await AsyncFileManager.default.removeItem(at: url)
throw error
}
}
let passData = try Data(contentsOf: url)
return try [PKPass(data: passData)]
}
func enumerateFiles(
in directory: URL,
withExtensions extensions: [String] = []
) async -> [URL] {
let enumerator = AsyncFileManager.default.enumerator(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey]
)
var result: [URL] = []
for await fileURL in enumerator {
do {
let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if resourceValues.isRegularFile == true, extensions.contains(fileURL.pathExtension) {
result.append(fileURL)
}
} catch {
Logger.module.error("Error reading file \(fileURL): \(error)")
}
}
return result
}
}
@@ -332,9 +332,4 @@ extension AsyncFileManager {
public func downloadsPath() async throws -> URL {
try await url(for: .documentDirectory, appending: "Downloads", create: true)
}
/// URL where temporary files are stored.
public var temporaryDirectory: URL {
fileManager.temporaryDirectory
}
}
@@ -8,8 +8,8 @@ import BraveShared
import Foundation
import os.log
class ZipImporter {
enum ZipImportError: Error {
public class ZipImporter {
public enum ZipImportError: Error {
case failedToUnzip
case invalidFileSystemURL
}
@@ -214,6 +214,7 @@ class DownloadHelperTests: XCTestCase {
MIMEType.html,
MIMEType.octetStream,
MIMEType.passbook,
MIMEType.passbookBundle,
MIMEType.pdf,
MIMEType.plainText,
MIMEType.png,