expo-paste-input 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 (40) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +123 -0
  3. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  6. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  7. package/android/.gradle/8.9/gc.properties +0 -0
  8. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  9. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  10. package/android/.gradle/vcs-1/gc.properties +0 -0
  11. package/android/build.gradle +43 -0
  12. package/android/src/main/AndroidManifest.xml +2 -0
  13. package/android/src/main/java/expo/modules/pasteinput/ExpoPasteInputModule.kt +14 -0
  14. package/android/src/main/java/expo/modules/pasteinput/ExpoPasteInputView.kt +418 -0
  15. package/build/TextInputWrapper.types.d.ts +22 -0
  16. package/build/TextInputWrapper.types.d.ts.map +1 -0
  17. package/build/TextInputWrapper.types.js +2 -0
  18. package/build/TextInputWrapper.types.js.map +1 -0
  19. package/build/TextInputWrapperView.d.ts +5 -0
  20. package/build/TextInputWrapperView.d.ts.map +1 -0
  21. package/build/TextInputWrapperView.js +17 -0
  22. package/build/TextInputWrapperView.js.map +1 -0
  23. package/build/TextInputWrapperView.web.d.ts +5 -0
  24. package/build/TextInputWrapperView.web.d.ts.map +1 -0
  25. package/build/TextInputWrapperView.web.js +10 -0
  26. package/build/TextInputWrapperView.web.js.map +1 -0
  27. package/build/index.d.ts +4 -0
  28. package/build/index.d.ts.map +1 -0
  29. package/build/index.js +4 -0
  30. package/build/index.js.map +1 -0
  31. package/expo-module.config.json +9 -0
  32. package/ios/ExpoPasteInput.podspec +23 -0
  33. package/ios/ExpoPasteInputModule.swift +11 -0
  34. package/ios/ExpoPasteInputView.swift +601 -0
  35. package/package.json +43 -0
  36. package/src/TextInputWrapper.types.ts +19 -0
  37. package/src/TextInputWrapperView.tsx +34 -0
  38. package/src/TextInputWrapperView.web.tsx +17 -0
  39. package/src/index.ts +5 -0
  40. package/tsconfig.json +9 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/native', 'universe/web'],
4
+ ignorePatterns: ['build'],
5
+ };
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # TextInputWrapper
2
+
3
+ A native Expo module for cross-platform paste event handling in React Native TextInput components.
4
+
5
+ ## Demo
6
+
7
+ | iOS | Android |
8
+ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
9
+ | <video src="https://github.com/user-attachments/assets/b54b15ac-5b98-4dc7-84d7-2e7d48e53e24" /> | <video src="https://github.com/user-attachments/assets/4d709a2c-2dca-431d-8972-05f01b7e5276" /> |
10
+
11
+ ## Overview
12
+
13
+ This is **not a published npm package**. This is the exact production-ready module used inside a real app. If you want to use it, copy the `modules/text-input-wrapper` folder into your Expo project and run it.
14
+
15
+ ### What it does
16
+
17
+ - Intercepts paste events on TextInput components
18
+ - Handles pasted text and images consistently across iOS and Android
19
+ - Supports pasting multiple images, including GIFs
20
+ - Returns file URIs for pasted images that you can use directly
21
+
22
+ ### Paste event payload
23
+
24
+ ```typescript
25
+ type PasteEventPayload =
26
+ | { type: "text"; value: string }
27
+ | { type: "images"; uris: string[] }
28
+ | { type: "unsupported" };
29
+ ```
30
+
31
+ ## How it works
32
+
33
+ The module wraps a TextInput and intercepts paste events at the native level before they reach the default handler.
34
+
35
+ ### iOS
36
+
37
+ On iOS, the module uses method swizzling to intercept `paste(_:)` on the underlying `UITextField`/`UITextView`. When a paste is detected:
38
+
39
+ - For **images**: The paste is intercepted, images are extracted from `UIPasteboard`, saved to temp files, and URIs are sent to JavaScript. GIFs are preserved as-is.
40
+ - For **text**: The original paste proceeds normally, and the pasted text is forwarded to JavaScript.
41
+
42
+ The wrapper view is transparent and passes through all touch events to children.
43
+
44
+ ### Android
45
+
46
+ On Android, the module uses `OnReceiveContentListener` (API 31+) and a custom `ActionMode.Callback` to intercept paste events from both keyboard and context menu:
47
+
48
+ - For **images**: The paste is consumed before Android shows the "Can't add images" toast. Images are decoded, saved to cache, and URIs are sent to JavaScript.
49
+ - For **text**: The original paste proceeds normally, and the pasted text is forwarded to JavaScript.
50
+
51
+ The wrapper view is non-interactive and delegates all touch events to children.
52
+
53
+ ## Usage
54
+
55
+ ### 1. Copy the module
56
+
57
+ Copy the `modules/text-input-wrapper` folder into your Expo project's `modules` directory.
58
+
59
+ ### 2. Run prebuild / pod install
60
+
61
+ ```bash
62
+ # If using Expo prebuild
63
+ npx expo prebuild
64
+
65
+ # Or if you already have native projects
66
+ cd ios && pod install
67
+ ```
68
+
69
+ ### 3. Import and use
70
+
71
+ Wrap your TextInput with `TextInputWrapper` and handle the `onPaste` callback:
72
+
73
+ ```tsx
74
+ import {
75
+ TextInputWrapper,
76
+ PasteEventPayload,
77
+ } from "@/modules/text-input-wrapper";
78
+ import { TextInput } from "react-native";
79
+
80
+ function MyInput() {
81
+ const handlePaste = (payload: PasteEventPayload) => {
82
+ if (payload.type === "images") {
83
+ // payload.uris contains file:// URIs for each pasted image
84
+ console.log("Pasted images:", payload.uris);
85
+ } else if (payload.type === "text") {
86
+ // payload.value contains the pasted text
87
+ console.log("Pasted text:", payload.value);
88
+ }
89
+ };
90
+
91
+ return (
92
+ <TextInputWrapper onPaste={handlePaste}>
93
+ <TextInput placeholder="Paste here..." />
94
+ </TextInputWrapper>
95
+ );
96
+ }
97
+ ```
98
+
99
+ ## Notes
100
+
101
+ - This is **intentionally not packaged as a library**
102
+ - It's meant to be copied, modified, and extended for your specific needs
103
+ - The image URIs point to temporary files — move or copy them if you need persistence
104
+ - Text paste events fire _after_ the text is inserted into the input
105
+ - Image paste events _prevent_ the default paste (since TextInput can't display images)
106
+
107
+ If you want to build a library on top of this, feel free. Please credit **Arunabh Verma** as inspiration.
108
+
109
+ ## Inspiration
110
+
111
+ This project exists because of inspiration from:
112
+
113
+ - **[Fernando Rojo](https://x.com/fernandorojo)** — Inspired by his blog post how paste input is implemented in the v0 app
114
+ - Blog + context: how native paste handling works in v0
115
+ - 𝕏: **[How we built the v0 iOS app](https://x.com/fernandorojo/status/1993098916456452464)**
116
+ - **[v0](https://v0.dev)** — The real-world product discussed in Fernando Rojo’s writing
117
+
118
+ Their work on pushing React Native closer to native platform conventions was the catalyst for building this.
119
+
120
+ ---
121
+
122
+ Built by **Arunabh Verma**
123
+ Demo: [X post ↗](https://x.com/iamarunabh/status/1997738168247062774)
File without changes
@@ -0,0 +1,2 @@
1
+ #Mon Feb 02 16:33:27 IST 2026
2
+ gradle.version=8.9
File without changes
@@ -0,0 +1,43 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'expo.modules.pasteinput'
4
+ version = '0.7.6'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
+ // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
+ // Most of the time, you may like to manage the Android SDK versions yourself.
15
+ def useManagedAndroidSdkVersions = false
16
+ if (useManagedAndroidSdkVersions) {
17
+ useDefaultAndroidSdkVersions()
18
+ } else {
19
+ buildscript {
20
+ // Simple helper that allows the root project to override versions declared by this library.
21
+ ext.safeExtGet = { prop, fallback ->
22
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23
+ }
24
+ }
25
+ project.android {
26
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
27
+ defaultConfig {
28
+ minSdkVersion safeExtGet("minSdkVersion", 24)
29
+ targetSdkVersion safeExtGet("targetSdkVersion", 36)
30
+ }
31
+ }
32
+ }
33
+
34
+ android {
35
+ namespace "expo.modules.pasteinput"
36
+ defaultConfig {
37
+ versionCode 1
38
+ versionName "0.7.6"
39
+ }
40
+ lintOptions {
41
+ abortOnError false
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,14 @@
1
+ package expo.modules.pasteinput
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class ExpoPasteInputModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("ExpoPasteInput")
9
+
10
+ View(ExpoPasteInputView::class) {
11
+ Events("onPaste")
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,418 @@
1
+ package expo.modules.pasteinput
2
+
3
+ import android.content.ClipboardManager
4
+ import android.content.Context
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import android.view.View
10
+ import android.view.ViewGroup
11
+ import android.view.ActionMode
12
+ import android.widget.EditText
13
+ import androidx.core.view.ContentInfoCompat
14
+ import androidx.core.view.OnReceiveContentListener
15
+ import androidx.core.view.ViewCompat
16
+ import expo.modules.kotlin.AppContext
17
+ import expo.modules.kotlin.viewevent.EventDispatcher
18
+ import expo.modules.kotlin.views.ExpoView
19
+ import java.io.File
20
+ import java.io.FileOutputStream
21
+ import java.io.InputStream
22
+ import java.io.IOException
23
+
24
+ class ExpoPasteInputView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
25
+ private val onPaste by EventDispatcher()
26
+ private var textInputView: EditText? = null
27
+ private var isMonitoring: Boolean = false
28
+ private var contentListener: OnReceiveContentListener? = null
29
+ private var originalActionModeCallback: ActionMode.Callback? = null
30
+ private var customActionModeCallback: ActionMode.Callback? = null
31
+
32
+ init {
33
+ // Make view completely transparent and non-interactive - only monitor paste events
34
+ setBackgroundColor(android.graphics.Color.TRANSPARENT)
35
+ isClickable = false
36
+ isFocusable = false
37
+ isFocusableInTouchMode = false
38
+ isEnabled = true // Keep enabled so children can receive events
39
+
40
+ // Enable monitoring when view is attached
41
+ addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
42
+ override fun onViewAttachedToWindow(v: View) {
43
+ startMonitoring()
44
+ }
45
+
46
+ override fun onViewDetachedFromWindow(v: View) {
47
+ stopMonitoring()
48
+ }
49
+ })
50
+ }
51
+
52
+ // Pass through all touch events to children - never intercept
53
+ override fun onInterceptTouchEvent(ev: android.view.MotionEvent?): Boolean {
54
+ return false
55
+ }
56
+
57
+ override fun onTouchEvent(event: android.view.MotionEvent?): Boolean {
58
+ return false
59
+ }
60
+
61
+ override fun dispatchTouchEvent(ev: android.view.MotionEvent?): Boolean {
62
+ return super.dispatchTouchEvent(ev)
63
+ }
64
+
65
+ override fun onViewAdded(child: View?) {
66
+ super.onViewAdded(child)
67
+ // Re-scan for text input when a new child is added
68
+ if (!isMonitoring) {
69
+ startMonitoring()
70
+ } else {
71
+ // If already monitoring, check if the new child is a text input
72
+ val newTextInput = findTextInputInView(child)
73
+ if (newTextInput != null && newTextInput != textInputView) {
74
+ // Found a different text input, switch to it
75
+ stopMonitoring()
76
+ startMonitoring()
77
+ }
78
+ }
79
+ }
80
+
81
+ private fun startMonitoring() {
82
+ if (isMonitoring) return
83
+
84
+ // Find TextInput (EditText) in view hierarchy
85
+ val foundTextInput = findTextInputInView(this) as? EditText
86
+
87
+ if (foundTextInput != null) {
88
+ textInputView = foundTextInput
89
+ isMonitoring = true
90
+ setupPasteHandling(foundTextInput)
91
+ }
92
+ }
93
+
94
+ private fun stopMonitoring() {
95
+ if (!isMonitoring) return
96
+
97
+ val editText = textInputView
98
+ if (editText != null) {
99
+ cleanupPasteHandling(editText)
100
+ }
101
+
102
+ isMonitoring = false
103
+ textInputView = null
104
+ contentListener = null
105
+ originalActionModeCallback = null
106
+ customActionModeCallback = null
107
+ }
108
+
109
+ private fun setupPasteHandling(editText: EditText) {
110
+ // Set up OnReceiveContentListener for Android 12+ (API 31+)
111
+ // This is the primary mechanism for handling image pastes
112
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
113
+ contentListener = createContentListener()
114
+ ViewCompat.setOnReceiveContentListener(
115
+ editText,
116
+ arrayOf("image/*", "text/plain"),
117
+ contentListener!!
118
+ )
119
+ }
120
+
121
+ // Intercept paste from context menu to prevent toast
122
+ enhanceOnTextContextMenuItem(editText)
123
+ }
124
+
125
+ private fun cleanupPasteHandling(editText: EditText) {
126
+ // Remove OnReceiveContentListener
127
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && contentListener != null) {
128
+ ViewCompat.setOnReceiveContentListener(editText, null, null)
129
+ }
130
+
131
+ // Restore original ActionMode.Callback
132
+ if (customActionModeCallback != null) {
133
+ editText.customSelectionActionModeCallback = originalActionModeCallback
134
+ }
135
+ }
136
+
137
+ private fun createContentListener(): OnReceiveContentListener {
138
+ return OnReceiveContentListener { view, payload ->
139
+ val clip = payload.clip
140
+ val itemCount = clip.itemCount
141
+
142
+ if (itemCount == 0) {
143
+ return@OnReceiveContentListener payload
144
+ }
145
+
146
+ // Collect images and GIFs separately
147
+ val imageUris = mutableListOf<Uri>()
148
+ val gifUris = mutableListOf<Uri>()
149
+ var textContent: String? = null
150
+
151
+ // Process each item in the clip
152
+ for (i in 0 until itemCount) {
153
+ val item = clip.getItemAt(i)
154
+
155
+ // Check for image URI
156
+ val uri = item.uri
157
+ if (uri != null) {
158
+ val mimeType = context.contentResolver.getType(uri)
159
+ if (mimeType != null && mimeType.startsWith("image/")) {
160
+ // Separate GIFs from regular images
161
+ if (mimeType == "image/gif") {
162
+ gifUris.add(uri)
163
+ } else {
164
+ imageUris.add(uri)
165
+ }
166
+ }
167
+ }
168
+
169
+ // Check for text
170
+ val text = item.text
171
+ if (!text.isNullOrEmpty() && textContent == null) {
172
+ textContent = text.toString()
173
+ }
174
+ }
175
+
176
+ // Handle GIFs and images (always as array, even for single item)
177
+ if (gifUris.isNotEmpty() || imageUris.isNotEmpty()) {
178
+ processMultipleImagePaste(imageUris, gifUris)
179
+ // Return null to completely consume the content and prevent default paste
180
+ // This prevents Android from showing the "Can't add images" toast
181
+ return@OnReceiveContentListener null
182
+ }
183
+
184
+ // Handle text
185
+ if (textContent != null) {
186
+ handleTextPaste(textContent)
187
+ // Allow default text paste behavior
188
+ return@OnReceiveContentListener payload
189
+ }
190
+
191
+ // Unsupported content type
192
+ handleUnsupportedPaste()
193
+ return@OnReceiveContentListener payload
194
+ }
195
+ }
196
+
197
+ private fun enhanceOnTextContextMenuItem(editText: EditText) {
198
+ // Intercept paste from context menu to prevent toast
199
+ // This must happen BEFORE Android tries to paste, otherwise the toast appears
200
+ try {
201
+ // Store original callback if it exists
202
+ originalActionModeCallback = editText.customSelectionActionModeCallback
203
+
204
+ // Set up a custom ActionMode.Callback to intercept paste from context menu
205
+ // This is the most reliable way to intercept paste before the toast appears
206
+ customActionModeCallback = object : ActionMode.Callback {
207
+ override fun onCreateActionMode(mode: ActionMode?, menu: android.view.Menu?): Boolean {
208
+ // Delegate to original callback if it exists
209
+ return originalActionModeCallback?.onCreateActionMode(mode, menu) ?: true
210
+ }
211
+
212
+ override fun onPrepareActionMode(mode: ActionMode?, menu: android.view.Menu?): Boolean {
213
+ // Delegate to original callback if it exists
214
+ return originalActionModeCallback?.onPrepareActionMode(mode, menu) ?: false
215
+ }
216
+
217
+ override fun onActionItemClicked(mode: ActionMode?, item: android.view.MenuItem?): Boolean {
218
+ if (item?.itemId == android.R.id.paste) {
219
+ // Intercept paste action
220
+ val clipboard = editText.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
221
+ val clipData = clipboard.primaryClip
222
+
223
+ if (clipData != null && clipData.itemCount > 0) {
224
+ // Collect images and GIFs separately
225
+ val imageUris = mutableListOf<Uri>()
226
+ val gifUris = mutableListOf<Uri>()
227
+ var textContent: String? = null
228
+
229
+ // Process all items in the clipboard
230
+ for (i in 0 until clipData.itemCount) {
231
+ val clipItem = clipData.getItemAt(i)
232
+ val uri = clipItem.uri
233
+
234
+ // Check if it's an image
235
+ if (uri != null) {
236
+ val mimeType = editText.context.contentResolver.getType(uri)
237
+ if (mimeType != null && mimeType.startsWith("image/")) {
238
+ // Separate GIFs from regular images
239
+ if (mimeType == "image/gif") {
240
+ gifUris.add(uri)
241
+ } else {
242
+ imageUris.add(uri)
243
+ }
244
+ }
245
+ }
246
+
247
+ // Check for text (only take first text item)
248
+ if (textContent == null) {
249
+ val text = clipItem.text
250
+ if (!text.isNullOrEmpty()) {
251
+ textContent = text.toString()
252
+ }
253
+ }
254
+ }
255
+
256
+ // Handle GIFs and images (always as array, even for single item)
257
+ if (gifUris.isNotEmpty() || imageUris.isNotEmpty()) {
258
+ processMultipleImagePaste(imageUris, gifUris)
259
+ mode?.finish()
260
+ return true // We handled it, prevent default paste
261
+ }
262
+
263
+ // Check for text
264
+ if (textContent != null) {
265
+ // For text, let the normal paste logic run, then notify JS
266
+ var handled = false
267
+
268
+ // 1) Let any existing callback handle it
269
+ if (originalActionModeCallback != null) {
270
+ handled = originalActionModeCallback!!.onActionItemClicked(mode, item)
271
+ }
272
+
273
+ // 2) If nothing handled it, fall back to EditText's default handler
274
+ if (!handled && item != null) {
275
+ handled = editText.onTextContextMenuItem(item.itemId)
276
+ }
277
+
278
+ if (handled) {
279
+ handleTextPaste(textContent)
280
+ }
281
+ mode?.finish()
282
+ return handled
283
+ }
284
+ }
285
+ }
286
+
287
+ // For other actions, delegate to original callback or return false
288
+ return originalActionModeCallback?.onActionItemClicked(mode, item) ?: false
289
+ }
290
+
291
+ override fun onDestroyActionMode(mode: ActionMode?) {
292
+ // Delegate to original callback if it exists
293
+ originalActionModeCallback?.onDestroyActionMode(mode)
294
+ }
295
+ }
296
+
297
+ editText.customSelectionActionModeCallback = customActionModeCallback
298
+
299
+ } catch (e: Exception) {
300
+ // If ActionMode.Callback approach fails, we'll rely on OnReceiveContentListener
301
+ // which should still work but may show the toast briefly
302
+ }
303
+ }
304
+
305
+ private fun findTextInputInView(view: View?): View? {
306
+ if (view == null) return null
307
+
308
+ val className = view.javaClass.simpleName
309
+ if (className.contains("ReactTextInput") ||
310
+ className.contains("EditText") ||
311
+ view is EditText) {
312
+ return view
313
+ }
314
+
315
+ if (view is ViewGroup) {
316
+ for (i in 0 until view.childCount) {
317
+ val child = view.getChildAt(i)
318
+ val found = findTextInputInView(child)
319
+ if (found != null) {
320
+ return found
321
+ }
322
+ }
323
+ }
324
+
325
+ return null
326
+ }
327
+
328
+ internal fun processMultipleImagePaste(imageUris: List<Uri>, gifUris: List<Uri> = emptyList()) {
329
+ try {
330
+ val filePaths = mutableListOf<String>()
331
+
332
+ // Process GIFs first - copy them directly without decoding
333
+ for (gifUri in gifUris) {
334
+ val gifPath = copyGifFile(gifUri)
335
+ if (gifPath != null) {
336
+ filePaths.add(gifPath)
337
+ }
338
+ }
339
+
340
+ // Process regular images - decode and compress
341
+ for (uri in imageUris) {
342
+ val inputStream = context.contentResolver.openInputStream(uri) ?: continue
343
+
344
+ // Use try-with-resources equivalent (use block) to ensure stream is closed
345
+ inputStream.use { stream ->
346
+ val bitmap = BitmapFactory.decodeStream(stream)
347
+
348
+ if (bitmap == null) {
349
+ return@use // Skip this image if we can't decode it
350
+ }
351
+
352
+ // Save to cache directory
353
+ val cacheDir = context.cacheDir
354
+ // Use UUID-like approach for better uniqueness: timestamp + counter
355
+ val fileName = "${System.currentTimeMillis()}_${filePaths.size}.jpg"
356
+ val file = File(cacheDir, fileName)
357
+
358
+ FileOutputStream(file).use { outputStream ->
359
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
360
+ outputStream.flush()
361
+ }
362
+
363
+ val filePath = "file://${file.absolutePath}"
364
+ filePaths.add(filePath)
365
+ }
366
+ }
367
+
368
+ if (filePaths.isEmpty()) {
369
+ handleUnsupportedPaste()
370
+ return
371
+ }
372
+
373
+ // Always use images format with array, even for single image
374
+ onPaste(mapOf(
375
+ "type" to "images",
376
+ "uris" to filePaths
377
+ ))
378
+ } catch (e: Exception) {
379
+ handleUnsupportedPaste()
380
+ }
381
+ }
382
+
383
+ private fun copyGifFile(uri: Uri): String? {
384
+ return try {
385
+ val inputStream = context.contentResolver.openInputStream(uri) ?: return null
386
+
387
+ // Save to cache directory
388
+ val cacheDir = context.cacheDir
389
+ // Use timestamp + counter for better uniqueness instead of random
390
+ val fileName = "${System.currentTimeMillis()}_${System.nanoTime()}.gif"
391
+ val file = File(cacheDir, fileName)
392
+
393
+ inputStream.use { input ->
394
+ FileOutputStream(file).use { output ->
395
+ input.copyTo(output)
396
+ output.flush()
397
+ }
398
+ }
399
+
400
+ "file://${file.absolutePath}"
401
+ } catch (e: Exception) {
402
+ null
403
+ }
404
+ }
405
+
406
+ private fun handleTextPaste(text: String) {
407
+ onPaste(mapOf(
408
+ "type" to "text",
409
+ "value" to text
410
+ ))
411
+ }
412
+
413
+ private fun handleUnsupportedPaste() {
414
+ onPaste(mapOf(
415
+ "type" to "unsupported"
416
+ ))
417
+ }
418
+ }
@@ -0,0 +1,22 @@
1
+ import type { ViewProps } from "react-native";
2
+ export type PasteEventPayload = {
3
+ type: "text";
4
+ value: string;
5
+ } | {
6
+ type: "images";
7
+ uris: string[];
8
+ } | {
9
+ type: "unsupported";
10
+ };
11
+ export interface TextInputWrapperViewProps extends ViewProps {
12
+ /**
13
+ * Callback fired when a paste event is detected.
14
+ * @param payload - The paste event payload containing type and content
15
+ */
16
+ onPaste?: (payload: PasteEventPayload) => void;
17
+ /**
18
+ * Child components to wrap. Typically a TextInput component.
19
+ */
20
+ children?: React.ReactNode;
21
+ }
22
+ //# sourceMappingURL=TextInputWrapper.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextInputWrapper.types.d.ts","sourceRoot":"","sources":["../src/TextInputWrapper.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAE5B,MAAM,WAAW,yBAA0B,SAAQ,SAAS;IAC1D;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAE/C;;OAEG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=TextInputWrapper.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextInputWrapper.types.js","sourceRoot":"","sources":["../src/TextInputWrapper.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ViewProps } from \"react-native\";\n\nexport type PasteEventPayload =\n | { type: \"text\"; value: string }\n | { type: \"images\"; uris: string[] }\n | { type: \"unsupported\" };\n\nexport interface TextInputWrapperViewProps extends ViewProps {\n /**\n * Callback fired when a paste event is detected.\n * @param payload - The paste event payload containing type and content\n */\n onPaste?: (payload: PasteEventPayload) => void;\n\n /**\n * Child components to wrap. Typically a TextInput component.\n */\n children?: React.ReactNode;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import * as React from "react";
2
+ import type { View } from "react-native";
3
+ import type { TextInputWrapperViewProps } from "./TextInputWrapper.types";
4
+ export declare const TextInputWrapperView: React.ForwardRefExoticComponent<TextInputWrapperViewProps & React.RefAttributes<View>>;
5
+ //# sourceMappingURL=TextInputWrapperView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextInputWrapperView.d.ts","sourceRoot":"","sources":["../src/TextInputWrapperView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAEV,yBAAyB,EAC1B,MAAM,0BAA0B,CAAC;AAIlC,eAAO,MAAM,oBAAoB,wFAqB/B,CAAC"}