Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metadata Network Connection #306

Merged
merged 18 commits into from
Apr 2, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class OrchestratedBiometricKycViewModel: ObservableObject {
private let allowNewEnroll: Bool
private let useStrictMode: Bool
private var extraPartnerParams: [String: String]
private let localMetadata = LocalMetadata()
private let metadataManager: MetadataManager = .shared
private var idInfo: IdInfo
private var consentInformation: ConsentInformation

Expand Down Expand Up @@ -170,7 +170,7 @@ class OrchestratedBiometricKycViewModel: ObservableObject {
jobType: .biometricKyc,
enrollment: false,
allowNewEnroll: allowNewEnroll,
localMetadata: localMetadata,
metadata: metadataManager.collectAllMetadata(),
partnerParams: extraPartnerParams
)
}
Expand All @@ -179,7 +179,7 @@ class OrchestratedBiometricKycViewModel: ObservableObject {
let prepUploadRequest = PrepUploadRequest(
partnerParams: authResponse.partnerParams.copy(extras: extraPartnerParams),
allowNewEnroll: String(allowNewEnroll), // TODO: - Fix when Michael changes this to boolean
metadata: localMetadata.metadata.items,
metadata: metadataManager.collectAllMetadata(),
timestamp: authResponse.timestamp,
signature: authResponse.signature
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ private let analysisSampleInterval: TimeInterval = 0.350
class DocumentCaptureViewModel: ObservableObject {
// Initializer properties
private let knownAspectRatio: Double?
private var localMetadata: LocalMetadata
private let metadataManager: MetadataManager = .shared
let metadataTimerStart = MonotonicTime()

// Other properties
private let defaultAspectRatio: Double
Expand Down Expand Up @@ -43,12 +44,10 @@ class DocumentCaptureViewModel: ObservableObject {

init(
knownAspectRatio: Double? = nil,
side: DocumentCaptureSide,
localMetadata: LocalMetadata
side: DocumentCaptureSide
) {
self.knownAspectRatio = knownAspectRatio
self.side = side
self.localMetadata = localMetadata
defaultAspectRatio = knownAspectRatio ?? 1.0
DispatchQueue.main.async { [self] in
idAspectRatio = defaultAspectRatio
Expand Down Expand Up @@ -101,13 +100,6 @@ class DocumentCaptureViewModel: ObservableObject {
})
}

let metadataTimerStart = MonotonicTime()

func updateLocalMetadata(_ newMetadata: LocalMetadata) {
self.localMetadata = newMetadata
objectWillChange.send()
}

@objc func showManualCapture() {
DispatchQueue.main.async {
self.showManualCaptureButton = true
Expand Down Expand Up @@ -161,16 +153,7 @@ class DocumentCaptureViewModel: ObservableObject {
/// Called if the user declines the image in the capture confirmation dialog.
func onRetry() {
documentImageOrigin = nil
switch side {
case .front:
localMetadata.metadata.removeAllOfType(Metadatum.DocumentFrontCaptureRetries.self)
localMetadata.metadata.removeAllOfType(Metadatum.DocumentFrontCaptureDuration.self)
localMetadata.metadata.removeAllOfType(Metadatum.DocumentFrontImageOrigin.self)
case .back:
localMetadata.metadata.removeAllOfType(Metadatum.DocumentBackCaptureRetries.self)
localMetadata.metadata.removeAllOfType(Metadatum.DocumentBackCaptureDuration.self)
localMetadata.metadata.removeAllOfType(Metadatum.DocumentBackImageOrigin.self)
}
resetDocumentCaptureMetadata()
retryCount += 1
DispatchQueue.main.async {
self.isCapturing = false
Expand All @@ -187,28 +170,55 @@ class DocumentCaptureViewModel: ObservableObject {
imageData: image,
aspectRatio: 1 / idAspectRatio
)
collectDocumentCaptureMetadata()
DispatchQueue.main.async { [self] in
documentImageToConfirm = croppedImage
isCapturing = false
}
}

private func resetDocumentCaptureMetadata() {
switch side {
case .front:
localMetadata.addMetadata(
Metadatum.DocumentFrontCaptureDuration(duration: metadataTimerStart.elapsedTime()))
localMetadata.addMetadata(Metadatum.DocumentFrontCaptureRetries(retries: retryCount))
metadataManager.removeMetadata(key: .documentFrontCaptureRetries)
metadataManager.removeMetadata(key: .documentFrontCaptureDuration)
metadataManager.removeMetadata(key: .documentFrontImageOrigin)
case .back:
metadataManager.removeMetadata(key: .documentBackCaptureRetries)
metadataManager.removeMetadata(key: .documentBackCaptureDuration)
metadataManager.removeMetadata(key: .documentBackImageOrigin)
}
}

private func collectDocumentCaptureMetadata() {
switch side {
case .front:
metadataManager.addMetadata(
key: .documentFrontCaptureDuration,
value: metadataTimerStart.elapsedTime().milliseconds()
)
metadataManager.addMetadata(
key: .documentFrontCaptureRetries,
value: String(retryCount)
)

if let documentImageOrigin {
localMetadata.addMetadata(
Metadatum.DocumentFrontImageOrigin(origin: documentImageOrigin))
metadataManager.addMetadata(key: .documentFrontImageOrigin, value: documentImageOrigin.rawValue)
}
case .back:
localMetadata.addMetadata(
Metadatum.DocumentBackCaptureDuration(duration: metadataTimerStart.elapsedTime()))
localMetadata.addMetadata(Metadatum.DocumentBackCaptureRetries(retries: retryCount))
metadataManager.addMetadata(
key: .documentBackCaptureDuration,
value: metadataTimerStart.elapsedTime().milliseconds()
)
metadataManager.addMetadata(
key: .documentBackCaptureRetries,
value: String(retryCount)
)

if let documentImageOrigin {
localMetadata.addMetadata(
Metadatum.DocumentBackImageOrigin(origin: documentImageOrigin))
metadataManager.addMetadata(key: .documentBackImageOrigin, value: documentImageOrigin.rawValue)
}
}
DispatchQueue.main.async { [self] in
documentImageToConfirm = croppedImage
isCapturing = false
}
}

/// Analyzes a single frame from the camera. No other frame will be processed until this one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class IOrchestratedDocumentVerificationViewModel<T, U: JobResult>: ObservableObj
var stepToRetry: DocumentCaptureFlow?
var didSubmitJob: Bool = false
var error: Error?
var localMetadata: LocalMetadata
let metadataManager: MetadataManager = .shared

// UI properties
@Published var acknowledgedInstructions = false
Expand All @@ -54,8 +54,7 @@ class IOrchestratedDocumentVerificationViewModel<T, U: JobResult>: ObservableObj
useStrictMode: Bool,
selfieFile: URL?,
jobType: JobType,
extraPartnerParams: [String: String] = [:],
localMetadata: LocalMetadata
extraPartnerParams: [String: String] = [:]
) {
self.userId = userId
self.jobId = jobId
Expand All @@ -69,7 +68,6 @@ class IOrchestratedDocumentVerificationViewModel<T, U: JobResult>: ObservableObj
self.selfieFile = selfieFile
self.jobType = jobType
self.extraPartnerParams = extraPartnerParams
self.localMetadata = localMetadata
}

func onFrontDocumentImageConfirmed(data: Data) {
Expand Down Expand Up @@ -204,15 +202,15 @@ class IOrchestratedDocumentVerificationViewModel<T, U: JobResult>: ObservableObj
jobType: jobType,
enrollment: false,
allowNewEnroll: allowNewEnroll,
localMetadata: localMetadata,
metadata: metadataManager.collectAllMetadata(),
partnerParams: extraPartnerParams
)
}
let authResponse = try await SmileID.api.authenticate(request: authRequest)
let prepUploadRequest = PrepUploadRequest(
partnerParams: authResponse.partnerParams.copy(extras: self.extraPartnerParams),
allowNewEnroll: String(allowNewEnroll), // TODO: - Fix when Michael changes this to boolean
metadata: localMetadata.metadata.items,
metadata: metadataManager.collectAllMetadata(),
timestamp: authResponse.timestamp,
signature: authResponse.signature
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public struct DocumentCaptureScreen: View {
let onError: (Error) -> Void
let onSkip: () -> Void

@EnvironmentObject private var localMetadata: LocalMetadata
@ObservedObject private var viewModel: DocumentCaptureViewModel

public init(
Expand Down Expand Up @@ -58,8 +57,7 @@ public struct DocumentCaptureScreen: View {

viewModel = DocumentCaptureViewModel(
knownAspectRatio: knownIdAspectRatio,
side: side,
localMetadata: LocalMetadata()
side: side
)
}

Expand All @@ -74,8 +72,6 @@ public struct DocumentCaptureScreen: View {
} else {
captureView
}
}.onAppear {
viewModel.updateLocalMetadata(localMetadata)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import SwiftUI

struct OrchestratedDocumentVerificationScreen: View {
@State private var localMetadata = LocalMetadata()
let countryCode: String
let documentType: String?
let captureBothSides: Bool
Expand Down Expand Up @@ -50,15 +49,13 @@ struct OrchestratedDocumentVerificationScreen: View {
useStrictMode: useStrictMode,
selfieFile: bypassSelfieCaptureWithFile,
jobType: .documentVerification,
extraPartnerParams: extraPartnerParams,
localMetadata: localMetadata
extraPartnerParams: extraPartnerParams
)
).environmentObject(localMetadata)
)
}
}

struct OrchestratedEnhancedDocumentVerificationScreen: View {
@State private var localMetadata = LocalMetadata()
let countryCode: String
let documentType: String?
let consentInformation: ConsentInformation
Expand Down Expand Up @@ -108,10 +105,9 @@ struct OrchestratedEnhancedDocumentVerificationScreen: View {
useStrictMode: useStrictMode,
selfieFile: bypassSelfieCaptureWithFile,
jobType: .enhancedDocumentVerification,
extraPartnerParams: extraPartnerParams,
localMetadata: localMetadata
extraPartnerParams: extraPartnerParams
)
).environmentObject(localMetadata)
)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SmileID/Classes/Helpers/LocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public class LocalStorage {
jobType: JobType,
enrollment: Bool,
allowNewEnroll: Bool,
localMetadata: LocalMetadata,
metadata: [Metadatum],
partnerParams: [String: String]
) throws {
do {
Expand All @@ -194,7 +194,7 @@ public class LocalStorage {
extras: partnerParams
),
allowNewEnroll: String(allowNewEnroll),
metadata: localMetadata.metadata.items,
metadata: metadata,
timestamp: "", // remove this so it is not stored offline
signature: "" // remove this so it is not stored offline
)
Expand Down
45 changes: 45 additions & 0 deletions Sources/SmileID/Classes/Helpers/NetworkMetadataProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import Network
import Combine

/// A class for determining the current network connection type (Wi-Fi, cellular, VPN, etc.)
class NetworkMetadataProvider {
private let monitor: NWPathMonitor
/// Array tracking connection types over time.
private var connectionTypes: [String] = []

init() {
monitor = NWPathMonitor()
monitor.pathUpdateHandler = { [weak self] path in
guard let self else { return }
let newConnection = if path.usesInterfaceType(.wifi) {
"wifi"
} else if path.usesInterfaceType(.cellular) {
"cellular"
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential difference with android smileidentity/android#580 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be the same behaviour and it gets json serialised into as string here -

if let jsonData = try? JSONSerialization.data(withJSONObject: connectionTypes, options: []),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend API expects the metadata values be converted into a json string no matter the type of data we are collecting.

"other"
}

if self.connectionTypes.last != newConnection {
self.connectionTypes.append(newConnection)
}
}

let queue = DispatchQueue(label: "NetworkMonitor")
monitor.start(queue: queue)
}

deinit {
monitor.cancel()
}
}

extension NetworkMetadataProvider: MetadataProvider {
func collectMetadata() -> [MetadataKey: String] {
if let jsonData = try? JSONSerialization.data(withJSONObject: connectionTypes, options: []),
let jsonString = String(data: jsonData, encoding: .utf8) {
return [.networkConnection: jsonString]
}
return [.networkConnection: "unknown"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

extension TimeInterval {
func milliseconds() -> String {
return String(Int(self * 1000))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation

func getIPAddress(useIPv4: Bool) -> String {
var address = ""
var ifaddr: UnsafeMutablePointer<ifaddrs>?

guard getifaddrs(&ifaddr) == 0 else {
return ""
}

var ptr = ifaddr
while ptr != nil {
defer { ptr = ptr?.pointee.ifa_next }

guard let interface = ptr?.pointee else {
return ""
}

let addrFamily = interface.ifa_addr.pointee.sa_family
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
let name = String(cString: interface.ifa_name)
if name == "en0" || name == "en1" || name == "pdp_ip0"
|| name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3" {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(
interface.ifa_addr,
socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count),
nil, socklen_t(0), NI_NUMERICHOST)
address = String(cString: hostname)

if (useIPv4 && addrFamily == UInt8(AF_INET))
|| (!useIPv4 && addrFamily == UInt8(AF_INET6)) {
if !useIPv4 {
if let percentIndex = address.firstIndex(of: "%") {
address = String(address[..<percentIndex])
.uppercased()
} else {
address = address.uppercased()
}
}
break
}
}
}
}

freeifaddrs(ifaddr)
return address
}
Loading
Loading