@faceaisdk/react-native-face-sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +252 -0
- package/android/build.gradle +53 -0
- package/android/libs/FaceSDKLib-release.aar +0 -0
- package/android/proguard-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/faceaisdk/reactnative/FaceRNModule.kt +383 -0
- package/android/src/main/java/com/faceaisdk/reactnative/FaceRNPackage.kt +16 -0
- package/ios/FaceAISDK/CustomToastView.swift +34 -0
- package/ios/FaceAISDK/FaceAINaviView.swift +212 -0
- package/ios/FaceAISDK/FaceSDKCameraView.swift +40 -0
- package/ios/FaceAISDK/FaceSDKLocalizer.swift +21 -0
- package/ios/FaceAISDK/LivenessDetectView.swift +317 -0
- package/ios/FaceAISDK/ScreenBrightnessHelper.swift +100 -0
- package/ios/FaceAISDK/TTSPlayer.swift +357 -0
- package/ios/FaceAISDK/VerifyFaceView.swift +284 -0
- package/ios/FaceAISDK/addFace/AddFaceByCamera.swift +207 -0
- package/ios/FaceAISDK/addFace/AddFaceByImage.swift +174 -0
- package/ios/FaceAISDK/addFace/ImagePicker.swift +52 -0
- package/ios/FaceAISDK/addFace/VerifyTwoFaceSimiView.swift +210 -0
- package/ios/FaceColorExtensions.swift +10 -0
- package/ios/FaceRNModule.h +9 -0
- package/ios/FaceRNModule.m +197 -0
- package/ios/FaceSDKSwiftManager.swift +277 -0
- package/ios/Resources/en.lproj/Localizable.strings +51 -0
- package/ios/Resources/light_too_high.png +0 -0
- package/ios/Resources/zh-Hans.lproj/Localizable.strings +51 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +112 -0
- package/lib/types.d.ts +39 -0
- package/lib/types.js +2 -0
- package/package.json +88 -0
- package/react-native-face-sdk.podspec +28 -0
- package/src/index.ts +184 -0
- package/src/types.ts +90 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.faceaisdk.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class FaceRNPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(FaceRNModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// Created by anylife on 2025/12/2.
|
|
5
|
+
|
|
6
|
+
// 定义 Toast 样式
|
|
7
|
+
enum ToastStyle {
|
|
8
|
+
case success
|
|
9
|
+
case failure
|
|
10
|
+
var backgroundColor: Color {
|
|
11
|
+
switch self {
|
|
12
|
+
case .success: return Color.faceMain
|
|
13
|
+
case .failure: return Color.red
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
struct CustomToastView: View {
|
|
18
|
+
let message: String
|
|
19
|
+
let style: ToastStyle
|
|
20
|
+
var body: some View {
|
|
21
|
+
VStack {
|
|
22
|
+
HStack {
|
|
23
|
+
Text(message)
|
|
24
|
+
.foregroundColor(.white)
|
|
25
|
+
.font(.system(size: 19).bold())
|
|
26
|
+
.padding(.vertical, 14)
|
|
27
|
+
.padding(.horizontal, 22)
|
|
28
|
+
}
|
|
29
|
+
.background(style.backgroundColor)
|
|
30
|
+
.cornerRadius(25)
|
|
31
|
+
.shadow(radius: 5)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import FaceAISDK_Core
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* iOS FaceAISDK navigation page, UI is for reference only.
|
|
6
|
+
* iOS FaceAISDK 功能导航页面,UI 仅供参考。
|
|
7
|
+
*/
|
|
8
|
+
struct FaceAINaviView: View {
|
|
9
|
+
|
|
10
|
+
// The FaceID value used for saving the face feature. Usually, it's the unique identifier of a person in your business system, such as an account ID or ID card number.
|
|
11
|
+
// 录入保存的 FaceID 值。一般是你的业务体系中个人的唯一编码,比如账号或身份证号。
|
|
12
|
+
private let faceID = "yourFaceID";
|
|
13
|
+
|
|
14
|
+
var onDismiss: (() -> Void)?
|
|
15
|
+
|
|
16
|
+
var body: some View {
|
|
17
|
+
NavigationView {
|
|
18
|
+
ZStack {
|
|
19
|
+
// 背景色铺满
|
|
20
|
+
Color.faceMain.ignoresSafeArea()
|
|
21
|
+
|
|
22
|
+
// 使用 ScrollView 适配小屏幕机型
|
|
23
|
+
ScrollView(.vertical, showsIndicators: false) {
|
|
24
|
+
VStack(spacing: 18) {
|
|
25
|
+
|
|
26
|
+
// --- 模块一:人脸录入 ---
|
|
27
|
+
VStack(spacing: 14) {
|
|
28
|
+
// 通过 SDK 相机录入人脸
|
|
29
|
+
NavigationLink(destination: AddFaceByCamera(
|
|
30
|
+
faceID: faceID,
|
|
31
|
+
addFacePerformanceMode: 1,
|
|
32
|
+
needShowConfirmDialog: true,
|
|
33
|
+
onDismiss: { result, feature in
|
|
34
|
+
print("🎆 AddFace Status: \(result), Feature: \(feature)")
|
|
35
|
+
}
|
|
36
|
+
)) {
|
|
37
|
+
MenuRowView(icon: "camera.viewfinder", title: "Add Face By Camera")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 通过图片录入人脸信息
|
|
41
|
+
NavigationLink(destination: AddFaceByImage(
|
|
42
|
+
faceID: faceID,
|
|
43
|
+
onDismiss: { result, feature in
|
|
44
|
+
print("🎆 AddFace Status: \(result), Feature: \(feature ?? "")")
|
|
45
|
+
}
|
|
46
|
+
)) {
|
|
47
|
+
MenuRowView(icon: "photo.on.rectangle.angled", title: "Add Face From Album")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
.padding(.top, 20)
|
|
51
|
+
|
|
52
|
+
// --- 模块二:识别与活体 ---
|
|
53
|
+
VStack(spacing: 14) {
|
|
54
|
+
// 人脸识别 + 活体检测
|
|
55
|
+
NavigationLink(destination: VerifyFaceView(
|
|
56
|
+
faceID: faceID,
|
|
57
|
+
// Threshold range [0.8, 0.9]. 阈值范围【0.8,0.9】。
|
|
58
|
+
threshold: 0.83,
|
|
59
|
+
|
|
60
|
+
// 1. Motion Liveness, 2. Motion + Color, 3. Color, 4. Silent Liveness only (the first three all include silent liveness).
|
|
61
|
+
// 1.动作活体 2.动作+炫彩 3.炫彩 4.仅静默活体(前三种都会带静默)。
|
|
62
|
+
livenessType: 1,
|
|
63
|
+
// 1. Open mouth, 2. Smile, 3. Blink, 4. Shake head, 5. Nod.
|
|
64
|
+
// 1.张嘴 2.微笑 3.眨眼 4.摇头 5.点头。
|
|
65
|
+
motionLiveness: "1,2,3,4,5",
|
|
66
|
+
// Timeout: 3-22 seconds. 超时时间:3-22秒。
|
|
67
|
+
motionLivenessTimeOut: 11,
|
|
68
|
+
// Number of motion steps. 动作步骤个数。
|
|
69
|
+
motionLivenessSteps:2,
|
|
70
|
+
|
|
71
|
+
onDismiss: {code, similarity, liveness in
|
|
72
|
+
print("🎆 Face Verify Status: \(code), Similarity: \(similarity), Liveness: \(liveness)")
|
|
73
|
+
}
|
|
74
|
+
)) {
|
|
75
|
+
MenuRowView(icon: "faceid", title: "Face Verify & Liveness")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 仅活体检测(建议动作活体+静默组合)
|
|
79
|
+
NavigationLink(destination: LivenessDetectView(
|
|
80
|
+
// 1. Motion Liveness, 2. Motion + Color, 3. Color, 4. Silent Liveness only (the first three all include silent liveness).
|
|
81
|
+
// 1. 动作活体 2.动作+炫彩 3.炫彩 4.仅静默活体(前三种都会带静默)。
|
|
82
|
+
livenessType: 1,
|
|
83
|
+
// 1. Open mouth, 2. Smile, 3. Blink, 4. Shake head, 5. Nod.
|
|
84
|
+
// 1. 张嘴 2.微笑 3.眨眼 4.摇头 5.点头。
|
|
85
|
+
motionLiveness: "1,2,3,4,5",
|
|
86
|
+
// Timeout in seconds. 超时时间(秒)。
|
|
87
|
+
motionLivenessTimeOut: 5,
|
|
88
|
+
// Number of motion steps. 动作步骤个数。
|
|
89
|
+
motionLivenessSteps:2,
|
|
90
|
+
//show Result Tips? For Flutter,RN,UNIApp plugin
|
|
91
|
+
showResultTips: true,
|
|
92
|
+
onDismiss: { code,liveness in
|
|
93
|
+
print("🎆 Liveness Result: \(code), Liveness Score: \(liveness)")
|
|
94
|
+
}
|
|
95
|
+
)) {
|
|
96
|
+
MenuRowView(icon: "person.crop.circle.badge.checkmark", title: "ONLY Liveness Detection")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
.padding(.top, 8)
|
|
100
|
+
|
|
101
|
+
// --- 模块三:功能辅助测试 ---
|
|
102
|
+
VStack(spacing: 14) {
|
|
103
|
+
// 判断 faceID 对应人脸特征值是否存在
|
|
104
|
+
Button(action: {
|
|
105
|
+
guard let faceFeature = UserDefaults.standard.string(forKey: faceID) else {
|
|
106
|
+
print("isFaceFeatureExist? : No ! ")
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
print("\n😊FaceFeature: \(faceFeature)")
|
|
110
|
+
}) {
|
|
111
|
+
MenuRowView(icon: "magnifyingglass.circle", title: "Is Face Feature Exist", showChevron: false)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 验证两张人脸的相似度
|
|
115
|
+
NavigationLink(destination: VerifyTwoFaceSimiView()) {
|
|
116
|
+
MenuRowView(icon: "person.2.crop.square.stack", title: "Verify Two Face Similarity")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
.padding(.top, 8)
|
|
120
|
+
|
|
121
|
+
Spacer().frame(height: 30)
|
|
122
|
+
|
|
123
|
+
// 打开关于我们的外部链接 (保持简洁风格)
|
|
124
|
+
Button(action: {
|
|
125
|
+
if let url = URL(string: "https://faceaisdk.github.io/index") {
|
|
126
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
127
|
+
if UIApplication.shared.canOpenURL(url) {
|
|
128
|
+
UIApplication.shared.open(url)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}) {
|
|
133
|
+
Text("About us")
|
|
134
|
+
.font(.system(size: 14, weight: .medium))
|
|
135
|
+
.foregroundColor(Color.white.opacity(0.8))
|
|
136
|
+
.underline()
|
|
137
|
+
}
|
|
138
|
+
.padding(.bottom, 40)
|
|
139
|
+
.padding(.top, 22)
|
|
140
|
+
}
|
|
141
|
+
.padding(.horizontal, 22)
|
|
142
|
+
.padding(.top, 22)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
146
|
+
// 顶部导航栏添加关闭按钮
|
|
147
|
+
.toolbar {
|
|
148
|
+
ToolbarItem(placement: .navigationBarLeading) {
|
|
149
|
+
Button(action: {
|
|
150
|
+
onDismiss?()
|
|
151
|
+
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
|
|
152
|
+
}) {
|
|
153
|
+
Image(systemName: "xmark")
|
|
154
|
+
.font(.system(size: 13, weight: .bold))
|
|
155
|
+
.foregroundColor(.white)
|
|
156
|
+
.padding(8)
|
|
157
|
+
.background(Circle().fill(Color.white.opacity(0.2)))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
.navigationViewStyle(.stack)
|
|
163
|
+
.onAppear {
|
|
164
|
+
// 视图显示时将屏幕亮度调至最大
|
|
165
|
+
ScreenBrightnessHelper.shared.maximizeBrightness()
|
|
166
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
167
|
+
UIScreen.main.brightness = 1.0
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// MARK: - 统一的菜单行组件
|
|
174
|
+
/// 用于美化导航列表的按钮卡片视图
|
|
175
|
+
struct MenuRowView: View {
|
|
176
|
+
var icon: String
|
|
177
|
+
|
|
178
|
+
// 将 String 改为 LocalizedStringKey,这样 SwiftUI 就会自动去 Localizable.strings 查找多语言
|
|
179
|
+
var title: LocalizedStringKey
|
|
180
|
+
|
|
181
|
+
var showChevron: Bool = true // 是否显示右侧的小箭头
|
|
182
|
+
|
|
183
|
+
var body: some View {
|
|
184
|
+
HStack(spacing: 16) {
|
|
185
|
+
Image(systemName: icon)
|
|
186
|
+
.font(.system(size: 22, weight: .light))
|
|
187
|
+
.frame(width: 30)
|
|
188
|
+
|
|
189
|
+
Text(title)
|
|
190
|
+
.font(.system(size: 17, weight: .semibold))
|
|
191
|
+
|
|
192
|
+
Spacer()
|
|
193
|
+
|
|
194
|
+
if showChevron {
|
|
195
|
+
Image(systemName: "chevron.right")
|
|
196
|
+
.font(.system(size: 14, weight: .semibold))
|
|
197
|
+
.foregroundColor(Color.white.opacity(0.5))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
.foregroundColor(.white)
|
|
201
|
+
.padding(.horizontal, 20)
|
|
202
|
+
.padding(.vertical, 18)
|
|
203
|
+
.background(
|
|
204
|
+
RoundedRectangle(cornerRadius: 16)
|
|
205
|
+
.fill(Color.white.opacity(0.15))
|
|
206
|
+
)
|
|
207
|
+
.overlay(
|
|
208
|
+
RoundedRectangle(cornerRadius: 16)
|
|
209
|
+
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import AVFoundation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 人脸相机预览管理
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
public struct FaceSDKCameraView: UIViewControllerRepresentable {
|
|
9
|
+
let session: AVCaptureSession
|
|
10
|
+
let cameraSize:CGFloat
|
|
11
|
+
|
|
12
|
+
public init(session: AVCaptureSession, cameraSize: CGFloat) {
|
|
13
|
+
self.session = session
|
|
14
|
+
self.cameraSize = cameraSize
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public func makeUIViewController(context: Context) -> UIViewController {
|
|
18
|
+
let viewController = UIViewController()
|
|
19
|
+
|
|
20
|
+
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
|
21
|
+
|
|
22
|
+
// Ensure square preview
|
|
23
|
+
previewLayer.videoGravity = .resizeAspectFill
|
|
24
|
+
previewLayer.frame = CGRect(x: 0, y: 0, width: cameraSize, height: cameraSize)
|
|
25
|
+
|
|
26
|
+
viewController.view.layer.addSublayer(previewLayer)
|
|
27
|
+
viewController.view.clipsToBounds = true
|
|
28
|
+
|
|
29
|
+
return viewController
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
|
|
33
|
+
// Update preview layer frame if needed
|
|
34
|
+
if let previewLayer = uiViewController.view.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
|
|
35
|
+
previewLayer.frame = CGRect(x: 0, y: 0, width: cameraSize, height:cameraSize)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
@objcMembers
|
|
4
|
+
public class FaceSDKLocalizer: NSObject {
|
|
5
|
+
/// 统一的多语言获取方法
|
|
6
|
+
/// - Parameters:
|
|
7
|
+
/// - key: 多语言 Key
|
|
8
|
+
/// - defaultValue: 如果找不到则返回的默认字符串
|
|
9
|
+
/// - Returns: 翻译后的字符串
|
|
10
|
+
public static func text(_ key: String, defaultValue: String? = nil) -> String {
|
|
11
|
+
// 使用 NSLocalizedString 从 Localizable.strings 中查找
|
|
12
|
+
let localizedString = NSLocalizedString(key, comment: "")
|
|
13
|
+
|
|
14
|
+
// 如果返回的字符串与 Key 相同,说明没有找到对应的翻译
|
|
15
|
+
if localizedString == key {
|
|
16
|
+
return defaultValue ?? key
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return localizedString
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import FaceAISDK_Core
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Liveness Detection (Supports motion, color flash, and silent liveness)
|
|
7
|
+
* UI style is for reference only and can be adjusted according to your business needs
|
|
8
|
+
* 活体检测(支持动作活体,炫彩活体,静默活体)UI 样式仅供参考,根据你的业务可自行调整
|
|
9
|
+
*/
|
|
10
|
+
struct LivenessDetectView: View {
|
|
11
|
+
@StateObject private var viewModel: VerifyFaceModel = VerifyFaceModel()
|
|
12
|
+
@State private var showToast = false
|
|
13
|
+
@State private var showLightHighDialog = false
|
|
14
|
+
@State private var showFailureDialog = false
|
|
15
|
+
@State private var isTipAppeared = false
|
|
16
|
+
|
|
17
|
+
@Environment(\.dismiss) private var dismiss
|
|
18
|
+
|
|
19
|
+
// Automatically control screen brightness
|
|
20
|
+
// 自动控制屏幕亮度
|
|
21
|
+
var autoControlBrightness: Bool = true
|
|
22
|
+
|
|
23
|
+
// 0. No liveness detection 1. Motion only 2. Motion + Color flash 3. Color flash only
|
|
24
|
+
// 0. 无需活体检测 1.仅仅动作 2.动作+炫彩 3.炫彩
|
|
25
|
+
let livenessType:Int
|
|
26
|
+
|
|
27
|
+
// Types of motion liveness: 1. Open mouth 2. Smile 3. Blink 4. Shake head 5. Nod
|
|
28
|
+
// 动作活体种类:1. 张张嘴 2.微笑 3.眨眨眼 4.摇摇头 5.点头
|
|
29
|
+
let motionLiveness:String
|
|
30
|
+
|
|
31
|
+
// Timeout in seconds
|
|
32
|
+
// 动作活体超时时间,秒
|
|
33
|
+
let motionLivenessTimeOut:Int
|
|
34
|
+
|
|
35
|
+
// Number of motion steps
|
|
36
|
+
// 动作活体个数
|
|
37
|
+
let motionLivenessSteps:Int
|
|
38
|
+
|
|
39
|
+
// show Result Tips? For Flutter,RN,UNIApp plugin
|
|
40
|
+
let showResultTips:Bool
|
|
41
|
+
|
|
42
|
+
// callback status liveness score,多加一个参数吧message
|
|
43
|
+
let onDismiss: (Int, Float) -> Void
|
|
44
|
+
|
|
45
|
+
// Multi-language tips can be provided based on the Code
|
|
46
|
+
// 可以根据Code进行多语言提示
|
|
47
|
+
private func localizedTip(for code: Int) -> String {
|
|
48
|
+
let key = "Face_Tips_Code_\(code)"
|
|
49
|
+
let defaultValue = "LivenessDetect Tips Code=\(code)"
|
|
50
|
+
let tipsString = NSLocalizedString(key, value: defaultValue, comment: "")
|
|
51
|
+
if code != 0 && code != 1 && code != 3 {
|
|
52
|
+
TTSPlayer.shared.speak(tipsString)
|
|
53
|
+
}
|
|
54
|
+
return tipsString
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
var body: some View {
|
|
59
|
+
ZStack {
|
|
60
|
+
VStack {
|
|
61
|
+
HStack {
|
|
62
|
+
Button(action: {
|
|
63
|
+
// 0 represents user cancellation 0代表用户取消
|
|
64
|
+
onDismiss(0,0.0)
|
|
65
|
+
dismiss()
|
|
66
|
+
}) {
|
|
67
|
+
Image(systemName: "chevron.left")
|
|
68
|
+
.font(.system(size: 16, weight: .semibold))
|
|
69
|
+
.foregroundColor(.black)
|
|
70
|
+
.padding(10)
|
|
71
|
+
.background(Color.gray.opacity(0.1))
|
|
72
|
+
.clipShape(Circle())
|
|
73
|
+
}
|
|
74
|
+
Spacer()
|
|
75
|
+
}
|
|
76
|
+
.padding(.horizontal, 10)
|
|
77
|
+
.padding(.top, 10)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if isTipAppeared {
|
|
81
|
+
Text(localizedTip(for: viewModel.sdkInterfaceTips.code))
|
|
82
|
+
.font(.system(size: 20).bold())
|
|
83
|
+
.padding(.horizontal, 20)
|
|
84
|
+
.padding(.vertical, 8)
|
|
85
|
+
.foregroundColor(.white)
|
|
86
|
+
.background(Color.faceMain)
|
|
87
|
+
.cornerRadius(20)
|
|
88
|
+
.id(viewModel.sdkInterfaceTips.code)
|
|
89
|
+
.transition(.asymmetric(
|
|
90
|
+
insertion: .scale(scale: 0.8).combined(with: .opacity),
|
|
91
|
+
removal: .opacity
|
|
92
|
+
))
|
|
93
|
+
.animation(.spring(response: 0.4, dampingFraction: 0.6), value: viewModel.sdkInterfaceTips.code)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
Text(localizedTip(for: viewModel.sdkInterfaceTipsExtra.code))
|
|
98
|
+
.font(.system(size: 20).bold())
|
|
99
|
+
.multilineTextAlignment(.center)
|
|
100
|
+
.padding(.bottom, 8)
|
|
101
|
+
.frame(minHeight: 30)
|
|
102
|
+
.foregroundColor(.black)
|
|
103
|
+
|
|
104
|
+
FaceSDKCameraView(session: viewModel.captureSession, cameraSize: FaceCameraSize)
|
|
105
|
+
.frame(width: FaceCameraSize, height: FaceCameraSize)
|
|
106
|
+
.aspectRatio(1.0, contentMode: .fit)
|
|
107
|
+
.padding(.vertical, 8)
|
|
108
|
+
.clipShape(Circle())
|
|
109
|
+
|
|
110
|
+
Spacer()
|
|
111
|
+
}
|
|
112
|
+
.padding()
|
|
113
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
114
|
+
.background(viewModel.colorFlash.ignoresSafeArea())
|
|
115
|
+
.navigationBarBackButtonHidden(true)
|
|
116
|
+
.navigationBarHidden(true)
|
|
117
|
+
|
|
118
|
+
if showToast && showResultTips {
|
|
119
|
+
// iOS 静默活体通过分数暂时调低为0.72
|
|
120
|
+
let isSuccess = viewModel.faceVerifyResult.liveness > 0.72
|
|
121
|
+
let toastStyle: ToastStyle = isSuccess ? .success : .failure
|
|
122
|
+
|
|
123
|
+
VStack {
|
|
124
|
+
|
|
125
|
+
Spacer()
|
|
126
|
+
CustomToastView(
|
|
127
|
+
message: "\(viewModel.faceVerifyResult.tips) \(viewModel.faceVerifyResult.liveness)",
|
|
128
|
+
style: toastStyle
|
|
129
|
+
)
|
|
130
|
+
.padding(.bottom, 77)
|
|
131
|
+
}
|
|
132
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
133
|
+
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
134
|
+
.zIndex(1)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Custom dialog for high light levels,光线过强自定义弹窗 (Dialog)
|
|
138
|
+
if showLightHighDialog {
|
|
139
|
+
ZStack {
|
|
140
|
+
VStack(spacing: 22) {
|
|
141
|
+
Text(viewModel.faceVerifyResult.tips)
|
|
142
|
+
.font(.system(size: 16).bold())
|
|
143
|
+
.fontWeight(.semibold)
|
|
144
|
+
.multilineTextAlignment(.center)
|
|
145
|
+
.foregroundColor(.black)
|
|
146
|
+
.padding(.horizontal,25)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if let uiImage = UIImage(named: "light_too_high") {
|
|
150
|
+
Image(uiImage: uiImage)
|
|
151
|
+
.resizable()
|
|
152
|
+
.scaledToFit()
|
|
153
|
+
.frame(maxHeight: 120)
|
|
154
|
+
.padding(.horizontal,1)}
|
|
155
|
+
|
|
156
|
+
Button(action: {
|
|
157
|
+
withAnimation {
|
|
158
|
+
showLightHighDialog = false
|
|
159
|
+
onDismiss(viewModel.faceVerifyResult.code,viewModel.faceVerifyResult.liveness)
|
|
160
|
+
dismiss()
|
|
161
|
+
}
|
|
162
|
+
}) {
|
|
163
|
+
Text("Confirm")
|
|
164
|
+
.font(.system(size: 18).bold())
|
|
165
|
+
.foregroundColor(.white)
|
|
166
|
+
.frame(maxWidth: .infinity)
|
|
167
|
+
.padding(.vertical, 10)
|
|
168
|
+
.background(Color.faceMain)
|
|
169
|
+
.cornerRadius(10)
|
|
170
|
+
}
|
|
171
|
+
.padding(.horizontal, 30)
|
|
172
|
+
}
|
|
173
|
+
.padding(.vertical, 22)
|
|
174
|
+
.background(Color.white)
|
|
175
|
+
.cornerRadius(20)
|
|
176
|
+
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 10)
|
|
177
|
+
.padding(.horizontal, 30)
|
|
178
|
+
}
|
|
179
|
+
.zIndex(2)
|
|
180
|
+
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Failure dialog when liveness detection fails (两按钮:知道了 / 重试)
|
|
184
|
+
if showFailureDialog {
|
|
185
|
+
ZStack {
|
|
186
|
+
VStack(spacing: 18) {
|
|
187
|
+
Text(viewModel.faceVerifyResult.tips)
|
|
188
|
+
.font(.system(size: 18).bold())
|
|
189
|
+
.fontWeight(.semibold)
|
|
190
|
+
.multilineTextAlignment(.center)
|
|
191
|
+
.foregroundColor(.black)
|
|
192
|
+
.padding(.vertical,18)
|
|
193
|
+
|
|
194
|
+
HStack(spacing: 12) {
|
|
195
|
+
Button(action: {
|
|
196
|
+
withAnimation {
|
|
197
|
+
showFailureDialog = false
|
|
198
|
+
showToast = true
|
|
199
|
+
_ = FaceImageManager.saveFaceImage(faceName: "Liveness", faceImage: viewModel.faceVerifyResult.faceImage)
|
|
200
|
+
}
|
|
201
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
202
|
+
withAnimation { showToast = false }
|
|
203
|
+
onDismiss(viewModel.faceVerifyResult.code, viewModel.faceVerifyResult.liveness)
|
|
204
|
+
dismiss()
|
|
205
|
+
}
|
|
206
|
+
}) {
|
|
207
|
+
Text("I Know")
|
|
208
|
+
.font(.system(size: 18).bold())
|
|
209
|
+
.foregroundColor(.black)
|
|
210
|
+
.frame(maxWidth: .infinity)
|
|
211
|
+
.padding(.vertical, 10)
|
|
212
|
+
.background(Color.white)
|
|
213
|
+
.cornerRadius(10)
|
|
214
|
+
.overlay(
|
|
215
|
+
RoundedRectangle(cornerRadius: 10)
|
|
216
|
+
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Button(action: {
|
|
221
|
+
withAnimation {
|
|
222
|
+
showFailureDialog = false
|
|
223
|
+
}
|
|
224
|
+
viewModel.reInit() //重新
|
|
225
|
+
}) {
|
|
226
|
+
Text("Retry")
|
|
227
|
+
.font(.system(size: 18).bold())
|
|
228
|
+
.foregroundColor(.white)
|
|
229
|
+
.frame(maxWidth: .infinity)
|
|
230
|
+
.padding(.vertical, 10)
|
|
231
|
+
.background(Color.faceMain)
|
|
232
|
+
.cornerRadius(10)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
.padding(.horizontal, 12)
|
|
236
|
+
}
|
|
237
|
+
.padding(.vertical, 18)
|
|
238
|
+
.background(Color.white)
|
|
239
|
+
.cornerRadius(20)
|
|
240
|
+
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 10)
|
|
241
|
+
.padding(.horizontal, 30)
|
|
242
|
+
}
|
|
243
|
+
.zIndex(2)
|
|
244
|
+
.transition(.scale(scale: 0.8).combined(with: .opacity))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
}
|
|
248
|
+
.onAppear {
|
|
249
|
+
if autoControlBrightness {
|
|
250
|
+
ScreenBrightnessHelper.shared.maximizeBrightness()
|
|
251
|
+
}
|
|
252
|
+
withAnimation(.spring(response: 0.6, dampingFraction: 0.7).delay(0.9)) {
|
|
253
|
+
isTipAppeared = true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
257
|
+
UIScreen.main.brightness = 1.0
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
viewModel.initFaceAISDK(faceIDFeature: "",
|
|
261
|
+
livenessType: livenessType,
|
|
262
|
+
onlyLiveness: true,
|
|
263
|
+
motionLiveness: motionLiveness,
|
|
264
|
+
motionLivenessTimeOut:motionLivenessTimeOut,
|
|
265
|
+
motionLivenessSteps:motionLivenessSteps)
|
|
266
|
+
}
|
|
267
|
+
.onChange(of: viewModel.faceVerifyResult.code) { newValue in
|
|
268
|
+
// 忽略默认状态(例如刚初始化或重试时变成 0),避免直接掉入底部的默认退出流程
|
|
269
|
+
if newValue == VerifyResultCode.DEFAULT { return }
|
|
270
|
+
|
|
271
|
+
// 根据不同的 code 值来决定是否显示 toast 或者弹窗
|
|
272
|
+
// 优先处理光线过强的专用弹窗
|
|
273
|
+
if newValue == VerifyResultCode.COLOR_LIVENESS_LIGHT_TOO_HIGH {
|
|
274
|
+
withAnimation {
|
|
275
|
+
showLightHighDialog = true
|
|
276
|
+
}
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 如果是下列失败码之一,则弹出失败对话框(允许用户知道了或重试),并返回以避免继续执行默认的 toast/退出流程
|
|
281
|
+
let failureCodes: [Int] = [
|
|
282
|
+
VerifyResultCode.MOTION_LIVENESS_TIMEOUT,
|
|
283
|
+
VerifyResultCode.NO_FACE_MULTI,
|
|
284
|
+
VerifyResultCode.COLOR_LIVENESS_FAILED,
|
|
285
|
+
VerifyResultCode.SILENT_LIVENESS_FAILED
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
if failureCodes.contains(newValue) {
|
|
289
|
+
withAnimation {
|
|
290
|
+
showFailureDialog = true
|
|
291
|
+
}
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 其余情况沿用原有流程:展示 toast -> 回调 -> 退出
|
|
296
|
+
withAnimation {
|
|
297
|
+
showFailureDialog = false
|
|
298
|
+
showToast = true
|
|
299
|
+
_ = FaceImageManager.saveFaceImage(faceName: "Liveness", faceImage: viewModel.faceVerifyResult.faceImage)
|
|
300
|
+
}
|
|
301
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
302
|
+
withAnimation { showToast = false }
|
|
303
|
+
onDismiss(viewModel.faceVerifyResult.code, viewModel.faceVerifyResult.liveness)
|
|
304
|
+
dismiss()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
.onDisappear {
|
|
309
|
+
if autoControlBrightness {
|
|
310
|
+
ScreenBrightnessHelper.shared.restoreBrightness()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
viewModel.stopFaceVerify()
|
|
314
|
+
}
|
|
315
|
+
.animation(.easeInOut(duration: 0.3), value: showToast)
|
|
316
|
+
}
|
|
317
|
+
}
|