Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8aa9169
feat: Add shareExtension to open the app
BaptGrv Nov 7, 2025
aa27fd8
feat: Add appGroup to share data between shareExtension and app
BaptGrv Nov 7, 2025
d994aa6
feat: Add func to copy the shared image into the appGroup
BaptGrv Nov 11, 2025
c9933c1
chore: Remove extraneous ')'
BaptGrv Nov 11, 2025
ae42802
feat: Add loader
PhilippeWeidmann Dec 8, 2025
e0a04f9
feat: Import items to shared container
PhilippeWeidmann Dec 8, 2025
2331cdc
feat: Prepare files for upload from share extension
PhilippeWeidmann Dec 10, 2025
dedd5a2
fix: Only handle https for kSuite links
PhilippeWeidmann Dec 10, 2025
bab13f2
feat: Add upload endpoint
PhilippeWeidmann Dec 10, 2025
03de8a9
ci: Ensure CI runs on protected branches
PhilippeWeidmann Dec 11, 2025
73946e2
refactor: Cleanup
PhilippeWeidmann Dec 11, 2025
13283ab
fix: Project.swift declaration
PhilippeWeidmann Dec 11, 2025
57d18b2
feat: Upload files
PhilippeWeidmann Dec 11, 2025
aa3f907
feat: Upload error
PhilippeWeidmann Dec 11, 2025
aa251e8
fix: Throw correct error
PhilippeWeidmann Dec 11, 2025
213c773
refactor: Rename jsmessage to jsfunction
PhilippeWeidmann Dec 11, 2025
208e3aa
feat: Subscribe to webview message
PhilippeWeidmann Dec 11, 2025
f7fb658
feat: Make apifetcher uploadFile cancellable
PhilippeWeidmann Dec 11, 2025
faaa424
feat: Handle cancel
PhilippeWeidmann Dec 11, 2025
80b20c7
feat: Cleanup after upload
PhilippeWeidmann Dec 11, 2025
a396cf1
feat: Add NSExtensionActivationRule for data
PhilippeWeidmann Dec 12, 2025
7df7be5
refactor: Remove unused code
PhilippeWeidmann Dec 12, 2025
330f7cf
feat: Share extension periphery cleanup (#93)
PhilippeWeidmann Dec 12, 2025
66ef78b
fix: Correct scheme
PhilippeWeidmann Dec 12, 2025
3ebb401
fix: Correct scheme (#94)
PhilippeWeidmann Dec 12, 2025
a94095f
fix: Return after opening URL
PhilippeWeidmann Dec 12, 2025
aaec047
refactor: Rename attachment folder
PhilippeWeidmann Dec 12, 2025
c62bf61
feat: Share extension feedback (#96)
PhilippeWeidmann Dec 12, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI workflow

on:
pull_request:
branches: [ main ]
branches: [ main, protected/* ]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/swiftformat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: SwiftFormat

on:
pull_request:
branches: [ main ]
branches: [ main, protected/* ]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: SwiftLint

on:
pull_request:
branches: [ main ]
branches: [ main, protected/* ]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down
11 changes: 9 additions & 2 deletions Euria/Sources/EuriaApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ struct EuriaApp: App {

@StateObject private var rootViewState = RootViewState()
@StateObject private var universalLinksState = UniversalLinksState()
@StateObject private var uploadManager = UploadManager()

var body: some Scene {
WindowGroup {
RootView()
.environmentObject(rootViewState)
.environmentObject(universalLinksState)
.environmentObject(uploadManager)
.ikButtonTheme(.euria)
.onOpenURL(perform: handleURL)
.onReceive(NotificationCenter.default.publisher(for: .openURL).receive(on: DispatchQueue.main)) { notification in
Expand All @@ -50,10 +52,15 @@ struct EuriaApp: App {

private func handleURL(_ url: URL) {
let linkHandler = UniversalLinkHandler()
guard let universalLink = linkHandler.handlePossibleUniversalLink(url) else {
if let universalLink = linkHandler.handlePossibleUniversalLink(url) {
universalLinksState.linkedWebView = universalLink
return
}

universalLinksState.linkedWebView = universalLink
if let importSessionUUID = linkHandler.handlePossibleImportSession(url),
case .mainView(let mainViewState) = rootViewState.state {
uploadManager.handleImportSession(uuid: importSessionUUID, userSession: mainViewState.userSession)
return
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
*/

import Foundation
import InfomaniakCore

public extension ApiEnvironment {
var euriaHost: String {
return "euria.\(host)"
}
struct FileUploadErrorJsResponse: Encodable, Sendable {
let ref: String
let error: String
}
26 changes: 26 additions & 0 deletions EuriaCore/Attachment Upload/FileUploadSucceedJsResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
Infomaniak Euria - iOS App
Copyright (C) 2025 Infomaniak Network SA

This program 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.

This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

struct FileUploadSucceedJsResponse: Encodable, Sendable {
let ref: String
let id: String
let name: String
let mimeType: String
}
65 changes: 65 additions & 0 deletions EuriaCore/Attachment Upload/ImportHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Infomaniak Euria - iOS App
Copyright (C) 2025 Infomaniak Network SA

This program 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.

This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

extension URL {
var importsDirectoryURL: URL {
appending(path: "imports", directoryHint: .isDirectory)
}
}

public struct ImportHelper {
public typealias ImportSessionUUID = String
public let importUUID: ImportSessionUUID
public let importURL: URL

public var importedFileURLs: [URL] {
let fileManager = FileManager.default
guard let contents = try? fileManager.contentsOfDirectory(
at: importURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]
) else {
return []
}
return contents
}

public init(baseURL: URL) {
self.init(baseURL: baseURL, importUUID: UUID().uuidString)
}

public init(baseURL: URL, importUUID: ImportSessionUUID) {
self.importUUID = importUUID
importURL = baseURL
.importsDirectoryURL
.appending(path: importUUID, directoryHint: .isDirectory)
}

public func moveURLsToImportDirectory(_ urls: [URL]) async throws {
let fileManager = FileManager.default

try fileManager.createDirectory(at: importURL, withIntermediateDirectories: true)

for url in urls {
let destinationURL = importURL.appending(path: url.lastPathComponent)
try fileManager.moveItem(at: url, to: destinationURL)
}
}
}
50 changes: 50 additions & 0 deletions EuriaCore/Attachment Upload/ImportedFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Infomaniak Euria - iOS App
Copyright (C) 2025 Infomaniak Network SA

This program 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.

This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

public struct ImportedFile: Encodable, Sendable {
let ref: String
let name: String
let mimeType: String
let size: Int64

let fileURL: URL

init(fileURL: URL) {
self.fileURL = fileURL

ref = UUID().uuidString
name = fileURL.lastPathComponent
do {
let resourceValues = try fileURL.resourceValues(forKeys: [.contentTypeKey, .fileSizeKey])
mimeType = resourceValues.contentType?.preferredMIMEType ?? "application/octet-stream"
size = Int64(resourceValues.fileSize ?? 0)
} catch {
mimeType = "application/octet-stream"
size = 0
}
}

enum CodingKeys: String, CodingKey {
case ref
case name
case mimeType
case size
}
}
132 changes: 132 additions & 0 deletions EuriaCore/Attachment Upload/UploadManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
Infomaniak Euria - iOS App
Copyright (C) 2025 Infomaniak Network SA

This program 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.

This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

@MainActor
public class UploadManager: ObservableObject, WebViewMessageSubscriber {

Check warning on line 22 in EuriaCore/Attachment Upload/UploadManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

conformance of 'UploadManager' to protocol 'WebViewMessageSubscriber' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

Check warning on line 22 in EuriaCore/Attachment Upload/UploadManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

conformance of 'UploadManager' to protocol 'WebViewMessageSubscriber' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode
enum DomainError: Error {
case containerUnavailable
case noValidFiles
case invalidOrganizationId
case bridgeCommunicationFailed
}

public weak var bridge: WebViewBridge? {
didSet {
bridge?.addSubscriber(self, topic: .cancelFileUpload)
}
}

private var currentUploadTasks: [String: Task<Void, Never>] = [:]

public init() {}

public func handleImportSession(uuid: String, userSession: any UserSessionable) {
guard !userSession.isGuest else { return }

Task {
try await handleImportSession(uuid: uuid, userSession: userSession)
}
}

func handleImportSession(uuid: ImportHelper.ImportSessionUUID, userSession: any UserSessionable) async throws {
guard let containerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier) else {
throw DomainError.containerUnavailable
}

cleanupOrphanUploadContainers(baseContainerURL: containerURL, currentImportUUID: uuid)

let importHelper = ImportHelper(baseURL: containerURL, importUUID: uuid)
let validImportedFiles = try await prepareUploadSessionWith(importHelper: importHelper)

let organizationId = try await getOrganizationId()

let uploadApiFetcher = UploadApiFetcher(apiFetcher: userSession.apiFetcher, organizationId: organizationId)

await validImportedFiles.asyncForEach { importedFile in

Check warning on line 63 in EuriaCore/Attachment Upload/UploadManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

sending value of non-Sendable type '(ImportedFile) async -> Void' risks causing data races; this is an error in the Swift 6 language mode
let uploadTask = Task {
do {
let result = try await uploadApiFetcher.uploadFile(importedFile: importedFile)
await bridge?.callFunction(FileUploadDone(
ref: importedFile.ref,
remoteId: result.id,
name: result.name,
mimeType: result.mimeType
))
} catch UploadApiFetcher.DomainError.apiError(let rawJson) {
await bridge?.callFunction(FileUploadError(ref: importedFile.ref, error: rawJson))
} catch {
await bridge?.callFunction(FileUploadError(ref: importedFile.ref, error: ""))
}
}

currentUploadTasks[importedFile.ref] = uploadTask
await uploadTask.value
currentUploadTasks[importedFile.ref] = nil
}

cleanupOrphanUploadContainers(baseContainerURL: containerURL, currentImportUUID: nil)
}

func prepareUploadSessionWith(importHelper: ImportHelper) async throws -> [ImportedFile] {
let importedFileURLs = importHelper.importedFileURLs
let importedFiles: [ImportedFile] = importedFileURLs.map { ImportedFile(fileURL: $0) }

guard let validFileUUIDs = await bridge?.callFunction(PrepareFilesForUpload(files: importedFiles)) else {
throw DomainError.bridgeCommunicationFailed
}

let validFiles = importedFiles.filter { validFileUUIDs.contains($0.ref) }

guard !validFiles.isEmpty else {
throw DomainError.noValidFiles
}

return validFiles
}

func getOrganizationId() async throws -> Int {
guard let organizationId = await bridge?.callFunction(GetCurrentOrganizationId()),
organizationId > 0 else {
throw DomainError.invalidOrganizationId
}

return organizationId
}

func cleanupOrphanUploadContainers(baseContainerURL: URL, currentImportUUID: String?) {
let fileManager = FileManager.default
guard let containerContents = try? fileManager.contentsOfDirectory(at: baseContainerURL.importsDirectoryURL,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles) else {
return
}

for containerURL in containerContents where containerURL.lastPathComponent != currentImportUUID {
try? fileManager.removeItem(at: containerURL)
}
}

public func handleMessage(topic: JSMessageTopic, body: Any) {
guard topic == .cancelFileUpload,
let ref = body as? String else { return }
currentUploadTasks[ref]?.cancel()
}
}
40 changes: 40 additions & 0 deletions EuriaCore/Networking/Endpoint+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Infomaniak Euria - iOS App
Copyright (C) 2025 Infomaniak Network SA

This program 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.

This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import InfomaniakCore

public extension ApiEnvironment {
var euriaHost: String {
return "euria.\(host)"
}
}

extension Endpoint {
private static var euriaHost: Endpoint {
return Endpoint(hostKeypath: \.euriaHost, path: "")
}

private static var base: Endpoint {
return .euriaHost.appending(path: "/api/1")
}

static func uploadFile(organizationId: Int) -> Endpoint {
return base.appending(path: "/accounts/\(organizationId)/files")
}
}
Loading
Loading