AI assistants: When adding or changing any native API exposed to TypeScript/JS, you MUST follow this entire document step by step. Do not summarize or skip steps. Use the checklist at the end before submitting.
This document is the single source of truth for exposing a native API to the TypeScript/JavaScript layer in this React Native library. The library supports both Legacy (Paper) and New Architecture (Fabric + TurboModules). Any new or changed native API MUST follow this recipe exactly so both architectures keep working.
If you are an AI assistant: You MUST follow every step below. Do not skip steps. Do not use only Legacy or only New Architecture. Do not invent a different pattern.
- Dual path: Every public API that talks to native code MUST branch on
isNewArchitectureEnabled(): New Architecture uses the TurboModule (or Fabric ref), Legacy usesNativeModules/UIManager/requireNativeComponent. - Spec-first for New Architecture: New Architecture APIs MUST be declared in a spec under
src/specs/and implemented in native (Androidnewarch, iOSTurbo/orFabric/). - One public surface: The public API (e.g.
index.jsor a class method) MUST be the only place callers use; internally it delegates to either the TurboModule/Fabric path or the Legacy path.
If the user has not already provided answers to the questions below, you MUST ask them and wait for their answers before proceeding to Step 1. Do not assume or guess answers. Do not write any code until you have the answers.
Ask the user:
- API / feature: What exactly should be bridged? (e.g. "a new TurboModule method getDocumentMetadata that returns title and page count")
- Platform: iOS only, Android only, or both?
- Module type: App-level TurboModule (no view ref), View-backed TurboModule (operates on a specific view), or Fabric component (props/events)?
- Method name(s) and signatures: Exact names and TypeScript signatures (e.g.
getDocumentMetadata(): Promise<{ title: string, pageCount: number }>). - Links to iOS/Android APIs or guides: URLs to the native API docs or guides that describe the APIs being bridged (e.g. iOS Swift/Obj-C reference, Android Kotlin/Java reference, or SDK guides). Use these to understand the native APIs when implementing.
- Extra context: Is the native API already implemented elsewhere? Different name on one platform? Any constraints?
Do not proceed to Step 1 until the user has answered. If the user's initial message already contains these answers (e.g. they used the prompt template and filled it in), you may skip asking and go to Step 1.
When you add or change methods on the PDFDocument TypeScript class (src/document/PDFDocument.ts), you MUST:
- Prefer the document-native modules over view modules:
- iOS: Call into the existing
PDFDocumentManagernative module (ios/PDFDocumentManager.swift+PDFDocumentManager.m) whenever possible, instead of adding new APIs toNutrientViewTurboModule/ view managers for document-centric behavior. - Android: Mirror the behavior via the existing document module on Android (
android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt), again avoiding view modules for operations that conceptually belong to the document.
- iOS: Call into the existing
- Pattern:
- JS/TS (
PDFDocument.ts) should invokeNativeModules.PDFDocumentManager.*/NativeModules.PDFDocumentModule.*style methods for document operations (page metadata, rotation, annotations, bookmarks, etc.). - Only use
NutrientView/view-backed TurboModules when the API truly acts on the view (UI state, toolbar, selection, visibility), not on the document model itself.
- JS/TS (
- Rationale: This keeps all document logic consolidated in the document managers on each platform, preserves parity between iOS (
PDFDocumentManager) and Android (PDFDocumentModule), and prevents leaking view-specific details into thePDFDocumentabstraction. - Note (iOS reload behavior):
PDFDocumentManager.swiftcan trigger a reload of the activePDFViewControllervia its delegate hookdelegate?.reloadControllerData?(). When you add document-centric behavior that changes what’s shown (like page rotation), prefer using this delegate path fromPDFDocumentManagerinstead of reaching for the view controller directly.
- TurboModule (no view ref): App-level APIs (e.g. license key,
present(),dismiss()).
Spec:src/specs/NativeNutrientModule.ts
Native: Androidsrc/newarch/java/, iOSios/Turbo/NutrientTurboModule.mm(or equivalent). - View-backed TurboModule (needs a view ref): APIs that operate on a specific
NutrientViewinstance (e.g.setPageIndex,saveCurrentDocument).
Spec:src/specs/NativeNutrientViewTurboModule.ts
Native: Androidsrc/newarch/java/, iOSios/Turbo/NutrientViewTurboModule.mm. - Fabric component: Rendering and props/events for the view.
Spec:src/specs/NutrientViewNativeComponent.ts
Native: Android Fabric component, iOSios/Fabric/.
You MUST implement both:
- The Legacy path:
NativeModules.<ModuleName>(and where applicableUIManager.dispatchViewManagerCommand/findNodeHandle). - The New Architecture path: TurboModule or Fabric component, as above.
-
File:
- App-level module:
pspdfkit-react-native/src/specs/NativeNutrientModule.ts - View-backed module:
pspdfkit-react-native/src/specs/NativeNutrientViewTurboModule.ts
- App-level module:
-
In the spec file:
- Add or update the
Specinterface (extendsTurboModule). - Use exact method names and types that match what the native side will implement.
- For events, use
EventEmitter<Payload>fromreact-native/Libraries/Types/CodegenTypes. - Export the spec and use
TurboModuleRegistry.getEnforcing<Spec>('NativeModuleName')(the string MUST match the name registered on native).
- Add or update the
-
Do NOT:
- Call
TurboModuleRegistry.getEnforcingfrom the public API layer; the public API must branch onisNewArchitectureEnabled()and then call into the TurboModule or LegacyNativeModules. - Add a new TurboModule without adding the same API to the Legacy path (or vice versa).
- Call
-
Android
- Implement the method in the appropriate TurboModule/ViewManager under
android/src/newarch/java/. - Ensure the module/view is registered so the name passed to
TurboModuleRegistry.getEnforcingresolves.
- Implement the method in the appropriate TurboModule/ViewManager under
-
iOS
- Implement the method in
ios/Turbo/(e.g.NutrientTurboModule.mmorNutrientViewTurboModule.mm). - For view-backed APIs, use the view registry (e.g.
NutrientViewRegistry) to resolve the view by identifier when required.
- Implement the method in
-
MUST: Keep method signatures (names, argument types, return types) in sync with the TypeScript spec and with the Legacy implementation behavior.
-
Android
- Implement or update the method in the appropriate module under
android/src/main/java/(Legacy). - Ensure it is exposed via the same module name used by
NativeModules.<ModuleName>on the JS side.
- Implement or update the method in the appropriate module under
-
iOS
- Implement or update the method in the appropriate Legacy module (e.g.
RCTPSPDFKitViewManager,RCTNutrientModule). - Ensure the module is registered so
NativeModules.<ModuleName>resolves. - Keep headers and implementations in sync: any selector you call from a view manager macro (e.g.
RCT_CUSTOM_VIEW_PROPERTY) or from Fabric must be declared in the corresponding*.h(e.g.RCTPSPDFKitView.h) and implemented in*.m. Missing header declarations will surface as “No visible @interface…” compile errors in the app.
- Implement or update the method in the appropriate Legacy module (e.g.
-
Where:
- For
NutrientViewref methods:pspdfkit-react-native/index.js(the class that wraps the view and its ref). - For app-level APIs:
pspdfkit-react-native/index.jsor the relevant export (e.g.Nutrientsingleton).
- For
-
Pattern (you MUST follow this structure):
methodName = function (arg1, arg2) { const { isNewArchitectureEnabled } = require('./lib/ArchitectureDetector'); if (isNewArchitectureEnabled()) { // New Architecture: TurboModule or Fabric ref return this._fabricRef.current?.methodName(arg1, arg2); // OR for app-level: return NativeNutrientModule.methodName(arg1, arg2); } // Legacy: NativeModules / UIManager if (Platform.OS === 'android') { // Android Legacy implementation return NativeModules.ModuleName.methodName(/* ... */); } else if (Platform.OS === 'ios') { return NativeModules.ModuleName.methodName(/* ... */); } };
-
Do NOT:
- Expose only the Legacy path or only the New Architecture path.
- Use the TurboModule directly in the public API without branching; the public API MUST branch first, then call either TurboModule or
NativeModules.
- MUST use the existing helper:
isNewArchitectureEnabled()from./lib/ArchitectureDetector(inindex.js) or from../ArchitectureDetector(insrc/). - Do NOT replace this with a different check (e.g.
__turboModuleProxyonly) or hardcode one architecture.
- If the public API is documented in
index.js, runnpm run generate-typessotypes/index.d.tsstays in sync. - Export any new types or constants from the appropriate entry (e.g.
index.jsorsrc/) so they are part of the public API.
Use this list to verify the recipe was followed. Every item MUST be true.
- The new or changed API is available on both Legacy and New Architecture.
- New Architecture path: TypeScript spec updated under
src/specs/and native implementation updated under Androidnewarchand iOSTurbo/(orFabric/for view props/events). - Legacy path: Native implementation updated under Android
src/main/java/and iOS Legacy modules. - Public API (e.g.
index.js) branches onisNewArchitectureEnabled()and then calls either the TurboModule/Fabric path or the Legacy path. - No direct use of
TurboModuleRegistry.getEnforcingin the public API layer; it is only used inside spec modules or Fabric wrappers. -
ArchitectureDetectoris used for the check; no ad-hoc or different detection. - Types/docs:
generate-types(and any docs step) has been run if the public surface changed. - For iOS, any new selectors used from
RCTPSPDFKitViewManager(RCT_CUSTOM_VIEW_PROPERTY, exported methods) or Fabric (NutrientView.mm) are declared inRCTPSPDFKitView.hand implemented inRCTPSPDFKitView.m, and these changes are mirrored into the app’snode_modules/@nutrient-sdk/react-native/ioscopy. - Copy native files into node_modules: After editing native code, you have copied every modified iOS file into
samples/Catalog/node_modules/@nutrient-sdk/react-native/ios/and every modified Android file intosamples/Catalog/node_modules/@nutrient-sdk/react-native/android/. Without this, the Catalog app will not see your changes. Usecpor equivalent; do not skip this step. - Native builds run and pass: You have run the iOS build (xcodebuild) and the Android build (gradlew assembleDebug) yourself via the Shell tool, read the output, fixed any errors, and re-run until both succeed. You do not consider the bridge complete until both builds pass.
After implementing native changes in the SDK root (pspdfkit-react-native/ios/ and/or android/), you MUST copy those changed files into the Catalog app’s copy of the SDK so the build uses your code:
- iOS: Copy each modified file under
pspdfkit-react-native/ios/(e.g.RCTPSPDFKitView.m,RCTPSPDFKitViewManager.m,Turbo/NutrientViewTurboModule.mm, etc.) topspdfkit-react-native/samples/Catalog/node_modules/@nutrient-sdk/react-native/ios/, preserving path (e.g.ios/Turbo/NutrientViewTurboModule.mm→node_modules/.../ios/Turbo/NutrientViewTurboModule.mm). - Android: Copy each modified file under
pspdfkit-react-native/android/topspdfkit-react-native/samples/Catalog/node_modules/@nutrient-sdk/react-native/android/, preserving path.
Use cp (or a script) from the Shell tool. npm run dev-build does not copy native sources; it only syncs JS/TS. The Catalog build reads from node_modules, so this copy is required.
After the checklist passes and after you have copied native edits into samples/Catalog/node_modules/@nutrient-sdk/react-native/, you MUST run the Catalog builds yourself to verify compilation:
- Run the builds using the Shell tool. Request
required_permissions: ["all"]and a long timeout (e.g. 300000–600000 ms). The user may need to approve the command; do not only provide the commands for the user to copy-paste—you must execute them and read the output. - iOS:
cd pspdfkit-react-native/samples/Catalog/ios && pod install && xcodebuild -workspace Catalog.xcworkspace -scheme Catalog -configuration Debug -sdk iphonesimulator build - Android:
cd pspdfkit-react-native/samples/Catalog/android && ./gradlew assembleDebug - If a build fails, read the error output, fix the issue in the SDK root and in
node_modules/@nutrient-sdk/react-native(keep them in sync), then re-run the failed build until it succeeds. Do not consider the bridge work complete until both iOS and Android builds succeed.
Wrong: Only implementing the Legacy path.
// WRONG: No New Architecture branch
saveDocument = function () {
return NativeModules.PSPDFKitViewManager.saveCurrentDocument(findNodeHandle(this._componentRef.current));
};Correct: Both paths.
// CORRECT: Branch, then Legacy or TurboModule/Fabric
saveCurrentDocument = function () {
const { isNewArchitectureEnabled } = require('./lib/ArchitectureDetector');
if (isNewArchitectureEnabled()) {
return this._fabricRef.current?.saveCurrentDocument();
}
if (Platform.OS === 'ios') {
return NativeModules.PSPDFKitViewManager.saveCurrentDocument(findNodeHandle(this._componentRef.current));
}
// ... Android Legacy
};Wrong: Calling the TurboModule directly from the public API without branching.
// WRONG: Public API must not assume New Architecture only
import NativeNutrientViewTurboModule from './src/specs/NativeNutrientViewTurboModule';
// ...
return NativeNutrientViewTurboModule.setPageIndex(pageIndex, animated);Correct: Public API branches, then delegates to TurboModule or Legacy.
// CORRECT: index.js or view wrapper branches first
const { isNewArchitectureEnabled } = require('./lib/ArchitectureDetector');
if (isNewArchitectureEnabled()) {
return this._fabricRef.current?.setPageIndex(pageIndex, animated);
}
return NativeModules.PSPDFKitViewManager.setPageIndex(pageIndex, animated, findNodeHandle(this._componentRef.current));- When you need exact native API signatures or behavior and the user hasn’t provided them:
- For iOS, start with the LLMS guide index:
https://www.nutrient.io/guides/ios/llms.txt - For Android, start with the LLMS guide index:
https://www.nutrient.io/guides/android/llms.txt
- For iOS, start with the LLMS guide index:
- Use these LLMS documents to discover the relevant API reference pages and feature guides before guessing any native method names, selectors, parameters, or semantics.
- When reading these LLMS reference files, do not stop at the first plausible API you find. Instead:
- Continue scanning for all APIs and guides related to the feature you’re implementing
- Only after you’ve seen the full set of relevant options, choose the API whose semantics best match the user’s requirements.
- When you need the precise method signatures and selectors, consult the official API references:
- iOS Objective‑C API reference:
https://www.nutrient.io/api/ios/documentation/overview?language=objc - iOS Swift API reference:
https://www.nutrient.io/api/ios/documentation/overview - Android Kotlin/Java API reference:
https://www.nutrient.io/api/android/
- iOS Objective‑C API reference:
| Purpose | Location |
|---|---|
| Architecture detection | src/ArchitectureDetector.ts |
| App-level TurboModule spec | src/specs/NativeNutrientModule.ts |
| View TurboModule spec | src/specs/NativeNutrientViewTurboModule.ts |
| Fabric component spec | src/specs/NutrientViewNativeComponent.ts |
| Public API (view + app methods) | index.js |
| Codegen config | package.json → codegenConfig |
| Legacy Android | android/src/main/java/ |
| New Arch Android | android/src/newarch/java/ |
| Legacy iOS | ios/ (e.g. ViewManagers, RCT* modules) |
| New Arch iOS | ios/Turbo/, ios/Fabric/ |
Once the customer has tested the changes and is happy with the result, the AI agent MUST help them create a patch so they can commit it to source control and reliably reapply it to the standard @nutrient-sdk/react-native package (e.g. after npm install).
- Implement changes in the local SDK (e.g. the cloned
pspdfkit-react-native/directory). - For testing, the app (e.g. Catalog or the customer's app) must run against the modified code. Do that by either:
- Running
npm run dev-buildfrom the SDK root so the built files are copied into the app'snode_modules/@nutrient-sdk/react-native, or - Applying the same edits under the app's
node_modules/@nutrient-sdk/react-nativeso the app uses the patched code.
- Running
- The customer tests; when they confirm they are happy, proceed to creating the patch.
A script does the same steps every time. Do not have the agent run patch-package steps manually—have it run this script instead.
From the app root (e.g. your app or samples/Catalog):
node node_modules/@nutrient-sdk/react-native/scripts/create-bridge-patch.jsFrom the SDK repo (e.g. after testing with Catalog):
npm run create-bridge-patch -- samples/Catalog
# or
node scripts/create-bridge-patch.js samples/CatalogThe script will:
- Ensure the app has
patch-packagein devDependencies and"postinstall": "patch-package"in scripts (and runnpm installif it added them). - Run
patch-packagefor@nutrient-sdk/react-native, creating or updatingpatches/@nutrient-sdk+react-native+X.Y.Z.patchin the app repo. - Tell the user to commit the
patches/directory.
MUST: Run the script only after the user has confirmed they are happy with the implementation. The agent should run the script only after the user says they want to save the changes for source control.
To avoid the AI inventing its own approach or skipping steps, give the AI this exact prompt and only fill in the bracketed parts. Do not let the AI work from a free-form description of what you want.
Why point the prompt at this .md file? The prompt is designed to point explicitly at this file (and, on Cursor, to include it via @ so it is in context). That way the agent is instructed to follow this recipe and has the full text available. We do not rely on the agent "listening to everything" in a long doc on its own—the prompt says "MUST follow the recipe in … BRIDGING.md" and "Read that file first," so the agent loads and follows this document.
1. Always load the recipe first (Cursor: use @ to include the file in context):
- Cursor: Start your message with
@pspdfkit-react-native/BRIDGING.mdso the recipe is in context. - Other tools: Paste the instruction below and ensure the AI has access to this repo; tell it to read
pspdfkit-react-native/BRIDGING.mdbefore doing anything.
2. Copy this prompt and fill in every [ ]:
You MUST follow the recipe in pspdfkit-react-native/BRIDGING.md step by step. Do not skip steps or use a different pattern. Read that file first. The user has already provided the requirements below; do not ask again—proceed to Step 1 of the recipe.
Requirements:
- **API / feature:** [e.g. "a new TurboModule method getDocumentMetadata that returns title and page count"]
- **Platform:** [iOS only / Android only / both]
- **Module:** [App-level TurboModule (NativeNutrientModule) / View-backed TurboModule (NativeNutrientViewTurboModule) / Fabric component]
- **Method name(s) and signatures:** [e.g. getDocumentMetadata(): Promise<{ title: string, pageCount: number }>]
- **Links to iOS/Android APIs or guides:** [e.g. "https://developer.apple.com/documentation/...", "https://developer.android.com/reference/...", or SDK guide URLs—use these to understand the native APIs when implementing]
- **Extra context:** [e.g. "already implemented in iOS in MyCustomModule.mm" or "none"]
After implementing:
1. Use the "Checklist before submitting" in BRIDGING.md and confirm each item.
2. Ensure changes exist both in the local SDK and (for testing) in the app's node_modules/@nutrient-sdk/react-native (e.g. via npm run dev-build from the SDK, or by applying the same edits under node_modules).
3. After the user confirms they are happy with the result, run the create-bridge-patch script so they can commit the patch. From the app root run: `node node_modules/@nutrient-sdk/react-native/scripts/create-bridge-patch.js` (or from SDK repo: `node scripts/create-bridge-patch.js samples/Catalog`). Then tell the user to commit the `patches/` directory to source control. Do not run patch-package steps manually—use the script.
Using this prompt (and including BRIDGING.md when possible) makes the AI follow the recipe instead of improvising.
This recipe ensures that customers who fork or clone the repo can add their own native APIs in a consistent way and keep Legacy and New Architecture working. Deviations lead to runtime errors or missing features on one architecture.