diff --git a/patches/react-native+0.81.5+004+TextInput-onPaste.patch b/patches/react-native+0.81.5+004+TextInput-onPaste.patch new file mode 100644 index 0000000000..aec6a25f2d --- /dev/null +++ b/patches/react-native+0.81.5+004+TextInput-onPaste.patch @@ -0,0 +1,673 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +index 8cc1369..6b42a26 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +@@ -449,6 +449,21 @@ export type AndroidTextInputNativeProps = $ReadOnly<{ + }>, + >, + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ?DirectEventHandler< ++ $ReadOnly<{ ++ target: Int32, ++ items: $ReadOnlyArray< ++ $ReadOnly<{ ++ type: string, ++ data: string, ++ }>, ++ >, ++ }>, ++ >, ++ + /** + * The string that will be rendered before text input has been entered. + */ +@@ -640,6 +655,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + topScroll: { + registrationName: 'onScroll', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + }, + validAttributes: { + acceptDragAndDropTypes: true, +@@ -694,6 +712,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + secureTextEntry: true, + textBreakStrategy: true, + onScroll: true, ++ onPaste: true, + onContentSizeChange: true, + disableFullscreenUI: true, + includeFontPadding: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +index 05bddcb..69c11a2 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +@@ -82,6 +82,9 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { + topContentSizeChange: { + registrationName: 'onContentSizeChange', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + topChangeSync: { + registrationName: 'onChangeSync', + }, +@@ -160,6 +163,7 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { + onSelectionChange: true, + onContentSizeChange: true, + onScroll: true, ++ onPaste: true, + onChangeSync: true, + onKeyPressSync: true, + }), +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +index b3ca156..2807af2 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +@@ -565,6 +565,16 @@ export interface TextInputSubmitEditingEventData { + export type TextInputSubmitEditingEvent = + NativeSyntheticEvent; + ++/** ++ * @see TextInputProps.onPaste ++ */ ++export interface TextInputPasteEventData extends TargetedEvent { ++items: Array<{ ++ type: string; ++ data: string; ++ }>; ++} ++ + /** + * @see https://reactnative.dev/docs/textinput#props + */ +@@ -896,6 +906,13 @@ export interface TextInputProps + */ + onKeyPress?: ((e: TextInputKeyPressEvent) => void) | undefined; + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ++ | ((e: NativeSyntheticEvent) => void) ++ | undefined; ++ + /** + * The string that will be rendered before text input has been entered + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +index 5fa8811..ad2bf61 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +@@ -137,6 +137,18 @@ export type TextInputSubmitEditingEvent = + export type TextInputEditingEvent = + NativeSyntheticEvent; + ++export type PasteEvent = SyntheticEvent< ++ $ReadOnly<{ ++ target: number, ++ items: $ReadOnlyArray< ++ $ReadOnly<{ ++ type: string, ++ data: string, ++ }>, ++ >, ++ }>, ++>; ++ + type DataDetectorTypesType = + | 'phoneNumber' + | 'link' +@@ -884,6 +896,11 @@ type TextInputBaseProps = $ReadOnly<{ + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ?(e: PasteEvent) => mixed, ++ + /** + * The string that will be rendered before text input has been entered. + */ +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +index 6e9c384..2c509eb 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +@@ -13,6 +13,10 @@ + #import + #import + ++#import ++#import ++#import ++ + @implementation RCTUITextView { + UILabel *_placeholderView; + UITextView *_detachedTextView; +@@ -209,7 +213,31 @@ - (void)scrollRangeToVisible:(NSRange)range + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; ++ if (clipboard.hasImages) { ++ for (NSItemProvider *itemProvider in clipboard.itemProviders) { ++ if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { ++ for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { ++ if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { ++ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); ++ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); ++ NSString *filePath = RCTTempFilePath(fileExtension, nil); ++ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; ++ NSData *fileData = [clipboard dataForPasteboardType:identifier]; ++ [fileData writeToFile:filePath atomically:YES]; ++ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; ++ break; ++ } ++ } ++ break; ++ } ++ } ++ } else { ++ if (clipboard.hasStrings) { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ } ++ [super paste:sender]; ++ } + } + + // Turn off scroll animation to fix flaky scrolling. +@@ -301,6 +329,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender + return NO; + } + ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ return YES; ++ } ++ + return [super canPerformAction:action withSender:sender]; + } + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +index 7187177..da00893 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +@@ -37,6 +37,8 @@ NS_ASSUME_NONNULL_BEGIN + + - (void)textInputDidChangeSelection; + ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data; ++ + @optional + + - (void)scrollViewDidScroll:(UIScrollView *)scrollView; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +index f1c32e6..0ce9dfe 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +@@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; + - (void)selectedTextRangeWasSet; ++- (void)didPaste:(NSString *)type withData:(NSString *)data; + + @end + +@@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN + - (instancetype)initWithTextView:(UITextView *)backedTextInputView; + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; ++- (void)didPaste:(NSString *)type withData:(NSString *)data; + + @end + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +index 82d9a79..8cc48ec 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +@@ -148,6 +148,11 @@ - (void)selectedTextRangeWasSet + [self textFieldProbablyDidChangeSelection]; + } + ++- (void)didPaste:(NSString *)type withData:(NSString *)data ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; ++} ++ + #pragma mark - Generalization + + - (void)textFieldProbablyDidChangeSelection +@@ -330,6 +335,11 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex + _previousSelectedTextRange = textRange; + } + ++- (void)didPaste:(NSString *)type withData:(NSString *)data ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; ++} ++ + #pragma mark - Generalization + + - (void)textViewProbablyDidChangeSelection +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +index 4804624..90b7081 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +@@ -37,6 +37,7 @@ NS_ASSUME_NONNULL_BEGIN + @property (nonatomic, copy, nullable) RCTDirectEventBlock onChange; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll; ++@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; + + @property (nonatomic, assign) NSInteger mostRecentEventCount; + @property (nonatomic, assign, readonly) NSInteger nativeEventCount; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +index 6a2d4f8..b6e6060 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +@@ -599,6 +599,26 @@ - (void)textInputDidChangeSelection + }); + } + ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data ++{ ++ if (!_onPaste) { ++ return; ++ } ++ ++ NSMutableArray *items = [NSMutableArray new]; ++ [items addObject:@{ ++ @"type" : type, ++ @"data" : data, ++ }]; ++ ++ NSDictionary *payload = @{ ++ @"target" : self.reactTag, ++ @"items" : items, ++ }; ++ ++ _onPaste(payload); ++} ++ + - (void)updateLocalData + { + [self enforceTextAttributesIfNeeded]; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index 47adc53..1865e0a 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -70,6 +70,7 @@ @implementation RCTBaseTextInputViewManager { + RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) ++RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock) + + RCT_EXPORT_SHADOW_PROPERTY(text, NSString) + RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString) +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +index 377f41e..b8f48e6 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +@@ -12,6 +12,10 @@ + #import + #import + ++#import ++#import ++#import ++ + @implementation RCTUITextField { + RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; + NSDictionary *_defaultTextAttributes; +@@ -180,6 +184,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender + return NO; + } + ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ return YES; ++ } ++ + return [super canPerformAction:action withSender:sender]; + } + +@@ -263,7 +271,31 @@ - (void)scrollRangeToVisible:(NSRange)range + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; ++ if (clipboard.hasImages) { ++ for (NSItemProvider *itemProvider in clipboard.itemProviders) { ++ if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { ++ for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { ++ if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { ++ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); ++ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); ++ NSString *filePath = RCTTempFilePath(fileExtension, nil); ++ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; ++ NSData *fileData = [clipboard dataForPasteboardType:identifier]; ++ [fileData writeToFile:filePath atomically:YES]; ++ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; ++ break; ++ } ++ } ++ break; ++ } ++ } ++ } else { ++ if (clipboard.hasStrings) { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ } ++ [super paste:sender]; ++ } + } + + #pragma mark - Layout +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index 577bebe..bbde7fd 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -516,6 +516,13 @@ - (void)textInputDidChangeSelection + } + } + ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data ++{ ++ if (_eventEmitter) { ++ static_cast(*_eventEmitter).onPaste(std::string([type UTF8String]), std::string([data UTF8String])); ++ } ++} ++ + #pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate) + + - (void)scrollViewDidScroll:(UIScrollView *)scrollView +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.kt +new file mode 100644 +index 0000000..4d812f7 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.kt +@@ -0,0 +1,17 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput ++ ++/** ++ * Implement this interface to be informed of paste event in the ++ * ReactTextEdit This is used by the ReactTextInputManager to forward events ++ * from the EditText to JS ++ */ ++fun interface PasteWatcher { ++ fun onPaste(type: String, data: String) ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +index 42f6e03..d145dd7 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +@@ -8,6 +8,10 @@ + package com.facebook.react.views.textinput + + import android.annotation.SuppressLint ++import android.content.ClipboardManager ++import android.content.ClipData ++import android.content.ClipDescription ++import android.content.ContentResolver + import android.content.Context + import android.content.res.Configuration + import android.graphics.Canvas +@@ -15,6 +19,8 @@ import android.graphics.Color + import android.graphics.Paint + import android.graphics.Rect + import android.graphics.drawable.Drawable ++import android.net.Uri ++import com.facebook.react.uimanager.UIManagerHelper.getReactContext + import android.os.Build + import android.os.Bundle + import android.text.Editable +@@ -128,6 +134,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat + private var selectionWatcher: SelectionWatcher? = null + private var contentSizeWatcher: ContentSizeWatcher? = null + private var scrollWatcher: ScrollWatcher? ++ private var pasteWatcher: PasteWatcher? + private var keyListener: InternalKeyListener? = null + private var detectScrollMovement = false + private var onKeyPress = false +@@ -212,6 +219,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat + keyListener = InternalKeyListener() + } + scrollWatcher = null ++ pasteWatcher = null + textAttributes = TextAttributes() + + applyTextAttributes() +@@ -356,9 +364,50 @@ public open class ReactEditText public constructor(context: Context) : AppCompat + * Called when a context menu option for the text view is selected. + * React Native replaces copy (as rich text) with copy as plain text. + */ +- override fun onTextContextMenuItem(id: Int): Boolean = +- super.onTextContextMenuItem( +- if (id == android.R.id.paste) android.R.id.pasteAsPlainText else id) ++ override fun onTextContextMenuItem(id: Int): Boolean { ++ val modifiedId = if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) { ++ android.R.id.pasteAsPlainText ++ } else { ++ id ++ } ++ ++ if (modifiedId == android.R.id.pasteAsPlainText) { ++ val clipboardManager = getContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager ++ val clipData = clipboardManager.primaryClip ++ if (clipData != null) { ++ val item = clipData.getItemAt(0) ++ val itemUri = item.uri ++ var type: String? = null ++ var data: String? = null ++ ++ if (itemUri != null) { ++ val cr = getReactContext(this).contentResolver ++ type = cr.getType(itemUri) ++ if (type != null && type != ClipDescription.MIMETYPE_TEXT_PLAIN) { ++ data = itemUri.toString() ++ if (pasteWatcher != null) { ++ pasteWatcher?.onPaste(type, data) ++ } ++ // Prevents default behavior to avoid inserting raw binary data into the text field ++ return true ++ } ++ } ++ ++ if (clipData.description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { ++ type = ClipDescription.MIMETYPE_TEXT_PLAIN ++ val text: CharSequence? = item.text ++ if (text != null) { ++ data = text.toString() ++ if (pasteWatcher != null) { ++ pasteWatcher?.onPaste(type, data) ++ } ++ // Don't return - let the system proceed with default text pasting behavior ++ } ++ } ++ } ++ } ++ return super.onTextContextMenuItem(id) ++ } + + internal fun clearFocusAndMaybeRefocus() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || !isInTouchMode) { +@@ -421,6 +470,10 @@ public open class ReactEditText public constructor(context: Context) : AppCompat + this.scrollWatcher = scrollWatcher + } + ++ public fun setPasteWatcher(pasteWatcher: PasteWatcher?) { ++ this.pasteWatcher = pasteWatcher ++ } ++ + /** + * Attempt to set a selection or fail silently. Intentionally meant to handle bad inputs. + * EventCounter is the same one used as with text. +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +index 42c13a1..075a206 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +@@ -133,6 +133,8 @@ public open class ReactTextInputManager public constructor() : + val eventTypeConstants = baseEventTypeConstants ?: mutableMapOf() + eventTypeConstants.putAll( + mapOf(getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"))) ++ eventTypeConstants.putAll( ++ mapOf("topPaste" to mapOf("registrationName" to "onPaste"))) + return eventTypeConstants + } + +@@ -327,6 +329,15 @@ public open class ReactTextInputManager public constructor() : + } + } + ++ @ReactProp(name = "onPaste", defaultBoolean = false) ++ public fun setOnPaste(view: ReactEditText, onPaste: Boolean) { ++ if (onPaste) { ++ view.setPasteWatcher(ReactPasteWatcher(view)) ++ } else { ++ view.setPasteWatcher(null) ++ } ++ } ++ + @ReactProp(name = "onKeyPress", defaultBoolean = false) + public fun setOnKeyPress(view: ReactEditText, onKeyPress: Boolean) { + view.setOnKeyPress(onKeyPress) +@@ -944,6 +955,24 @@ public open class ReactTextInputManager public constructor() : + } + } + ++ private class ReactPasteWatcher(editText: ReactEditText) : PasteWatcher { ++ private val mReactEditText: ReactEditText = editText ++ private val mEventDispatcher: EventDispatcher? ++ private val mSurfaceId: Int ++ ++ init { ++ val reactContext = getReactContext(editText) ++ mEventDispatcher = getEventDispatcher(reactContext, editText) ++ mSurfaceId = UIManagerHelper.getSurfaceId(reactContext) ++ } ++ ++ override fun onPaste(type: String, data: String) { ++ mEventDispatcher?.dispatchEvent( ++ ReactTextInputPasteEvent(mSurfaceId, mReactEditText.id, type, data) ++ ) ++ } ++ } ++ + override fun getExportedViewConstants(): Map = + mapOf( + "AutoCapitalizationType" to +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt +new file mode 100644 +index 0000000..6f5b10b +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt +@@ -0,0 +1,61 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput ++ ++import androidx.annotation.Nullable ++import com.facebook.react.bridge.Arguments ++import com.facebook.react.bridge.WritableMap ++import com.facebook.react.bridge.WritableArray ++import com.facebook.react.uimanager.common.ViewUtil ++import com.facebook.react.uimanager.events.Event ++ ++/** ++ * Event emitted by EditText native view when clipboard content is pasted ++ */ ++public class ReactTextInputPasteEvent : Event { ++ ++ public companion object { ++ private const val EVENT_NAME = "topPaste" ++ } ++ ++ private val mType: String ++ private val mData: String ++ ++ @Deprecated("Use constructor with surfaceId") ++ public constructor(viewId: Int, type: String, data: String) : ++ this(ViewUtil.NO_SURFACE_ID, viewId, type, data) ++ ++ public constructor(surfaceId: Int, viewId: Int, type: String, data: String) : ++ super(surfaceId, viewId) { ++ mType = type ++ mData = data ++ } ++ ++ override fun getEventName(): String { ++ return EVENT_NAME ++ } ++ ++ override fun canCoalesce(): Boolean { ++ return false ++ } ++ ++ @Nullable ++ override fun getEventData(): WritableMap? { ++ val eventData = Arguments.createMap() ++ ++ val items: WritableArray = Arguments.createArray() ++ val item: WritableMap = Arguments.createMap() ++ item.putString("type", mType) ++ item.putString("data", mData) ++ items.pushMap(item) ++ ++ eventData.putArray("items", items) ++ ++ return eventData ++ } ++} +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp +index a9bc219..7ebec12 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp +@@ -177,6 +177,19 @@ void TextInputEventEmitter::onScroll(const Metrics& textInputMetrics) const { + }); + } + ++void TextInputEventEmitter::onPaste(const std::string& type, const std::string& data) const { ++ dispatchEvent("onPaste", [type, data](jsi::Runtime& runtime) { ++ auto payload = jsi::Object(runtime); ++ auto items = jsi::Array(runtime, 1); ++ auto item = jsi::Object(runtime); ++ item.setProperty(runtime, "type", type); ++ item.setProperty(runtime, "data", data); ++ items.setValueAtIndex(runtime, 0, item); ++ payload.setProperty(runtime, "items", items); ++ return payload; ++ }); ++} ++ + void TextInputEventEmitter::dispatchTextInputEvent( + const std::string& name, + const Metrics& textInputMetrics, +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h +index dbce575..5b03581 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h +@@ -44,6 +44,7 @@ class TextInputEventEmitter : public ViewEventEmitter { + void onSubmitEditing(const Metrics& textInputMetrics) const; + void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; + void onScroll(const Metrics& textInputMetrics) const; ++ void onPaste(const std::string& type, const std::string& data) const; + + private: + void dispatchTextInputEvent(