diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift index 822373e296..4692da0270 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift @@ -23,8 +23,9 @@ public enum LivenessEventKind { self.rawValue = rawValue } - public static let challenge = Self(rawValue: "ServerSessionInformationEvent") + public static let sessionInformation = Self(rawValue: "ServerSessionInformationEvent") public static let disconnect = Self(rawValue: "DisconnectionEvent") + public static let challenge = Self(rawValue: "ChallengeEvent") } case server(Server) @@ -60,6 +61,7 @@ extension LivenessEventKind: CustomDebugStringConvertible { public var debugDescription: String { switch self { case .server(.challenge): return ".server(.challenge)" + case .server(.sessionInformation): return ".server(.sessionInformation)" case .server(.disconnect): return ".server(.disconnect)" case .client(.initialFaceDetected): return ".client(.initialFaceDetected)" case .client(.video): return ".client(.video)" diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFinalCientEvent.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFinalClientEvent.swift similarity index 55% rename from AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFinalCientEvent.swift rename to AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFinalClientEvent.swift index 13f7ba1221..749df67a24 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFinalCientEvent.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFinalClientEvent.swift @@ -26,11 +26,13 @@ public struct FinalClientEvent { extension LivenessEvent where T == FinalClientEvent { @_spi(PredictionsFaceLiveness) - public static func final(event: FinalClientEvent) throws -> Self { - - let clientEvent = ClientSessionInformationEvent( - challenge: .init( - faceMovementAndLightChallenge: .init( + public static func final(event: FinalClientEvent, + challenge: Challenge) throws -> Self { + let clientChallengeType: ClientChallenge.ChallengeType + switch challenge.type { + case .faceMovementAndLightChallenge: + clientChallengeType = .faceMovementAndLightChallenge( + challenge: .init( challengeID: event.initialClientEvent.challengeID, targetFace: .init( boundingBox: .init(boundingBox: event.targetFace.initialEvent.boundingBox), @@ -46,7 +48,26 @@ extension LivenessEvent where T == FinalClientEvent { videoEndTimeStamp: Date().epochMilliseconds ) ) - ) + case .faceMovementChallenge: + clientChallengeType = .faceMovementChallenge( + challenge: .init( + challengeID: event.initialClientEvent.challengeID, + targetFace: .init( + boundingBox: .init(boundingBox: event.targetFace.initialEvent.boundingBox), + faceDetectedInTargetPositionStartTimestamp: event.targetFace.initialEvent.startTimestamp, + faceDetectedInTargetPositionEndTimestamp: event.targetFace.endTimestamp + ), + initialFace: .init( + boundingBox: .init(boundingBox: event.initialClientEvent.initialFaceLocation.boundingBox), + initialFaceDetectedTimeStamp: event.initialClientEvent.initialFaceLocation.startTimestamp + ), + videoStartTimestamp: nil, + videoEndTimeStamp: Date().epochMilliseconds + ) + ) + } + + let clientEvent = ClientSessionInformationEvent(challenge: .init(clientChallengeType: clientChallengeType)) let payload = try JSONEncoder().encode(clientEvent) return .init( payload: payload, diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFreshnessEvent.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFreshnessEvent.swift index 7bd2d22887..39a1fb8121 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFreshnessEvent.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessFreshnessEvent.swift @@ -29,18 +29,20 @@ extension LivenessEvent where T == FreshnessEvent { public static func freshness(event: FreshnessEvent) throws -> Self { let clientEvent = ClientSessionInformationEvent( challenge: .init( - faceMovementAndLightChallenge: .init( - challengeID: event.challengeID, - targetFace: nil, - initialFace: nil, - videoStartTimestamp: nil, - colorDisplayed: .init( - currentColor: .init(rgb: event.color), - sequenceNumber: event.sequenceNumber, - currentColorStartTimeStamp: event.timestamp, - previousColor: .init(rgb: event.previousColor) - ), - videoEndTimeStamp: nil + clientChallengeType: .faceMovementAndLightChallenge( + challenge: .init( + challengeID: event.challengeID, + targetFace: nil, + initialFace: nil, + videoStartTimestamp: nil, + colorDisplayed: .init( + currentColor: .init(rgb: event.color), + sequenceNumber: event.sequenceNumber, + currentColorStartTimeStamp: event.timestamp, + previousColor: .init(rgb: event.previousColor) + ), + videoEndTimeStamp: nil + ) ) ) ) diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessInitialClientEvent.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessInitialClientEvent.swift index 9b522f9680..b586992171 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessInitialClientEvent.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessInitialClientEvent.swift @@ -26,15 +26,20 @@ public struct InitialClientEvent { extension LivenessEvent where T == InitialClientEvent { @_spi(PredictionsFaceLiveness) - public static func initialFaceDetected(event: InitialClientEvent) throws -> Self { + public static func initialFaceDetected( + event: InitialClientEvent, + challenge: Challenge + ) throws -> Self { let initialFace = InitialFace( boundingBox: .init(boundingBox: event.initialFaceLocation.boundingBox), initialFaceDetectedTimeStamp: event.initialFaceLocation.startTimestamp ) - let clientSessionInformationEvent = ClientSessionInformationEvent( - challenge: .init( - faceMovementAndLightChallenge: .init( + let clientChallengeType: ClientChallenge.ChallengeType + switch challenge.type { + case .faceMovementAndLightChallenge: + clientChallengeType = .faceMovementAndLightChallenge( + challenge: .init( challengeID: event.challengeID, targetFace: nil, initialFace: initialFace, @@ -43,8 +48,21 @@ extension LivenessEvent where T == InitialClientEvent { videoEndTimeStamp: nil ) ) + case .faceMovementChallenge: + clientChallengeType = .faceMovementChallenge( + challenge: .init( + challengeID: event.challengeID, + targetFace: nil, + initialFace: initialFace, + videoStartTimestamp: event.videoStartTimestamp, + videoEndTimeStamp: nil + ) + ) + } + + let clientSessionInformationEvent = ClientSessionInformationEvent( + challenge: .init(clientChallengeType: clientChallengeType) ) - let payload = try JSONEncoder().encode(clientSessionInformationEvent) return .init( payload: payload, diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/DTOMapping.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/DTOMapping.swift index 9fc9cca08a..64d52ebfc6 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/DTOMapping.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/DTOMapping.swift @@ -8,8 +8,18 @@ import Foundation func ovalChallenge(from event: ServerSessionInformationEvent) -> FaceLivenessSession.OvalMatchChallenge { - let challengeConfig = event.sessionInformation.challenge.faceMovementAndLightChallenge.challengeConfig - let ovalParameters = event.sessionInformation.challenge.faceMovementAndLightChallenge.ovalParameters + let challengeConfig: ChallengeConfig + let ovalParameters: OvalParameters + + switch event.sessionInformation.challenge.type { + case .faceMovementAndLightChallenge(let challenge): + challengeConfig = challenge.challengeConfig + ovalParameters = challenge.ovalParameters + case .faceMovementChallenge(let challenge): + challengeConfig = challenge.challengeConfig + ovalParameters = challenge.ovalParameters + } + let ovalBoundingBox = FaceLivenessSession.BoundingBox.init( x: Double(ovalParameters.centerX - ovalParameters.width / 2), y: Double(ovalParameters.centerY - ovalParameters.height / 2), @@ -37,44 +47,46 @@ func ovalChallenge(from event: ServerSessionInformationEvent) -> FaceLivenessSes ) } -func colorChallenge(from event: ServerSessionInformationEvent) -> FaceLivenessSession.ColorChallenge { - let displayColors = event.sessionInformation.challenge - .faceMovementAndLightChallenge.colorSequences - .map({ color -> FaceLivenessSession.DisplayColor in +func colorChallenge(from event: ServerSessionInformationEvent) -> FaceLivenessSession.ColorChallenge? { + switch event.sessionInformation.challenge.type { + case .faceMovementAndLightChallenge(let challenge): + let displayColors = challenge.colorSequences + .map({ color -> FaceLivenessSession.DisplayColor in - let duration: Double - let shouldScroll: Bool - switch (color.downscrollDuration, color.flatDisplayDuration) { - case (...0, 0...): - duration = Double(color.flatDisplayDuration) - shouldScroll = false - default: - duration = Double(color.downscrollDuration) - shouldScroll = true - } + let duration: Double + let shouldScroll: Bool + switch (color.downscrollDuration, color.flatDisplayDuration) { + case (...0, 0...): + duration = Double(color.flatDisplayDuration) + shouldScroll = false + default: + duration = Double(color.downscrollDuration) + shouldScroll = true + } - precondition( - color.freshnessColor.rgb.count == 3, - """ - Received invalid freshness colors. - Expected 3 values (r, g, b), received: \(color.freshnessColor.rgb.count) - """ - ) + precondition( + color.freshnessColor.rgb.count == 3, + """ + Received invalid freshness colors. + Expected 3 values (r, g, b), received: \(color.freshnessColor.rgb.count) + """ + ) - return .init( - rgb: .init( - red: Double(color.freshnessColor.rgb[0]) / 255, - green: Double(color.freshnessColor.rgb[1]) / 255, - blue: Double(color.freshnessColor.rgb[2]) / 255, - _values: color.freshnessColor.rgb - ), - duration: duration, - shouldScroll: shouldScroll - ) - }) - return .init( - colors: displayColors - ) + return .init( + rgb: .init( + red: Double(color.freshnessColor.rgb[0]) / 255, + green: Double(color.freshnessColor.rgb[1]) / 255, + blue: Double(color.freshnessColor.rgb[2]) / 255, + _values: color.freshnessColor.rgb + ), + duration: duration, + shouldScroll: shouldScroll + ) + }) + return .init(colors: displayColors) + case .faceMovementChallenge: + return nil + } } func sessionConfiguration(from event: ServerSessionInformationEvent) -> FaceLivenessSession.SessionConfiguration { @@ -83,3 +95,10 @@ func sessionConfiguration(from event: ServerSessionInformationEvent) -> FaceLive ovalMatchChallenge: ovalChallenge(from: event) ) } + +func challengeType(from event: ChallengeEvent) -> Challenge { + .init( + version: event.version, + type: event.type + ) +} diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+Challenge.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+Challenge.swift new file mode 100644 index 0000000000..d399f166d7 --- /dev/null +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+Challenge.swift @@ -0,0 +1,15 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension FaceLivenessSession { + public static let supportedChallenges: [Challenge] = [ + Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge), + Challenge(version: "1.0.0", type: .faceMovementChallenge) + ] +} diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+SessionConfiguration.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+SessionConfiguration.swift index 139e28f0ed..7b9b6040eb 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+SessionConfiguration.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Model/FaceLivenessSession+SessionConfiguration.swift @@ -10,10 +10,13 @@ import Foundation extension FaceLivenessSession { @_spi(PredictionsFaceLiveness) public struct SessionConfiguration { - public let colorChallenge: ColorChallenge + public let colorChallenge: ColorChallenge? public let ovalMatchChallenge: OvalMatchChallenge - public init(colorChallenge: ColorChallenge, ovalMatchChallenge: OvalMatchChallenge) { + public init( + colorChallenge: ColorChallenge? = nil, + ovalMatchChallenge: OvalMatchChallenge + ) { self.colorChallenge = colorChallenge self.ovalMatchChallenge = ovalMatchChallenge } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift index d2eec8d96e..736f5541ee 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift @@ -36,7 +36,8 @@ extension AWSPredictionsPlugin { let session = FaceLivenessSession( websocket: WebSocketSession(), signer: signer, - baseURL: url + baseURL: url, + options: options ) session.onServiceException = { completion(.failure($0)) } @@ -48,7 +49,16 @@ extension AWSPredictionsPlugin { extension FaceLivenessSession { @_spi(PredictionsFaceLiveness) public struct Options { - public init() {} + public let viewId: String + public let preCheckViewEnabled: Bool + + public init( + faceLivenessDetectorViewId: String, + preCheckViewEnabled: Bool + ) { + self.viewId = faceLivenessDetectorViewId + self.preCheckViewEnabled = preCheckViewEnabled + } } } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/Challenge.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/Challenge.swift new file mode 100644 index 0000000000..d98fccae0e --- /dev/null +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/Challenge.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +@_spi(PredictionsFaceLiveness) +public struct Challenge { + public let version: String + public let type: ChallengeType + + public init(version: String, type: ChallengeType) { + self.version = version + self.type = type + } + + public func queryParameterString() -> String { + return self.type.rawValue + "_" + self.version + } +} + +@_spi(PredictionsFaceLiveness) +public enum ChallengeType: String, Codable { + case faceMovementChallenge = "FaceMovementChallenge" + case faceMovementAndLightChallenge = "FaceMovementAndLightChallenge" +} diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift index 092532d8e3..586e5ad446 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift @@ -16,6 +16,7 @@ public final class FaceLivenessSession: LivenessService { let signer: SigV4Signer let baseURL: URL var serverEventListeners: [LivenessEventKind.Server: (FaceLivenessSession.SessionConfiguration) -> Void] = [:] + var challengeTypeListeners: [LivenessEventKind.Server: (Challenge) -> Void] = [:] var onComplete: (ServerDisconnection) -> Void = { _ in } var serverDate: Date? var savedURLForReconnect: URL? @@ -25,6 +26,7 @@ public final class FaceLivenessSession: LivenessService { case normal case reconnect } + let options: FaceLivenessSession.Options private let livenessServiceDispatchQueue = DispatchQueue( label: "com.amazon.aws.amplify.liveness.service", @@ -33,12 +35,14 @@ public final class FaceLivenessSession: LivenessService { init( websocket: WebSocketSession, signer: SigV4Signer, - baseURL: URL + baseURL: URL, + options: FaceLivenessSession.Options ) { self.eventStreamEncoder = EventStream.Encoder() self.eventStreamDecoder = EventStream.Decoder() self.signer = signer self.baseURL = baseURL + self.options = options self.websocket = websocket @@ -69,16 +73,27 @@ public final class FaceLivenessSession: LivenessService { ) { serverEventListeners[event] = listener } + + public func register(listener: @escaping (Challenge) -> Void, on event: LivenessEventKind.Server) { + challengeTypeListeners[event] = listener + } public func closeSocket(with code: URLSessionWebSocketTask.CloseCode) { websocket.close(with: code) } - public func initializeLivenessStream(withSessionID sessionID: String, userAgent: String = "") throws { + public func initializeLivenessStream(withSessionID sessionID: String, + userAgent: String = "", + challenges: [Challenge] = FaceLivenessSession.supportedChallenges) throws { var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) + components?.queryItems = [ URLQueryItem(name: "session-id", value: sessionID), - URLQueryItem(name: "challenge-versions", value: "FaceMovementAndLightChallenge_1.0.0"), + URLQueryItem(name: "precheck-view-enabled", value: options.preCheckViewEnabled ? "1":"0"), + // TODO: Change this after confirmation + URLQueryItem(name: "attempt-id", value: options.viewId), + URLQueryItem(name: "challenge-versions", + value: challenges.map({$0.queryParameterString()}).joined(separator: ",")), URLQueryItem(name: "video-width", value: "480"), URLQueryItem(name: "video-height", value: "640"), URLQueryItem(name: "x-amz-user-agent", value: userAgent) @@ -140,6 +155,9 @@ public final class FaceLivenessSession: LivenessService { if let payload = try? JSONDecoder().decode(ServerSessionInformationEvent.self, from: message.payload) { let sessionConfiguration = sessionConfiguration(from: payload) self.serverEventListeners[.challenge]?(sessionConfiguration) + } else if let payload = try? JSONDecoder().decode(ChallengeEvent.self, from: message.payload) { + let challengeType = challengeType(from: payload) + self.challengeTypeListeners[.challenge]?(challengeType) } else if (try? JSONDecoder().decode(DisconnectEvent.self, from: message.payload)) != nil { onComplete(.disconnectionEvent) return .stopAndInvalidateSession @@ -157,6 +175,14 @@ public final class FaceLivenessSession: LivenessService { let serverEvent = LivenessEventKind.Server(rawValue: eventType.value) switch serverEvent { case .challenge: + // :event-type ChallengeEvent + let payload = try JSONDecoder().decode( + ChallengeEvent.self, from: message.payload + ) + let challengeType = challengeType(from: payload) + challengeTypeListeners[.challenge]?(challengeType) + return true + case .sessionInformation: // :event-type ServerSessionInformationEvent let payload = try JSONDecoder().decode( ServerSessionInformationEvent.self, from: message.payload diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSessionRepresentable.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSessionRepresentable.swift index 896ef5769b..49f2dbe295 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSessionRepresentable.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSessionRepresentable.swift @@ -19,12 +19,19 @@ public protocol LivenessService { func register(onComplete: @escaping (ServerDisconnection) -> Void) - func initializeLivenessStream(withSessionID sessionID: String, userAgent: String) throws + func initializeLivenessStream(withSessionID sessionID: String, + userAgent: String, + challenges: [Challenge]) throws func register( listener: @escaping (FaceLivenessSession.SessionConfiguration) -> Void, on event: LivenessEventKind.Server ) + + func register( + listener: @escaping (Challenge) -> Void, + on event: LivenessEventKind.Server + ) func closeSocket(with code: URLSessionWebSocketTask.CloseCode) } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ChallengeEvent.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ChallengeEvent.swift new file mode 100644 index 0000000000..8401c8ec20 --- /dev/null +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ChallengeEvent.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct ChallengeEvent: Codable { + let version: String + let type: ChallengeType + + enum CodingKeys: String, CodingKey { + case version = "Version" + case type = "Type" + } +} diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ClientChallenge.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ClientChallenge.swift index 97b3067010..af2e4157d5 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ClientChallenge.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ClientChallenge.swift @@ -8,9 +8,47 @@ import Foundation struct ClientChallenge: Codable { - let faceMovementAndLightChallenge: FaceMovementAndLightClientChallenge? - + let type: ChallengeType + + init(clientChallengeType: ChallengeType) { + self.type = clientChallengeType + } + enum CodingKeys: String, CodingKey { case faceMovementAndLightChallenge = "FaceMovementAndLightChallenge" + case faceMovementChallenge = "FaceMovementChallenge" + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self.type { + case .faceMovementChallenge(let faceMovementServerChallenge): + try container.encode(faceMovementServerChallenge, forKey: .faceMovementChallenge) + case .faceMovementAndLightChallenge(let faceMovementAndLightServerChallenge): + try container.encode(faceMovementAndLightServerChallenge, forKey: .faceMovementAndLightChallenge) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let value = try? container.decode(FaceMovementClientChallenge.self, forKey: .faceMovementChallenge) { + self.type = .faceMovementChallenge(challenge: value) + } else if let value = try? container.decode(FaceMovementAndLightClientChallenge.self, forKey: .faceMovementAndLightChallenge) { + self.type = .faceMovementAndLightChallenge(challenge: value) + } else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Unexpected data format" + ) + ) + } + } +} + +extension ClientChallenge { + enum ChallengeType: Codable { + case faceMovementChallenge(challenge: FaceMovementClientChallenge) + case faceMovementAndLightChallenge(challenge: FaceMovementAndLightClientChallenge) } } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/FaceMovementClientChallenge.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/FaceMovementClientChallenge.swift new file mode 100644 index 0000000000..9ab47ccf47 --- /dev/null +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/FaceMovementClientChallenge.swift @@ -0,0 +1,24 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct FaceMovementClientChallenge: Codable { + let challengeID: String + let targetFace: TargetFace? + let initialFace: InitialFace? + let videoStartTimestamp: UInt64? + let videoEndTimeStamp: UInt64? + + enum CodingKeys: String, CodingKey { + case challengeID = "ChallengeId" + case targetFace = "TargetFace" + case initialFace = "InitialFace" + case videoStartTimestamp = "VideoStartTimestamp" + case videoEndTimeStamp = "VideoEndTimestamp" + } +} diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/FaceMovementServerChallenge.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/FaceMovementServerChallenge.swift new file mode 100644 index 0000000000..0c3101c533 --- /dev/null +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/FaceMovementServerChallenge.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +struct FaceMovementServerChallenge: Codable { + let ovalParameters: OvalParameters + let challengeConfig: ChallengeConfig + + enum CodingKeys: String, CodingKey { + case challengeConfig = "ChallengeConfig" + case ovalParameters = "OvalParameters" + } +} diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ServerChallenge.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ServerChallenge.swift index d811c2a7f6..49aed90b5b 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ServerChallenge.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/ServiceModel/ServerChallenge.swift @@ -8,9 +8,43 @@ import Foundation struct ServerChallenge: Codable { - let faceMovementAndLightChallenge: FaceMovementAndLightServerChallenge + let type: ChallengeType enum CodingKeys: String, CodingKey { case faceMovementAndLightChallenge = "FaceMovementAndLightChallenge" + case faceMovementChallenge = "FaceMovementChallenge" + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self.type { + case .faceMovementChallenge(let faceMovementServerChallenge): + try container.encode(faceMovementServerChallenge, forKey: .faceMovementChallenge) + case .faceMovementAndLightChallenge(let faceMovementAndLightServerChallenge): + try container.encode(faceMovementAndLightServerChallenge, forKey: .faceMovementAndLightChallenge) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let value = try? container.decode(FaceMovementServerChallenge.self, forKey: .faceMovementChallenge) { + self.type = .faceMovementChallenge(challenge: value) + } else if let value = try? container.decode(FaceMovementAndLightServerChallenge.self, forKey: .faceMovementAndLightChallenge) { + self.type = .faceMovementAndLightChallenge(challenge: value) + } else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Unexpected data format" + ) + ) + } + } +} + +extension ServerChallenge { + enum ChallengeType: Codable { + case faceMovementChallenge(challenge: FaceMovementServerChallenge) + case faceMovementAndLightChallenge(challenge: FaceMovementAndLightServerChallenge) } }