expo-secure-keypad-jsi 0.1.2 → 0.1.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/android/src/main/java/expo/modules/securekeypadjsi/KeyboardCanvasView.kt +8 -8
- package/android/src/main/java/expo/modules/securekeypadjsi/KeypadCanvasView.kt +5 -8
- package/android/src/main/java/expo/modules/securekeypadjsi/ThemedKeypadView.kt +45 -0
- package/ios/KeyboardCoreGraphicsView.swift +8 -8
- package/ios/KeypadCoreGraphicsView.swift +51 -8
- package/package.json +1 -1
|
@@ -105,7 +105,7 @@ class KeyboardCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
105
105
|
private val actionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { textAlign = Paint.Align.CENTER }
|
|
106
106
|
private val gap = 6f * density
|
|
107
107
|
|
|
108
|
-
private
|
|
108
|
+
private val pressed = PressedKeyState(this)
|
|
109
109
|
|
|
110
110
|
init {
|
|
111
111
|
isClickable = true
|
|
@@ -226,7 +226,7 @@ class KeyboardCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
226
226
|
|
|
227
227
|
for ((index, entry) in keyRects.withIndex()) {
|
|
228
228
|
val (rect, key) = entry
|
|
229
|
-
val held = pressedHighlight && index ==
|
|
229
|
+
val held = pressedHighlight && index == pressed.index
|
|
230
230
|
keyPaint.color = keyFill(held)
|
|
231
231
|
canvas.drawRoundRect(rect, r, r, keyPaint)
|
|
232
232
|
val label = labelFor(key)
|
|
@@ -257,18 +257,15 @@ class KeyboardCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
257
257
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
258
258
|
when (event.actionMasked) {
|
|
259
259
|
MotionEvent.ACTION_DOWN -> {
|
|
260
|
-
|
|
261
|
-
invalidate()
|
|
260
|
+
pressed.press(keyRects.indexOfFirst { it.first.contains(event.x, event.y) })
|
|
262
261
|
return true
|
|
263
262
|
}
|
|
264
263
|
MotionEvent.ACTION_CANCEL -> {
|
|
265
|
-
|
|
266
|
-
invalidate()
|
|
264
|
+
pressed.clear()
|
|
267
265
|
return true
|
|
268
266
|
}
|
|
269
267
|
MotionEvent.ACTION_UP -> {
|
|
270
|
-
|
|
271
|
-
invalidate()
|
|
268
|
+
pressed.release()
|
|
272
269
|
val key = keyRects.firstOrNull { it.first.contains(event.x, event.y) }?.second
|
|
273
270
|
if (key != null) handleKey(key)
|
|
274
271
|
performClick()
|
|
@@ -297,11 +294,14 @@ class KeyboardCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
297
294
|
ACTION_TOGGLE -> {
|
|
298
295
|
layer = if (layer == Layer.LETTERS) Layer.SYMBOLS else Layer.LETTERS
|
|
299
296
|
shift = ShiftState.OFF
|
|
297
|
+
// A new key set: the lit index would now point at another key.
|
|
298
|
+
pressed.clear()
|
|
300
299
|
layoutKeys()
|
|
301
300
|
invalidate()
|
|
302
301
|
}
|
|
303
302
|
ACTION_SYMBOL_PAGE -> {
|
|
304
303
|
layer = if (layer == Layer.SYMBOLS) Layer.SYMBOLS_EXTRA else Layer.SYMBOLS
|
|
304
|
+
pressed.clear()
|
|
305
305
|
layoutKeys()
|
|
306
306
|
invalidate()
|
|
307
307
|
}
|
|
@@ -64,7 +64,7 @@ class KeypadCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
64
64
|
private val actionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { textAlign = Paint.Align.CENTER }
|
|
65
65
|
private val rect = RectF()
|
|
66
66
|
|
|
67
|
-
private
|
|
67
|
+
private val pressed = PressedKeyState(this)
|
|
68
68
|
|
|
69
69
|
private var cellW = 0f
|
|
70
70
|
private var cellH = 0f
|
|
@@ -114,7 +114,7 @@ class KeypadCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
114
114
|
rect.set(left, top, left + cellW, top + cellH)
|
|
115
115
|
|
|
116
116
|
val digit = digitAtCell[cell]
|
|
117
|
-
val held = pressedHighlight && cell ==
|
|
117
|
+
val held = pressedHighlight && cell == pressed.index
|
|
118
118
|
val r = keyCornerRadiusDp * density
|
|
119
119
|
keyPaint.color = keyFill(held)
|
|
120
120
|
// Action cells have no key fill. A held one gets the pressedKeyColor
|
|
@@ -158,18 +158,15 @@ class KeypadCanvasView(context: Context) : View(context), ThemedKeypadView {
|
|
|
158
158
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
159
159
|
when (event.actionMasked) {
|
|
160
160
|
MotionEvent.ACTION_DOWN -> {
|
|
161
|
-
|
|
162
|
-
invalidate()
|
|
161
|
+
pressed.press(cellAt(event.x, event.y))
|
|
163
162
|
return true
|
|
164
163
|
}
|
|
165
164
|
MotionEvent.ACTION_CANCEL -> {
|
|
166
|
-
|
|
167
|
-
invalidate()
|
|
165
|
+
pressed.clear()
|
|
168
166
|
return true
|
|
169
167
|
}
|
|
170
168
|
MotionEvent.ACTION_UP -> {
|
|
171
|
-
|
|
172
|
-
invalidate()
|
|
169
|
+
pressed.release()
|
|
173
170
|
val cell = cellAt(event.x, event.y)
|
|
174
171
|
when {
|
|
175
172
|
cell in 0..11 && digitAtCell[cell] >= 0 -> {
|
|
@@ -7,6 +7,8 @@ import android.graphics.Color
|
|
|
7
7
|
import android.graphics.Paint
|
|
8
8
|
import android.graphics.RectF
|
|
9
9
|
import android.graphics.Typeface
|
|
10
|
+
import android.os.SystemClock
|
|
11
|
+
import android.view.View
|
|
10
12
|
import com.facebook.react.common.assets.ReactFontManager
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -55,6 +57,49 @@ internal fun pressedDim(color: Int, held: Boolean): Int =
|
|
|
55
57
|
color
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Which key is lit, kept lit for at least [MIN_LIT_MS] after the press.
|
|
62
|
+
*
|
|
63
|
+
* When DOWN and UP land within one frame — a quick tap, or a touch the system
|
|
64
|
+
* held back and then released whole — unlighting on UP clears the key before a
|
|
65
|
+
* frame is drawn. iOS does this at the left and right screen edges (see the
|
|
66
|
+
* PressedKeyState twin in ios/KeypadCoreGraphicsView.swift); this keeps both
|
|
67
|
+
* platforms showing the same feedback. [View] guards its own pressed state the
|
|
68
|
+
* same way with `ViewConfiguration.getPressedStateDuration`.
|
|
69
|
+
*/
|
|
70
|
+
internal class PressedKeyState(private val view: View) {
|
|
71
|
+
companion object {
|
|
72
|
+
/** About one ordinary tap, so an edge tap reads like one in the middle. */
|
|
73
|
+
const val MIN_LIT_MS = 100L
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var index = -1
|
|
77
|
+
private set
|
|
78
|
+
private var litAt = 0L
|
|
79
|
+
private val unlight = Runnable { clear() }
|
|
80
|
+
|
|
81
|
+
fun press(index: Int) {
|
|
82
|
+
view.removeCallbacks(unlight)
|
|
83
|
+
this.index = index
|
|
84
|
+
litAt = SystemClock.uptimeMillis()
|
|
85
|
+
view.invalidate()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Finger lifted: unlit now, or once the key has been lit for [MIN_LIT_MS]. */
|
|
89
|
+
fun release() {
|
|
90
|
+
view.removeCallbacks(unlight)
|
|
91
|
+
val remaining = MIN_LIT_MS - (SystemClock.uptimeMillis() - litAt)
|
|
92
|
+
if (index >= 0 && remaining > 0) view.postDelayed(unlight, remaining) else clear()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Unlit at once: a cancelled touch, or a key set that changed under it. */
|
|
96
|
+
fun clear() {
|
|
97
|
+
view.removeCallbacks(unlight)
|
|
98
|
+
index = -1
|
|
99
|
+
view.invalidate()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
58
103
|
/** Key fill: [ThemedKeypadView.pressedKeyColor] while held when set, else the 70% dim. */
|
|
59
104
|
internal fun ThemedKeypadView.keyFill(held: Boolean): Int =
|
|
60
105
|
if (held) pressedKeyColor ?: pressedDim(keyColor, true) else keyColor
|
|
@@ -82,7 +82,7 @@ final class KeyboardCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
82
82
|
private var dummySlots = [Int](repeating: -1, count: 5)
|
|
83
83
|
|
|
84
84
|
private var keyRects: [(CGRect, Key)] = []
|
|
85
|
-
private var
|
|
85
|
+
private lazy var pressed = PressedKeyState(view: self)
|
|
86
86
|
private let gap: CGFloat = 6
|
|
87
87
|
|
|
88
88
|
override init(frame: CGRect) {
|
|
@@ -205,7 +205,7 @@ final class KeyboardCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
205
205
|
for (index, entry) in keyRects.enumerated() {
|
|
206
206
|
let (frame, key) = entry
|
|
207
207
|
let path = UIBezierPath(roundedRect: frame, cornerRadius: keyCornerRadius)
|
|
208
|
-
keyFill(pressedHighlight && index ==
|
|
208
|
+
keyFill(pressedHighlight && index == pressed.index).setFill()
|
|
209
209
|
path.fill()
|
|
210
210
|
if let label = labelFor(key) {
|
|
211
211
|
let isChar = key.code >= 0
|
|
@@ -231,18 +231,15 @@ final class KeyboardCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
231
231
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
232
232
|
guard let touch = touches.first else { return }
|
|
233
233
|
let p = touch.location(in: self)
|
|
234
|
-
|
|
235
|
-
setNeedsDisplay()
|
|
234
|
+
pressed.press(keyRects.firstIndex(where: { $0.0.contains(p) }) ?? -1)
|
|
236
235
|
}
|
|
237
236
|
|
|
238
237
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
239
|
-
|
|
240
|
-
setNeedsDisplay()
|
|
238
|
+
pressed.clear()
|
|
241
239
|
}
|
|
242
240
|
|
|
243
241
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
244
|
-
|
|
245
|
-
setNeedsDisplay()
|
|
242
|
+
pressed.release()
|
|
246
243
|
guard let touch = touches.first else { return }
|
|
247
244
|
let p = touch.location(in: self)
|
|
248
245
|
guard let key = keyRects.first(where: { $0.0.contains(p) })?.1 else { return }
|
|
@@ -271,10 +268,13 @@ final class KeyboardCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
271
268
|
case Code.toggle:
|
|
272
269
|
layer_ = layer_ == .letters ? .symbols : .letters
|
|
273
270
|
shift = .off
|
|
271
|
+
// A new key set: the lit index would now point at another key.
|
|
272
|
+
pressed.clear()
|
|
274
273
|
layoutKeys()
|
|
275
274
|
setNeedsDisplay()
|
|
276
275
|
case Code.symbolPage:
|
|
277
276
|
layer_ = layer_ == .symbols ? .symbolsExtra : .symbols
|
|
277
|
+
pressed.clear()
|
|
278
278
|
layoutKeys()
|
|
279
279
|
setNeedsDisplay()
|
|
280
280
|
default:
|
|
@@ -38,7 +38,7 @@ final class KeypadCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
38
38
|
private let clearCell = 9
|
|
39
39
|
private let backspaceCell = 11
|
|
40
40
|
private var digitAtCell = [Int](repeating: -1, count: 12)
|
|
41
|
-
private var
|
|
41
|
+
private lazy var pressed = PressedKeyState(view: self)
|
|
42
42
|
private let gap: CGFloat = 12
|
|
43
43
|
|
|
44
44
|
override init(frame: CGRect) {
|
|
@@ -80,7 +80,7 @@ final class KeypadCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
80
80
|
let row = CGFloat(cell / 3)
|
|
81
81
|
let frame = CGRect(x: col * (cw + gap), y: row * (ch + gap), width: cw, height: ch)
|
|
82
82
|
let digit = digitAtCell[cell]
|
|
83
|
-
let held = pressedHighlight && cell ==
|
|
83
|
+
let held = pressedHighlight && cell == pressed.index
|
|
84
84
|
let path = UIBezierPath(roundedRect: frame, cornerRadius: keyCornerRadius)
|
|
85
85
|
// Action cells have no key fill. A held one gets the pressedKeyColor fill
|
|
86
86
|
// when the theme sets it; otherwise the glyph itself carries the dim.
|
|
@@ -116,18 +116,15 @@ final class KeypadCoreGraphicsView: UIView, ThemableKeypadView {
|
|
|
116
116
|
|
|
117
117
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
118
118
|
guard let touch = touches.first else { return }
|
|
119
|
-
|
|
120
|
-
setNeedsDisplay()
|
|
119
|
+
pressed.press(cellAt(touch.location(in: self)))
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
124
|
-
|
|
125
|
-
setNeedsDisplay()
|
|
123
|
+
pressed.clear()
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
129
|
-
|
|
130
|
-
setNeedsDisplay()
|
|
127
|
+
pressed.release()
|
|
131
128
|
guard let touch = touches.first else { return }
|
|
132
129
|
let cell = cellAt(touch.location(in: self))
|
|
133
130
|
guard cell >= 0, cell < 12 else { return }
|
|
@@ -168,6 +165,52 @@ protocol ThemableKeypadView: AnyObject {
|
|
|
168
165
|
/// shows, and the fallback whenever the native CSPRNG layout is unavailable.
|
|
169
166
|
let identityDigitLayout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
|
|
170
167
|
|
|
168
|
+
/// Which key is lit, kept lit for at least `minimumLit` after the press.
|
|
169
|
+
///
|
|
170
|
+
/// Near the left and right screen edges iOS holds a touch back while it decides
|
|
171
|
+
/// whether a system edge gesture is starting, and on a quick tap releases
|
|
172
|
+
/// touchesBegan and touchesEnded together. Unlighting on touchesEnded then
|
|
173
|
+
/// clears the key before a frame is drawn, which is why edge keys (and the edge
|
|
174
|
+
/// half of the wide ⌫) showed no press colour, or only sometimes. A key that is
|
|
175
|
+
/// held longer still lights late, once iOS lets the touch through; only the
|
|
176
|
+
/// host app can opt its edges out of system gestures.
|
|
177
|
+
final class PressedKeyState {
|
|
178
|
+
/// About one ordinary tap, so an edge tap reads like one in the middle.
|
|
179
|
+
static let minimumLit: CFTimeInterval = 0.1
|
|
180
|
+
|
|
181
|
+
private(set) var index = -1
|
|
182
|
+
private var litAt: CFTimeInterval = 0
|
|
183
|
+
private var pendingUnlight: DispatchWorkItem?
|
|
184
|
+
private weak var view: UIView?
|
|
185
|
+
|
|
186
|
+
init(view: UIView) { self.view = view }
|
|
187
|
+
|
|
188
|
+
func press(_ index: Int) {
|
|
189
|
+
pendingUnlight?.cancel()
|
|
190
|
+
self.index = index
|
|
191
|
+
litAt = CACurrentMediaTime()
|
|
192
|
+
view?.setNeedsDisplay()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Finger lifted: unlit now, or once the key has been lit for `minimumLit`.
|
|
196
|
+
func release() {
|
|
197
|
+
let remaining = Self.minimumLit - (CACurrentMediaTime() - litAt)
|
|
198
|
+
guard index >= 0, remaining > 0 else { clear(); return }
|
|
199
|
+
pendingUnlight?.cancel()
|
|
200
|
+
let work = DispatchWorkItem { [weak self] in self?.clear() }
|
|
201
|
+
pendingUnlight = work
|
|
202
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + remaining, execute: work)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Unlit at once: a cancelled touch, or a key set that changed under it.
|
|
206
|
+
func clear() {
|
|
207
|
+
pendingUnlight?.cancel()
|
|
208
|
+
pendingUnlight = nil
|
|
209
|
+
index = -1
|
|
210
|
+
view?.setNeedsDisplay()
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
171
214
|
extension ThemableKeypadView {
|
|
172
215
|
/// Key fill: `pressedKeyColor` while held when set, else the 70% dim.
|
|
173
216
|
func keyFill(_ held: Bool) -> UIColor {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-secure-keypad-jsi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Secure PIN / password keypad for Expo (New Architecture). Natively rendered shuffled keys, the entered value never enters the JS runtime, mlock'd C++ buffer, OpenSSL RSA-OAEP ciphertext only. Screen-capture blocking and device integrity are the host app's job.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|