expo-secure-keypad-jsi 0.1.0

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +393 -0
  3. package/SecureKeypadJsi.podspec +45 -0
  4. package/android/CMakeLists.txt +28 -0
  5. package/android/build.gradle +58 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/JniBridge.cpp +118 -0
  8. package/android/src/main/java/expo/modules/securekeypadjsi/KeyboardCanvasView.kt +291 -0
  9. package/android/src/main/java/expo/modules/securekeypadjsi/KeypadCanvasView.kt +183 -0
  10. package/android/src/main/java/expo/modules/securekeypadjsi/PinSessionHandle.kt +54 -0
  11. package/android/src/main/java/expo/modules/securekeypadjsi/SecureKeypadJsiModule.kt +35 -0
  12. package/android/src/main/java/expo/modules/securekeypadjsi/SecureKeypadJsiView.kt +317 -0
  13. package/android/src/main/java/expo/modules/securekeypadjsi/ThemeColor.kt +27 -0
  14. package/android/src/main/java/expo/modules/securekeypadjsi/ThemedKeypadView.kt +78 -0
  15. package/build/SecureKeypad.d.ts +51 -0
  16. package/build/SecureKeypad.d.ts.map +1 -0
  17. package/build/SecureKeypad.js +51 -0
  18. package/build/SecureKeypad.js.map +1 -0
  19. package/build/SecureKeypadJsi.types.d.ts +94 -0
  20. package/build/SecureKeypadJsi.types.d.ts.map +1 -0
  21. package/build/SecureKeypadJsi.types.js +2 -0
  22. package/build/SecureKeypadJsi.types.js.map +1 -0
  23. package/build/SecureKeypadJsiView.d.ts +6 -0
  24. package/build/SecureKeypadJsiView.d.ts.map +1 -0
  25. package/build/SecureKeypadJsiView.js +10 -0
  26. package/build/SecureKeypadJsiView.js.map +1 -0
  27. package/build/index.d.ts +5 -0
  28. package/build/index.d.ts.map +1 -0
  29. package/build/index.js +5 -0
  30. package/build/index.js.map +1 -0
  31. package/common/CMakeLists.txt +49 -0
  32. package/common/include/esk/Base64.h +20 -0
  33. package/common/include/esk/Cleanse.h +17 -0
  34. package/common/include/esk/Error.h +37 -0
  35. package/common/include/esk/KeypadCore.h +69 -0
  36. package/common/include/esk/PinEncryptor.h +39 -0
  37. package/common/include/esk/PublicKeyValidator.h +37 -0
  38. package/common/include/esk/SecureBuffer.h +60 -0
  39. package/common/include/esk/SecureRandom.h +24 -0
  40. package/common/include/esk/esk_c_api.h +61 -0
  41. package/common/src/Base64.cpp +43 -0
  42. package/common/src/Error.cpp +21 -0
  43. package/common/src/KeypadCore.cpp +105 -0
  44. package/common/src/PinEncryptor.cpp +110 -0
  45. package/common/src/PublicKeyValidator.cpp +111 -0
  46. package/common/src/SecureBuffer.cpp +108 -0
  47. package/common/src/SecureRandom.cpp +42 -0
  48. package/common/src/esk_c_api.cpp +158 -0
  49. package/docs/BUILD.ko.md +33 -0
  50. package/docs/BUILD.md +36 -0
  51. package/docs/README.ko.md +286 -0
  52. package/expo-module.config.json +10 -0
  53. package/ios/EskKeypadBridge.h +35 -0
  54. package/ios/EskKeypadBridge.mm +77 -0
  55. package/ios/KeyboardCoreGraphicsView.swift +257 -0
  56. package/ios/KeypadCoreGraphicsView.swift +197 -0
  57. package/ios/SecureKeypadJsiModule.swift +26 -0
  58. package/ios/SecureKeypadJsiView.swift +278 -0
  59. package/package.json +80 -0
@@ -0,0 +1,317 @@
1
+ // Copyright (c) 2026 expo-secure-keypad-jsi contributors. MIT License.
2
+ package expo.modules.securekeypadjsi
3
+
4
+ import android.content.Context
5
+ import android.graphics.Color
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.view.View
9
+ import android.widget.FrameLayout
10
+ import expo.modules.kotlin.AppContext
11
+ import expo.modules.kotlin.viewevent.EventDispatcher
12
+ import expo.modules.kotlin.views.ExpoView
13
+ import java.util.Collections
14
+ import java.util.WeakHashMap
15
+
16
+ /**
17
+ * Hosts the Canvas keypad (digit) or QWERTY keyboard (full) and owns the
18
+ * [PinSessionHandle]. JS receives only the entered-key count and the
19
+ * ciphertext envelope. Screen capture and device integrity are deliberately
20
+ * out of scope — see README.
21
+ */
22
+ class SecureKeypadJsiView(context: Context, appContext: AppContext) :
23
+ ExpoView(context, appContext), KeypadCanvasView.Listener, KeyboardCanvasView.Listener {
24
+
25
+ companion object {
26
+ // Digit pad policy cap; the full keyboard uses the 64-byte payload cap.
27
+ // Mirrors kMaxPinLength / kMaxSecretLength in the C++ core so the
28
+ // autoSubmit comparison below agrees with what the core actually stores.
29
+ private const val MAX_DIGIT_CAP = 12
30
+ private const val MAX_FULL_CAP = 64
31
+
32
+ // Live views, so the module's activity-background hook can reach them.
33
+ // Weak keys: a view that Fabric dropped without OnViewDestroys must not
34
+ // be pinned here. Main thread only.
35
+ private val liveViews: MutableSet<SecureKeypadJsiView> =
36
+ Collections.newSetFromMap(WeakHashMap())
37
+
38
+ /** Called by [SecureKeypadJsiModule] when the host activity pauses. */
39
+ fun onActivityEntersBackground() {
40
+ for (v in liveViews.toList()) v.onAppBackground()
41
+ }
42
+ }
43
+
44
+ private val onDigitCountChanged by EventDispatcher<Map<String, Any>>()
45
+ private val onComplete by EventDispatcher<Map<String, Any>>()
46
+ private val onError by EventDispatcher<Map<String, Any>>()
47
+
48
+ private var digitKeypad: KeypadCanvasView? = null
49
+ private var fullKeyboard: KeyboardCanvasView? = null
50
+ private var handle: PinSessionHandle? = null
51
+ private val mainHandler = Handler(Looper.getMainLooper())
52
+
53
+ // Rebuilt on the next frame so a batch of prop updates collapses into one
54
+ // native session.
55
+ private var publicKeyPem: String? = null
56
+ private var keypadType = "digit" // digit | full
57
+ private var minLength = 4
58
+ private var maxLength = 6
59
+ private var autoSubmit = true
60
+ private var shuffleMode = KeypadCanvasView.ShuffleMode.MOUNT
61
+ private var theme: Map<String, Any?>? = null
62
+
63
+ private var rebuildScheduled = false
64
+ // Set by dispose(). A rebuild posted before OnViewDestroys must not create a
65
+ // session nobody will ever destroy.
66
+ private var disposed = false
67
+
68
+ // Last count handed to JS. A rebuild reports 0 for the fresh session even
69
+ // when JS was already at 0, and that redundant event can land before the JS
70
+ // component has mounted (a React "state update on a component that hasn't
71
+ // mounted yet" warning). Nothing changed, so do not send it.
72
+ private var lastReportedCount = 0
73
+
74
+ init {
75
+ setBackgroundColor(Color.TRANSPARENT)
76
+ rebuildInputView()
77
+ applyAccessibilityHardening()
78
+ scheduleRebuild()
79
+ liveViews.add(this)
80
+ }
81
+
82
+ /** maxLength after the per-type clamp the C++ core applies. */
83
+ private fun effectiveMaxLength(): Int =
84
+ maxLength.coerceIn(4, if (keypadType == "full") MAX_FULL_CAP else MAX_DIGIT_CAP)
85
+
86
+ fun setPublicKeyPem(pem: String) { publicKeyPem = pem; scheduleRebuild() }
87
+ fun setMinLength(v: Int) { minLength = v.coerceIn(4, 64); scheduleRebuild() }
88
+ fun setMaxLength(v: Int) { maxLength = v.coerceIn(4, 64); scheduleRebuild() }
89
+ fun setAutoSubmit(v: Boolean) { autoSubmit = v }
90
+
91
+ fun setKeypadType(v: String) {
92
+ val next = if (v == "full") "full" else "digit"
93
+ if (next == keypadType) return
94
+ keypadType = next
95
+ rebuildInputView()
96
+ scheduleRebuild()
97
+ }
98
+
99
+ fun setShuffle(mode: String) {
100
+ shuffleMode = when (mode) {
101
+ "perKey" -> KeypadCanvasView.ShuffleMode.PER_KEY
102
+ "off" -> KeypadCanvasView.ShuffleMode.OFF
103
+ else -> KeypadCanvasView.ShuffleMode.MOUNT
104
+ }
105
+ digitKeypad?.let { it.shuffleMode = shuffleMode; it.reshuffle() }
106
+ fullKeyboard?.let { it.shuffleMode = shuffleMode; it.reshuffle() }
107
+ }
108
+
109
+ fun setTheme(t: Map<String, Any?>) {
110
+ theme = t
111
+ digitKeypad?.let { applyTheme(it) }
112
+ fullKeyboard?.let { applyTheme(it) }
113
+ }
114
+
115
+ // #RRGGBB / #RRGGBBAA, same reading as the iOS side.
116
+ private fun safeColor(s: String, fallback: Int): Int = ThemeColor.parse(s) ?: fallback
117
+
118
+ private fun applyTheme(v: ThemedKeypadView) {
119
+ val t = theme ?: return
120
+ (t["keyColor"] as? String)?.let { v.keyColor = safeColor(it, v.keyColor) }
121
+ (t["keyTextColor"] as? String)?.let { v.keyTextColor = safeColor(it, v.keyTextColor) }
122
+ (t["actionTextColor"] as? String)?.let { v.actionTextColor = safeColor(it, v.actionTextColor) }
123
+ (t["cornerRadius"] as? Number)?.let { v.keyCornerRadiusDp = it.toFloat() }
124
+ (t["digitTextSize"] as? Number)?.let { v.digitTextSizeDp = it.toFloat() }
125
+ (t["pressedHighlight"] as? Boolean)?.let { v.pressedHighlight = it }
126
+ // Absent key vs. explicit null: both mean "system font", so this one is
127
+ // assigned unconditionally instead of only on a hit.
128
+ v.fontFamily = (t["fontFamily"] as? String)?.takeIf { it.isNotEmpty() }
129
+ v.invalidate()
130
+ }
131
+
132
+ private fun rebuildInputView() {
133
+ removeAllViews()
134
+ digitKeypad = null
135
+ fullKeyboard = null
136
+ val lp = FrameLayout.LayoutParams(
137
+ FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
138
+ if (keypadType == "full") {
139
+ val v = KeyboardCanvasView(context)
140
+ v.listener = this
141
+ v.shuffleMode = shuffleMode
142
+ v.digitsProvider = { handle?.shuffledLayout() ?: identityLayout() }
143
+ applyTheme(v)
144
+ addView(v, lp)
145
+ layoutChildNow(v)
146
+ fullKeyboard = v
147
+ } else {
148
+ val v = KeypadCanvasView(context)
149
+ v.listener = this
150
+ v.shuffleMode = shuffleMode
151
+ v.layoutProvider = { handle?.shuffledLayout() ?: identityLayout() }
152
+ applyTheme(v)
153
+ addView(v, lp)
154
+ layoutChildNow(v)
155
+ digitKeypad = v
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Sizes a child that was added *after* the first layout pass — what a
161
+ * `keypadType` switch does. Fabric drives layout from the shadow tree and
162
+ * ignores the [requestLayout] that `addView` schedules (React Native
163
+ * #17968), so such a child would stay 0x0 and draw nothing. At init the view
164
+ * has no size yet and the normal mount cascade handles it, hence the guard.
165
+ */
166
+ private fun layoutChildNow(child: View) {
167
+ if (width <= 0 || height <= 0) return
168
+ child.measure(
169
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
170
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
171
+ child.layout(0, 0, width, height)
172
+ }
173
+
174
+ // A parent resize (rotation, a container animating) arrives as a real layout
175
+ // pass, but the children are laid out from their *measured* size, which is
176
+ // stale after a mid-life swap. Re-measure them against the new bounds.
177
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
178
+ super.onSizeChanged(w, h, oldw, oldh)
179
+ digitKeypad?.let { layoutChildNow(it) }
180
+ fullKeyboard?.let { layoutChildNow(it) }
181
+ }
182
+
183
+ private fun scheduleRebuild() {
184
+ if (rebuildScheduled) return
185
+ rebuildScheduled = true
186
+ mainHandler.post {
187
+ rebuildScheduled = false
188
+ rebuildHandle()
189
+ }
190
+ }
191
+
192
+ private fun rebuildHandle() {
193
+ if (disposed) return
194
+ // A new session starts empty; tell JS so a masked-dot UI never shows
195
+ // digits that no longer exist in the buffer.
196
+ val hadSession = handle != null
197
+ handle?.destroy()
198
+
199
+ val type = if (keypadType == "full") PinSessionHandle.TYPE_FULL else PinSessionHandle.TYPE_DIGIT
200
+ val h = PinSessionHandle(minLength = minLength, maxLength = maxLength, keypadType = type)
201
+ handle = h
202
+ digitKeypad?.reshuffle()
203
+ fullKeyboard?.reshuffle()
204
+ if (hadSession) reportCount(0)
205
+
206
+ publicKeyPem?.let { pem ->
207
+ val err = h.arm(pem)
208
+ if (err != null) {
209
+ onError(mapOf("code" to err, "phase" to "arm"))
210
+ }
211
+ }
212
+ }
213
+
214
+ // KeypadCanvasView.Listener (digit pad)
215
+
216
+ override fun onDigitPressed(digit: Int) {
217
+ val h = handle ?: return
218
+ val count = h.press(digit)
219
+ // Negative = the core refused the call (ESK_COUNT_ERROR); the buffer is
220
+ // unchanged, so do not tell JS "0 entered".
221
+ if (count < 0) return
222
+ reportCount(count)
223
+ if (autoSubmit && count >= effectiveMaxLength()) submit()
224
+ }
225
+
226
+ // KeyboardCanvasView.Listener (full keyboard)
227
+
228
+ override fun onKeyPressed(asciiChar: Int) {
229
+ val h = handle ?: return
230
+ val count = h.pressKey(asciiChar)
231
+ if (count < 0) return
232
+ reportCount(count)
233
+ if (autoSubmit && count >= effectiveMaxLength()) submit()
234
+ }
235
+
236
+ override fun onDone() = submit()
237
+
238
+ // Shared listener members
239
+
240
+ override fun onBackspace() {
241
+ val h = handle ?: return
242
+ val count = h.backspace()
243
+ if (count < 0) return
244
+ reportCount(count)
245
+ }
246
+
247
+ override fun onClear() {
248
+ val h = handle ?: return
249
+ h.clear()
250
+ reportCount(0)
251
+ }
252
+
253
+ // filterTouchesWhenObscured already dropped the touch; this only lets the
254
+ // host explain why the keypad did not respond (overlay app, screen dimmer).
255
+ override fun onObscuredTouch() {
256
+ onError(mapOf("code" to "ERR_OBSCURED_TOUCH", "phase" to "input"))
257
+ }
258
+
259
+ private fun reportCount(count: Int) {
260
+ if (count == lastReportedCount) return
261
+ lastReportedCount = count
262
+ onDigitCountChanged(mapOf("count" to count))
263
+ }
264
+
265
+ fun submit() {
266
+ val h = handle ?: return
267
+ val result = h.submit()
268
+ if (result.startsWith("{")) {
269
+ onComplete(mapOf("envelope" to result))
270
+ reportCount(0)
271
+ } else {
272
+ onError(mapOf("code" to result, "phase" to "submit"))
273
+ }
274
+ }
275
+
276
+ fun clearPin() {
277
+ handle?.clear()
278
+ reportCount(0)
279
+ }
280
+
281
+ // Backgrounding = the host activity pausing (home button, app switcher) or
282
+ // this view leaving the window (react-native-screens detaches inactive
283
+ // screens). Either way the buffer is wiped and JS is told the count is 0,
284
+ // so the masked-dot UI cannot drift from the real buffer.
285
+ private fun onAppBackground() {
286
+ val h = handle ?: return
287
+ h.onBackground()
288
+ reportCount(0)
289
+ }
290
+
291
+ override fun onDetachedFromWindow() {
292
+ onAppBackground()
293
+ super.onDetachedFromWindow()
294
+ }
295
+
296
+ private fun applyAccessibilityHardening() {
297
+ // Unconditional. The keys are canvas glyphs with no child nodes, so a11y
298
+ // services could never read them anyway; hiding the container too denies
299
+ // a malicious AccessibilityService even the fact that a keypad is here.
300
+ // The keypad is not screen-reader usable by design — see README.
301
+ importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
302
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
303
+ importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
304
+ }
305
+ }
306
+
307
+ fun dispose() {
308
+ disposed = true
309
+ // Drop any rebuild still queued from a prop set in this same frame;
310
+ // otherwise it would run after us and leak a fresh session.
311
+ mainHandler.removeCallbacksAndMessages(null)
312
+ rebuildScheduled = false
313
+ liveViews.remove(this)
314
+ handle?.destroy()
315
+ handle = null
316
+ }
317
+ }
@@ -0,0 +1,27 @@
1
+ // Copyright (c) 2026 expo-secure-keypad-jsi contributors. MIT License.
2
+ package expo.modules.securekeypadjsi
3
+
4
+ /**
5
+ * Parses the theme colour format the JS API documents: `#RRGGBB` or
6
+ * `#RRGGBBAA`, alpha last. `android.graphics.Color.parseColor` is deliberately
7
+ * not used — it reads 8-digit values as `#AARRGGBB` and accepts colour names,
8
+ * so the same string would render differently from iOS. Pure Kotlin so the
9
+ * JVM unit test needs no Android runtime.
10
+ */
11
+ object ThemeColor {
12
+ /** Packed ARGB int (what Paint.color takes), or null when not `#RRGGBB` / `#RRGGBBAA`. */
13
+ fun parse(s: String): Int? {
14
+ var hex = s.trim()
15
+ if (hex.startsWith("#")) hex = hex.substring(1)
16
+ if (hex.length != 6 && hex.length != 8) return null
17
+ if (!hex.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) return null
18
+ val value = hex.toLong(16)
19
+ val r: Long; val g: Long; val b: Long; val a: Long
20
+ if (hex.length == 6) {
21
+ r = (value shr 16) and 0xff; g = (value shr 8) and 0xff; b = value and 0xff; a = 0xff
22
+ } else {
23
+ r = (value shr 24) and 0xff; g = (value shr 16) and 0xff; b = (value shr 8) and 0xff; a = value and 0xff
24
+ }
25
+ return ((a shl 24) or (r shl 16) or (g shl 8) or b).toInt()
26
+ }
27
+ }
@@ -0,0 +1,78 @@
1
+ // Copyright (c) 2026 expo-secure-keypad-jsi contributors. MIT License.
2
+ package expo.modules.securekeypadjsi
3
+
4
+ import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.RectF
9
+ import android.graphics.Typeface
10
+ import com.facebook.react.common.assets.ReactFontManager
11
+
12
+ /** Identity 0..9 order: the fallback whenever the native CSPRNG layout is unavailable. */
13
+ internal fun identityLayout(): ByteArray = ByteArray(10) { it.toByte() }
14
+
15
+ /**
16
+ * The theme surface both canvas views expose, so [SecureKeypadJsiView] applies
17
+ * a JS theme map through one code path instead of one per keypad type.
18
+ */
19
+ internal interface ThemedKeypadView {
20
+ var keyColor: Int
21
+ var keyTextColor: Int
22
+ var actionTextColor: Int
23
+ var keyCornerRadiusDp: Float
24
+ var digitTextSizeDp: Float
25
+
26
+ /** Digit / character glyphs only; action glyphs stay on the system font. */
27
+ var fontFamily: String?
28
+
29
+ /** false hides the press feedback entirely, for apps that cannot block capture. */
30
+ var pressedHighlight: Boolean
31
+
32
+ fun invalidate()
33
+ }
34
+
35
+ /**
36
+ * A held key is drawn at 70% opacity. One rule reads correctly on any theme,
37
+ * so no per-theme luminance branch is needed.
38
+ */
39
+ internal fun pressedDim(color: Int, held: Boolean): Int =
40
+ if (held) {
41
+ Color.argb((Color.alpha(color) * 0.7f).toInt(), Color.red(color), Color.green(color), Color.blue(color))
42
+ } else {
43
+ color
44
+ }
45
+
46
+ /** Shared by both canvas views: baseline-corrected centred text. */
47
+ internal fun drawCentered(canvas: Canvas, text: String, r: RectF, paint: Paint) {
48
+ val cx = r.centerX()
49
+ val cy = r.centerY() - (paint.descent() + paint.ascent()) / 2f
50
+ canvas.drawText(text, cx, cy, paint)
51
+ }
52
+
53
+
54
+ /**
55
+ * Resolves a theme `fontFamily` to a [Typeface], memoised because `onDraw`
56
+ * runs on every press and a miss hits the asset manager.
57
+ *
58
+ * [ReactFontManager] is the same registry `expo-font`'s `loadAsync` writes
59
+ * into, so a family loaded from JS resolves here under the name JS used, and
60
+ * `assets/fonts/<name>.ttf` bundled at build time resolves too. An unknown
61
+ * name degrades to [Typeface.create]'s own fallback (the system font) rather
62
+ * than throwing.
63
+ */
64
+ internal object EskFont {
65
+ private val cache = HashMap<String, Typeface>()
66
+
67
+ fun resolve(context: Context, family: String?): Typeface? {
68
+ if (family.isNullOrEmpty()) return null
69
+ cache[family]?.let { return it }
70
+ val typeface = try {
71
+ ReactFontManager.getInstance().getTypeface(family, Typeface.NORMAL, context.assets)
72
+ } catch (_: Throwable) {
73
+ // No React font registry on the classpath, or a corrupt font file.
74
+ Typeface.create(family, Typeface.NORMAL)
75
+ }
76
+ return typeface?.also { cache[family] = it }
77
+ }
78
+ }
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+ import { type StyleProp, type ViewStyle } from 'react-native';
3
+ import type { KeypadError, KeypadTheme, KeypadType, SecureKeypadHandle, ShuffleMode } from './SecureKeypadJsi.types';
4
+ export interface SecureKeypadProps {
5
+ /** Server RSA-2048+ public key, PEM SubjectPublicKeyInfo. Required. */
6
+ publicKey: string;
7
+ /**
8
+ * 'digit' = 3x4 shuffled PIN pad. 'full' = QWERTY keyboard with shift and
9
+ * symbol layers (its Enter key submits). Default 'digit'.
10
+ */
11
+ keypadType?: KeypadType;
12
+ /** Defaults: 4 / 6 ('digit') or 4 / 64 ('full'). Clamped to 4~12 / 4~64. */
13
+ minLength?: number;
14
+ maxLength?: number;
15
+ shuffle?: ShuffleMode;
16
+ /**
17
+ * Auto-encrypt at maxLength. Default true for 'digit', false for 'full' —
18
+ * passwords are variable-length, so the keyboard's Enter key submits.
19
+ */
20
+ autoSubmit?: boolean;
21
+ theme?: KeypadTheme;
22
+ /**
23
+ * Applied to the keypad, or to the container when `accessory` is set.
24
+ */
25
+ style?: StyleProp<ViewStyle>;
26
+ /**
27
+ * Rendered directly above the keypad, inside the same container — the place
28
+ * for a masked-length indicator or a confirm button when the keypad covers
29
+ * the field it fills (bottom sheets, overlays). Ordinary React content: it
30
+ * never receives the entered value, only what you pass it.
31
+ */
32
+ accessory?: React.ReactNode;
33
+ /** Ciphertext envelope JSON — send this to your server. */
34
+ onComplete?: (envelope: string) => void;
35
+ /** Drive your masked-dot UI from this. */
36
+ onDigitCountChanged?: (count: number) => void;
37
+ /**
38
+ * Key rejected at mount ('arm'), encryption failed on submit ('submit'), or
39
+ * Android dropped an obscured touch ('input', "ERR_OBSCURED_TOUCH").
40
+ */
41
+ onError?: (error: KeypadError) => void;
42
+ }
43
+ /**
44
+ * Unwraps `nativeEvent` for the caller. PIN digits never enter JavaScript —
45
+ * `onComplete` yields only the ciphertext envelope. The buffer is wiped
46
+ * whenever the app backgrounds or the view leaves the window; that is not
47
+ * configurable.
48
+ */
49
+ export declare const SecureKeypad: React.ForwardRefExoticComponent<SecureKeypadProps & React.RefAttributes<SecureKeypadHandle>>;
50
+ export default SecureKeypad;
51
+ //# sourceMappingURL=SecureKeypad.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SecureKeypad.d.ts","sourceRoot":"","sources":["../src/SecureKeypad.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAoB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhF,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,WAAW,EACZ,MAAM,yBAAyB,CAAC;AAGjC,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE5B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,0CAA0C;IAC1C,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CACxC;AAwBD;;;;;GAKG;AACH,eAAO,MAAM,YAAY,8FAsDxB,CAAC;AAIF,eAAe,YAAY,CAAC"}
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import SecureKeypadJsiView from './SecureKeypadJsiView';
4
+ // Yoga sizes Expo views purely from style — their shadow node has no measure
5
+ // function, so a keypad with no height renders at zero. Applied only when the
6
+ // caller gave nothing to size it by. 3x4 near-square keys for the digit pad;
7
+ // the QWERTY keyboard is 5 rows of ~10 keys, so it is wider than tall.
8
+ const digitSizing = { aspectRatio: 3 / 4 };
9
+ const fullSizing = { aspectRatio: 4 / 3 };
10
+ // With an accessory the caller's size lands on the container; the keypad
11
+ // then fills whatever the accessory leaves.
12
+ const fillRest = { flex: 1 };
13
+ function hasOwnSize(style) {
14
+ const flat = StyleSheet.flatten(style);
15
+ if (!flat)
16
+ return false;
17
+ return (flat.height != null ||
18
+ flat.aspectRatio != null ||
19
+ flat.flex != null ||
20
+ flat.flexGrow != null ||
21
+ flat.flexBasis != null);
22
+ }
23
+ /**
24
+ * Unwraps `nativeEvent` for the caller. PIN digits never enter JavaScript —
25
+ * `onComplete` yields only the ciphertext envelope. The buffer is wiped
26
+ * whenever the app backgrounds or the view leaves the window; that is not
27
+ * configurable.
28
+ */
29
+ export const SecureKeypad = React.forwardRef((props, ref) => {
30
+ const { publicKey, keypadType = 'digit', minLength = 4, maxLength = keypadType === 'full' ? 64 : 6, shuffle = 'mount', autoSubmit = keypadType !== 'full', theme, style, accessory, onComplete, onDigitCountChanged, onError, } = props;
31
+ const sized = React.useMemo(() => hasOwnSize(style), [style]);
32
+ const defaultSizing = keypadType === 'full' ? fullSizing : digitSizing;
33
+ const hasAccessory = accessory != null && accessory !== false;
34
+ let keypadStyle;
35
+ if (hasAccessory) {
36
+ keypadStyle = sized ? fillRest : defaultSizing;
37
+ }
38
+ else {
39
+ keypadStyle = sized ? style : [defaultSizing, style];
40
+ }
41
+ const keypad = (<SecureKeypadJsiView ref={ref} publicKey={publicKey} keypadType={keypadType} minLength={minLength} maxLength={maxLength} shuffle={shuffle} autoSubmit={autoSubmit} theme={theme} style={keypadStyle} onComplete={(e) => onComplete?.(e.nativeEvent.envelope)} onDigitCountChanged={(e) => onDigitCountChanged?.(e.nativeEvent.count)} onError={(e) => onError?.(e.nativeEvent)}/>);
42
+ if (!hasAccessory)
43
+ return keypad;
44
+ return (<View style={style}>
45
+ {accessory}
46
+ {keypad}
47
+ </View>);
48
+ });
49
+ SecureKeypad.displayName = 'SecureKeypad';
50
+ export default SecureKeypad;
51
+ //# sourceMappingURL=SecureKeypad.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SecureKeypad.js","sourceRoot":"","sources":["../src/SecureKeypad.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from 'react';\nimport { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';\n\nimport type {\n KeypadError,\n KeypadTheme,\n KeypadType,\n SecureKeypadHandle,\n ShuffleMode,\n} from './SecureKeypadJsi.types';\nimport SecureKeypadJsiView from './SecureKeypadJsiView';\n\nexport interface SecureKeypadProps {\n /** Server RSA-2048+ public key, PEM SubjectPublicKeyInfo. Required. */\n publicKey: string;\n /**\n * 'digit' = 3x4 shuffled PIN pad. 'full' = QWERTY keyboard with shift and\n * symbol layers (its Enter key submits). Default 'digit'.\n */\n keypadType?: KeypadType;\n /** Defaults: 4 / 6 ('digit') or 4 / 64 ('full'). Clamped to 4~12 / 4~64. */\n minLength?: number;\n maxLength?: number;\n shuffle?: ShuffleMode;\n /**\n * Auto-encrypt at maxLength. Default true for 'digit', false for 'full' —\n * passwords are variable-length, so the keyboard's Enter key submits.\n */\n autoSubmit?: boolean;\n theme?: KeypadTheme;\n /**\n * Applied to the keypad, or to the container when `accessory` is set.\n */\n style?: StyleProp<ViewStyle>;\n /**\n * Rendered directly above the keypad, inside the same container — the place\n * for a masked-length indicator or a confirm button when the keypad covers\n * the field it fills (bottom sheets, overlays). Ordinary React content: it\n * never receives the entered value, only what you pass it.\n */\n accessory?: React.ReactNode;\n\n /** Ciphertext envelope JSON — send this to your server. */\n onComplete?: (envelope: string) => void;\n /** Drive your masked-dot UI from this. */\n onDigitCountChanged?: (count: number) => void;\n /**\n * Key rejected at mount ('arm'), encryption failed on submit ('submit'), or\n * Android dropped an obscured touch ('input', \"ERR_OBSCURED_TOUCH\").\n */\n onError?: (error: KeypadError) => void;\n}\n\n// Yoga sizes Expo views purely from style — their shadow node has no measure\n// function, so a keypad with no height renders at zero. Applied only when the\n// caller gave nothing to size it by. 3x4 near-square keys for the digit pad;\n// the QWERTY keyboard is 5 rows of ~10 keys, so it is wider than tall.\nconst digitSizing: ViewStyle = { aspectRatio: 3 / 4 };\nconst fullSizing: ViewStyle = { aspectRatio: 4 / 3 };\n// With an accessory the caller's size lands on the container; the keypad\n// then fills whatever the accessory leaves.\nconst fillRest: ViewStyle = { flex: 1 };\n\nfunction hasOwnSize(style: StyleProp<ViewStyle>): boolean {\n const flat = StyleSheet.flatten(style);\n if (!flat) return false;\n return (\n flat.height != null ||\n flat.aspectRatio != null ||\n flat.flex != null ||\n flat.flexGrow != null ||\n flat.flexBasis != null\n );\n}\n\n/**\n * Unwraps `nativeEvent` for the caller. PIN digits never enter JavaScript —\n * `onComplete` yields only the ciphertext envelope. The buffer is wiped\n * whenever the app backgrounds or the view leaves the window; that is not\n * configurable.\n */\nexport const SecureKeypad = React.forwardRef<SecureKeypadHandle, SecureKeypadProps>(\n (props, ref) => {\n const {\n publicKey,\n keypadType = 'digit',\n minLength = 4,\n maxLength = keypadType === 'full' ? 64 : 6,\n shuffle = 'mount',\n autoSubmit = keypadType !== 'full',\n theme,\n style,\n accessory,\n onComplete,\n onDigitCountChanged,\n onError,\n } = props;\n\n const sized = React.useMemo(() => hasOwnSize(style), [style]);\n const defaultSizing = keypadType === 'full' ? fullSizing : digitSizing;\n const hasAccessory = accessory != null && accessory !== false;\n\n let keypadStyle: StyleProp<ViewStyle>;\n if (hasAccessory) {\n keypadStyle = sized ? fillRest : defaultSizing;\n } else {\n keypadStyle = sized ? style : [defaultSizing, style];\n }\n\n const keypad = (\n <SecureKeypadJsiView\n ref={ref}\n publicKey={publicKey}\n keypadType={keypadType}\n minLength={minLength}\n maxLength={maxLength}\n shuffle={shuffle}\n autoSubmit={autoSubmit}\n theme={theme}\n style={keypadStyle}\n onComplete={(e) => onComplete?.(e.nativeEvent.envelope)}\n onDigitCountChanged={(e) => onDigitCountChanged?.(e.nativeEvent.count)}\n onError={(e) => onError?.(e.nativeEvent)}\n />\n );\n\n if (!hasAccessory) return keypad;\n\n return (\n <View style={style}>\n {accessory}\n {keypad}\n </View>\n );\n }\n);\n\nSecureKeypad.displayName = 'SecureKeypad';\n\nexport default SecureKeypad;\n"]}
@@ -0,0 +1,94 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ /**
3
+ * Which step failed. 'arm' = public key rejected at mount, 'submit' =
4
+ * encryption failed, 'input' = a touch was rejected before reaching the
5
+ * keypad (Android tapjacking filter; see KeypadError.code). None of these is
6
+ * a security finding — hooking, device posture and screen capture are out of
7
+ * scope, see README.
8
+ */
9
+ export type KeypadErrorPhase = 'arm' | 'submit' | 'input';
10
+ export type ShuffleMode = 'mount' | 'perKey' | 'off';
11
+ /**
12
+ * 'digit' = 3x4 shuffled PIN pad.
13
+ * 'full' = QWERTY keyboard with shift + symbol layers. The digit
14
+ * row is fully shuffled and each character row gets a blank dummy key at a
15
+ * random slot, so the same character is not at the same coordinate across
16
+ * sessions. Layout and shift state live only in native code.
17
+ */
18
+ export type KeypadType = 'digit' | 'full';
19
+ export interface KeypadError {
20
+ /**
21
+ * Stable code, e.g. "ERR_WEAK_KEY", "ERR_TOO_SHORT". Phase 'input' only
22
+ * emits "ERR_OBSCURED_TOUCH": Android dropped a touch because another window
23
+ * (overlay app, screen dimmer) was drawn over the keypad. The tap was never
24
+ * registered — tell the user to close the overlay.
25
+ */
26
+ code: string;
27
+ phase: KeypadErrorPhase;
28
+ }
29
+ /**
30
+ * Colors are #RRGGBB or #RRGGBBAA. Sizes are density-independent (dp on
31
+ * Android, points on iOS), so the same number looks the same on both.
32
+ */
33
+ export interface KeypadTheme {
34
+ keyColor?: string;
35
+ keyTextColor?: string;
36
+ actionTextColor?: string;
37
+ cornerRadius?: number;
38
+ digitTextSize?: number;
39
+ /**
40
+ * Font for the digit / character glyphs. Any name the platform can already
41
+ * resolve: a family registered by `expo-font`'s `loadAsync` / `useFonts`, a
42
+ * font bundled at build time, or a system family ("Courier", "monospace").
43
+ * Unresolvable names fall back to the system font instead of throwing.
44
+ *
45
+ * NOTE: the action glyphs (⌫ ✕ ⇧ ⏎) always render in the system font. Most
46
+ * custom fonts have no glyph for them, and a missing glyph would draw as
47
+ * tofu (□) on an unlabelled key. For `keypadType: 'full'`, pick a font that
48
+ * covers all of printable ASCII (0x21~0x7E) or some keys will show tofu.
49
+ */
50
+ fontFamily?: string;
51
+ /**
52
+ * Whether a held key dims to 70% opacity (default true). NOTE: any visible
53
+ * press effect lets a screen recording reconstruct the input — block capture
54
+ * at the app level (FLAG_SECURE / UIScreen.isCaptured) or pass false here.
55
+ */
56
+ pressedHighlight?: boolean;
57
+ }
58
+ export type OnCompleteEvent = {
59
+ nativeEvent: {
60
+ envelope: string;
61
+ };
62
+ };
63
+ export type OnDigitCountChangedEvent = {
64
+ nativeEvent: {
65
+ count: number;
66
+ };
67
+ };
68
+ export type OnErrorEvent = {
69
+ nativeEvent: KeypadError;
70
+ };
71
+ /** Raw native view props; most apps want `SecureKeypad` instead. */
72
+ export interface SecureKeypadJsiViewProps {
73
+ /** Server RSA-2048+ public key, PEM SubjectPublicKeyInfo. */
74
+ publicKey: string;
75
+ /** Default 'digit'. */
76
+ keypadType?: KeypadType;
77
+ /** Clamped to 4~12 ('digit') or 4~64 ('full'). */
78
+ minLength?: number;
79
+ maxLength?: number;
80
+ shuffle?: ShuffleMode;
81
+ autoSubmit?: boolean;
82
+ theme?: KeypadTheme;
83
+ onComplete?: (event: OnCompleteEvent) => void;
84
+ onDigitCountChanged?: (event: OnDigitCountChangedEvent) => void;
85
+ onError?: (event: OnErrorEvent) => void;
86
+ style?: StyleProp<ViewStyle>;
87
+ }
88
+ export interface SecureKeypadHandle {
89
+ /** Clears the entered digits; does not disarm. */
90
+ clear: () => Promise<void>;
91
+ /** Encrypts now if the PIN meets minLength. */
92
+ submit: () => Promise<void>;
93
+ }
94
+ //# sourceMappingURL=SecureKeypadJsi.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SecureKeypadJsi.types.d.ts","sourceRoot":"","sources":["../src/SecureKeypadJsi.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1D,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AACpE,MAAM,MAAM,wBAAwB,GAAG;IAAE,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAC1E,MAAM,MAAM,YAAY,GAAG;IAAE,WAAW,EAAE,WAAW,CAAA;CAAE,CAAC;AAExD,oEAAoE;AACpE,MAAM,WAAW,wBAAwB;IACvC,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,IAAI,CAAC;IAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,+CAA+C;IAC/C,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=SecureKeypadJsi.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SecureKeypadJsi.types.js","sourceRoot":"","sources":["../src/SecureKeypadJsi.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\n/**\n * Which step failed. 'arm' = public key rejected at mount, 'submit' =\n * encryption failed, 'input' = a touch was rejected before reaching the\n * keypad (Android tapjacking filter; see KeypadError.code). None of these is\n * a security finding — hooking, device posture and screen capture are out of\n * scope, see README.\n */\nexport type KeypadErrorPhase = 'arm' | 'submit' | 'input';\n\nexport type ShuffleMode = 'mount' | 'perKey' | 'off';\n\n/**\n * 'digit' = 3x4 shuffled PIN pad.\n * 'full' = QWERTY keyboard with shift + symbol layers. The digit\n * row is fully shuffled and each character row gets a blank dummy key at a\n * random slot, so the same character is not at the same coordinate across\n * sessions. Layout and shift state live only in native code.\n */\nexport type KeypadType = 'digit' | 'full';\n\nexport interface KeypadError {\n /**\n * Stable code, e.g. \"ERR_WEAK_KEY\", \"ERR_TOO_SHORT\". Phase 'input' only\n * emits \"ERR_OBSCURED_TOUCH\": Android dropped a touch because another window\n * (overlay app, screen dimmer) was drawn over the keypad. The tap was never\n * registered — tell the user to close the overlay.\n */\n code: string;\n phase: KeypadErrorPhase;\n}\n\n/**\n * Colors are #RRGGBB or #RRGGBBAA. Sizes are density-independent (dp on\n * Android, points on iOS), so the same number looks the same on both.\n */\nexport interface KeypadTheme {\n keyColor?: string;\n keyTextColor?: string;\n actionTextColor?: string;\n cornerRadius?: number;\n digitTextSize?: number;\n /**\n * Font for the digit / character glyphs. Any name the platform can already\n * resolve: a family registered by `expo-font`'s `loadAsync` / `useFonts`, a\n * font bundled at build time, or a system family (\"Courier\", \"monospace\").\n * Unresolvable names fall back to the system font instead of throwing.\n *\n * NOTE: the action glyphs (⌫ ✕ ⇧ ⏎) always render in the system font. Most\n * custom fonts have no glyph for them, and a missing glyph would draw as\n * tofu (□) on an unlabelled key. For `keypadType: 'full'`, pick a font that\n * covers all of printable ASCII (0x21~0x7E) or some keys will show tofu.\n */\n fontFamily?: string;\n /**\n * Whether a held key dims to 70% opacity (default true). NOTE: any visible\n * press effect lets a screen recording reconstruct the input — block capture\n * at the app level (FLAG_SECURE / UIScreen.isCaptured) or pass false here.\n */\n pressedHighlight?: boolean;\n}\n\nexport type OnCompleteEvent = { nativeEvent: { envelope: string } };\nexport type OnDigitCountChangedEvent = { nativeEvent: { count: number } };\nexport type OnErrorEvent = { nativeEvent: KeypadError };\n\n/** Raw native view props; most apps want `SecureKeypad` instead. */\nexport interface SecureKeypadJsiViewProps {\n /** Server RSA-2048+ public key, PEM SubjectPublicKeyInfo. */\n publicKey: string;\n /** Default 'digit'. */\n keypadType?: KeypadType;\n /** Clamped to 4~12 ('digit') or 4~64 ('full'). */\n minLength?: number;\n maxLength?: number;\n shuffle?: ShuffleMode;\n autoSubmit?: boolean;\n theme?: KeypadTheme;\n onComplete?: (event: OnCompleteEvent) => void;\n onDigitCountChanged?: (event: OnDigitCountChangedEvent) => void;\n onError?: (event: OnErrorEvent) => void;\n style?: StyleProp<ViewStyle>;\n}\n\nexport interface SecureKeypadHandle {\n /** Clears the entered digits; does not disarm. */\n clear: () => Promise<void>;\n /** Encrypts now if the PIN meets minLength. */\n submit: () => Promise<void>;\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import * as React from 'react';
2
+ import type { SecureKeypadHandle, SecureKeypadJsiViewProps } from './SecureKeypadJsi.types';
3
+ /** Most apps should use `SecureKeypad` instead. */
4
+ declare const SecureKeypadJsiView: React.ForwardRefExoticComponent<SecureKeypadJsiViewProps & React.RefAttributes<SecureKeypadHandle>>;
5
+ export default SecureKeypadJsiView;
6
+ //# sourceMappingURL=SecureKeypadJsiView.d.ts.map