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..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; @@ -73,11 +74,15 @@ 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.ui.signatures.SignaturePickerFragment; import com.pspdfkit.listeners.OnVisibilityChangedListener; import com.pspdfkit.listeners.SimpleDocumentListener; import com.pspdfkit.react.PDFDocumentModule; @@ -95,6 +100,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 +186,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 +308,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 +411,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 +981,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 +1361,85 @@ 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 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. + */ + 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) { + 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; + } + public JSONObject convertConfiguration() { try { JSONObject config = new JSONObject(); @@ -1442,6 +1540,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/documentation/form-readonly-and-signature-interception.md b/documentation/form-readonly-and-signature-interception.md new file mode 100644 index 00000000..a84f4614 --- /dev/null +++ b/documentation/form-readonly-and-signature-interception.md @@ -0,0 +1,284 @@ +# 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 (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`. + +### 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. +- **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. + +--- + +## 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 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. + +## 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. 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..61d297b5 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,83 @@ - (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; + } + + // 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; + [candidateControllers addObjectsFromArray:navigationController.viewControllers]; + if (navigationController.visibleViewController != nil) { + [candidateControllers addObject:navigationController.visibleViewController]; + } + } else { + [candidateControllers addObject:presented]; + } + + 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; + } + + [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/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, + }, +}; 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