From 3fff082ed7dd3ec3536b25409a70465140350625 Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Fri, 17 Jul 2026 15:39:26 +0300 Subject: [PATCH 1/4] Add form field read-only control and signature interception bridge Exposes two native form capabilities through NutrientView on both the Legacy (Paper) and New (Fabric + TurboModule) architectures: - setFormFieldReadOnly(fullyQualifiedName, readOnly, persist): locks or unlocks a single form field. iOS maps persist=true to PSPDFFormField.isReadOnly (persisted PDF flag) and persist=false to isEditable (session-only). Android updates the LOCKEDCONTENTS flag on the field's widget annotations, preserving existing flags. - dismissSignaturePad(): dismisses the native signature creation UI when presented, resolving false otherwise. Type-checks the presented controller/fragment so unrelated UI is never dismissed. - interceptSignatureFields prop + onSignatureFieldTapped event: consumes taps on signature form fields (iOS PSPDFViewControllerDelegate tap handling, Android FormManager.OnFormElementClickedListener) and emits the field's fully qualified name and page index to JS instead of presenting the native signature UI. Verified with Catalog builds on both platforms (gradlew assembleDebug, xcodebuild), npm test, npm run lint, and regenerated types. Co-Authored-By: Claude Fable 5 --- .../pspdfkit/react/ReactPdfViewManager.java | 34 +++++ .../PdfViewSignatureFieldTappedEvent.java | 54 ++++++++ .../main/java/com/pspdfkit/views/PdfView.java | 89 +++++++++++++ .../views/PdfViewDocumentListener.java | 22 +++- .../FabricOnSignatureFieldTappedEvent.kt | 40 ++++++ .../ReactInstantPdfViewManagerFabric.kt | 3 + .../react/fabric/ReactPdfViewManagerFabric.kt | 11 ++ .../react/turbo/NutrientViewTurboModule.java | 36 ++++++ index.js | 121 ++++++++++++++++++ ios/Fabric/NutrientView.mm | 13 ++ ios/RCTPSPDFKitView.h | 10 ++ ios/RCTPSPDFKitView.m | 75 +++++++++++ ios/RCTPSPDFKitViewManager.m | 32 +++++ ios/Turbo/NutrientViewTurboModule.mm | 38 +++++- lib/NutrientViewFabric.js | 18 +++ src/NutrientViewFabric.tsx | 21 +++ src/specs/NativeNutrientViewTurboModule.ts | 6 + src/specs/NutrientViewNativeComponent.ts | 12 ++ types/index.d.ts | 34 +++++ types/index.d.ts.map | 2 +- 20 files changed, 668 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/pspdfkit/react/events/PdfViewSignatureFieldTappedEvent.java create mode 100644 android/src/newarch/java/io/nutrient/react/events/FabricOnSignatureFieldTappedEvent.kt diff --git a/android/src/main/java/com/pspdfkit/react/ReactPdfViewManager.java b/android/src/main/java/com/pspdfkit/react/ReactPdfViewManager.java index 10f9919a..234fb8bd 100644 --- a/android/src/main/java/com/pspdfkit/react/ReactPdfViewManager.java +++ b/android/src/main/java/com/pspdfkit/react/ReactPdfViewManager.java @@ -79,6 +79,8 @@ public class ReactPdfViewManager extends ViewGroupManager { public static final int COMMAND_SET_EXCLUDED_ANNOTATIONS = 14; public static final int COMMAND_SET_USER_INTERFACE_VISIBLE = 15; public static final int COMMAND_EXECUTE_ACTION = 16; + public static final int COMMAND_SET_FORM_FIELD_READ_ONLY = 17; + public static final int COMMAND_DISMISS_SIGNATURE_PAD = 18; private final CompositeDisposable annotationDisposables = new CompositeDisposable(); @@ -132,6 +134,8 @@ public Map getCommandsMap() { commandMap.put("setExcludedAnnotations", COMMAND_SET_EXCLUDED_ANNOTATIONS); commandMap.put("setUserInterfaceVisible", COMMAND_SET_USER_INTERFACE_VISIBLE); commandMap.put("executeAction", COMMAND_EXECUTE_ACTION); + commandMap.put("setFormFieldReadOnly", COMMAND_SET_FORM_FIELD_READ_ONLY); + commandMap.put("dismissSignaturePad", COMMAND_DISMISS_SIGNATURE_PAD); return commandMap; } @@ -249,6 +253,11 @@ public void setDisableAutomaticSaving(PdfView view, boolean disableAutomaticSavi view.setDisableAutomaticSaving(disableAutomaticSaving); } + @ReactProp(name = "interceptSignatureFields") + public void setInterceptSignatureFields(PdfView view, boolean interceptSignatureFields) { + view.setInterceptSignatureFields(interceptSignatureFields); + } + @ReactProp(name = "annotationAuthorName") public void setAnnotationAuthorName(PdfView view, String annotationAuthorName) { PSPDFKitPreferences.get(view.getContext()).setAnnotationCreator(annotationAuthorName); @@ -362,6 +371,31 @@ public void receiveCommand(@NonNull final PdfView root, int commandId, @Nullable annotationDisposables.add(annotationDisposable); } break; + case COMMAND_SET_FORM_FIELD_READ_ONLY: + if (args != null && args.size() == 3) { + final int requestId = args.getInt(0); + Disposable readOnlyDisposable = root.setFormFieldReadOnly(args.getString(1), args.getBoolean(2)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(fieldFound -> { + root.getEventDispatcher().dispatchEvent(new PdfViewDataReturnedEvent(root.getId(), requestId, fieldFound)); + }, throwable -> { + root.getEventDispatcher().dispatchEvent(new PdfViewDataReturnedEvent(root.getId(), requestId, throwable)); + }); + annotationDisposables.add(readOnlyDisposable); + } + break; + case COMMAND_DISMISS_SIGNATURE_PAD: + if (args != null && args.size() == 1) { + final int requestId = args.getInt(0); + try { + boolean result = root.dismissSignaturePad(); + root.getEventDispatcher().dispatchEvent(new PdfViewDataReturnedEvent(root.getId(), requestId, result)); + } catch (Exception e) { + root.getEventDispatcher().dispatchEvent(new PdfViewDataReturnedEvent(root.getId(), requestId, e)); + } + } + break; case COMMAND_REMOVE_FRAGMENT: // Removing a fragment like this is not recommended, but it can be used as a workaround // to stop `react-native-screens` from crashing the App when the back button is pressed. diff --git a/android/src/main/java/com/pspdfkit/react/events/PdfViewSignatureFieldTappedEvent.java b/android/src/main/java/com/pspdfkit/react/events/PdfViewSignatureFieldTappedEvent.java new file mode 100644 index 00000000..89dcbf42 --- /dev/null +++ b/android/src/main/java/com/pspdfkit/react/events/PdfViewSignatureFieldTappedEvent.java @@ -0,0 +1,54 @@ +/* + * PdfViewSignatureFieldTappedEvent.java + * + * PSPDFKit + * + * Copyright © 2021-2026 PSPDFKit GmbH. All rights reserved. + * + * THIS SOURCE CODE AND ANY ACCOMPANYING DOCUMENTATION ARE PROTECTED BY INTERNATIONAL COPYRIGHT LAW + * AND MAY NOT BE RESOLD OR REDISTRIBUTED. USAGE IS BOUND TO THE PSPDFKIT LICENSE AGREEMENT. + * UNAUTHORIZED REPRODUCTION OR DISTRIBUTION IS SUBJECT TO CIVIL AND CRIMINAL PENALTIES. + * This notice may not be removed from this file. + */ + +package com.pspdfkit.react.events; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event sent by the {@link com.pspdfkit.views.PdfView} when a signature form field was tapped + * while signature interception is enabled. + */ +public class PdfViewSignatureFieldTappedEvent extends Event { + + public static final String EVENT_NAME = "pdfViewSignatureFieldTapped"; + + @NonNull + private final String fullyQualifiedName; + private final int pageIndex; + + public PdfViewSignatureFieldTappedEvent(@IdRes int viewId, @NonNull String fullyQualifiedName, int pageIndex) { + super(viewId); + this.fullyQualifiedName = fullyQualifiedName; + this.pageIndex = pageIndex; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + WritableMap eventData = Arguments.createMap(); + eventData.putString("fullyQualifiedName", fullyQualifiedName); + eventData.putInt("pageIndex", pageIndex); + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), eventData); + } +} diff --git a/android/src/main/java/com/pspdfkit/views/PdfView.java b/android/src/main/java/com/pspdfkit/views/PdfView.java index b3214316..4d43c9dc 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfView.java +++ b/android/src/main/java/com/pspdfkit/views/PdfView.java @@ -73,11 +73,14 @@ import com.pspdfkit.document.providers.ContentResolverDataProvider; import com.pspdfkit.document.providers.DataProvider; import com.pspdfkit.exceptions.InvalidPasswordException; +import com.pspdfkit.annotations.WidgetAnnotation; import com.pspdfkit.forms.ChoiceFormElement; import com.pspdfkit.forms.ComboBoxFormElement; import com.pspdfkit.forms.EditableButtonFormElement; +import com.pspdfkit.forms.FormElement; import com.pspdfkit.forms.FormField; import com.pspdfkit.forms.TextFormElement; +import com.pspdfkit.ui.signatures.ElectronicSignatureFragment; import com.pspdfkit.listeners.OnVisibilityChangedListener; import com.pspdfkit.listeners.SimpleDocumentListener; import com.pspdfkit.react.PDFDocumentModule; @@ -95,6 +98,7 @@ import com.pspdfkit.react.events.PdfViewDocumentSaveFailedEvent; import com.pspdfkit.react.events.PdfViewDocumentSavedEvent; import com.pspdfkit.react.events.PdfViewNavigationButtonClickedEvent; +import com.pspdfkit.react.events.PdfViewSignatureFieldTappedEvent; import com.pspdfkit.react.events.CustomToolbarButtonTappedEvent; import com.pspdfkit.react.events.PdfViewStateChangedEvent; import com.pspdfkit.react.helper.ConversionHelpers; @@ -180,6 +184,7 @@ public interface PdfViewDelegate { void onAnnotationTapped(Annotation annotation); void onAnnotationsChanged(String eventType, Annotation annotation); void onShouldExecuteAction(String requestId, Action action, int pageIndex, @Nullable String url); + void onSignatureFieldTapped(String fullyQualifiedName, int pageIndex); } // Event data structure for state changes @@ -301,6 +306,9 @@ public static class StateChangedEvent { private boolean suppressShouldExecuteAction = false; private boolean hasShouldExecuteAction = false; + /** When enabled, tapping a signature form field emits onSignatureFieldTapped instead of opening the native signature UI. */ + private boolean interceptSignatureFields = false; + public PdfView(@NonNull Context context) { this(context, false); } @@ -401,6 +409,14 @@ boolean hasShouldExecuteAction() { return hasShouldExecuteAction; } + public void setInterceptSignatureFields(boolean interceptSignatureFields) { + this.interceptSignatureFields = interceptSignatureFields; + } + + public boolean isInterceptSignatureFields() { + return interceptSignatureFields; + } + @Nullable public Integer getComponentReferenceId() { return componentReferenceId; @@ -963,6 +979,7 @@ public void onDocumentLoaded(@NonNull PdfDocument document) { pdfFragment.addDocumentListener(pdfViewDocumentListener); pdfFragment.addOnFormElementSelectedListener(pdfViewDocumentListener); pdfFragment.addOnFormElementDeselectedListener(pdfViewDocumentListener); + pdfFragment.addOnFormElementClickedListener(pdfViewDocumentListener); pdfFragment.addOnAnnotationSelectedListener(pdfViewDocumentListener); pdfFragment.addOnAnnotationUpdatedListener(pdfViewDocumentListener); pdfFragment.addDocumentScrollListener(pdfViewDocumentListener); @@ -1342,6 +1359,77 @@ public Maybe setFormFieldValue(@NonNull String formElementName, @NonNul }); } + /** + * Makes all widgets of the form field with the given fully qualified name read-only or editable + * by updating the {@link AnnotationFlags#LOCKEDCONTENTS} flag on the associated widget + * annotations. {@link AnnotationFlags#READONLY} is ignored for widget annotations, which is why + * the locked-contents flag is used instead. The change persists once the document is saved. + */ + public Single setFormFieldReadOnly(@NonNull String fullyQualifiedName, boolean readOnly) { + return Single.fromCallable(() -> { + PdfDocument currentDocument = document; + if (currentDocument == null) { + throw new IllegalStateException("No document is loaded."); + } + + boolean found = false; + for (FormElement element : currentDocument.getFormProvider().getFormElements()) { + if (!fullyQualifiedName.equals(element.getFullyQualifiedName())) { + continue; + } + + WidgetAnnotation widget = element.getAnnotation(); + if (widget == null) { + continue; + } + + EnumSet currentFlags = widget.getFlags(); + EnumSet updatedFlags = currentFlags.isEmpty() + ? EnumSet.noneOf(AnnotationFlags.class) + : EnumSet.copyOf(currentFlags); + + if (readOnly) { + updatedFlags.add(AnnotationFlags.LOCKEDCONTENTS); + } else { + updatedFlags.remove(AnnotationFlags.LOCKEDCONTENTS); + } + + widget.setFlags(updatedFlags); + found = true; + } + + return found; + }); + } + + /** + * Dismisses the currently presented electronic signature UI, if any. + * + * @return {@code true} when a signature UI was found and dismissed, {@code false} otherwise. + */ + public boolean dismissSignaturePad() { + boolean dismissed = false; + List fragmentManagers = new ArrayList<>(); + if (fragmentManager != null) { + fragmentManagers.add(fragmentManager); + } + if (fragment != null && fragment.isAdded()) { + fragmentManagers.add(fragment.getChildFragmentManager()); + PdfFragment pdfFragment = fragment.getPdfFragment(); + if (pdfFragment != null && pdfFragment.isAdded()) { + fragmentManagers.add(pdfFragment.getParentFragmentManager()); + fragmentManagers.add(pdfFragment.getChildFragmentManager()); + } + } + for (FragmentManager manager : fragmentManagers) { + if (manager.findFragmentByTag(ElectronicSignatureFragment.FRAGMENT_TAG) != null) { + ElectronicSignatureFragment.dismiss(manager); + dismissed = true; + } + } + return dismissed; + } + public JSONObject convertConfiguration() { try { JSONObject config = new JSONObject(); @@ -1442,6 +1530,7 @@ public static Map> createDefaultEventRegistrationMa map.put(CustomToolbarButtonTappedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomToolbarButtonTapped")); map.put(CustomAnnotationContextualMenuItemTappedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomAnnotationContextualMenuItemTapped")); map.put(CustomTextSelectionContextualMenuItemTappedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomTextSelectionContextualMenuItemTapped")); + map.put(PdfViewSignatureFieldTappedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onSignatureFieldTapped")); return map; } diff --git a/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java b/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java index f694301e..d7dac2f2 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java +++ b/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java @@ -32,6 +32,7 @@ import com.pspdfkit.forms.FormElement; import com.pspdfkit.forms.FormField; import com.pspdfkit.forms.FormListeners; +import com.pspdfkit.forms.FormType; import com.pspdfkit.listeners.DocumentListener; import com.pspdfkit.listeners.scrolling.DocumentScrollListener; import com.pspdfkit.listeners.scrolling.ScrollState; @@ -42,6 +43,7 @@ import com.pspdfkit.react.events.PdfViewDocumentLoadedEvent; import com.pspdfkit.react.events.PdfViewDocumentSaveFailedEvent; import com.pspdfkit.react.events.PdfViewDocumentSavedEvent; +import com.pspdfkit.react.events.PdfViewSignatureFieldTappedEvent; import com.pspdfkit.ui.special_mode.controller.AnnotationSelectionController; import com.pspdfkit.ui.special_mode.manager.AnnotationManager; import com.pspdfkit.ui.special_mode.manager.FormManager; @@ -50,7 +52,7 @@ import java.util.List; import java.util.Map; -class PdfViewDocumentListener implements DocumentListener, com.pspdfkit.ui.annotations.OnAnnotationSelectedListener, AnnotationProvider.OnAnnotationUpdatedListener, FormListeners.OnFormFieldUpdatedListener, FormManager.OnFormElementSelectedListener, FormManager.OnFormElementDeselectedListener, DocumentScrollListener, BookmarkProvider.BookmarkListener { +class PdfViewDocumentListener implements DocumentListener, com.pspdfkit.ui.annotations.OnAnnotationSelectedListener, AnnotationProvider.OnAnnotationUpdatedListener, FormListeners.OnFormFieldUpdatedListener, FormManager.OnFormElementSelectedListener, FormManager.OnFormElementDeselectedListener, FormManager.OnFormElementClickedListener, DocumentScrollListener, BookmarkProvider.BookmarkListener { @NonNull private final PdfView parent; @@ -420,6 +422,24 @@ public void onFormElementDeselected(@NonNull FormElement formElement, boolean b) } } + @Override + public boolean onFormElementClicked(@NonNull FormElement formElement) { + if (!parent.isInterceptSignatureFields() || formElement.getType() != FormType.SIGNATURE) { + // Not intercepted: let Nutrient perform its default form element handling. + return false; + } + + String fullyQualifiedName = formElement.getFullyQualifiedName(); + int pageIndex = formElement.getAnnotation().getPageIndex(); + if (isFabricMode && fabricDelegate != null) { + fabricDelegate.onSignatureFieldTapped(fullyQualifiedName, pageIndex); + } else { + dispatchEvent(new PdfViewSignatureFieldTappedEvent(parent.getId(), fullyQualifiedName, pageIndex)); + } + // Consume the click so the native signature UI is not presented. + return true; + } + @Override public void onScrollStateChanged(@NonNull ScrollState scrollState) { // Nothing to do here diff --git a/android/src/newarch/java/io/nutrient/react/events/FabricOnSignatureFieldTappedEvent.kt b/android/src/newarch/java/io/nutrient/react/events/FabricOnSignatureFieldTappedEvent.kt new file mode 100644 index 00000000..c1447e15 --- /dev/null +++ b/android/src/newarch/java/io/nutrient/react/events/FabricOnSignatureFieldTappedEvent.kt @@ -0,0 +1,40 @@ +/* + * Copyright © 2018-2026 PSPDFKit GmbH. All rights reserved. + * + * THIS SOURCE CODE AND ANY ACCOMPANYING DOCUMENTATION ARE PROTECTED BY INTERNATIONAL COPYRIGHT LAW + * AND MAY NOT BE RESOLD OR REDISTRIBUTED. USAGE IS BOUND TO THE PSPDFKIT LICENSE AGREEMENT. + * UNAUTHORIZED REPRODUCTION OR DISTRIBUTION IS SUBJECT TO CIVIL AND CRIMINAL PENALTIES. + * This notice may not be removed from this file. + */ + +package io.nutrient.react.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +/** + * Fabric event emitted when a signature form field is tapped while + * interceptSignatureFields is enabled. + */ +class FabricOnSignatureFieldTappedEvent( + surfaceId: Int, + viewId: Int, + private val fullyQualifiedName: String, + private val pageIndex: Int +) : Event(surfaceId, viewId) { + + companion object { + // Match Codegen's top-level name for BubblingEventHandler + const val EVENT_NAME = "topSignatureFieldTapped" + } + + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap { + val map = Arguments.createMap() + map.putString("fullyQualifiedName", fullyQualifiedName) + map.putInt("pageIndex", pageIndex) + return map + } +} diff --git a/android/src/newarch/java/io/nutrient/react/fabric/ReactInstantPdfViewManagerFabric.kt b/android/src/newarch/java/io/nutrient/react/fabric/ReactInstantPdfViewManagerFabric.kt index 67b4691d..e228b55f 100644 --- a/android/src/newarch/java/io/nutrient/react/fabric/ReactInstantPdfViewManagerFabric.kt +++ b/android/src/newarch/java/io/nutrient/react/fabric/ReactInstantPdfViewManagerFabric.kt @@ -130,6 +130,9 @@ class ReactInstantPdfViewManagerFabric : ViewGroupManager(), Nut override fun onShouldExecuteAction(requestId: String, action: com.pspdfkit.annotations.actions.Action, pageIndex: Int, url: String?) { eventDispatcher?.dispatchEvent(FabricOnShouldExecuteActionEvent(com.facebook.react.uimanager.UIManagerHelper.getSurfaceId(reactContext), pdfView.id, requestId, pageIndex, action, url)) } + override fun onSignatureFieldTapped(fullyQualifiedName: String, pageIndex: Int) { + // Signature interception is not supported on the Instant view. + } }) return pdfView } else { diff --git a/android/src/newarch/java/io/nutrient/react/fabric/ReactPdfViewManagerFabric.kt b/android/src/newarch/java/io/nutrient/react/fabric/ReactPdfViewManagerFabric.kt index bf373ce7..ab6575cd 100644 --- a/android/src/newarch/java/io/nutrient/react/fabric/ReactPdfViewManagerFabric.kt +++ b/android/src/newarch/java/io/nutrient/react/fabric/ReactPdfViewManagerFabric.kt @@ -48,6 +48,7 @@ import io.nutrient.react.events.FabricOnNavigationButtonClickedEvent import io.nutrient.react.events.FabricOnAnnotationTappedEvent import io.nutrient.react.events.FabricOnAnnotationsChangedEvent import io.nutrient.react.events.FabricOnShouldExecuteActionEvent +import io.nutrient.react.events.FabricOnSignatureFieldTappedEvent import com.pspdfkit.react.NutrientViewRegistry class ReactPdfViewManagerFabric : ViewGroupManager(), NutrientViewManagerInterface { @@ -196,6 +197,11 @@ class ReactPdfViewManagerFabric : ViewGroupManager(), NutrientViewManag val surfaceId = UIManagerHelper.getSurfaceId(reactContext) eventDispatcher?.dispatchEvent(FabricOnReadyEvent(surfaceId, pdfView.id)) } + + override fun onSignatureFieldTapped(fullyQualifiedName: String, pageIndex: Int) { + val surfaceId = UIManagerHelper.getSurfaceId(reactContext) + eventDispatcher?.dispatchEvent(FabricOnSignatureFieldTappedEvent(surfaceId, pdfView.id, fullyQualifiedName, pageIndex)) + } }) pdfView.inject( @@ -274,6 +280,7 @@ class ReactPdfViewManagerFabric : ViewGroupManager(), NutrientViewManag map["onAnnotationTapped"] = mapOf("registrationName" to "onAnnotationTapped") map["onAnnotationsChanged"] = mapOf("registrationName" to "onAnnotationsChanged") map["onShouldExecuteAction"] = mapOf("registrationName" to "onShouldExecuteAction") + map["onSignatureFieldTapped"] = mapOf("registrationName" to "onSignatureFieldTapped") return map } @@ -376,6 +383,10 @@ class ReactPdfViewManagerFabric : ViewGroupManager(), NutrientViewManag view.setDisableDefaultActionForTappedAnnotations(value) } + override fun setInterceptSignatureFields(view: PdfView, value: Boolean) { + view.setInterceptSignatureFields(value) + } + override fun setHasShouldExecuteAction(view: PdfView, value: Boolean) { view.setHasShouldExecuteAction(value) } diff --git a/android/src/newarch/java/io/nutrient/react/turbo/NutrientViewTurboModule.java b/android/src/newarch/java/io/nutrient/react/turbo/NutrientViewTurboModule.java index 33b3dbb6..5a9359a2 100644 --- a/android/src/newarch/java/io/nutrient/react/turbo/NutrientViewTurboModule.java +++ b/android/src/newarch/java/io/nutrient/react/turbo/NutrientViewTurboModule.java @@ -248,6 +248,42 @@ public void setUserInterfaceVisible(String reference, boolean visible, Promise p } } + @Override + public void setFormFieldReadOnly(String reference, String fullyQualifiedName, boolean readOnly, boolean persist, Promise promise) { + PdfView view = NutrientViewRegistry.getInstance().getViewForId(reference); + if (view == null) { + promise.reject(Errors.VIEW_NOT_FOUND, "No view found for reference: " + reference); + return; + } + + // Android widget-annotation flags are always written to the document model, + // so the persist argument has no transient counterpart here. + view.setFormFieldReadOnly(fullyQualifiedName, readOnly) + .subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io()) + .observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread()) + .subscribe( + promise::resolve, + throwable -> promise.reject(Errors.OPERATION_FAILED, throwable.getMessage()) + ); + } + + @Override + public void dismissSignaturePad(String reference, Promise promise) { + PdfView view = NutrientViewRegistry.getInstance().getViewForId(reference); + if (view == null) { + promise.reject(Errors.VIEW_NOT_FOUND, "No view found for reference: " + reference); + return; + } + + view.post(() -> { + try { + promise.resolve(view.dismissSignaturePad()); + } catch (Exception e) { + promise.reject(Errors.OPERATION_FAILED, e.getMessage()); + } + }); + } + @Override public void destroyView(String reference) { PdfView view = NutrientViewRegistry.getInstance().getViewForId(reference); diff --git a/index.js b/index.js index 9565cac2..058d52bf 100644 --- a/index.js +++ b/index.js @@ -143,6 +143,7 @@ class NutrientView extends React.Component { onReady={this._onReady} onShouldExecuteAction={hasShouldExecute ? this._onShouldExecuteAction : undefined} hasShouldExecuteAction={hasShouldExecute} + onSignatureFieldTapped={this._onSignatureFieldTapped} /> ); } else { @@ -173,6 +174,7 @@ class NutrientView extends React.Component { onReady={this._onReady} onShouldExecuteAction={hasShouldExecute ? this._onShouldExecuteAction : undefined} hasShouldExecuteAction={hasShouldExecute} + onSignatureFieldTapped={this._onSignatureFieldTapped} /> ); } @@ -244,6 +246,15 @@ class NutrientView extends React.Component { } }; + /** + * @ignore + */ + _onSignatureFieldTapped = event => { + if (this.props.onSignatureFieldTapped) { + this.props.onSignatureFieldTapped(event.nativeEvent); + } + }; + /** * @ignore */ @@ -698,6 +709,97 @@ class NutrientView extends React.Component { } }; + /** + * Makes the form field with the supplied fully qualified name read-only or editable. + * + * On iOS, when ```persist``` is ```true``` the PDF form field flag is modified so the change survives saving the document, and when ```false``` interaction is only disabled for the current viewer session without changing the saved PDF. + * On Android, the change is applied to the widget annotation flags of the form field and persists once the document is saved; the ```persist``` argument is ignored. + * + * @method setFormFieldReadOnly + * @memberof NutrientView + * @param { string } fullyQualifiedName The fully qualified name of the form field. + * @param { boolean } readOnly ```true``` to make the form field read-only, ```false``` to make it editable again. + * @param { boolean } [persist] Whether the change should be written to the PDF form field flags (default ```true```). iOS only. + * @example + * const result = await this.pdfRef.current.setFormFieldReadOnly('Name_Last', true, true); + * + * @returns { Promise } A promise resolving to ```true``` when the form field was updated, and rejecting when no matching form field was found. + */ + setFormFieldReadOnly = function (fullyQualifiedName, readOnly, persist = true) { + const { isNewArchitectureEnabled } = require('./lib/ArchitectureDetector'); + if (isNewArchitectureEnabled()) { + return this._fabricRef.current?.setFormFieldReadOnly( + fullyQualifiedName, + readOnly, + persist, + ); + } + if (Platform.OS === 'android') { + let requestId = this._nextRequestId++; + let requestMap = this._requestMap; + + // We create a promise here that will be resolved once onDataReturned is called. + let promise = new Promise(function (resolve, reject) { + requestMap[requestId] = { resolve: resolve, reject: reject }; + }); + + UIManager.dispatchViewManagerCommand( + findNodeHandle(this._componentRef.current), + this._getViewManagerConfig('RCTPSPDFKitView').Commands + .setFormFieldReadOnly, + [requestId, fullyQualifiedName, readOnly], + ); + + return promise; + } else if (Platform.OS === 'ios') { + return NativeModules.PSPDFKitViewManager.setFormFieldReadOnly( + fullyQualifiedName, + readOnly, + persist, + findNodeHandle(this._componentRef.current), + ); + } + }; + + /** + * Dismisses the native signature creation UI if it is currently presented. + * + * @method dismissSignaturePad + * @memberof NutrientView + * @example + * const dismissed = await this.pdfRef.current.dismissSignaturePad(); + * + * @returns { Promise } A promise resolving to ```true``` when a signature UI was dismissed, and ```false``` when no signature UI was presented. + */ + dismissSignaturePad = function () { + const { isNewArchitectureEnabled } = require('./lib/ArchitectureDetector'); + if (isNewArchitectureEnabled()) { + return this._fabricRef.current?.dismissSignaturePad(); + } + if (Platform.OS === 'android') { + let requestId = this._nextRequestId++; + let requestMap = this._requestMap; + + // We create a promise here that will be resolved once onDataReturned is called. + let promise = new Promise(function (resolve, reject) { + requestMap[requestId] = { resolve: resolve, reject: reject }; + }); + + UIManager.dispatchViewManagerCommand( + findNodeHandle(this._componentRef.current), + this._getViewManagerConfig('RCTPSPDFKitView').Commands + .dismissSignaturePad, + [requestId], + ); + + return promise; + } else if (Platform.OS === 'ios') { + return NativeModules.PSPDFKitViewManager.dismissSignaturePad( + findNodeHandle(this._componentRef.current), + ); + } + }; + /** * Sets the left bar button items for the specified view mode. * Note: The same button item cannot be added to both the left and right bar button items simultaneously. @@ -1329,6 +1431,13 @@ NutrientView.propTypes = { * @memberof NutrientView */ disableDefaultActionForTappedAnnotations: PropTypes.bool, + /** + * When enabled, tapping a signature form field will emit the ```onSignatureFieldTapped``` callback and suppress the native signature creation UI. Disabled by default (```false```). + * Use this to replace Nutrient's signature flow with a custom signing workflow. + * @type {boolean} + * @memberof NutrientView + */ + interceptSignatureFields: PropTypes.bool, /** * Controls whether or not the document will automatically be saved. Defaults to automatically saving (```false```). * @type {boolean} @@ -1432,6 +1541,18 @@ NutrientView.propTypes = { * }} */ onAnnotationTapped: PropTypes.func, + /** + * Callback that's called when a signature form field is tapped while ```interceptSignatureFields``` is enabled. + * The result contains the fully qualified name of the signature form field and the page index it is located on. + * @type {function} + * @memberof NutrientView + * @example + * onSignatureFieldTapped={result => { + * // { fullyQualifiedName: 'Applicant.Signature', pageIndex: 2 } + * // Start a custom signing workflow here. + * }} + */ + onSignatureFieldTapped: PropTypes.func, /** * Callback that's called when an annotation is added, changed, or removed. * The result contains the type of change, as well as an array of the InstantJSON annotations. diff --git a/ios/Fabric/NutrientView.mm b/ios/Fabric/NutrientView.mm index 3a4e7efd..023de132 100644 --- a/ios/Fabric/NutrientView.mm +++ b/ios/Fabric/NutrientView.mm @@ -309,6 +309,18 @@ - (void)pspdfView:(RCTPSPDFKitView *)view didRequestShouldExecuteActionWithPaylo _eventEmitter->onShouldExecuteAction(eventPayload); } +- (void)pspdfView:(RCTPSPDFKitView *)view didTapSignatureFieldWithFullyQualifiedName:(NSString *)fullyQualifiedName pageIndex:(NSInteger)pageIndex { + if (!_eventEmitter) { + return; + } + + facebook::react::NutrientViewEventEmitter::OnSignatureFieldTapped payload{ + fullyQualifiedName ? std::string([fullyQualifiedName UTF8String]) : std::string(""), + (int)pageIndex + }; + _eventEmitter->onSignatureFieldTapped(payload); +} + - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { [super updateProps:props oldProps:oldProps]; @@ -359,6 +371,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _disableDefaultActionForTappedAnnotations = newProps->disableDefaultActionForTappedAnnotations; _view.disableDefaultActionForTappedAnnotations = _disableDefaultActionForTappedAnnotations; _view.hasShouldExecuteAction = newProps->hasShouldExecuteAction; + _view.interceptSignatureFields = newProps->interceptSignatureFields; _showNavigationButtonInToolbar = newProps->showNavigationButtonInToolbar; if (!newProps->availableFontNamesJSONString.empty()) { _availableFontNamesJSONString = RCTNSStringFromString(newProps->availableFontNamesJSONString); diff --git a/ios/RCTPSPDFKitView.h b/ios/RCTPSPDFKitView.h index 2e7bf320..43346c9a 100644 --- a/ios/RCTPSPDFKitView.h +++ b/ios/RCTPSPDFKitView.h @@ -44,6 +44,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) RCTBubblingEventBlock onShouldExecuteAction; /// Internal flag tracking whether onShouldExecuteAction is implemented in JS. @property (nonatomic, assign) BOOL hasShouldExecuteAction; +/// When enabled, tapping a signature form field emits onSignatureFieldTapped and +/// suppresses Nutrient's default signature UI. +@property (nonatomic) BOOL interceptSignatureFields; +@property (nonatomic, copy) RCTBubblingEventBlock onSignatureFieldTapped; @property (nonatomic, copy, nullable) NSArray *availableFontNames; @property (nonatomic, copy, nullable) NSString *selectedFontName; @property (nonatomic) BOOL showDownloadableFonts; @@ -81,6 +85,10 @@ NS_ASSUME_NONNULL_BEGIN /// Forms - (NSDictionary *)getFormFieldValue:(NSString *)fullyQualifiedName; - (BOOL)setFormFieldValue:(NSString *)value fullyQualifiedName:(NSString *)fullyQualifiedName; +- (BOOL)setFormFieldReadOnly:(NSString *)fullyQualifiedName readOnly:(BOOL)readOnly persist:(BOOL)persist; + +/// Electronic Signatures +- (BOOL)dismissSignaturePad; /// Toolbar buttons customizations - (void)setLeftBarButtonItems:(nullable NSArray *)items forViewMode:(nullable NSString *) viewMode animated:(BOOL)animated; @@ -124,6 +132,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)pspdfView:(RCTPSPDFKitView *)view didTapAnnotation:(PSPDFAnnotation *)annotation; /** Action execution callback for New Architecture bridging */ - (void)pspdfView:(RCTPSPDFKitView *)view didRequestShouldExecuteActionWithPayload:(NSDictionary *)payload; +/** Signature form field tap callback for New Architecture bridging */ +- (void)pspdfView:(RCTPSPDFKitView *)view didTapSignatureFieldWithFullyQualifiedName:(NSString *)fullyQualifiedName pageIndex:(NSInteger)pageIndex; @end NS_ASSUME_NONNULL_END diff --git a/ios/RCTPSPDFKitView.m b/ios/RCTPSPDFKitView.m index b415762d..7c35d755 100644 --- a/ios/RCTPSPDFKitView.m +++ b/ios/RCTPSPDFKitView.m @@ -403,6 +403,25 @@ - (BOOL)pdfViewController:(PSPDFViewController *)pdfController didTapOnAnnotatio self.onAnnotationTapped(updatedDictionary); } } + // Signature interception: when enabled, consume taps on signature form fields and + // notify JS instead of presenting Nutrient's default signature UI. + if (self.interceptSignatureFields && [annotation isKindOfClass:PSPDFSignatureFormElement.class]) { + PSPDFSignatureFormElement *signatureElement = (PSPDFSignatureFormElement *)annotation; + NSString *fullyQualifiedName = signatureElement.fullyQualifiedFieldName ?: @""; + NSInteger signaturePageIndex = (NSInteger)signatureElement.pageIndex; + if ([self.delegate respondsToSelector:@selector(pspdfView:didTapSignatureFieldWithFullyQualifiedName:pageIndex:)]) { + // Fabric path. + [(id)self.delegate pspdfView:self didTapSignatureFieldWithFullyQualifiedName:fullyQualifiedName pageIndex:signaturePageIndex]; + } else if (self.onSignatureFieldTapped) { + // Legacy (Paper) path. + self.onSignatureFieldTapped(@{ + @"fullyQualifiedName": fullyQualifiedName, + @"pageIndex": @(signaturePageIndex) + }); + } + // The application handled the tap. Suppress the SDK default signature UI. + return YES; + } // When onShouldExecuteAction is present under New Architecture (Fabric), return NO so // PSPDFKit proceeds to shouldExecuteAction:, where we intercept and delegate to JS. // On legacy (Paper) we always let PSPDFKit handle taps normally and simply honor @@ -757,6 +776,62 @@ - (BOOL)setFormFieldValue:(NSString *)value fullyQualifiedName:(NSString *)fully return success; } +- (BOOL)setFormFieldReadOnly:(NSString *)fullyQualifiedName readOnly:(BOOL)readOnly persist:(BOOL)persist { + if (fullyQualifiedName.length == 0) { + NSLog(@"Invalid fully qualified name."); + return NO; + } + + PSPDFDocument *document = self.pdfController.document; + VALIDATE_DOCUMENT(document, NO) + + PSPDFFormField *formField = [document.formParser findFieldWithFullFieldName:fullyQualifiedName]; + if (formField == nil) { + return NO; + } + + if (persist) { + // Modifies the PDF form field flag. Persisted once the document is saved. + formField.isReadOnly = readOnly; + } else { + // Disables interaction for the current viewer session only. + formField.isEditable = !readOnly; + } + + [self.pdfController reloadData]; + return YES; +} + +// MARK: - Electronic Signatures + +- (BOOL)dismissSignaturePad { + UIViewController *presented = self.pdfController.presentedViewController; + if (presented == nil) { + return NO; + } + + UIViewController *visibleController = presented; + if ([presented isKindOfClass:UINavigationController.class]) { + UINavigationController *navigationController = (UINavigationController *)presented; + visibleController = navigationController.visibleViewController ?: navigationController; + } + + // Only dismiss Nutrient's signature UI. NSClassFromString is used so both the + // Electronic Signatures controller and the legacy Annotations signature controller + // are recognized without a compile-time dependency on either. + Class signatureCreationControllerClass = NSClassFromString(@"PSPDFSignatureCreationViewController"); + Class legacySignatureControllerClass = NSClassFromString(@"PSPDFSignatureViewController"); + BOOL isSignatureController = + (signatureCreationControllerClass != Nil && [visibleController isKindOfClass:signatureCreationControllerClass]) || + (legacySignatureControllerClass != Nil && [visibleController isKindOfClass:legacySignatureControllerClass]); + if (!isSignatureController) { + return NO; + } + + [presented.presentingViewController dismissViewControllerAnimated:YES completion:NULL]; + return YES; +} + // MARK: - Notifications - (void)annotationChangedNotification:(NSNotification *)notification { diff --git a/ios/RCTPSPDFKitViewManager.m b/ios/RCTPSPDFKitViewManager.m index 3ac1503e..cb601096 100644 --- a/ios/RCTPSPDFKitViewManager.m +++ b/ios/RCTPSPDFKitViewManager.m @@ -175,6 +175,10 @@ @implementation RCTPSPDFKitViewManager RCT_EXPORT_VIEW_PROPERTY(onShouldExecuteAction, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(interceptSignatureFields, BOOL) + +RCT_EXPORT_VIEW_PROPERTY(onSignatureFieldTapped, RCTBubblingEventBlock) + RCT_CUSTOM_VIEW_PROPERTY(availableFontNames, NSArray, RCTPSPDFKitView) { [NutrientPropsFontHelper applyAvailableFontNamesFromJSON:json toView:view]; } @@ -303,6 +307,34 @@ @implementation RCTPSPDFKitViewManager }); } +RCT_EXPORT_METHOD(setFormFieldReadOnly:(NSString *)fullyQualifiedName readOnly:(BOOL)readOnly persist:(BOOL)persist reactTag:(nonnull NSNumber *)reactTag resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_main_queue(), ^{ + RCTPSPDFKitView *component = (RCTPSPDFKitView *)[self.bridge.uiManager viewForReactTag:reactTag]; + if (component == nil || component.pdfController.document == nil) { + reject(@"viewer_unavailable", @"The Nutrient viewer or document is not available.", nil); + return; + } + BOOL success = [component setFormFieldReadOnly:fullyQualifiedName readOnly:readOnly persist:persist]; + if (success) { + resolve(@YES); + } else { + reject(@"field_not_found", @"No form field with that fully qualified name was found.", nil); + } + }); +} + +RCT_EXPORT_METHOD(dismissSignaturePad:(nonnull NSNumber *)reactTag resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_main_queue(), ^{ + RCTPSPDFKitView *component = (RCTPSPDFKitView *)[self.bridge.uiManager viewForReactTag:reactTag]; + if (component == nil) { + reject(@"viewer_unavailable", @"The Nutrient viewer is not available.", nil); + return; + } + BOOL dismissed = [component dismissSignaturePad]; + resolve(@(dismissed)); + }); +} + RCT_EXPORT_METHOD(setLeftBarButtonItems:(nullable NSArray *)items viewMode:(nullable NSString *)viewMode animated:(BOOL)animated reactTag:(nonnull NSNumber *)reactTag) { dispatch_async(dispatch_get_main_queue(), ^{ RCTPSPDFKitView *component = (RCTPSPDFKitView *)[self.bridge.uiManager viewForReactTag:reactTag]; diff --git a/ios/Turbo/NutrientViewTurboModule.mm b/ios/Turbo/NutrientViewTurboModule.mm index d92d7baa..072deca5 100644 --- a/ios/Turbo/NutrientViewTurboModule.mm +++ b/ios/Turbo/NutrientViewTurboModule.mm @@ -268,10 +268,46 @@ - (void)setToolbar:(nonnull NSString *)reference toolbar:(nonnull NSString *)too }); } -- (void)destroyView:(nonnull NSString *)reference { +- (void)destroyView:(nonnull NSString *)reference { // No-Op. Only Android. } +- (void)setFormFieldReadOnly:(nonnull NSString *)reference + fullyQualifiedName:(nonnull NSString *)fullyQualifiedName + readOnly:(BOOL)readOnly + persist:(BOOL)persist + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + dispatch_async(dispatch_get_main_queue(), ^{ + RCTPSPDFKitView *view = [[NutrientViewRegistry shared] viewForId:reference]; + if (!view) { + reject(ERR_VIEW_NOT_FOUND, @"Fabric view not found for reference", [self _makeErrorWithCode:ERR_VIEW_NOT_FOUND message:@"Fabric view not found for reference"]); + return; + } + + BOOL success = [view setFormFieldReadOnly:fullyQualifiedName readOnly:readOnly persist:persist]; + if (success) { + resolve(@YES); + } else { + reject(ERR_OPERATION, @"No form field with that fully qualified name was found.", [self _makeErrorWithCode:ERR_OPERATION message:@"No form field with that fully qualified name was found."]); + } + }); +} + +- (void)dismissSignaturePad:(nonnull NSString *)reference + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + dispatch_async(dispatch_get_main_queue(), ^{ + RCTPSPDFKitView *view = [[NutrientViewRegistry shared] viewForId:reference]; + if (!view) { + reject(ERR_VIEW_NOT_FOUND, @"Fabric view not found for reference", [self _makeErrorWithCode:ERR_VIEW_NOT_FOUND message:@"Fabric view not found for reference"]); + return; + } + + resolve(@([view dismissSignaturePad])); + }); +} + - (NSDictionary *)dictionaryFromJSONString:(NSString *)jsonString { if (jsonString == nil) { return nil; } NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; diff --git a/lib/NutrientViewFabric.js b/lib/NutrientViewFabric.js index e5a4d7bb..ad0c5915 100644 --- a/lib/NutrientViewFabric.js +++ b/lib/NutrientViewFabric.js @@ -116,6 +116,13 @@ var NutrientViewFabric = forwardRef(function (props, ref) { }, executeAction: function (requestId, allow) { return NativeNutrientViewTurboModule_1.default.executeAction(instanceId.toString(), requestId, allow); + }, + setFormFieldReadOnly: function (fullyQualifiedName, readOnly, persist) { + if (persist === void 0) { persist = true; } + return NativeNutrientViewTurboModule_1.default.setFormFieldReadOnly(instanceId.toString(), fullyQualifiedName, readOnly, persist); + }, + dismissSignaturePad: function () { + return NativeNutrientViewTurboModule_1.default.dismissSignaturePad(instanceId.toString()); } }); }, [instanceId]); var fabricProps = __assign(__assign({}, props), { @@ -221,7 +228,18 @@ var NutrientViewFabric = forwardRef(function (props, ref) { // Paper passed an empty object props.onReady({}); } + : undefined, onSignatureFieldTapped: props.onSignatureFieldTapped + ? function (e) { + var _a; + var native = (_a = e === null || e === void 0 ? void 0 : e.nativeEvent) !== null && _a !== void 0 ? _a : e; + props.onSignatureFieldTapped({ + fullyQualifiedName: native === null || native === void 0 ? void 0 : native.fullyQualifiedName, + pageIndex: native === null || native === void 0 ? void 0 : native.pageIndex, + }); + } : undefined, + // Explicit boolean so native always gets a defined value when the prop is omitted + interceptSignatureFields: props.interceptSignatureFields === true, // Internal flag so native only intercepts actions when a JS handler is present hasShouldExecuteAction: !!props.onShouldExecuteAction, // Convert numeric instanceId to string for React Native nativeID diff --git a/src/NutrientViewFabric.tsx b/src/NutrientViewFabric.tsx index 70873ac7..fc0e4efb 100644 --- a/src/NutrientViewFabric.tsx +++ b/src/NutrientViewFabric.tsx @@ -27,6 +27,8 @@ export interface NutrientViewFabricRef { destroyView: () => void; setPageIndex: (pageIndex: number, animated: boolean) => Promise | void; executeAction: (requestId: string, allow: boolean) => Promise | void; + setFormFieldReadOnly: (fullyQualifiedName: string, readOnly: boolean, persist?: boolean) => Promise; + dismissSignaturePad: () => Promise; } // Fabric component using the actual native component @@ -107,6 +109,14 @@ const NutrientViewFabric = forwardRef((props executeAction: (requestId: string, allow: boolean) => { return NativeNutrientViewTurboModule.executeAction(instanceId.toString(), requestId, allow); + }, + + setFormFieldReadOnly: (fullyQualifiedName: string, readOnly: boolean, persist: boolean = true) => { + return NativeNutrientViewTurboModule.setFormFieldReadOnly(instanceId.toString(), fullyQualifiedName, readOnly, persist); + }, + + dismissSignaturePad: () => { + return NativeNutrientViewTurboModule.dismissSignaturePad(instanceId.toString()); } }), [instanceId]); @@ -223,6 +233,17 @@ const NutrientViewFabric = forwardRef((props (props as any).onReady({}); } : undefined, + onSignatureFieldTapped: (props as any).onSignatureFieldTapped + ? (e: any) => { + const native = e?.nativeEvent ?? e; + (props as any).onSignatureFieldTapped({ + fullyQualifiedName: native?.fullyQualifiedName, + pageIndex: native?.pageIndex, + }); + } + : undefined, + // Explicit boolean so native always gets a defined value when the prop is omitted + interceptSignatureFields: (props as any).interceptSignatureFields === true, // Internal flag so native only intercepts actions when a JS handler is present hasShouldExecuteAction: !!(props as any).onShouldExecuteAction, // Convert numeric instanceId to string for React Native nativeID diff --git a/src/specs/NativeNutrientViewTurboModule.ts b/src/specs/NativeNutrientViewTurboModule.ts index ca8ee648..23f2e66b 100644 --- a/src/specs/NativeNutrientViewTurboModule.ts +++ b/src/specs/NativeNutrientViewTurboModule.ts @@ -117,6 +117,12 @@ export interface Spec extends TurboModule { setExcludedAnnotations: (reference: string, annotations: string[]) => void; setUserInterfaceVisible: (reference: string, visible: boolean) => Promise; destroyView: (reference: string) => void; + + // Forms + setFormFieldReadOnly: (reference: string, fullyQualifiedName: string, readOnly: boolean, persist: boolean) => Promise; + + // Electronic Signatures + dismissSignaturePad: (reference: string) => Promise; } export default TurboModuleRegistry.getEnforcing('NutrientViewTurboModule'); \ No newline at end of file diff --git a/src/specs/NutrientViewNativeComponent.ts b/src/specs/NutrientViewNativeComponent.ts index 3f80dc37..946aa8d7 100644 --- a/src/specs/NutrientViewNativeComponent.ts +++ b/src/specs/NutrientViewNativeComponent.ts @@ -449,6 +449,11 @@ export interface NativeProps extends ViewProps { // Basic interaction settings disableDefaultActionForTappedAnnotations?: boolean; + /** + * When true, tapping a signature form field emits onSignatureFieldTapped and + * suppresses the native signature UI. + */ + interceptSignatureFields?: WithDefault; /** * Internal flag used so native knows whether onShouldExecuteAction is actually * implemented on the JS side. When false, native should not intercept actions. @@ -503,6 +508,13 @@ export interface NativeProps extends ViewProps { actionType?: string; url?: string; }>; + /** + * Called when a signature form field is tapped while interceptSignatureFields is enabled. + */ + onSignatureFieldTapped?: BubblingEventHandler<{ + fullyQualifiedName: string; + pageIndex: Int32; + }>; } // Export the Fabric native component with Nutrient prefix diff --git a/types/index.d.ts b/types/index.d.ts index e4a5e4c2..81fe104e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -893,6 +893,10 @@ declare class NutrientView extends React.Component { * @ignore */ _onAnnotationsChanged: (event: any) => void; + /** + * @ignore + */ + _onSignatureFieldTapped: (event: any) => void; /** * @ignore */ @@ -1055,6 +1059,34 @@ declare class NutrientView extends React.Component { * @returns { Promise } A promise resolving to ```true``` if the value was set successfully, and ```false``` if an error occurred. */ setFormFieldValue: (fullyQualifiedName: string, value: string) => Promise; + /** + * Makes the form field with the supplied fully qualified name read-only or editable. + * + * On iOS, when ```persist``` is ```true``` the PDF form field flag is modified so the change survives saving the document, and when ```false``` interaction is only disabled for the current viewer session without changing the saved PDF. + * On Android, the change is applied to the widget annotation flags of the form field and persists once the document is saved; the ```persist``` argument is ignored. + * + * @method setFormFieldReadOnly + * @memberof NutrientView + * @param { string } fullyQualifiedName The fully qualified name of the form field. + * @param { boolean } readOnly ```true``` to make the form field read-only, ```false``` to make it editable again. + * @param { boolean } [persist] Whether the change should be written to the PDF form field flags (default ```true```). iOS only. + * @example + * const result = await this.pdfRef.current.setFormFieldReadOnly('Name_Last', true, true); + * + * @returns { Promise } A promise resolving to ```true``` when the form field was updated, and rejecting when no matching form field was found. + */ + setFormFieldReadOnly: (fullyQualifiedName: string, readOnly: boolean, persist?: boolean) => Promise; + /** + * Dismisses the native signature creation UI if it is currently presented. + * + * @method dismissSignaturePad + * @memberof NutrientView + * @example + * const dismissed = await this.pdfRef.current.dismissSignaturePad(); + * + * @returns { Promise } A promise resolving to ```true``` when a signature UI was dismissed, and ```false``` when no signature UI was presented. + */ + dismissSignaturePad: () => Promise; /** * Sets the left bar button items for the specified view mode. * Note: The same button item cannot be added to both the left and right bar button items simultaneously. @@ -1267,6 +1299,7 @@ declare namespace NutrientView { let hideNavigationBar: boolean; let showCloseButton: boolean; let disableDefaultActionForTappedAnnotations: boolean; + let interceptSignatureFields: boolean; let disableAutomaticSaving: boolean; let annotationAuthorName: string; let imageSaveMode: string; @@ -1277,6 +1310,7 @@ declare namespace NutrientView { let onDocumentSaved: Function; let onDocumentSaveFailed: Function; let onAnnotationTapped: Function; + let onSignatureFieldTapped: Function; let onAnnotationsChanged: Function; let onStateChanged: Function; let onCustomToolbarButtonTapped: Function; diff --git a/types/index.d.ts.map b/types/index.d.ts.map index 3a283b7f..4414a01d 100644 --- a/types/index.d.ts.map +++ b/types/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":"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"} \ No newline at end of file From da0d83002fc1b459cb7f6b7da22dafd2e01c4607 Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Fri, 17 Jul 2026 16:08:13 +0300 Subject: [PATCH 2/4] Add Catalog example exercising signature interception APIs Adds a SignatureInterception example screen used to verify the new bridge at runtime on the Android emulator and iOS simulator via Maestro: signature tap interception (on/off), setFormFieldReadOnly lock/unlock, and dismissSignaturePad on an open and closed signature UI. Co-Authored-By: Claude Fable 5 --- samples/Catalog/Catalog.tsx | 5 + samples/Catalog/ExamplesNavigationMenu.tsx | 9 ++ .../examples/SignatureInterception.tsx | 145 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 samples/Catalog/examples/SignatureInterception.tsx diff --git a/samples/Catalog/Catalog.tsx b/samples/Catalog/Catalog.tsx index 52748484..25fd90d6 100644 --- a/samples/Catalog/Catalog.tsx +++ b/samples/Catalog/Catalog.tsx @@ -27,6 +27,7 @@ import Measurement from './examples/Measurement'; import { OpenImageDocument } from './examples/OpenImageDocument'; import { ProgrammaticAnnotations } from './examples/ProgrammaticAnnotations'; import { ProgrammaticFormFilling } from './examples/ProgrammaticFormFilling'; +import { SignatureInterception } from './examples/SignatureInterception'; import { NutrientViewComponent } from './examples/NutrientViewComponent'; import { SaveAs } from './examples/SaveAs'; import { SplitPDF } from './examples/SplitPDF'; @@ -99,6 +100,10 @@ class Catalog extends React.Component { name="ProgrammaticFormFilling" component={ProgrammaticFormFilling} /> + { + component.props.navigation.push('SignatureInterception'); + }, + }, { key: 'item11', name: 'Split PDF', diff --git a/samples/Catalog/examples/SignatureInterception.tsx b/samples/Catalog/examples/SignatureInterception.tsx new file mode 100644 index 00000000..e053844b --- /dev/null +++ b/samples/Catalog/examples/SignatureInterception.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; +import NutrientView from '@nutrient-sdk/react-native'; + +import { + formDocumentName, + formDocumentPath, + pspdfkitColor, + writableFormDocumentPath, +} from '../configuration/Constants'; +import { + renderWithBaseExampleSafeArea, + useBaseExampleAutoHidingHeader, +} from '../helpers/ExampleScreenLayoutHelpers'; +import { extractFromAssetsIfMissing } from '../helpers/FileSystemHelpers'; + +/** + * Test screen for the signature interception and form field read-only bridge: + * - interceptSignatureFields + onSignatureFieldTapped + * - setFormFieldReadOnly + * - dismissSignaturePad + */ +export const SignatureInterception = ({ navigation }: any) => { + const pdfRef = useRef(null); + const [intercept, setIntercept] = useState(true); + const [status, setStatus] = useState('ready'); + const [documentPath, setDocumentPath] = useState(formDocumentPath); + useBaseExampleAutoHidingHeader(navigation); + + useEffect(() => { + // Forms are not editable on read-only android_asset documents, so copy the + // document to a writable location first. + extractFromAssetsIfMissing(formDocumentName, () => { + setDocumentPath(writableFormDocumentPath); + }); + }, []); + + const lockField = async (readOnly: boolean) => { + try { + const result = await (pdfRef.current as any)?.setFormFieldReadOnly( + 'Name_Last', + readOnly, + true, + ); + setStatus(`readOnly=${readOnly} result=${result}`); + } catch (error: any) { + setStatus(`readOnly error: ${error?.message ?? JSON.stringify(error)}`); + } + }; + + const dismissPad = async () => { + try { + const result = await (pdfRef.current as any)?.dismissSignaturePad(); + setStatus(`dismissed=${result}`); + } catch (error: any) { + setStatus(`dismiss error: ${error?.message ?? JSON.stringify(error)}`); + } + }; + + const dismissPadDelayed = () => { + setStatus('dismissing in 4s...'); + setTimeout(dismissPad, 4000); + }; + + return ( + + { + setStatus( + `sigTapped name=${event.fullyQualifiedName} page=${event.pageIndex}`, + ); + }} + configuration={{ + documentLabelEnabled: false, + disableAutomaticSaving: true, + }} + style={styles.pdfColor} + /> + {renderWithBaseExampleSafeArea(insets => ( + + + {status} + + + { + setIntercept(!intercept); + setStatus(`intercept=${!intercept}`); + }} + accessibilityLabel="Toggle Intercept"> + {`Intercept: ${intercept ? 'ON' : 'OFF'}`} + + lockField(true)} accessibilityLabel="Lock Field"> + Lock + + lockField(false)} accessibilityLabel="Unlock Field"> + Unlock + + + Dismiss + + + Dismiss 4s + + + + ))} + + ); +}; + +const styles = { + flex: { flex: 1 }, + pdfColor: { flex: 1, color: pspdfkitColor }, + status: { + fontSize: 13, + padding: 6, + color: '#333333', + backgroundColor: '#ffffcc', + }, + column: { + flexDirection: 'column' as 'column', + alignItems: 'center' as 'center', + overflow: 'visible' as 'visible', + }, + horizontalContainer: { + flexDirection: 'row' as 'row', + justifyContent: 'space-between' as 'space-between', + alignItems: 'center' as 'center', + padding: 6, + overflow: 'visible' as 'visible', + }, + button: { + padding: 8, + fontSize: 13, + color: pspdfkitColor, + textAlign: 'center' as 'center', + backgroundColor: '#f0f0f0', + borderRadius: 5, + marginHorizontal: 3, + }, +}; From 7c44f6147e1d2ccea509a811ccb2a6fee8946eae Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Fri, 17 Jul 2026 16:18:54 +0300 Subject: [PATCH 3/4] Add implementation guide for form read-only and signature interception Standalone guide for customizing an SDK fork with the new APIs: public API semantics and platform differences, per-layer implementation walkthrough for both architectures, build/test instructions, runtime verification results, and packaging options. Co-Authored-By: Claude Fable 5 --- ...orm-readonly-and-signature-interception.md | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 documentation/form-readonly-and-signature-interception.md diff --git a/documentation/form-readonly-and-signature-interception.md b/documentation/form-readonly-and-signature-interception.md new file mode 100644 index 00000000..229d5fb6 --- /dev/null +++ b/documentation/form-readonly-and-signature-interception.md @@ -0,0 +1,282 @@ +# Customization Guide: Form Field Read-Only Control & Signature Interception + +This guide describes how to extend a fork of the Nutrient React Native SDK (`@nutrient-sdk/react-native`) with three native form capabilities: + +1. **`setFormFieldReadOnly(fullyQualifiedName, readOnly, persist?)`** — lock or unlock a single form field at runtime. +2. **`interceptSignatureFields` + `onSignatureFieldTapped`** — intercept taps on signature form fields and suppress the native signature UI, so the application can run its own signing workflow. +3. **`dismissSignaturePad()`** — programmatically dismiss the native signature creation UI. + +The implementation supports **both React Native architectures** (Legacy/Paper and New Architecture with Fabric + TurboModules), following the repository's [BRIDGING.md](../BRIDGING.md) recipe. It was implemented and verified against: + +| Component | Version | +|---|---| +| Nutrient React Native SDK | 4.4.0 | +| Nutrient Android SDK | 11.5.1 | +| Nutrient iOS SDK (PSPDFKit) | 26.10.0 | +| React Native | 0.83.x | + +All changes live on the `feature/form-readonly-signature-interception` branch. Runtime behavior was verified on an Android emulator and iOS simulator (New Architecture) with automated UI tests; see [Verification](#verification). + +--- + +## Public API + +```tsx +import NutrientView from '@nutrient-sdk/react-native'; + +const pdfRef = useRef(null); + + { + // Native signature UI is suppressed. Start your custom signing flow here. + }} +/>; + +// Lock a single field (persisted into the PDF form field flags on iOS, +// widget annotation flags on Android — survives saving the document): +await pdfRef.current?.setFormFieldReadOnly('Applicant.Contact.Email', true, true); + +// iOS only: disable interaction for the current viewer session without +// modifying the saved PDF: +await pdfRef.current?.setFormFieldReadOnly('Applicant.Contact.Email', true, false); + +// Re-enable: +await pdfRef.current?.setFormFieldReadOnly('Applicant.Contact.Email', false); + +// Dismiss the native signature UI if presented. Resolves `true` when a +// signature UI was dismissed, `false` when none was open (safe to call +// repeatedly): +const dismissed = await pdfRef.current?.dismissSignaturePad(); +``` + +### Semantics and platform differences + +| Aspect | iOS | Android | +|---|---|---| +| `persist: true` | Sets `PSPDFFormField.isReadOnly` (PDF form field flag; persists once the document is saved) | Adds `AnnotationFlags.LOCKEDCONTENTS` to every widget annotation of the field, preserving existing flags (persists once saved) | +| `persist: false` | Sets `PSPDFFormField.isEditable = false` (session-only; not written to the PDF) | Not supported — behaves like `persist: true` | +| Missing field | Promise rejects (`field_not_found` on Paper, `OPERATION_FAILED` on New Architecture) | Promise resolves `false` (legacy) / rejects (New Architecture) | +| `dismissSignaturePad()` targeting | Type-checks the presented controller against `PSPDFSignatureCreationViewController` (Electronic Signatures) and legacy `PSPDFSignatureViewController`; never dismisses unrelated modals | Locates `ElectronicSignatureFragment` by its public `FRAGMENT_TAG` across candidate fragment managers; never dismisses unrelated dialogs | +| Signature tap interception | `PSPDFViewControllerDelegate didTapOnAnnotation:` — consumes taps on `PSPDFSignatureFormElement` | `FormManager.OnFormElementClickedListener` — consumes clicks on `FormType.SIGNATURE` elements | + +**Why `LOCKEDCONTENTS` on Android:** the Android SDK's `FormField` exposes `isReadOnly()` as a getter only, and `AnnotationFlags.READONLY` is documented as ignored for widget annotations. Updating the widget's `LOCKEDCONTENTS` flag is the supported way to make an existing widget non-editable. Always copy the existing flag set and add/remove only `LOCKEDCONTENTS`. + +### Important caveats + +- **Forms must be interactive.** On Android, a document opened straight from `file:///android_asset/...` is read-only: form fields render without highlights, taps degrade to page taps, and `onSignatureFieldTapped` never fires. Copy the document to a writable location first (see `extractFromAssetsIfMissing` in the Catalog's `ProgrammaticFormFilling` example). +- **Saving is the caller's responsibility.** `setFormFieldReadOnly(..., persist: true)` mutates the in-memory document only. Call `saveCurrentDocument()` afterwards to persist. +- **Not a security boundary.** A PDF read-only flag is a UI/document constraint. Server-side validation must still verify which fields the user may change and whether a signature is authorized. +- **Toolbar-initiated signatures are out of scope.** Interception covers signature *form field* taps. A signature started from the annotation toolbar is not intercepted; remove or replace the toolbar item if that flow must also be suppressed. +- **The signature UI is modal.** While it is presented, React Native UI cannot be tapped. To exercise `dismissSignaturePad()` while the UI is open, call it from a timer/event rather than a button press. + +--- + +## Implementation walkthrough + +Every layer below is required. Skipping the New Architecture layer produces a bridge that silently does nothing on Fabric builds, and vice versa. + +### 1. TypeScript codegen specs (New Architecture) + +**`src/specs/NativeNutrientViewTurboModule.ts`** — add the imperative methods to the `Spec` interface. All view-backed TurboModule methods take the view `reference` (the component's `nativeID`) as their first argument: + +```ts +// Forms +setFormFieldReadOnly: (reference: string, fullyQualifiedName: string, readOnly: boolean, persist: boolean) => Promise; + +// Electronic Signatures +dismissSignaturePad: (reference: string) => Promise; +``` + +**`src/specs/NutrientViewNativeComponent.ts`** — add the prop and the event to `NativeProps`: + +```ts +interceptSignatureFields?: WithDefault; + +onSignatureFieldTapped?: BubblingEventHandler<{ + fullyQualifiedName: string; + pageIndex: Int32; +}>; +``` + +Codegen (run automatically during app builds) generates from these: the Android `NutrientViewManagerInterface.setInterceptSignatureFields`, the `NativeNutrientViewTurboModuleSpec` base class, and the iOS C++ `NutrientViewEventEmitter::OnSignatureFieldTapped` emitter — everything the native layers below implement against. + +### 2. Fabric wrapper — `src/NutrientViewFabric.tsx` + +- Add both methods to the `NutrientViewFabricRef` interface and to the `useImperativeHandle` block, delegating to `NativeNutrientViewTurboModule` with `instanceId.toString()` as the reference. +- Pass `interceptSignatureFields: (props as any).interceptSignatureFields === true` (an explicit boolean so native always receives a defined value). +- Re-shape the event payload for the public callback: + +```ts +onSignatureFieldTapped: (props as any).onSignatureFieldTapped + ? (e: any) => { + const native = e?.nativeEvent ?? e; + (props as any).onSignatureFieldTapped({ + fullyQualifiedName: native?.fullyQualifiedName, + pageIndex: native?.pageIndex, + }); + } + : undefined, +``` + +### 3. Public API — `index.js` + +Each method branches on `isNewArchitectureEnabled()` first (Fabric ref), then falls back to the Legacy paths (Android view-manager command with a `requestId` promise, iOS `NativeModules.PSPDFKitViewManager`). See `setFormFieldReadOnly` and `dismissSignaturePad` in `index.js` for the exact pattern — it mirrors `setFormFieldValue`. + +Also add: +- `_onSignatureFieldTapped` handler (unwraps `event.nativeEvent`), +- `onSignatureFieldTapped={this._onSignatureFieldTapped}` in **both** the Android and iOS render branches, +- `interceptSignatureFields: PropTypes.bool` and `onSignatureFieldTapped: PropTypes.func` with JSDoc (the JSDoc generates `types/index.d.ts` — run `npm run build-and-generate-types`). + +### 4. iOS — Legacy (Paper) + +**`ios/RCTPSPDFKitView.h`** +- Properties: `@property (nonatomic) BOOL interceptSignatureFields;` and `@property (nonatomic, copy) RCTBubblingEventBlock onSignatureFieldTapped;` +- Methods: `- (BOOL)setFormFieldReadOnly:readOnly:persist:` and `- (BOOL)dismissSignaturePad;` +- Delegate protocol addition (Fabric event path): `- (void)pspdfView:didTapSignatureFieldWithFullyQualifiedName:pageIndex:` + +**`ios/RCTPSPDFKitView.m`** +- In the existing `pdfViewController:didTapOnAnnotation:...` delegate method, before the action-handler logic, intercept signature form elements. Both emission paths must be handled — the Fabric delegate takes precedence, the Paper bubbling block is the fallback — and the method returns `YES` to suppress the SDK's default signature UI: + +```objc +if (self.interceptSignatureFields && [annotation isKindOfClass:PSPDFSignatureFormElement.class]) { + PSPDFSignatureFormElement *signatureElement = (PSPDFSignatureFormElement *)annotation; + NSString *fullyQualifiedName = signatureElement.fullyQualifiedFieldName ?: @""; + NSInteger signaturePageIndex = (NSInteger)signatureElement.pageIndex; + if ([self.delegate respondsToSelector:@selector(pspdfView:didTapSignatureFieldWithFullyQualifiedName:pageIndex:)]) { + [(id)self.delegate pspdfView:self didTapSignatureFieldWithFullyQualifiedName:fullyQualifiedName pageIndex:signaturePageIndex]; + } else if (self.onSignatureFieldTapped) { + self.onSignatureFieldTapped(@{ @"fullyQualifiedName": fullyQualifiedName, @"pageIndex": @(signaturePageIndex) }); + } + return YES; +} +``` + +- `setFormFieldReadOnly` resolves the field via `[document.formParser findFieldWithFullFieldName:]`, then sets `formField.isReadOnly = readOnly` (persist) or `formField.isEditable = !readOnly` (transient), and calls `[self.pdfController reloadData]`. Guard with the `VALIDATE_DOCUMENT` macro like the neighboring form methods. +- `dismissSignaturePad` inspects `self.pdfController.presentedViewController` (unwrapping a `UINavigationController` to its `visibleViewController`), type-checks against `NSClassFromString(@"PSPDFSignatureCreationViewController")` and `NSClassFromString(@"PSPDFSignatureViewController")`, and only then dismisses. `NSClassFromString` avoids a compile-time dependency on the Swift-defined Electronic Signatures controller. + +**`ios/RCTPSPDFKitViewManager.m`** +- `RCT_EXPORT_VIEW_PROPERTY(interceptSignatureFields, BOOL)` and `RCT_EXPORT_VIEW_PROPERTY(onSignatureFieldTapped, RCTBubblingEventBlock)` +- Two `RCT_EXPORT_METHOD`s following the `setFormFieldValue` pattern: resolve the view via `[self.bridge.uiManager viewForReactTag:reactTag]` on the main queue, call the view method, resolve/reject. Error codes used: `viewer_unavailable`, `field_not_found`. + +### 5. iOS — New Architecture + +**`ios/Turbo/NutrientViewTurboModule.mm`** — implement both spec methods; resolve the view with `[[NutrientViewRegistry shared] viewForId:reference]` on the main queue and call the same `RCTPSPDFKitView` methods. + +**`ios/Fabric/NutrientView.mm`** +- In `updateProps:`, forward the prop: `_view.interceptSignatureFields = newProps->interceptSignatureFields;` +- Implement the new delegate callback, firing the codegen emitter: + +```objc +- (void)pspdfView:(RCTPSPDFKitView *)view didTapSignatureFieldWithFullyQualifiedName:(NSString *)fullyQualifiedName pageIndex:(NSInteger)pageIndex { + if (!_eventEmitter) { return; } + facebook::react::NutrientViewEventEmitter::OnSignatureFieldTapped payload{ + fullyQualifiedName ? std::string([fullyQualifiedName UTF8String]) : std::string(""), + (int)pageIndex + }; + _eventEmitter->onSignatureFieldTapped(payload); +} +``` + +### 6. Android — shared `PdfView` (both architectures) + +**`android/src/main/java/com/pspdfkit/views/PdfView.java`** +- Field + accessors: `private boolean interceptSignatureFields = false;` with `setInterceptSignatureFields` / `isInterceptSignatureFields`. +- Extend the `PdfViewDelegate` interface with `void onSignatureFieldTapped(String fullyQualifiedName, int pageIndex);` (every Fabric manager implementing the interface must add an override — see step 8). +- In `preparePdfFragment`, register the click listener alongside the existing form listeners: `pdfFragment.addOnFormElementClickedListener(pdfViewDocumentListener);` +- `setFormFieldReadOnly(String, boolean)` returns `Single`: iterate `document.getFormProvider().getFormElements()`, match `getFullyQualifiedName()`, copy each widget's `EnumSet` and add/remove `LOCKEDCONTENTS`. Return whether any widget matched; throw when no document is loaded. +- `dismissSignaturePad()` collects candidate `FragmentManager`s (the injected activity manager, the `PdfUiFragment`'s child manager, and — only when `isAdded()` — the `PdfFragment`'s parent and child managers), and for each that hosts a fragment tagged `ElectronicSignatureFragment.FRAGMENT_TAG`, calls `ElectronicSignatureFragment.dismiss(manager)`. Returns whether anything was dismissed. The `isAdded()` guards matter: `getParentFragmentManager()` throws on detached fragments, which would turn a benign "nothing open" call into a rejection. + +**`android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java`** +- Add `FormManager.OnFormElementClickedListener` to the implemented interfaces (its default `isFormElementClickable` returns `true`, so registration alone does not change behavior). +- Implement the click handler with the dual Fabric/Paper emission path used by every other callback in this class: + +```java +@Override +public boolean onFormElementClicked(@NonNull FormElement formElement) { + if (!parent.isInterceptSignatureFields() || formElement.getType() != FormType.SIGNATURE) { + return false; // Not intercepted: Nutrient performs its default handling. + } + String fullyQualifiedName = formElement.getFullyQualifiedName(); + int pageIndex = formElement.getAnnotation().getPageIndex(); + if (isFabricMode && fabricDelegate != null) { + fabricDelegate.onSignatureFieldTapped(fullyQualifiedName, pageIndex); + } else { + dispatchEvent(new PdfViewSignatureFieldTappedEvent(parent.getId(), fullyQualifiedName, pageIndex)); + } + return true; // Consume the click; the native signature UI is not presented. +} +``` + +### 7. Android — Legacy (Paper) + +- **`android/src/main/java/com/pspdfkit/react/events/PdfViewSignatureFieldTappedEvent.java`** (new): an `Event` subclass with `EVENT_NAME = "pdfViewSignatureFieldTapped"` and a `{fullyQualifiedName, pageIndex}` payload. +- **`PdfView.createDefaultEventRegistrationMap()`**: register it under registration name `onSignatureFieldTapped`. +- **`android/src/main/java/com/pspdfkit/react/ReactPdfViewManager.java`**: + - `COMMAND_SET_FORM_FIELD_READ_ONLY = 17` and `COMMAND_DISMISS_SIGNATURE_PAD = 18`, registered in `getCommandsMap()`. + - `receiveCommand` cases following the `setFormFieldValue` pattern: `requestId = args.getInt(0)`, results dispatched back through `PdfViewDataReturnedEvent`, disposables added to `annotationDisposables`. + - `@ReactProp(name = "interceptSignatureFields")` setter delegating to the view. + +### 8. Android — New Architecture + +- **`android/src/newarch/java/io/nutrient/react/events/FabricOnSignatureFieldTappedEvent.kt`** (new): `EVENT_NAME = "topSignatureFieldTapped"` (Codegen's top-level name for a `BubblingEventHandler` named `onSignatureFieldTapped`). +- **`android/src/newarch/java/io/nutrient/react/fabric/ReactPdfViewManagerFabric.kt`**: + - `override fun onSignatureFieldTapped(...)` in the anonymous `PdfViewDelegate`, dispatching the Fabric event. + - `override fun setInterceptSignatureFields(view: PdfView, value: Boolean)` (the codegen interface gains this from the spec change). + - `map["onSignatureFieldTapped"] = mapOf("registrationName" to "onSignatureFieldTapped")` in `getExportedCustomBubblingEventTypeConstants()`. +- **`ReactInstantPdfViewManagerFabric.kt`**: add a no-op `onSignatureFieldTapped` override (the Instant view does not support interception). +- **`android/src/newarch/java/io/nutrient/react/turbo/NutrientViewTurboModule.java`**: implement both spec methods; resolve the view via `NutrientViewRegistry`, subscribe to the `Single` on `Schedulers.io()` / observe on the main thread for `setFormFieldReadOnly`, and `view.post(...)` for `dismissSignaturePad` (fragment operations need the main thread). + +### 9. Regenerate types + +```bash +npm run build-and-generate-types +``` + +This rebuilds `lib/` from `src/` and regenerates `types/index.d.ts` from the JSDoc in `index.js`. + +--- + +## Building and testing your fork + +Follow [BRIDGING.md](../BRIDGING.md) for the full build-validation loop. Summary: + +```bash +# Catalog app deps. NOTE: use --ignore-scripts with npm — the Catalog's +# postinstall script deletes node_modules through the file:../.. symlink +# that npm creates (the repo's scripts assume yarn). +cd samples/Catalog && npm install --legacy-peer-deps --ignore-scripts + +# Android (compiles main + newarch source sets and runs codegen): +cd android && ./gradlew assembleDebug +# For an emulator on Apple Silicon: ./gradlew assembleDebug -PreactNativeArchitectures=arm64-v8a + +# iOS: +cd ../ios && pod install +xcodebuild -workspace Catalog.xcworkspace -scheme Catalog -configuration Debug -sdk iphonesimulator build +``` + +The Catalog contains a **Signature Interception** example (`samples/Catalog/examples/SignatureInterception.tsx`) exercising all three APIs — use it as a manual test harness. Remember the writable-document caveat above when testing with your own PDFs. + +## Verification + +The following was verified live (Android emulator + iOS simulator, New Architecture, Maestro-driven): + +- Interception ON: tapping the signature field emits `{fullyQualifiedName: 'EMPLOYEE SIGNATURE', pageIndex: 0}` and the native signature UI does not open. +- Interception OFF: the default native "Add Signature" UI opens unchanged. +- `dismissSignaturePad()` resolves `true` and closes the UI when open; resolves `false` when nothing is presented; safe to call repeatedly. +- `setFormFieldReadOnly('Name_Last', true)` blocks the editor for that field only (siblings unaffected); `false` restores editability. On iOS the editable-field highlight visibly disappears while locked. + +Not yet covered (recommended before production): save-and-reopen persistence assertions, fields with multiple widgets, device rotation while the signature UI is open, and both-architecture runs of the same matrix on physical devices. + +## Shipping the customization + +Options, in order of preference for most teams: + +1. **Fork** this repository, keep the feature branch rebased on SDK releases, and build the package from the fork. +2. **`patch-package`**: after validating in your app, run `node node_modules/@nutrient-sdk/react-native/scripts/create-bridge-patch.js` from the app root and commit the generated `patches/` directory. Revalidate the patch on every SDK upgrade — especially the class names called out below. +3. **Upstream**: propose the API to Nutrient; the surface is intentionally SDK-version-agnostic. + +On SDK upgrades, re-verify the version-sensitive touch points: `PSPDFSignatureCreationViewController` / `PSPDFSignatureViewController` (iOS presented-controller type checks), `ElectronicSignatureFragment.FRAGMENT_TAG` (Android), and the `LOCKEDCONTENTS` widget-flag behavior. From 69ad4366ef318b78ba662538ac49e11766f569dc Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Fri, 17 Jul 2026 16:48:52 +0300 Subject: [PATCH 4/4] Recognize saved-signature picker UI in dismissSignaturePad dismissSignaturePad only targeted the signature creation UI, so it worked on the first tap of a signature field but failed on later tries once a saved signature existed (or under the legacy signature flow), when the SDK presents the saved-signature picker instead. Android now scans candidate fragment managers for instances of both ElectronicSignatureFragment and SignaturePickerFragment (instanceof instead of tag lookup, since the picker's tag is private) and dismisses via the matching static helper. iOS now checks the entire presented navigation stack against the creation controller, the legacy signature controller, and PSPDFSignatureSelectorViewController. Verified on the Android emulator: after saving a signature, tapping the field opens the "Signatures" picker and dismissSignaturePad now closes it (previously returned false). Creation-UI cycles re-verified on both platforms; a signed field's annotation context menu is still correctly left alone (resolves false). Co-Authored-By: Claude Fable 5 --- .../main/java/com/pspdfkit/views/PdfView.java | 18 ++++++-- ...orm-readonly-and-signature-interception.md | 6 ++- ios/RCTPSPDFKitView.m | 41 ++++++++++++++----- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/android/src/main/java/com/pspdfkit/views/PdfView.java b/android/src/main/java/com/pspdfkit/views/PdfView.java index 4d43c9dc..b9518597 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfView.java +++ b/android/src/main/java/com/pspdfkit/views/PdfView.java @@ -37,6 +37,7 @@ import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import com.facebook.react.bridge.Arguments; @@ -81,6 +82,7 @@ import com.pspdfkit.forms.FormField; import com.pspdfkit.forms.TextFormElement; import com.pspdfkit.ui.signatures.ElectronicSignatureFragment; +import com.pspdfkit.ui.signatures.SignaturePickerFragment; import com.pspdfkit.listeners.OnVisibilityChangedListener; import com.pspdfkit.listeners.SimpleDocumentListener; import com.pspdfkit.react.PDFDocumentModule; @@ -1403,7 +1405,10 @@ public Single setFormFieldReadOnly(@NonNull String fullyQualifiedName, } /** - * Dismisses the currently presented electronic signature UI, if any. + * Dismisses the currently presented signature UI, if any. Depending on the license and + * whether saved signatures exist, the SDK presents either the electronic signature + * creation UI ({@link ElectronicSignatureFragment}) or the saved-signature picker + * ({@link SignaturePickerFragment}), so both are checked. * * @return {@code true} when a signature UI was found and dismissed, {@code false} otherwise. */ @@ -1422,9 +1427,14 @@ public boolean dismissSignaturePad() { } } for (FragmentManager manager : fragmentManagers) { - if (manager.findFragmentByTag(ElectronicSignatureFragment.FRAGMENT_TAG) != null) { - ElectronicSignatureFragment.dismiss(manager); - dismissed = true; + for (Fragment presentedFragment : manager.getFragments()) { + if (presentedFragment instanceof ElectronicSignatureFragment) { + ElectronicSignatureFragment.dismiss(manager); + dismissed = true; + } else if (presentedFragment instanceof SignaturePickerFragment) { + SignaturePickerFragment.dismiss(manager); + dismissed = true; + } } } return dismissed; diff --git a/documentation/form-readonly-and-signature-interception.md b/documentation/form-readonly-and-signature-interception.md index 229d5fb6..a84f4614 100644 --- a/documentation/form-readonly-and-signature-interception.md +++ b/documentation/form-readonly-and-signature-interception.md @@ -59,7 +59,7 @@ const dismissed = await pdfRef.current?.dismissSignaturePad(); | `persist: true` | Sets `PSPDFFormField.isReadOnly` (PDF form field flag; persists once the document is saved) | Adds `AnnotationFlags.LOCKEDCONTENTS` to every widget annotation of the field, preserving existing flags (persists once saved) | | `persist: false` | Sets `PSPDFFormField.isEditable = false` (session-only; not written to the PDF) | Not supported — behaves like `persist: true` | | Missing field | Promise rejects (`field_not_found` on Paper, `OPERATION_FAILED` on New Architecture) | Promise resolves `false` (legacy) / rejects (New Architecture) | -| `dismissSignaturePad()` targeting | Type-checks the presented controller against `PSPDFSignatureCreationViewController` (Electronic Signatures) and legacy `PSPDFSignatureViewController`; never dismisses unrelated modals | Locates `ElectronicSignatureFragment` by its public `FRAGMENT_TAG` across candidate fragment managers; never dismisses unrelated dialogs | +| `dismissSignaturePad()` targeting | Type-checks the presented controller (including a wrapping navigation stack) against `PSPDFSignatureCreationViewController` (Electronic Signatures), legacy `PSPDFSignatureViewController`, and the saved-signature `PSPDFSignatureSelectorViewController`; never dismisses unrelated modals | Scans candidate fragment managers for `ElectronicSignatureFragment` (creation UI) *and* `SignaturePickerFragment` (saved-signature picker / legacy flow) instances and dismisses via the matching static helper; never dismisses unrelated dialogs | | Signature tap interception | `PSPDFViewControllerDelegate didTapOnAnnotation:` — consumes taps on `PSPDFSignatureFormElement` | `FormManager.OnFormElementClickedListener` — consumes clicks on `FormType.SIGNATURE` elements | **Why `LOCKEDCONTENTS` on Android:** the Android SDK's `FormField` exposes `isReadOnly()` as a getter only, and `AnnotationFlags.READONLY` is documented as ignored for widget annotations. Updating the widget's `LOCKEDCONTENTS` flag is the supported way to make an existing widget non-editable. Always copy the existing flag set and add/remove only `LOCKEDCONTENTS`. @@ -71,6 +71,7 @@ const dismissed = await pdfRef.current?.dismissSignaturePad(); - **Not a security boundary.** A PDF read-only flag is a UI/document constraint. Server-side validation must still verify which fields the user may change and whether a signature is authorized. - **Toolbar-initiated signatures are out of scope.** Interception covers signature *form field* taps. A signature started from the annotation toolbar is not intercepted; remove or replace the toolbar item if that flow must also be suppressed. - **The signature UI is modal.** While it is presented, React Native UI cannot be tapped. To exercise `dismissSignaturePad()` while the UI is open, call it from a timer/event rather than a button press. +- **The signature UI is not always the creation pad.** Once a signature has been saved to the signature store (or under the legacy signature flow), tapping a signature field presents the saved-signature *picker* instead of the creation UI — `dismissSignaturePad()` recognizes both (see the targeting row above). And once a field is *signed*, tapping it selects the signature annotation (context menu) rather than opening any signature UI, so `dismissSignaturePad()` correctly resolves `false` in that state. --- @@ -266,7 +267,8 @@ The following was verified live (Android emulator + iOS simulator, New Architect - Interception ON: tapping the signature field emits `{fullyQualifiedName: 'EMPLOYEE SIGNATURE', pageIndex: 0}` and the native signature UI does not open. - Interception OFF: the default native "Add Signature" UI opens unchanged. -- `dismissSignaturePad()` resolves `true` and closes the UI when open; resolves `false` when nothing is presented; safe to call repeatedly. +- `dismissSignaturePad()` resolves `true` and closes the UI when open; resolves `false` when nothing is presented; safe to call repeatedly across multiple open/close cycles. +- With a saved signature in the store, tapping the field opens the saved-signature picker ("Signatures" list on Android) — `dismissSignaturePad()` dismisses it too (`true`). - `setFormFieldReadOnly('Name_Last', true)` blocks the editor for that field only (siblings unaffected); `false` restores editability. On iOS the editable-field highlight visibly disappears while locked. Not yet covered (recommended before production): save-and-reopen persistence assertions, fields with multiple widgets, device rotation while the signature UI is open, and both-architecture runs of the same matrix on physical devices. diff --git a/ios/RCTPSPDFKitView.m b/ios/RCTPSPDFKitView.m index 7c35d755..61d297b5 100644 --- a/ios/RCTPSPDFKitView.m +++ b/ios/RCTPSPDFKitView.m @@ -810,20 +810,41 @@ - (BOOL)dismissSignaturePad { return NO; } - UIViewController *visibleController = presented; + // Depending on the license and whether saved signatures exist, the SDK presents the + // Electronic Signatures creation controller, the legacy Annotations signature + // controller, or the saved-signature selector — possibly wrapped in a navigation + // controller. Collect every candidate controller so all variants are recognized. + // NSClassFromString is used to avoid a compile-time dependency on either flow. + NSMutableArray *candidateControllers = [NSMutableArray array]; if ([presented isKindOfClass:UINavigationController.class]) { UINavigationController *navigationController = (UINavigationController *)presented; - visibleController = navigationController.visibleViewController ?: navigationController; + [candidateControllers addObjectsFromArray:navigationController.viewControllers]; + if (navigationController.visibleViewController != nil) { + [candidateControllers addObject:navigationController.visibleViewController]; + } + } else { + [candidateControllers addObject:presented]; } - // Only dismiss Nutrient's signature UI. NSClassFromString is used so both the - // Electronic Signatures controller and the legacy Annotations signature controller - // are recognized without a compile-time dependency on either. - Class signatureCreationControllerClass = NSClassFromString(@"PSPDFSignatureCreationViewController"); - Class legacySignatureControllerClass = NSClassFromString(@"PSPDFSignatureViewController"); - BOOL isSignatureController = - (signatureCreationControllerClass != Nil && [visibleController isKindOfClass:signatureCreationControllerClass]) || - (legacySignatureControllerClass != Nil && [visibleController isKindOfClass:legacySignatureControllerClass]); + NSArray *signatureControllerClassNames = @[ + @"PSPDFSignatureCreationViewController", + @"PSPDFSignatureViewController", + @"PSPDFSignatureSelectorViewController", + ]; + + BOOL isSignatureController = NO; + for (UIViewController *candidate in candidateControllers) { + for (NSString *className in signatureControllerClassNames) { + Class controllerClass = NSClassFromString(className); + if (controllerClass != Nil && [candidate isKindOfClass:controllerClass]) { + isSignatureController = YES; + break; + } + } + if (isSignatureController) { + break; + } + } if (!isSignatureController) { return NO; }