Enabling Advanced Injection Attack Detection

Face Capture has introduced a new workflow option for advanced injection attack detection, LIGHT_TRACE_STRICT_MODE_OPTIMIZATIONS. This option adds more advanced injection attack detection to your workflow, but it is not enabled by default. The reason for this is the additional considerations for end user applications when using this option, detailed below:
  • This option is intended for Charlie workflow only.

  • It is recommended to advise users to avoid taking pictures outside in direct sunlight. Direct sunlight may increase errors for live users.

  • Enabling this option requires additional time to process data after the subject’s photo is taken. To ensure a smooth end user experience, a POST_CAPTURE_PROCESSING status will be provided. While this is occurring, it is advised to update the UI display as the SDK will no longer be displaying the camera feed.

This guide describes how to enable strict mode optimizations and how to handle the extra processing time on the UI by displaying an animation during the POST_CAPTURE_PROCESSING phase.

1. Enabling strict mode optimizations

Before starting the Face Capture process, set the LIGHT_TRACE_STRICT_MODE_OPTIMIZATIONS workflow property to true. The default value is false.

try self.workflow?.setPropertyBool(property: .LIGHT_TRACE_STRICT_MODE_OPTIMIZATIONS, value: true)

Note

When LIGHT_TRACE_STRICT_MODE_OPTIMIZATIONS is enabled, expect an additional ~1 second delay on the CaptureSessionStatus.POST_CAPTURE_PROCESSING callback before the status transitions to COMPLETED. To improve the user experience during this delay, it is recommended to display an animation (e.g. a progress indicator followed by a checkmark).

2. Displaying an animation during post-capture processing

The flow is:

  • While the status is POST_CAPTURE_PROCESSING — show a progress animation.

  • When the status becomes COMPLETED — hide the progress and play a checkmark animation; finalize the capture when the animation ends.

2.1 Checkmark View

The checkmark path and its draw-on animation are defined together in a reusable UIView subclass, CheckmarkView, using CAShapeLayer and CABasicAnimation.

import UIKit

/// Draws a checkmark path and animates it stroke-by-stroke.
final class CheckmarkView: UIView {

    private let checkLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayer()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayer()
    }

    private func setupLayer() {
        backgroundColor = .clear
        checkLayer.strokeColor = UIColor.systemGreen.cgColor
        checkLayer.fillColor = UIColor.clear.cgColor
        checkLayer.lineWidth = 6
        checkLayer.lineCap = .round
        checkLayer.lineJoin = .round
        checkLayer.strokeEnd = 0
        layer.addSublayer(checkLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        checkLayer.frame = bounds
        checkLayer.path = checkPath(in: bounds).cgPath
    }

    /// Checkmark path, scaled to fit within a 96x96 viewport.
    private func checkPath(in rect: CGRect) -> UIBezierPath {
        let scaleX = rect.width / 96
        let scaleY = rect.height / 96
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 28 * scaleX, y: 50 * scaleY))
        path.addLine(to: CGPoint(x: 42 * scaleX, y: 64 * scaleY))
        path.addLine(to: CGPoint(x: 70 * scaleX, y: 32 * scaleY))
        return path
    }

    /// Plays the draw-on animation and calls completion when it finishes.
    func playAnimation(duration: CFTimeInterval = 1.0, completion: (() -> Void)? = nil) {
        checkLayer.strokeEnd = 0

        CATransaction.begin()
        CATransaction.setCompletionBlock {
            completion?()
        }

        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = duration
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

        checkLayer.strokeEnd = 1 // set model value to end state
        checkLayer.add(animation, forKey: "strokeEndAnimation")

        CATransaction.commit()
    }

    func reset() {
        checkLayer.removeAllAnimations()
        checkLayer.strokeEnd = 0
    }
}

Key points:

  • strokeEnd controls how much of the path’s stroke is drawn, from 0 (nothing) to 1 (fully drawn) — this is what produces the “drawn stroke-by-stroke” effect.

  • CABasicAnimation(keyPath: “strokeEnd”) animates that property over the given duration.

  • The CATransaction completion block fires once the animation finishes, so callers can chain further logic (e.g. finalizing the capture) after the checkmark has fully drawn.

2.2 Overlay Views

Add a spinner and a CheckmarkView as overlay subviews in your capture view controller, positioned with Auto Layout:

private let postProcessingSpinner: UIActivityIndicatorView = {
    let spinner = UIActivityIndicatorView(style: .large)
    spinner.hidesWhenStopped = true
    spinner.isHidden = true
    return spinner
}()

private let checkmarkView: CheckmarkView = {
    let view = CheckmarkView()
    view.isHidden = true
    return view
}()

private func setupOverlayViews() {
    view.addSubview(postProcessingSpinner)
    view.addSubview(checkmarkView)

    postProcessingSpinner.translatesAutoresizingMaskIntoConstraints = false
    checkmarkView.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint.activate([
        postProcessingSpinner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        postProcessingSpinner.centerYAnchor.constraint(equalTo: view.centerYAnchor),

        checkmarkView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        checkmarkView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        checkmarkView.widthAnchor.constraint(equalToConstant: 96),
        checkmarkView.heightAnchor.constraint(equalToConstant: 96)
    ])
}

Call setupOverlayViews() from viewDidLoad, before starting the capture session:

override func viewDidLoad() {
    super.viewDidLoad()
    setupOverlayViews()
    setupFaceCapture()
}

2.3 View Controller Logic

Inside onStatusChanged, react to the polling status callbacks. While in .POST_CAPTURE_PROCESSING, show the progress; on .COMPLETED, play the checkmark animation and only finalize when it ends.

private var isPostProcessingVisible = false
private var isCompletedHandled = false

private func onStatusChanged(_ status: FaceCapture.CaptureSessionStatus?) {
    // ... existing isMonitoringState handling ...

    DispatchQueue.main.async { [weak self] in
        guard let self = self else { return }
        if let status = status {
            switch status {
            // ... other cases ...
            case .POST_CAPTURE_PROCESSING:
                self.faceCaptureControl.switchSilhouetteColorToCapturing()
                self.faceCaptureControl.toggleAllUI(all: false)
                self.showPostProcessing()
            case .COMPLETED:
                self.faceCaptureControl.toggleAllUI(all: false)
                self.playCheckmarkThenEnd()
            // ... other cases ...
            }
        }
    }
}

private func showPostProcessing() {
    guard !isCompletedHandled, !isPostProcessingVisible else { return }

    isPostProcessingVisible = true
    checkmarkView.isHidden = true
    postProcessingSpinner.isHidden = false
    postProcessingSpinner.startAnimating()
}

private func playCheckmarkThenEnd() {
    guard !isCompletedHandled else { return }

    isCompletedHandled = true
    isPostProcessingVisible = false

    postProcessingSpinner.stopAnimating()
    postProcessingSpinner.isHidden = true
    checkmarkView.isHidden = false
    checkmarkView.reset()

    checkmarkView.playAnimation { [weak self] in
        self?.onCaptureEnd()
    }
}

private func onCaptureEnd() {
    // Finalize the capture here — e.g. dismiss, fetch server package, navigate away, etc.
    self.faceCaptureControl.updateFeedbackLabel("Processing")
    self.alertModalDialog(title: "Success", message: "Capture completed successfully")
}

Reset both flags whenever a new session starts (e.g. at the top of setupFaceCapture()), so a retried capture isn’t silently blocked by stale state from a prior session:

private func setupFaceCapture() {
    isPostProcessingVisible = false
    isCompletedHandled = false
    // ... rest of setup ...
}

2.4 Required Imports

import UIKit

No additional imports are needed — CAShapeLayer, CABasicAnimation, and CATransaction are all available through UIKit.

2.5 Guard Flags

Two flags prevent double-handling of status callbacks, which can arrive multiple times:

  • isPostProcessingVisible — ensures the progress UI is only shown once per session.

  • isCompletedHandled — ensures the completion animation and onCaptureEnd() run only once, even if .COMPLETED is delivered repeatedly.