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

Box Oval Interface for Selfie Capture Screen #250

Merged
merged 79 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
4538471
improve code and file structure
tobitech Sep 19, 2024
5f93f25
add the liveness guides
tobitech Sep 19, 2024
7c6b949
add some config values to liveness guides view.
tobitech Sep 19, 2024
4c4bfa0
change lottie animation frame
tobitech Sep 20, 2024
1c51bf0
Merge branch 'main' into new-camera-ui
tobitech Sep 20, 2024
3b71441
run pod install
tobitech Sep 20, 2024
b6c4315
Merge branch 'new-smart-selfie-capture' into new-camera-ui
tobitech Sep 20, 2024
2a71246
setup animation progress time for the liiveness guide lottie animation.
tobitech Sep 20, 2024
b32cf77
fix homeview fore each warnings.
tobitech Sep 23, 2024
4e0c5e1
connect face bounds detection to the the indicator.
tobitech Sep 23, 2024
ddca3b7
add throttling to camera feed.
tobitech Sep 23, 2024
1199bac
use full screen cover to present home screen products.
tobitech Sep 23, 2024
924c09d
control progress arc visibility based on progress.
tobitech Sep 24, 2024
8795bb7
add a dummy submit function.
tobitech Sep 24, 2024
47b3a42
setup timers.
tobitech Sep 24, 2024
176a17d
add animation to the progress fill arcs.
tobitech Sep 25, 2024
e5801dd
refactor the components of liveness guides view.
tobitech Sep 26, 2024
c1de792
setup delay timer and introduce state for the current animation that …
tobitech Sep 26, 2024
b52ec73
Merge branch 'main' into connect-viewmodel-to-new-ui
tobitech Sep 26, 2024
d98aa78
Merge branch 'new-smart-selfie-capture' into connect-viewmodel-to-new-ui
tobitech Sep 26, 2024
04b303c
update arc shape init
tobitech Sep 26, 2024
ce7626d
add new lottie files. define a guide animation enum to hold the anima…
tobitech Sep 27, 2024
9f8ab57
some refactoring.
tobitech Sep 27, 2024
1437248
create a validator class for the face observation data.
tobitech Sep 27, 2024
ebb3052
Merge branch 'new-smart-selfie-capture' into connect-viewmodel-to-new-ui
tobitech Sep 30, 2024
42c3eb8
refactor buffer image processing and communication between face detec…
tobitech Sep 30, 2024
91899a8
remove some debug views. some refactoring and improvements.
tobitech Oct 1, 2024
374d943
make capture instruction strings localizable
tobitech Oct 1, 2024
fe5932b
introduce current liveness task into the face validator to set the ri…
tobitech Oct 2, 2024
2a2fa10
fix cropping for selfie quality check.
tobitech Oct 2, 2024
cac8f26
show or hide the circular ring and the liveness guides based on wheth…
tobitech Oct 3, 2024
7725bc1
add processing view to view captured images.
tobitech Oct 3, 2024
6fc1153
Merge branch 'main' into connect-viewmodel-to-new-ui
tobitech Oct 3, 2024
a75c3eb
Merge branch 'new-smart-selfie-capture' into connect-viewmodel-to-new-ui
tobitech Oct 3, 2024
0830442
reset animation as user is completing liveness checks
tobitech Oct 3, 2024
3e7cedb
inject current liveness task into liveness guide to control which pro…
tobitech Oct 4, 2024
8f63895
processing screen layout. introduce a backport of stateobject.
tobitech Oct 5, 2024
e9999de
run pod install to import missing lottie files.
tobitech Oct 7, 2024
be0bd37
present selfie capture flow in navigation view, programmatically navi…
tobitech Oct 7, 2024
7c4de65
refactor view appear setup and reset selfie capture state variables
tobitech Oct 7, 2024
e1876fc
import submit method from selfie viewmodel.
tobitech Oct 7, 2024
cbecb45
extract submit selfie functionality into a new class to manage the su…
tobitech Oct 7, 2024
640d069
some refactoring
tobitech Oct 7, 2024
47fbb2a
move backport and stateobject to helper folder.
tobitech Oct 8, 2024
c961ee8
code formatting.
tobitech Oct 8, 2024
900f46e
restore threshold value
tobitech Oct 8, 2024
13eed78
replace ObservedObject with StateObject in HomeView so that it's init…
tobitech Oct 8, 2024
78e02b7
use proxy size instead of frame for window size calculation and face …
tobitech Oct 8, 2024
95886a4
remove stateobject backport
tobitech Oct 8, 2024
e7efa55
use a delegate to communicate selfie submission updates to selfie vie…
tobitech Oct 9, 2024
f5ec3de
update processing changes on main thread.
tobitech Oct 9, 2024
029418c
inject failure reason into the api call for submitting selfie
tobitech Oct 9, 2024
c44b1e5
rename selfie submission manager
tobitech Oct 9, 2024
ed02fb3
reset the threshold for timeout for liveness check.
tobitech Oct 9, 2024
57444ca
Merge branch 'new-smart-selfie-capture' into selfie-submission
tobitech Oct 10, 2024
661c43a
remove presentation mode variable.
tobitech Oct 10, 2024
91289f0
introduce an environment key to manage dismissing of the selfie captu…
tobitech Oct 10, 2024
46c6eaf
improve error handling of selfie capture.
tobitech Oct 17, 2024
3cdcc99
localise strings.
tobitech Oct 17, 2024
d0e3d91
code formatting.
tobitech Oct 17, 2024
4583c76
Merge branch 'main' into selfie-submission
tobitech Oct 24, 2024
7794c43
pod install
tobitech Oct 24, 2024
04693d5
make loader background color themeable.
tobitech Oct 24, 2024
b788dc9
Merge branch 'new-smart-selfie-capture' into selfie-submission
tobitech Oct 24, 2024
745d9bb
Merge branch 'main' into selfie-submission
tobitech Oct 28, 2024
7e6b56c
run pod install.
tobitech Oct 28, 2024
4853187
Merge branch 'new-smart-selfie-capture' into selfie-submission
tobitech Oct 28, 2024
129e2fa
fix missing files and build errors.
tobitech Oct 29, 2024
19d2a54
redesign selfie capture screen to use box and oval for camera area an…
tobitech Oct 29, 2024
38f1a67
add a view to preview selfie image, also add an actions view.
tobitech Oct 29, 2024
908f16d
redesign the the progress arcs for active liveness.
tobitech Oct 30, 2024
017f455
improvements to validating face bounding box. add a frame to selfie p…
tobitech Oct 30, 2024
ae1bc49
remove processing view, add a view state to control visibility of dif…
tobitech Oct 30, 2024
5878d77
adjust face size and position evaluation. fix layout for attribution …
tobitech Oct 31, 2024
7bb2e64
change active liveness progress colours
tobitech Nov 1, 2024
781abb8
code formatting.
tobitech Nov 4, 2024
1039a64
update faceboundmultiplier constant.
tobitech Nov 5, 2024
3d58a2f
refactor task timer.
tobitech Nov 6, 2024
26034a1
improve submission handling.
tobitech Nov 6, 2024
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
100 changes: 50 additions & 50 deletions Example/SmileID.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

26 changes: 12 additions & 14 deletions Example/SmileID/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ struct HomeView: View {
_viewModel = StateObject(wrappedValue: HomeViewModel(config: config))
}

let columns = [GridItem(.flexible()), GridItem(.flexible())]

var body: some View {
NavigationView {
VStack(spacing: 24) {
Text("Test Our Products")
.font(SmileID.theme.header2)
.foregroundColor(.black)

MyVerticalGrid(
maxColumns: 2,
items: [
ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns) {
ProductCell(
image: "smart_selfie_enroll",
name: "SmartSelfie™ Enrollment",
Expand All @@ -38,7 +38,7 @@ struct HomeView: View {
)
)
}
),
)
ProductCell(
image: "smart_selfie_authentication",
name: "SmartSelfie™ Authentication",
Expand All @@ -51,7 +51,7 @@ struct HomeView: View {
delegate: viewModel
)
}
),
)
ProductCell(
image: "smart_selfie_enroll",
name: "SmartSelfie™ Enrollment (Strict Mode)",
Expand All @@ -71,7 +71,7 @@ struct HomeView: View {
)
)
}
),
)
ProductCell(
image: "smart_selfie_authentication",
name: "SmartSelfie™ Authentication (Strict Mode)",
Expand All @@ -85,7 +85,7 @@ struct HomeView: View {
delegate: viewModel
)
}
),
)
ProductCell(
image: "enhanced_kyc",
name: "Enhanced KYC",
Expand All @@ -101,7 +101,7 @@ struct HomeView: View {
)
)
}
),
)
ProductCell(
image: "biometric",
name: "Biometric KYC",
Expand All @@ -117,7 +117,7 @@ struct HomeView: View {
)
)
}
),
)
ProductCell(
image: "document",
name: "\nDocument Verification",
Expand All @@ -131,7 +131,7 @@ struct HomeView: View {
delegate: viewModel
)
}
),
)
ProductCell(
image: "enhanced_doc_v",
name: "Enhanced Document Verification",
Expand All @@ -146,10 +146,8 @@ struct HomeView: View {
)
}
)
].map {
AnyView($0)
}
)
}

Text("Partner \(viewModel.partnerId) - Version \(version) - Build \(build)")
.font(SmileID.theme.body)
Expand Down
11 changes: 7 additions & 4 deletions Example/SmileID/Home/ProductCell.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import SmileID
import SwiftUI

struct ProductCell: View {
struct ProductCell<Content: View>: View {
let image: String
let name: String
let onClick: (() -> Void)?
@ViewBuilder let content: () -> any View
@ViewBuilder let content: () -> Content
@State private var isPresented: Bool = false

init(
image: String,
name: String,
onClick: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> any View
@ViewBuilder content: @escaping () -> Content
) {
self.image = image
self.name = name
Expand Down Expand Up @@ -44,7 +44,10 @@ struct ProductCell: View {
.fullScreenCover(
isPresented: $isPresented,
content: {
AnyView(content())
NavigationView {
content()
}
.environment(\.modalMode, $isPresented)
}
)
}
Expand Down
11 changes: 8 additions & 3 deletions Sources/SmileID/Classes/FaceDetector/FaceDetectorV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ class FaceDetectorV2: NSObject {
let faceObservation = faceDetections.first,
let faceQualityObservation = faceQualityObservations.first
else {
self.resultDelegate?.faceDetector(self, didFailWithError: FaceDetectorError.noFaceDetected)
self.resultDelegate?.faceDetector(
self, didFailWithError: FaceDetectorError.noFaceDetected)
return
}

let convertedBoundingBox =
self.viewDelegate?.convertFromMetadataToPreviewRect(rect: faceObservation.boundingBox) ?? .zero
self.viewDelegate?.convertFromMetadataToPreviewRect(
rect: faceObservation.boundingBox) ?? .zero

let uiImage = UIImage(pixelBuffer: imageBuffer)
let brightness = self.calculateBrightness(uiImage)
Expand Down Expand Up @@ -166,7 +168,10 @@ class FaceDetectorV2: NSObject {
}

let croppedImage = UIImage(cgImage: croppedCGImage)
guard let resizedImage = croppedImage.pixelBuffer(width: cropSize.width, height: cropSize.height) else {
guard
let resizedImage = croppedImage.pixelBuffer(
width: cropSize.width, height: cropSize.height)
else {
throw FaceDetectorError.unableToCropImage
}

Expand Down
37 changes: 21 additions & 16 deletions Sources/SmileID/Classes/FaceDetector/FaceValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class FaceValidator {
// MARK: Constants
private let selfieQualityThreshold: Float = 0.5
private let luminanceThreshold: ClosedRange<Int> = 80...200
private let faceBoundsMultiplier: CGFloat = 1.2
private let faceBoundsMultiplier: CGFloat = 1.5
private let faceBoundsThreshold: CGFloat = 50

init() {}
Expand All @@ -33,7 +33,10 @@ final class FaceValidator {
currentLivenessTask: LivenessTask?
) {
// check face bounds
let faceBoundsState = checkAcceptableBounds(using: faceGeometry.boundingBox)
let faceBoundsState = checkFaceSizeAndPosition(
using: faceGeometry.boundingBox,
shouldCheckCentering: currentLivenessTask == nil
)
let isAcceptableBounds = faceBoundsState == .detectedFaceAppropriateSizeAndPosition

// check brightness
Expand Down Expand Up @@ -98,22 +101,26 @@ final class FaceValidator {
}

// MARK: Validation Checks
private func checkAcceptableBounds(using boundingBox: CGRect) -> FaceBoundsState {
if boundingBox.width > faceBoundsMultiplier * faceLayoutGuideFrame.width {
private func checkFaceSizeAndPosition(using boundingBox: CGRect, shouldCheckCentering: Bool) -> FaceBoundsState {
let maxFaceWidth = faceLayoutGuideFrame.width - 20
let minFaceWidth = faceLayoutGuideFrame.width / faceBoundsMultiplier

if boundingBox.width > maxFaceWidth {
return .detectedFaceTooLarge
} else if boundingBox.width * faceBoundsMultiplier < faceLayoutGuideFrame.width {
} else if boundingBox.width < minFaceWidth {
return .detectedFaceTooSmall
} else {
if abs(
boundingBox.midX - faceLayoutGuideFrame.midX
) > faceBoundsThreshold {
return .detectedFaceOffCentre
} else if abs(boundingBox.midY - faceLayoutGuideFrame.midY) > faceBoundsThreshold {
}

if shouldCheckCentering {
let horizontalOffset = abs(boundingBox.midX - faceLayoutGuideFrame.midX)
let verticalOffset = abs(boundingBox.midY - faceLayoutGuideFrame.midY)

if horizontalOffset > faceBoundsThreshold || verticalOffset > faceBoundsThreshold {
return .detectedFaceOffCentre
} else {
return .detectedFaceAppropriateSizeAndPosition
}
}

return .detectedFaceAppropriateSizeAndPosition
}

private func checkSelfieQuality(_ value: SelfieQualityData) -> Bool {
Expand All @@ -125,8 +132,6 @@ final class FaceValidator {
_ isAcceptableBrightness: Bool,
_ isAcceptableSelfieQuality: Bool
) -> Bool {
return isAcceptableBounds &&
isAcceptableBrightness &&
isAcceptableSelfieQuality
return isAcceptableBounds && isAcceptableBrightness && isAcceptableSelfieQuality
}
}
20 changes: 14 additions & 6 deletions Sources/SmileID/Classes/FaceDetector/LivenessCheckManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,25 @@ class LivenessCheckManager: ObservableObject {

/// Resets the task timer to the initial timeout duration.
private func resetTaskTimer() {
stopTaskTimer()
taskTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
self.elapsedTime += 1
if self.elapsedTime == self.taskTimeoutDuration {
self.handleTaskTimeout()
}
guard taskTimer == nil else { return }
DispatchQueue.main.async {
self.taskTimer = Timer.scheduledTimer(
timeInterval: 1.0, target: self, selector: #selector(self.taskTimerFired),
tobitech marked this conversation as resolved.
Show resolved Hide resolved
userInfo: nil,
repeats: true)
}
}

@objc private func taskTimerFired() {
self.elapsedTime += 1
if self.elapsedTime == self.taskTimeoutDuration {
self.handleTaskTimeout()
}
}

/// Stops the current task timer.
private func stopTaskTimer() {
guard taskTimer != nil else { return }
taskTimer?.invalidate()
taskTimer = nil
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/SmileID/Classes/Helpers/NavigationHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,14 @@ extension View {
}
}
}

public struct ModalModeKey: EnvironmentKey {
public static let defaultValue = Binding<Bool>.constant(false)
}

extension EnvironmentValues {
public var modalMode: Binding<Bool> {
get { self[ModalModeKey.self] }
set { self[ModalModeKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class SmileIDResourcesHelper {
public static var ConsentContactDetails = SmileIDResourcesHelper.image("ConsentContactDetails")!
public static var ConsentDocumentInfo = SmileIDResourcesHelper.image("ConsentDocumentInfo")!
public static var ConsentPersonalInfo = SmileIDResourcesHelper.image("ConsentPersonalInfo")!
public static var Loader = SmileIDResourcesHelper.image("Loader")!

/// Size of font.
public static let pointSize: CGFloat = 16
Expand Down
11 changes: 11 additions & 0 deletions Sources/SmileID/Classes/Networking/Models/FailureReason.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

public enum FailureReason {
case activeLivenessTimedOut

var key: String {
switch self {
case .activeLivenessTimedOut: return "mobile_active_liveness_timed_out"
}
}
}
Loading
Loading