@simasfin/smmf-sdk 0.0.0-stage → 0.2.3
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/README.md +84 -2
- package/android/build.gradle +51 -0
- package/android/maven/id/co/onepass/onepass-sdk/0.2.3/onepass-sdk-0.2.3.aar +0 -0
- package/android/maven/id/co/onepass/onepass-sdk/0.2.3/onepass-sdk-0.2.3.pom +52 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/id/co/onepass/expo/OnePassSignatureExpoView.kt +25 -0
- package/android/src/main/java/id/co/onepass/expo/OnepassExpoModule.kt +134 -0
- package/app.plugin.js +1 -0
- package/build/OnePassSignatureView.d.ts +19 -0
- package/build/OnePassSignatureView.d.ts.map +1 -0
- package/build/OnePassSignatureView.js +15 -0
- package/build/OnePassSignatureView.js.map +1 -0
- package/build/index.d.ts +21 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +10 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/OnePassSDK/DesignSystem.swift +139 -0
- package/ios/OnePassSDK/IconView.swift +54 -0
- package/ios/OnePassSDK/IconsaxPath.swift +216 -0
- package/ios/OnePassSDK/Liveness/FaceAnalyzer.swift +114 -0
- package/ios/OnePassSDK/Liveness/FaceLandmarkMetrics.swift +28 -0
- package/ios/OnePassSDK/Liveness/FaceSnapshotMapper.swift +37 -0
- package/ios/OnePassSDK/Liveness/LivenessGate.swift +155 -0
- package/ios/OnePassSDK/Liveness/LivenessImagePreparer.swift +44 -0
- package/ios/OnePassSDK/Liveness/LivenessViewController.swift +724 -0
- package/ios/OnePassSDK/Ocr/CoverFitCrop.swift +37 -0
- package/ios/OnePassSDK/Ocr/KtpFrameOverlayView.swift +98 -0
- package/ios/OnePassSDK/Ocr/KtpImagePreparer.swift +90 -0
- package/ios/OnePassSDK/Ocr/KtpOcrViewController.swift +703 -0
- package/ios/OnePassSDK/Ocr/KtpPhotoInspector.swift +93 -0
- package/ios/OnePassSDK/Ocr/KtpQualityGate.swift +185 -0
- package/ios/OnePassSDK/OnePass.swift +28 -0
- package/ios/OnePassSDK/Resources/hindari_pemakaian.png +0 -0
- package/ios/OnePassSDK/Resources/inter_bold.ttf +0 -0
- package/ios/OnePassSDK/Resources/inter_medium.ttf +0 -0
- package/ios/OnePassSDK/Resources/inter_regular.ttf +0 -0
- package/ios/OnePassSDK/Resources/inter_semibold.ttf +0 -0
- package/ios/OnePassSDK/Resources/ktp_illustration.webp +0 -0
- package/ios/OnePassSDK/Resources/logo_smmf.png +0 -0
- package/ios/OnePassSDK/Resources/placeholder_head.png +0 -0
- package/ios/OnePassSDK/Signature/SignaturePadView.swift +144 -0
- package/ios/OnePassSignatureExpoView.swift +22 -0
- package/ios/OnepassExpo.podspec +23 -0
- package/ios/OnepassExpoModule.swift +77 -0
- package/licenses/Iconsax-MIT.txt +26 -0
- package/licenses/Inter-OFL-1.1.txt +93 -0
- package/package.json +49 -4
- package/plugin/src/inject.js +31 -0
- package/plugin/withOnePass.js +32 -0
- package/scripts/sync-native.js +84 -0
- package/src/OnePassSignatureView.tsx +34 -0
- package/src/index.ts +44 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import Vision
|
|
3
|
+
|
|
4
|
+
final class KtpPhotoInspector {
|
|
5
|
+
|
|
6
|
+
private static let metricSampleWidth = 640
|
|
7
|
+
|
|
8
|
+
struct Inspection {
|
|
9
|
+
let quality: KtpQualityResult
|
|
10
|
+
let textBounds: CGRect?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func inspect(image: UIImage, completion: @escaping (Inspection) -> Void) {
|
|
14
|
+
guard let cgImage = image.cgImage else {
|
|
15
|
+
completion(Inspection(quality: passthrough(), textBounds: nil))
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
let imageSize = CGSize(width: cgImage.width, height: cgImage.height)
|
|
19
|
+
let request = VNRecognizeTextRequest { request, _ in
|
|
20
|
+
let observations = request.results as? [VNRecognizedTextObservation] ?? []
|
|
21
|
+
let boxes = observations.map { observation in
|
|
22
|
+
CGRect(
|
|
23
|
+
x: observation.boundingBox.minX * imageSize.width,
|
|
24
|
+
y: (1 - observation.boundingBox.maxY) * imageSize.height,
|
|
25
|
+
width: observation.boundingBox.width * imageSize.width,
|
|
26
|
+
height: observation.boundingBox.height * imageSize.height
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
let text = observations
|
|
30
|
+
.compactMap { $0.topCandidates(1).first?.string }
|
|
31
|
+
.joined(separator: "\n")
|
|
32
|
+
let bounds = KtpAutoCrop.union(boxes)
|
|
33
|
+
let luma = Self.grayscaleSamples(cgImage)
|
|
34
|
+
let quality = KtpQualityGate.evaluate(
|
|
35
|
+
recognizedText: text,
|
|
36
|
+
textCoverage: bounds.map { KtpAutoCrop.coverage($0, imageSize: imageSize) } ?? 0,
|
|
37
|
+
textTouchesEdge: bounds.map { KtpAutoCrop.touchesEdge($0, imageSize: imageSize) } ?? false,
|
|
38
|
+
sharpness: ImageQualityMetrics.sharpness(
|
|
39
|
+
luma: luma.values,
|
|
40
|
+
width: luma.width,
|
|
41
|
+
height: luma.height
|
|
42
|
+
),
|
|
43
|
+
glareRatio: ImageQualityMetrics.glareRatio(luma: luma.values)
|
|
44
|
+
)
|
|
45
|
+
completion(Inspection(quality: quality, textBounds: bounds))
|
|
46
|
+
}
|
|
47
|
+
request.recognitionLevel = .accurate
|
|
48
|
+
request.usesLanguageCorrection = false
|
|
49
|
+
|
|
50
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
51
|
+
let handler = VNImageRequestHandler(cgImage: cgImage, orientation: .up)
|
|
52
|
+
do {
|
|
53
|
+
try handler.perform([request])
|
|
54
|
+
} catch {
|
|
55
|
+
completion(Inspection(quality: self?.passthrough() ?? KtpQualityResult(passed: true, issue: nil, message: nil), textBounds: nil))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private func passthrough() -> KtpQualityResult {
|
|
61
|
+
KtpQualityResult(passed: true, issue: nil, message: nil)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private struct Grayscale {
|
|
65
|
+
let values: [Int]
|
|
66
|
+
let width: Int
|
|
67
|
+
let height: Int
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static func grayscaleSamples(_ cgImage: CGImage) -> Grayscale {
|
|
71
|
+
let scale = CGFloat(metricSampleWidth) / CGFloat(cgImage.width)
|
|
72
|
+
let width = scale < 1 ? metricSampleWidth : cgImage.width
|
|
73
|
+
let height = scale < 1 ? Int(CGFloat(cgImage.height) * scale) : cgImage.height
|
|
74
|
+
guard width > 0, height > 0 else { return Grayscale(values: [], width: 0, height: 0) }
|
|
75
|
+
|
|
76
|
+
var pixels = [UInt8](repeating: 0, count: width * height)
|
|
77
|
+
guard
|
|
78
|
+
let context = CGContext(
|
|
79
|
+
data: &pixels,
|
|
80
|
+
width: width,
|
|
81
|
+
height: height,
|
|
82
|
+
bitsPerComponent: 8,
|
|
83
|
+
bytesPerRow: width,
|
|
84
|
+
space: CGColorSpaceCreateDeviceGray(),
|
|
85
|
+
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
86
|
+
)
|
|
87
|
+
else {
|
|
88
|
+
return Grayscale(values: [], width: 0, height: 0)
|
|
89
|
+
}
|
|
90
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
91
|
+
return Grayscale(values: pixels.map { Int($0) }, width: width, height: height)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
public enum KtpQualityIssue {
|
|
5
|
+
case textNotReadable
|
|
6
|
+
case cropped
|
|
7
|
+
case tooSmall
|
|
8
|
+
case glare
|
|
9
|
+
case blurry
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public struct KtpQualityResult: Equatable {
|
|
13
|
+
public let passed: Bool
|
|
14
|
+
public let issue: KtpQualityIssue?
|
|
15
|
+
public let message: String?
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public enum KtpQualityGate {
|
|
19
|
+
|
|
20
|
+
public static let minLabelMatches = 3
|
|
21
|
+
public static let minTextCoverage: Float = 0.12
|
|
22
|
+
public static let minSharpness: Double = 60
|
|
23
|
+
public static let maxGlareRatio: Float = 0.12
|
|
24
|
+
|
|
25
|
+
private static let requiredLabels = [
|
|
26
|
+
"PROVINSI",
|
|
27
|
+
"NIK",
|
|
28
|
+
"NAMA",
|
|
29
|
+
"LAHIR",
|
|
30
|
+
"KELAMIN",
|
|
31
|
+
"ALAMAT",
|
|
32
|
+
"AGAMA",
|
|
33
|
+
"PEKERJAAN",
|
|
34
|
+
"KEWARGANEGARAAN",
|
|
35
|
+
"BERLAKU",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
public static func evaluate(
|
|
39
|
+
recognizedText: String,
|
|
40
|
+
textCoverage: Float,
|
|
41
|
+
textTouchesEdge: Bool,
|
|
42
|
+
sharpness: Double,
|
|
43
|
+
glareRatio: Float
|
|
44
|
+
) -> KtpQualityResult {
|
|
45
|
+
guard isReadable(recognizedText) else {
|
|
46
|
+
return failure(
|
|
47
|
+
.textNotReadable,
|
|
48
|
+
"Teks KTP belum terbaca jelas. Pastikan KTP tidak tertutup dan cahaya cukup."
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
if textTouchesEdge {
|
|
52
|
+
return failure(.cropped, "KTP terpotong. Masukkan seluruh KTP ke dalam kotak.")
|
|
53
|
+
}
|
|
54
|
+
if textCoverage < minTextCoverage {
|
|
55
|
+
return failure(.tooSmall, "KTP terlalu jauh. Dekatkan KTP hingga memenuhi kotak.")
|
|
56
|
+
}
|
|
57
|
+
if glareRatio > maxGlareRatio {
|
|
58
|
+
return failure(
|
|
59
|
+
.glare,
|
|
60
|
+
"Ada cahaya menyilaukan di KTP. Miringkan sedikit lalu ambil ulang."
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
if sharpness < minSharpness {
|
|
64
|
+
return failure(.blurry, "Foto buram. Tahan HP lebih stabil lalu ambil ulang.")
|
|
65
|
+
}
|
|
66
|
+
return KtpQualityResult(passed: true, issue: nil, message: nil)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static func isReadable(_ recognizedText: String) -> Bool {
|
|
70
|
+
let upper = recognizedText.uppercased()
|
|
71
|
+
if containsNik(upper) { return true }
|
|
72
|
+
return requiredLabels.filter { upper.contains($0) }.count >= minLabelMatches
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static func containsNik(_ text: String) -> Bool {
|
|
76
|
+
var digitRun = 0
|
|
77
|
+
for character in text {
|
|
78
|
+
if character.isNumber {
|
|
79
|
+
digitRun += 1
|
|
80
|
+
if digitRun == 16 { return true }
|
|
81
|
+
} else {
|
|
82
|
+
digitRun = 0
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static func failure(_ issue: KtpQualityIssue, _ message: String) -> KtpQualityResult {
|
|
89
|
+
KtpQualityResult(passed: false, issue: issue, message: message)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public enum KtpAutoCrop {
|
|
94
|
+
|
|
95
|
+
public static let paddingRatio: CGFloat = 0.06
|
|
96
|
+
public static let edgeMarginRatio: CGFloat = 0.02
|
|
97
|
+
|
|
98
|
+
public static func union(_ boxes: [CGRect]) -> CGRect? {
|
|
99
|
+
guard let first = boxes.first else { return nil }
|
|
100
|
+
return boxes.dropFirst().reduce(first) { $0.union($1) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public static func coverage(_ bounds: CGRect, imageSize: CGSize) -> Float {
|
|
104
|
+
let area = imageSize.width * imageSize.height
|
|
105
|
+
guard area > 0 else { return 0 }
|
|
106
|
+
return Float((bounds.width * bounds.height) / area)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public static func touchesEdge(_ bounds: CGRect, imageSize: CGSize) -> Bool {
|
|
110
|
+
let marginX = imageSize.width * edgeMarginRatio
|
|
111
|
+
let marginY = imageSize.height * edgeMarginRatio
|
|
112
|
+
return bounds.minX <= marginX
|
|
113
|
+
|| bounds.minY <= marginY
|
|
114
|
+
|| bounds.maxX >= imageSize.width - marginX
|
|
115
|
+
|| bounds.maxY >= imageSize.height - marginY
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public static func cropTo(
|
|
119
|
+
bounds: CGRect,
|
|
120
|
+
imageSize: CGSize,
|
|
121
|
+
targetRatio: CGFloat
|
|
122
|
+
) -> CropRegion {
|
|
123
|
+
let padded = bounds.insetBy(
|
|
124
|
+
dx: -bounds.width * paddingRatio,
|
|
125
|
+
dy: -bounds.height * paddingRatio
|
|
126
|
+
)
|
|
127
|
+
var width = padded.width
|
|
128
|
+
var height = padded.height
|
|
129
|
+
if width / height > targetRatio {
|
|
130
|
+
height = width / targetRatio
|
|
131
|
+
} else {
|
|
132
|
+
width = height * targetRatio
|
|
133
|
+
}
|
|
134
|
+
let cappedWidth = min(width, imageSize.width)
|
|
135
|
+
let cappedHeight = min(cappedWidth / targetRatio, imageSize.height)
|
|
136
|
+
let finalWidth = min(cappedWidth, cappedHeight * targetRatio)
|
|
137
|
+
let finalHeight = finalWidth / targetRatio
|
|
138
|
+
|
|
139
|
+
let left = max(0, min(padded.midX - finalWidth / 2, imageSize.width - finalWidth))
|
|
140
|
+
let top = max(0, min(padded.midY - finalHeight / 2, imageSize.height - finalHeight))
|
|
141
|
+
|
|
142
|
+
return CropRegion(
|
|
143
|
+
left: Int(left.rounded()),
|
|
144
|
+
top: Int(top.rounded()),
|
|
145
|
+
width: max(1, Int(finalWidth.rounded())),
|
|
146
|
+
height: max(1, Int(finalHeight.rounded()))
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public enum ImageQualityMetrics {
|
|
152
|
+
|
|
153
|
+
private static let overexposedLuma = 250
|
|
154
|
+
|
|
155
|
+
public static func sharpness(luma: [Int], width: Int, height: Int) -> Double {
|
|
156
|
+
guard width >= 3, height >= 3, luma.count >= width * height else { return 0 }
|
|
157
|
+
var sum = 0.0
|
|
158
|
+
var sumSquares = 0.0
|
|
159
|
+
var count = 0
|
|
160
|
+
for y in 1..<(height - 1) {
|
|
161
|
+
for x in 1..<(width - 1) {
|
|
162
|
+
let index = y * width + x
|
|
163
|
+
let value = Double(
|
|
164
|
+
4 * luma[index]
|
|
165
|
+
- luma[index - 1]
|
|
166
|
+
- luma[index + 1]
|
|
167
|
+
- luma[index - width]
|
|
168
|
+
- luma[index + width]
|
|
169
|
+
)
|
|
170
|
+
sum += value
|
|
171
|
+
sumSquares += value * value
|
|
172
|
+
count += 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
guard count > 0 else { return 0 }
|
|
176
|
+
let mean = sum / Double(count)
|
|
177
|
+
return sumSquares / Double(count) - mean * mean
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public static func glareRatio(luma: [Int]) -> Float {
|
|
181
|
+
guard !luma.isEmpty else { return 0 }
|
|
182
|
+
let overexposed = luma.filter { $0 >= overexposedLuma }.count
|
|
183
|
+
return Float(overexposed) / Float(luma.count)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
public enum OnePass {
|
|
4
|
+
|
|
5
|
+
public static func startLiveness(
|
|
6
|
+
from presenter: UIViewController,
|
|
7
|
+
input: LivenessInput = LivenessInput(),
|
|
8
|
+
completion: @escaping (LivenessResult) -> Void
|
|
9
|
+
) {
|
|
10
|
+
let controller = LivenessViewController(input: input) { result in
|
|
11
|
+
presenter.dismiss(animated: true) { completion(result) }
|
|
12
|
+
}
|
|
13
|
+
controller.modalPresentationStyle = .fullScreen
|
|
14
|
+
presenter.present(controller, animated: true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public static func startKtpOcr(
|
|
18
|
+
from presenter: UIViewController,
|
|
19
|
+
input: KtpOcrInput = KtpOcrInput(),
|
|
20
|
+
completion: @escaping (KtpOcrResult) -> Void
|
|
21
|
+
) {
|
|
22
|
+
let controller = KtpOcrViewController(input: input) { result in
|
|
23
|
+
presenter.dismiss(animated: true) { completion(result) }
|
|
24
|
+
}
|
|
25
|
+
controller.modalPresentationStyle = .fullScreen
|
|
26
|
+
presenter.present(controller, animated: true)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
public final class SignaturePadView: UIView {
|
|
4
|
+
|
|
5
|
+
public var onBegin: (() -> Void)?
|
|
6
|
+
public var onEnd: (() -> Void)?
|
|
7
|
+
|
|
8
|
+
public var penColor: UIColor = .black {
|
|
9
|
+
didSet { setNeedsDisplay() }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public var strokeWidth: CGFloat = 2 {
|
|
13
|
+
didSet { setNeedsDisplay() }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private var strokes: [UIBezierPath] = []
|
|
17
|
+
private var dots: [CGPoint] = []
|
|
18
|
+
private var current: UIBezierPath?
|
|
19
|
+
private var moved = false
|
|
20
|
+
private var lastPoint: CGPoint = .zero
|
|
21
|
+
private var lockedScrollViews: [UIScrollView] = []
|
|
22
|
+
|
|
23
|
+
public var isEmpty: Bool { strokes.isEmpty && dots.isEmpty }
|
|
24
|
+
|
|
25
|
+
public override init(frame: CGRect) {
|
|
26
|
+
super.init(frame: frame)
|
|
27
|
+
backgroundColor = .clear
|
|
28
|
+
isOpaque = false
|
|
29
|
+
isMultipleTouchEnabled = false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@available(*, unavailable)
|
|
33
|
+
required init?(coder: NSCoder) { fatalError("tidak dipakai") }
|
|
34
|
+
|
|
35
|
+
public func clear() {
|
|
36
|
+
strokes.removeAll()
|
|
37
|
+
dots.removeAll()
|
|
38
|
+
current = nil
|
|
39
|
+
setNeedsDisplay()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public func toBase64Png() -> String? {
|
|
43
|
+
guard !isEmpty, bounds.width > 0, bounds.height > 0 else { return nil }
|
|
44
|
+
let format = UIGraphicsImageRendererFormat.default()
|
|
45
|
+
format.opaque = false
|
|
46
|
+
let image = UIGraphicsImageRenderer(bounds: bounds, format: format).image { _ in
|
|
47
|
+
drawStrokes()
|
|
48
|
+
}
|
|
49
|
+
return image.pngData()?.base64EncodedString()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public override func draw(_ rect: CGRect) {
|
|
53
|
+
drawStrokes()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func drawStrokes() {
|
|
57
|
+
penColor.setStroke()
|
|
58
|
+
penColor.setFill()
|
|
59
|
+
for path in strokes {
|
|
60
|
+
path.lineWidth = strokeWidth
|
|
61
|
+
path.lineCapStyle = .round
|
|
62
|
+
path.lineJoinStyle = .round
|
|
63
|
+
path.stroke()
|
|
64
|
+
}
|
|
65
|
+
for dot in dots {
|
|
66
|
+
UIBezierPath(
|
|
67
|
+
arcCenter: dot,
|
|
68
|
+
radius: strokeWidth / 2,
|
|
69
|
+
startAngle: 0,
|
|
70
|
+
endAngle: .pi * 2,
|
|
71
|
+
clockwise: true
|
|
72
|
+
).fill()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func beginStroke(at point: CGPoint) {
|
|
77
|
+
let path = UIBezierPath()
|
|
78
|
+
path.move(to: point)
|
|
79
|
+
current = path
|
|
80
|
+
moved = false
|
|
81
|
+
lastPoint = point
|
|
82
|
+
lockScrollViews()
|
|
83
|
+
onBegin?()
|
|
84
|
+
setNeedsDisplay()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func continueStroke(to point: CGPoint) {
|
|
88
|
+
guard let path = current else { return }
|
|
89
|
+
if !moved {
|
|
90
|
+
strokes.append(path)
|
|
91
|
+
moved = true
|
|
92
|
+
}
|
|
93
|
+
let mid = CGPoint(x: (point.x + lastPoint.x) / 2, y: (point.y + lastPoint.y) / 2)
|
|
94
|
+
path.addQuadCurve(to: mid, controlPoint: lastPoint)
|
|
95
|
+
lastPoint = point
|
|
96
|
+
setNeedsDisplay()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func endStroke(at point: CGPoint) {
|
|
100
|
+
guard let path = current else { return }
|
|
101
|
+
if moved { path.addLine(to: point) } else { dots.append(point) }
|
|
102
|
+
current = nil
|
|
103
|
+
unlockScrollViews()
|
|
104
|
+
onEnd?()
|
|
105
|
+
setNeedsDisplay()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func lockScrollViews() {
|
|
109
|
+
var view = superview
|
|
110
|
+
while let candidate = view {
|
|
111
|
+
if let scroll = candidate as? UIScrollView, scroll.isScrollEnabled {
|
|
112
|
+
scroll.isScrollEnabled = false
|
|
113
|
+
lockedScrollViews.append(scroll)
|
|
114
|
+
}
|
|
115
|
+
view = candidate.superview
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private func unlockScrollViews() {
|
|
120
|
+
lockedScrollViews.forEach { $0.isScrollEnabled = true }
|
|
121
|
+
lockedScrollViews.removeAll()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
125
|
+
guard let touch = touches.first else { return }
|
|
126
|
+
beginStroke(at: touch.location(in: self))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
130
|
+
guard let touch = touches.first else { return }
|
|
131
|
+
let samples = event?.coalescedTouches(for: touch) ?? [touch]
|
|
132
|
+
samples.forEach { continueStroke(to: $0.location(in: self)) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
136
|
+
guard let touch = touches.first else { return }
|
|
137
|
+
endStroke(at: touch.location(in: self))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
141
|
+
guard let touch = touches.first else { return }
|
|
142
|
+
endStroke(at: touch.location(in: self))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public final class OnePassSignatureExpoView: ExpoView {
|
|
5
|
+
|
|
6
|
+
let pad = SignaturePadView(frame: .zero)
|
|
7
|
+
let onBegin = EventDispatcher()
|
|
8
|
+
let onEnd = EventDispatcher()
|
|
9
|
+
|
|
10
|
+
public required init(appContext: AppContext? = nil) {
|
|
11
|
+
super.init(appContext: appContext)
|
|
12
|
+
clipsToBounds = true
|
|
13
|
+
addSubview(pad)
|
|
14
|
+
pad.onBegin = { [weak self] in self?.onBegin([:]) }
|
|
15
|
+
pad.onEnd = { [weak self] in self?.onEnd([:]) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public override func layoutSubviews() {
|
|
19
|
+
super.layoutSubviews()
|
|
20
|
+
pad.frame = bounds
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'OnepassExpo'
|
|
3
|
+
s.version = '0.1.0'
|
|
4
|
+
s.summary = 'Expo module untuk liveness wajah, foto KTP, dan kanvas tanda tangan'
|
|
5
|
+
s.description = 'Binding Expo untuk liveness wajah, foto KTP, dan kanvas tanda tangan.'
|
|
6
|
+
s.author = 'simasfin'
|
|
7
|
+
s.homepage = 'https://www.npmjs.com/package/@simasfin/smmf-sdk'
|
|
8
|
+
s.platforms = { :ios => '13.4' }
|
|
9
|
+
s.source = { :git => '' }
|
|
10
|
+
s.static_framework = true
|
|
11
|
+
|
|
12
|
+
s.dependency 'ExpoModulesCore'
|
|
13
|
+
|
|
14
|
+
s.pod_target_xcconfig = {
|
|
15
|
+
'DEFINES_MODULE' => 'YES',
|
|
16
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
s.source_files = 'OnepassExpoModule.swift', 'OnePassSignatureExpoView.swift', 'OnePassSDK/**/*.swift'
|
|
20
|
+
s.resource_bundles = {
|
|
21
|
+
'OnePassSDK' => ['OnePassSDK/Resources/*']
|
|
22
|
+
}
|
|
23
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class OnepassExpoModule: Module {
|
|
5
|
+
|
|
6
|
+
public func definition() -> ModuleDefinition {
|
|
7
|
+
Name("OnepassExpo")
|
|
8
|
+
|
|
9
|
+
AsyncFunction("startLiveness") { (options: [String: Any], promise: Promise) in
|
|
10
|
+
guard let presenter = self.appContext?.utilities?.currentViewController() else {
|
|
11
|
+
promise.reject("ONEPASS_NO_VIEW_CONTROLLER", "Tidak ada view controller aktif")
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
let input = LivenessInput(primaryColorHex: options["primaryColorHex"] as? String)
|
|
15
|
+
OnePass.startLiveness(from: presenter, input: input) { result in
|
|
16
|
+
promise.resolve([
|
|
17
|
+
"status": Self.statusName(result.status),
|
|
18
|
+
"imageBase64": result.imageBase64 as Any?,
|
|
19
|
+
"errorMessage": result.errorMessage as Any?,
|
|
20
|
+
])
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
.runOnQueue(.main)
|
|
24
|
+
|
|
25
|
+
AsyncFunction("startKtpOcr") { (options: [String: Any], promise: Promise) in
|
|
26
|
+
guard let presenter = self.appContext?.utilities?.currentViewController() else {
|
|
27
|
+
promise.reject("ONEPASS_NO_VIEW_CONTROLLER", "Tidak ada view controller aktif")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
let input = KtpOcrInput(primaryColorHex: options["primaryColorHex"] as? String)
|
|
31
|
+
OnePass.startKtpOcr(from: presenter, input: input) { result in
|
|
32
|
+
promise.resolve([
|
|
33
|
+
"status": Self.statusName(result.status),
|
|
34
|
+
"imageBase64": result.imageBase64 as Any?,
|
|
35
|
+
"errorMessage": result.errorMessage as Any?,
|
|
36
|
+
])
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
.runOnQueue(.main)
|
|
40
|
+
|
|
41
|
+
View(OnePassSignatureExpoView.self) {
|
|
42
|
+
Events("onBegin", "onEnd")
|
|
43
|
+
|
|
44
|
+
Prop("penColor") { (view: OnePassSignatureExpoView, color: String?) in
|
|
45
|
+
view.pad.penColor = color.map { UIColor(hex: $0) } ?? .black
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Prop("strokeWidth") { (view: OnePassSignatureExpoView, width: Double?) in
|
|
49
|
+
view.pad.strokeWidth = CGFloat(width ?? 2)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
AsyncFunction("clear") { (view: OnePassSignatureExpoView) in
|
|
53
|
+
view.pad.clear()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
AsyncFunction("readSignature") { (view: OnePassSignatureExpoView) -> String? in
|
|
57
|
+
view.pad.toBase64Png()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static func statusName(_ status: LivenessStatus) -> String {
|
|
63
|
+
switch status {
|
|
64
|
+
case .success: return "SUCCESS"
|
|
65
|
+
case .failed: return "FAILED"
|
|
66
|
+
case .cancelled: return "CANCELLED"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static func statusName(_ status: KtpOcrStatusCode) -> String {
|
|
71
|
+
switch status {
|
|
72
|
+
case .success: return "SUCCESS"
|
|
73
|
+
case .failed: return "FAILED"
|
|
74
|
+
case .cancelled: return "CANCELLED"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Iconsax icon set
|
|
2
|
+
Copyright (c) Vuesax (https://iconsax.io)
|
|
3
|
+
|
|
4
|
+
Didistribusikan melalui paket npm "iconsax-react-native" yang berlisensi MIT.
|
|
5
|
+
Ikon Camera, Flash, FlashSlash, CloseSquare, Danger, dan User pada SDK ini
|
|
6
|
+
dihasilkan dari data path SVG paket tersebut.
|
|
7
|
+
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|