Skip to content

Commit b14f11c

Browse files
committed
Add Associated Domains entitlement and Universal Links handling. (#183)
* NFC-141 Add Associated Domains entitlement and Universal Links hanlding. * NFC-141 Refactor WebEidUriUtil.
1 parent 640ea87 commit b14f11c

6 files changed

Lines changed: 250 additions & 48 deletions

File tree

RIADigiDoc.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@
263263
Util/Signature/SignatureUtilProtocol.swift,
264264
Util/Theme/ThemeSettings.swift,
265265
Util/Theme/ThemeSettingsProtocol.swift,
266+
Util/WebEid/WebEidUriUtil.swift,
266267
ViewModel/AdvancedSettingsViewModel.swift,
267268
ViewModel/CertificateDetailViewModel.swift,
268269
ViewModel/Crypto/DecryptRootViewModel.swift,

RIADigiDoc/RIADigiDoc.entitlements

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>com.apple.developer.associated-domains</key>
6+
<array>
7+
<string>applinks:riadigidoc.ee</string>
8+
</array>
59
<key>com.apple.developer.nfc.readersession.formats</key>
610
<array>
711
<string>TAG</string>

RIADigiDoc/UI/Component/HomeView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ struct HomeView: View {
308308
}
309309

310310
private func handleIncoming(url: URL) {
311-
let webEidURL = (url.scheme == "web-eid-mobile") ? url : nil
311+
let webEidURL = (WebEidUriUtil.isWebEidUri(url)) ? url : nil
312312

313313
let externalFileURLs: [URL] = (webEidURL != nil) ? [] : getExternalFileURLs(from: url)
314314
handleFiles(externalFileURLs)

RIADigiDoc/UI/Component/WebEid/WebEidView.swift

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,52 @@ struct WebEidView: View {
5050
)
5151
}
5252

53+
private var alertTitle: String {
54+
languageSettings.localized(
55+
viewModel.alertMessageKey ?? "",
56+
viewModel.alertMessageExtraArguments
57+
)
58+
}
59+
60+
private var alertInfoURL: URL? {
61+
guard
62+
let messageUrl = viewModel.alertMessageUrl,
63+
!messageUrl.isEmpty
64+
else {
65+
return nil
66+
}
67+
68+
let localizedUrl = languageSettings.localized(messageUrl)
69+
guard let url = URL(string: localizedUrl), UIApplication.shared.canOpenURL(url) else {
70+
return nil
71+
}
72+
73+
return url
74+
}
75+
76+
private func handleWebEidOperation(for url: URL) {
77+
let operation = WebEidUriUtil.getOperation(from: url)
78+
switch operation {
79+
case .auth:
80+
viewModel.handleAuth(url: url)
81+
case .cert:
82+
viewModel.handleCertificate(url: url)
83+
case .sign:
84+
viewModel.handleSign(url: url)
85+
case .unknown:
86+
viewModel.handleUnknown(url: url)
87+
}
88+
}
89+
90+
private func activateWebEidSession() {
91+
Task {
92+
if await viewModel.isWebEidSessionActive() {
93+
await nfcViewModel.clearTempCAN()
94+
}
95+
await viewModel.setWebEidSessionActive(true)
96+
}
97+
}
98+
5399
init(
54100
webEidUrl: URL,
55101
) {
@@ -119,40 +165,22 @@ struct WebEidView: View {
119165
}
120166
}
121167
.alert(
122-
languageSettings.localized(
123-
viewModel.alertMessageKey ?? "",
124-
viewModel.alertMessageExtraArguments
125-
),
168+
alertTitle,
126169
isPresented: $viewModel.showAlertMessage
127170
) {
128171
Button(languageSettings.localized("OK")) {
129172
viewModel.resetErrors()
130173
}
131174

132-
if let messageUrl = viewModel.alertMessageUrl, !messageUrl.isEmpty {
175+
if let alertInfoURL {
133176
Button(languageSettings.localized("Additional information")) {
134-
if let url = URL(string: languageSettings.localized(messageUrl)),
135-
UIApplication.shared.canOpenURL(url) {
136-
openURL(url)
137-
}
177+
openURL(alertInfoURL)
138178
viewModel.resetErrors()
139179
}
140180
}
141181
}
142182
.onAppear {
143-
if let host = webEidUrl.host {
144-
145-
switch host {
146-
case "auth":
147-
viewModel.handleAuth(url: webEidUrl)
148-
case "cert":
149-
viewModel.handleCertificate(url: webEidUrl)
150-
case "sign":
151-
viewModel.handleSign(url: webEidUrl)
152-
default:
153-
viewModel.handleUnknown(url: webEidUrl)
154-
}
155-
}
183+
handleWebEidOperation(for: webEidUrl)
156184
}
157185
.onChange(of: viewModel.relyingPartyResponseEvents) { _, responseURL in
158186
guard let responseURL else { return }
@@ -166,35 +194,13 @@ struct WebEidView: View {
166194
Toast.show(errorMessage)
167195
}
168196
.onChange(of: webEidUrl) {_, url in
169-
if let host = url.host {
170-
171-
switch host {
172-
case "auth":
173-
viewModel.handleAuth(url: url)
174-
case "cert":
175-
viewModel.handleCertificate(url: url)
176-
case "sign":
177-
viewModel.handleSign(url: url)
178-
default:
179-
viewModel.handleUnknown(url: url)
180-
}
181-
}
197+
handleWebEidOperation(for: url)
182198
}
183199
.onChange(of: viewModel.authRequest) {_, _ in
184-
Task {
185-
if await viewModel.isWebEidSessionActive() {
186-
await nfcViewModel.clearTempCAN()
187-
}
188-
await viewModel.setWebEidSessionActive(true)
189-
}
200+
activateWebEidSession()
190201
}
191202
.onChange(of: viewModel.certRequest) {_, _ in
192-
Task {
193-
if await viewModel.isWebEidSessionActive() {
194-
await nfcViewModel.clearTempCAN()
195-
}
196-
await viewModel.setWebEidSessionActive(true)
197-
}
203+
activateWebEidSession()
198204
}
199205
}
200206
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2017 - 2026 Riigi Infosüsteemi Amet
3+
*
4+
* This library is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2.1 of the License, or (at your option) any later version.
8+
*
9+
* This library is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with this library; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
*/
19+
20+
import Foundation
21+
22+
public enum WebEidOperation: String, CaseIterable, Sendable {
23+
case auth
24+
case cert
25+
case sign
26+
case unknown
27+
28+
public static func fromOperation(_ operation: String) -> WebEidOperation {
29+
Self.allCases.first { $0.rawValue == operation } ?? WebEidOperation.unknown
30+
}
31+
}
32+
33+
public enum WebEidUriUtil {
34+
private static let customScheme = "web-eid-mobile"
35+
private static let appLinksHost = "riadigidoc.ee"
36+
37+
public static func isWebEidUri(_ url: URL) -> Bool {
38+
getOperation(from: url) != WebEidOperation.unknown
39+
}
40+
41+
public static func getOperation(from url: URL) -> WebEidOperation {
42+
var operation: String?
43+
44+
#if DEBUG
45+
let isCustomSchemeMatch = url.scheme == customScheme
46+
#else
47+
let isCustomSchemeMatch = false
48+
#endif
49+
50+
if isCustomSchemeMatch {
51+
operation = url.host
52+
} else if url.scheme == "https", url.host == appLinksHost {
53+
operation = url.pathComponents.dropFirst().first
54+
} else {
55+
operation = WebEidOperation.unknown.rawValue
56+
}
57+
58+
59+
guard let operation else { return WebEidOperation.unknown }
60+
return WebEidOperation.fromOperation(operation)
61+
}
62+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2017 - 2026 Riigi Infosüsteemi Amet
3+
*
4+
* This library is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2.1 of the License, or (at your option) any later version.
8+
*
9+
* This library is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with this library; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
*/
19+
20+
21+
import Foundation
22+
import Testing
23+
24+
struct WebEidUriUtilTests {
25+
26+
private func makeURL(_ string: String) -> URL {
27+
URL(string: string)!
28+
}
29+
30+
@Test
31+
func isWebEidUri_appLinks_auth() {
32+
#expect(WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/auth")))
33+
}
34+
35+
@Test
36+
func isWebEidUri_appLinks_cert() {
37+
#expect(WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/cert")))
38+
}
39+
40+
@Test
41+
func isWebEidUri_appLinks_sign() {
42+
#expect(WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/sign")))
43+
}
44+
45+
@Test
46+
func isWebEidUri_appLinks_unknownOperation() {
47+
#expect(!WebEidUriUtil.isWebEidUri(makeURL("https://riadigidoc.ee/unknown")))
48+
}
49+
50+
@Test
51+
func isWebEidUri_wrongHost() {
52+
#expect(!WebEidUriUtil.isWebEidUri(makeURL("https://evil.com/auth")))
53+
}
54+
55+
@Test
56+
func isWebEidUri_contentScheme() {
57+
#expect(!WebEidUriUtil.isWebEidUri(makeURL("content://some/path")))
58+
}
59+
60+
@Test
61+
func isWebEidUri_fileScheme() {
62+
#expect(!WebEidUriUtil.isWebEidUri(makeURL("file:///some/path")))
63+
}
64+
65+
@Test
66+
func getOperation_appLinks_auth() {
67+
#expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/auth#dGVzdA")) == .auth)
68+
}
69+
70+
@Test
71+
func getOperation_appLinks_cert() {
72+
#expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/cert#dGVzdA")) == .cert)
73+
}
74+
75+
@Test
76+
func getOperation_appLinks_sign() {
77+
#expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/sign#dGVzdA")) == .sign)
78+
}
79+
80+
@Test
81+
func getOperation_appLinks_unknownOperation_returnsNil() {
82+
#expect(WebEidUriUtil.getOperation(from: makeURL("https://riadigidoc.ee/unknown")) == .unknown)
83+
}
84+
85+
@Test
86+
func getOperation_unrelatedUri_returnsNil() {
87+
#expect(WebEidUriUtil.getOperation(from: makeURL("https://example.com/auth")) == .unknown)
88+
}
89+
90+
@Test
91+
func isWebEidUri_customScheme_auth() {
92+
#expect(WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://auth")))
93+
}
94+
95+
@Test
96+
func isWebEidUri_customScheme_cert() {
97+
#expect(WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://cert")))
98+
}
99+
100+
@Test
101+
func isWebEidUri_customScheme_sign() {
102+
#expect(WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://sign")))
103+
}
104+
105+
@Test
106+
func isWebEidUri_customScheme_unknownOperation() {
107+
#expect(!WebEidUriUtil.isWebEidUri(makeURL("web-eid-mobile://unknown")))
108+
}
109+
110+
@Test
111+
func getOperation_customScheme_auth() {
112+
#expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://auth#dGVzdA")) == .auth)
113+
}
114+
115+
@Test
116+
func getOperation_customScheme_cert() {
117+
#expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://cert#dGVzdA")) == .cert)
118+
}
119+
120+
@Test
121+
func getOperation_customScheme_sign() {
122+
#expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://sign#dGVzdA")) == .sign)
123+
}
124+
125+
@Test
126+
func getOperation_unknownOperation_returnsNil() {
127+
#expect(WebEidUriUtil.getOperation(from: makeURL("web-eid-mobile://unknown")) == .unknown)
128+
}
129+
}

0 commit comments

Comments
 (0)