Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions Euria/Resources/Euria.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:euria.infomaniak.com</string>
<string>applinks:ksuite.infomaniak.com</string>
<string>activitycontinuation:ksuite.infomaniak.com</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.infomaniak.euria</string>
Expand Down
19 changes: 19 additions & 0 deletions Euria/Sources/EuriaApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,32 @@ struct EuriaApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate

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

var body: some Scene {
WindowGroup {
RootView()
.environmentObject(rootViewState)
.environmentObject(universalLinksState)
.ikButtonTheme(.euria)
.onOpenURL(perform: handleURL)
}
.defaultAppStorage(.shared)
}

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

Task {
// Sometimes, when navigating from a universal link, Euria can’t access the local storage right away,
// which causes the user to be logged out.
// To avoid this, we wait a few milliseconds before updating the state, giving Euria time to access it.
try? await Task.sleep(for: .milliseconds(500))

universalLinksState.linkedWebView = universalLink
}
}
}
67 changes: 67 additions & 0 deletions EuriaCore/UniversalLinkHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
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 struct IdentifiableURL: Identifiable, Equatable {
public var id: String { return url.absoluteString }
public let url: URL

init(url: URL) {
self.url = url
}

init?(string: String) {
guard let url = URL(string: string) else { return nil }
self.init(url: url)
}
}

public struct UniversalLinkHandler: Sendable {
public init() {}

public func handlePossibleUniversalLink(_ url: URL) -> IdentifiableURL? {
if let euriaUniversalLink = tryToHandleEuriaUniversalLink(url) {
return euriaUniversalLink
}

if let kSuiteUniversalLink = tryToHandleKSuiteUniversalLink(url) {
return kSuiteUniversalLink
}

return nil
}

private func tryToHandleEuriaUniversalLink(_ url: URL) -> IdentifiableURL? {
guard url.host() == ApiEnvironment.current.euriaHost else { return nil }

Check warning on line 52 in EuriaCore/UniversalLinkHandler.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

reference to static property 'current' is not concurrency-safe because it involves shared mutable state; this is an error in the Swift 6 language mode
return IdentifiableURL(url: url)
}

private func tryToHandleKSuiteUniversalLink(_ url: URL) -> IdentifiableURL? {
let urlPath = url.path()

if urlPath.starts(with: "/all"), let range = urlPath.range(of: "euria") {
let remainingPath = String(urlPath[range.upperBound...])
return IdentifiableURL(string: "https://\(ApiEnvironment.current.euriaHost)\(remainingPath)")

Check warning on line 61 in EuriaCore/UniversalLinkHandler.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

reference to static property 'current' is not concurrency-safe because it involves shared mutable state; this is an error in the Swift 6 language mode
} else {
let remainingPath = urlPath.replacingOccurrences(of: "/euria", with: "")
return IdentifiableURL(string: "https://\(ApiEnvironment.current.euriaHost)\(remainingPath)")

Check warning on line 64 in EuriaCore/UniversalLinkHandler.swift

View workflow job for this annotation

GitHub Actions / Build and Test project

reference to static property 'current' is not concurrency-safe because it involves shared mutable state; this is an error in the Swift 6 language mode
}
}
}
26 changes: 26 additions & 0 deletions EuriaCoreUI/Shared State/UniversalLinksState.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 EuriaCore
import Foundation

public class UniversalLinksState: ObservableObject {
@Published public var linkedWebView: IdentifiableURL?

public init() {}
}
19 changes: 14 additions & 5 deletions EuriaFeatures/MainView/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import SwiftUI
import WebKit

public struct MainView: View {
@EnvironmentObject private var universalLinksState: UniversalLinksState

@StateObject private var webViewDelegate: EuriaWebViewDelegate

@State private var webViewURL: URL
@State private var isShowingWebView = true
@State private var isShowingErrorAlert = false

Expand All @@ -46,17 +49,17 @@ public struct MainView: View {
}

public init(session: any UserSessionable) {
_webViewDelegate = StateObject(wrappedValue: EuriaWebViewDelegate(
host: ApiEnvironment.current.euriaHost,
session: session
))
let euriaHost = ApiEnvironment.current.euriaHost

_webViewURL = State(wrappedValue: URL(string: "https://\(euriaHost)/")!)
_webViewDelegate = StateObject(wrappedValue: EuriaWebViewDelegate(host: euriaHost, session: session))
}

public var body: some View {
ZStack {
if isShowingWebView {
WebView(
url: URL(string: "https://\(ApiEnvironment.current.euriaHost)/")!,
url: webViewURL,
webConfiguration: webViewDelegate.webConfiguration,
webViewCoordinator: webViewDelegate
)
Expand Down Expand Up @@ -89,6 +92,12 @@ public struct MainView: View {
guard !webViewDelegate.isLoaded else { return }
isShowingWebView = isConnected
}
.onChange(of: universalLinksState.linkedWebView) { identifiableURL in
guard let identifiableURL else { return }

webViewURL = identifiableURL.url
universalLinksState.linkedWebView = nil
}
.sceneLifecycle(willEnterForeground: willEnterForeground)
}

Expand Down
20 changes: 17 additions & 3 deletions EuriaFeatures/MainView/WebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ final class EuriaWebView: WKWebView {

struct WebView<WebViewCoordinator>: UIViewRepresentable {
let url: URL
var webConfiguration = WKWebViewConfiguration()
let webConfiguration: WKWebViewConfiguration
var webViewCoordinator: WebViewCoordinator?

init(url: URL, webConfiguration: WKWebViewConfiguration = WKWebViewConfiguration(), webViewCoordinator: WebViewCoordinator?) {
self.url = url
self.webConfiguration = webConfiguration
self.webViewCoordinator = webViewCoordinator
}

func makeUIView(context: Context) -> WKWebView {
let webView = EuriaWebView(frame: .zero, configuration: webConfiguration)
setupWebView(webView, coordinator: webViewCoordinator)
Expand All @@ -42,8 +48,13 @@ struct WebView<WebViewCoordinator>: UIViewRepresentable {
return webView
}

func updateUIView(_ uiView: WKWebView, context: Context) {
// Update the view.
func updateUIView(_ webView: WKWebView, context: Context) {
guard url != webView.url else {
return
}

let request = URLRequest(url: url)
webView.load(request)
}

private func setupWebView(_ webView: WKWebView, coordinator webViewCoordinator: WebViewCoordinator?) {
Expand All @@ -63,6 +74,9 @@ struct WebView<WebViewCoordinator>: UIViewRepresentable {
if let uiDelegate = webViewCoordinator as? WKUIDelegate {
webView.uiDelegate = uiDelegate
}

let request = URLRequest(url: url)
webView.load(request)
}

private func configureScrollView(_ webView: WKWebView) {
Expand Down
Loading