@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.
Files changed (35) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +252 -0
  3. package/android/build.gradle +53 -0
  4. package/android/libs/FaceSDKLib-release.aar +0 -0
  5. package/android/proguard-rules.pro +3 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/com/faceaisdk/reactnative/FaceRNModule.kt +383 -0
  8. package/android/src/main/java/com/faceaisdk/reactnative/FaceRNPackage.kt +16 -0
  9. package/ios/FaceAISDK/CustomToastView.swift +34 -0
  10. package/ios/FaceAISDK/FaceAINaviView.swift +212 -0
  11. package/ios/FaceAISDK/FaceSDKCameraView.swift +40 -0
  12. package/ios/FaceAISDK/FaceSDKLocalizer.swift +21 -0
  13. package/ios/FaceAISDK/LivenessDetectView.swift +317 -0
  14. package/ios/FaceAISDK/ScreenBrightnessHelper.swift +100 -0
  15. package/ios/FaceAISDK/TTSPlayer.swift +357 -0
  16. package/ios/FaceAISDK/VerifyFaceView.swift +284 -0
  17. package/ios/FaceAISDK/addFace/AddFaceByCamera.swift +207 -0
  18. package/ios/FaceAISDK/addFace/AddFaceByImage.swift +174 -0
  19. package/ios/FaceAISDK/addFace/ImagePicker.swift +52 -0
  20. package/ios/FaceAISDK/addFace/VerifyTwoFaceSimiView.swift +210 -0
  21. package/ios/FaceColorExtensions.swift +10 -0
  22. package/ios/FaceRNModule.h +9 -0
  23. package/ios/FaceRNModule.m +197 -0
  24. package/ios/FaceSDKSwiftManager.swift +277 -0
  25. package/ios/Resources/en.lproj/Localizable.strings +51 -0
  26. package/ios/Resources/light_too_high.png +0 -0
  27. package/ios/Resources/zh-Hans.lproj/Localizable.strings +51 -0
  28. package/lib/index.d.ts +22 -0
  29. package/lib/index.js +112 -0
  30. package/lib/types.d.ts +39 -0
  31. package/lib/types.js +2 -0
  32. package/package.json +88 -0
  33. package/react-native-face-sdk.podspec +28 -0
  34. package/src/index.ts +184 -0
  35. 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
+ }