Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions android/src/main/java/com/pspdfkit/react/ReactPdfViewManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public class ReactPdfViewManager extends ViewGroupManager<PdfView> {
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();

Expand Down Expand Up @@ -132,6 +134,8 @@ public Map<String, Integer> 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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PdfViewSignatureFieldTappedEvent> {

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);
}
}
99 changes: 99 additions & 0 deletions android/src/main/java/com/pspdfkit/views/PdfView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1342,6 +1361,85 @@ public Maybe<Boolean> 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<Boolean> 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<AnnotationFlags> currentFlags = widget.getFlags();
EnumSet<AnnotationFlags> 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<FragmentManager> 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();
Expand Down Expand Up @@ -1442,6 +1540,7 @@ public static Map<String, Map<String, String>> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FabricOnSignatureFieldTappedEvent>(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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ class ReactInstantPdfViewManagerFabric : ViewGroupManager<InstantPdfView>(), 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 {
Expand Down
Loading