Skip to content

feat(rn-protect,rn-davinci): add PingOne Protect React Native bridge (SDKS-5129) - #61

Open
pingidentity-gaurav wants to merge 3 commits into
mainfrom
feat/SDKS-5129-rn-davinci-protect-collector
Open

feat(rn-protect,rn-davinci): add PingOne Protect React Native bridge (SDKS-5129)#61
pingidentity-gaurav wants to merge 3 commits into
mainfrom
feat/SDKS-5129-rn-davinci-protect-collector

Conversation

@pingidentity-gaurav

@pingidentity-gaurav pingidentity-gaurav commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduces @ping-identity/rn-protect — a new React Native package bridging the native PingOne Protect SDK on iOS and Android
  • Adds daVinci.collectProtect() to DaVinciClient for running PROTECT collector data collection inside a DaVinci flow
  • Wires modules.protect.logger support so protect operations get a dedicated scoped logger separate from the top-level DaVinci logger
  • Gates the DaVinci debug panel behind a DAVINCI_SHOW_DEBUG_PANEL env flag (default false)

Changes

@ping-identity/rn-protect (new package)

  • startProtect(config?), pauseBehavioralData(options?), resumeBehavioralData(options?) standalone async functions
  • Dual-arch native bridge (TurboModule + classic) on both iOS (Swift/ObjC++) and Android (Kotlin)
  • Full unit test coverage (JS + Android Robolectric + iOS XCTest)

@ping-identity/rn-davinci

  • collectProtect(options?) on DaVinciClient
  • PROTECT collector mapping and serialization on both platforms
  • ProtectLifecyclePayload.loggerIdmodules.protect.logger resolved to a native logger id at configure time
  • DaVinciHandle.protectLoggerId — protect logger falls back to the DaVinci-level logger when not set

PingSampleApp

  • modules.protect wired into sampleDaVinciConfig with appLogger
  • useDaVinciClientPanelController calls davinciClient.collectProtect() directly - no separate protect client needed

PingTestRunner / CI

  • JS integration tests for @ping-identity/rn-protect and collectProtect on @ping-identity/rn-davinci
  • Integration tests wired into js-unit-tests.yml CI step
  • RNPingProtect-Unit-Tests added to iOS test scheme and test-native-ios.sh
  • rn-protect Robolectric tests added to test-native-android.sh

Test plan

  • Android Robolectric: ./gradlew :ping-identity_rn-protect:testDebugUnitTest :ping-identity_rn-davinci:testDebugUnitTest - 62 tasks, all pass
  • iOS XCTest: RNPingProtect-Unit-Tests and RNPingDavinci-Unit-Tests pass (pre-existing flaky timeout in testDisposeRemovesDaVinciFromRegistry is unrelated)
  • JS unit tests: 208 pass (davinci), 23 pass (protect)
  • Integration tests: 382 pass (yarn test:runner:integration)
  • Sample app builds and launches on both Android emulator and iOS simulator

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added PingOne Protect support for DaVinci flows, including data collection and behavioral-data pause/resume controls.
    • Added configurable Protect lifecycle settings, logging, and error handling on Android and iOS.
    • Integrated Protect collectors into form submission workflows.
    • Improved sample app forms with multiline text support and optional debugging details.
  • Documentation
    • Added Protect package documentation, API guidance, changelog, and licensing information.
  • Tests
    • Expanded JavaScript, Android, and iOS coverage for Protect integration and DaVinci collection.

…(SDKS-5129)

Introduces @ping-identity/rn-protect — a new React Native package that
bridges the native PingOne Protect SDK on iOS and Android, plus wires
a PROTECT collector into the DaVinci flow via daVinci.collectProtect().

Key changes:
- New packages/protect package: startProtect(), pauseBehavioralData(),
  resumeBehavioralData() standalone functions with dual-arch bridge
  (TurboModule + classic) on both platforms
- rn-davinci: collectProtect() on DaVinciClient, PROTECT collector
  mapping and serialization on both platforms, modules.protect logger
  support, ProtectLifecyclePayload.loggerId parsing
- PingSampleApp: modules.protect wired into sampleDaVinciConfig;
  useDaVinciClientPanelController uses davinciClient.collectProtect()
  directly; DaVinci debug panel gated behind DAVINCI_SHOW_DEBUG_PANEL
  env flag (default false)
- JS and native unit tests for all new code; integration tests added
  to PingTestRunner and wired into CI (js-unit-tests.yml)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.06878% with 349 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.20%. Comparing base (2670c57) to head (835a546).

Files with missing lines Patch % Lines
.../com/pingidentity/rnprotect/RNPingProtectCommon.kt 0.00% 124 Missing ⚠️
packages/protect/ios/RNPingProtectCommon.swift 60.00% 76 Missing ⚠️
.../com/pingidentity/rndavinci/RNPingDavinciCommon.kt 0.00% 43 Missing ⚠️
packages/davinci/ios/RNPingDavinciCommon.swift 10.34% 26 Missing ⚠️
...identity/rndavinci/factory/DaVinciClientFactory.kt 0.00% 13 Missing ⚠️
packages/protect/src/NativeRNPingProtect.ts 53.57% 13 Missing ⚠️
.../com/pingidentity/rnprotect/RNPingProtectModule.kt 0.00% 11 Missing ⚠️
...ackages/davinci/ios/Mapper/DaVinciNodeMapper.swift 56.52% 10 Missing ⚠️
packages/protect/ios/RNPingProtectImpl.swift 57.14% 9 Missing ⚠️
...pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt 61.11% 0 Missing and 7 partials ⚠️
... and 9 more

❌ Your patch check has failed because the patch coverage (74.24%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (71.20%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main      #61      +/-   ##
============================================
- Coverage     71.70%   71.20%   -0.51%     
- Complexity      187      216      +29     
============================================
  Files           161      167       +6     
  Lines         18890    19706     +816     
  Branches        674      729      +55     
============================================
+ Hits          13546    14032     +486     
- Misses         5271     5590     +319     
- Partials         73       84      +11     
Flag Coverage Δ
android 17.13% <20.07%> (+0.06%) ⬆️
ios 80.38% <79.11%> (+0.03%) ⬆️
javascript 88.11% <76.92%> (-3.21%) ⬇️
Components Coverage Δ
Bridge glue 50.58% <50.76%> (-0.40%) ⬇️
Pure logic 81.08% <74.24%> (-0.36%) ⬇️
Files with missing lines Coverage Δ
...entity/rndavinci/config/ProtectLifecyclePayload.kt 100.00% <100.00%> (ø)
...kages/davinci/ios/Config/DaVinciConfigParser.swift 99.09% <100.00%> (+0.37%) ⬆️
...ges/davinci/ios/Factory/DaVinciClientFactory.swift 66.95% <100.00%> (+0.28%) ⬆️
.../davinci/ios/Tests/DaVinciClientFactoryTests.swift 98.56% <100.00%> (+0.24%) ⬆️
...s/davinci/ios/Tests/DaVinciConfigParserTests.swift 98.09% <100.00%> (+0.57%) ⬆️
...ges/davinci/ios/Tests/DaVinciNodeMapperTests.swift 99.53% <100.00%> (+0.05%) ⬆️
packages/davinci/src/collectorHelpers.ts 92.80% <ø> (ø)
packages/davinci/src/davinciMethods.ts 82.97% <100.00%> (+1.16%) ⬆️
packages/protect/src/types/protect.types.ts 100.00% <100.00%> (ø)
.../com/pingidentity/rndavinci/RNPingDavinciModule.kt 0.00% <0.00%> (ø)
... and 18 more

... and 5 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2670c57...835a546. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ForgeRock.github.io/ping-react-native-sdk/docs-preview/pr-61/

Built to branch gh-pages at 2026-08-11 14:31 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds @ping-identity/rn-protect and integrates PingOne Protect with DaVinci on Android and iOS. It adds JavaScript APIs, native bridges, collector handling, lifecycle configuration, sample-app support, and cross-platform tests.

Changes

PingOne Protect integration

Layer / File(s) Summary
DaVinci JavaScript API
packages/davinci/src/**
Adds Protect configuration, collector types, collectProtect, native forwarding, error handling, and integration-required collector routing.
Native DaVinci support
packages/davinci/android/**, packages/davinci/ios/**
Adds Protect lifecycle configuration, collector mapping, SDK setup, native bridge methods, and platform-specific error handling.
Protect package
packages/protect/**
Adds the React Native Protect package with public APIs, native Android and iOS implementations, configuration mapping, lifecycle controls, and package tooling.
Sample application
PingSampleApp/**
Configures Protect, handles Protect collectors before submission, adds Protect errors, supports multiline fields, and adds optional node metadata and debug rendering.
Validation and integration
PingTestRunner/**, .github/workflows/js-unit-tests.yml, AGENTS.md
Adds Protect integration tests, native test targets, Jest mappings and mocks, workflow execution, and Markdown licensing guidance.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: rodrigoareis, tsdamas

Sequence Diagram(s)

sequenceDiagram
  participant SampleApp
  participant DaVinciClient
  participant NativeDaVinci
  participant ProtectSDK
  SampleApp->>DaVinciClient: collectProtect(index)
  DaVinciClient->>NativeDaVinci: forward DaVinci ID and options
  NativeDaVinci->>ProtectSDK: collect indexed PROTECT collector
  ProtectSDK-->>NativeDaVinci: resolve or reject collection
  NativeDaVinci-->>DaVinciClient: return collection result
  DaVinciClient-->>SampleApp: continue submission or expose protectError
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the addition of the PingOne Protect React Native bridge and related DaVinci integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/SDKS-5129-rn-davinci-protect-collector

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts (1)

180-193: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Serialize collection and submission.

loading is checked before onProtectCollect, but this code sets no local in-flight guard while collectProtect is awaiting. Rapid taps can start multiple collection loops and then call next multiple times. Add a ref or state guard around the complete collection-plus-submit operation and clear it in finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts` around
lines 180 - 193, Update onSubmit to use a local in-flight ref or state guard
covering both onProtectCollect and next, setting it before starting the
asynchronous operation and returning early when already active. Clear the guard
in a finally handler so it resets after success or failure, and include any new
dependencies required by the hook.
🟡 Minor comments (7)
packages/protect/src/__tests__/index.test.tsx-113-120 (1)

113-120: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Three tests claim to verify ProtectError wrapping but assert only the message text. Each test rejects the native mock with new Error('PROTECT_INITIALIZE_ERROR') and then asserts rejects.toThrow('PROTECT_INITIALIZE_ERROR'). The unwrapped native error satisfies that assertion, so the tests pass even when no wrapping occurs. Assert the type, error, and message fields required by the repository error contract at each site.

  • packages/protect/src/__tests__/index.test.tsx#L113-L120: assert the rejected value from startProtect exposes type, error, and message.
  • packages/protect/src/__tests__/index.test.tsx#L194-L204: assert the rejected value from pauseBehavioralData exposes type, error, and message.
  • packages/protect/src/__tests__/index.test.tsx#L238-L248: assert the rejected value from resumeBehavioralData exposes type, error, and message.

As per coding guidelines: "Use GenericError with type, error, and message".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/src/__tests__/index.test.tsx` around lines 113 - 120,
Strengthen the error-wrapping assertions in the three tests for startProtect,
pauseBehavioralData, and resumeBehavioralData at
packages/protect/src/__tests__/index.test.tsx lines 113-120, 194-204, and
238-248. Capture each rejected value and assert its GenericError contract fields
type, error, and message, rather than checking only the native error message;
all three sites require direct test updates.

Source: Coding guidelines

packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt-790-808 (1)

790-808: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The resolvedFormFieldType nil-result tests use indistinguishable fixtures on both platforms. Each platform declares two tests, "field missing" and "no form present", but both build the same node input. As a result the branch where a form exists and does not contain the collector key is untested on either platform.

  • packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt#L790-L808: the single-argument makeNode helper at lines 45-48 injects {"form": {}}, so line 805 also has a form. Change resolvedFormFieldTypeReturnsNullWhenFieldMissing to build a form whose fields array omits the collector key, and change resolvedFormFieldTypeReturnsNullWhenNoFormPresent to pass an input object without the form key.
  • packages/davinci/ios/Tests/DaVinciNodeMapperTests.swift#L707-L719: both tests pass input: [:]. Change testResolvedFormFieldTypeReturnsNilWhenFieldMissing to supply a form with a fields array that omits the collector key, and keep input: [:] only for the no-form test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt`
around lines 790 - 808, The nil-result tests currently use indistinguishable
fixtures, leaving the existing-form/missing-field branch untested. In
packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt:790-808,
update resolvedFormFieldTypeReturnsNullWhenFieldMissing to provide a form whose
fields omit the collector key, while
resolvedFormFieldTypeReturnsNullWhenNoFormPresent must use input without form;
make the equivalent changes in
packages/davinci/ios/Tests/DaVinciNodeMapperTests.swift:707-719 for
testResolvedFormFieldTypeReturnsNilWhenFieldMissing and keep input: [:] only in
the no-form test.
packages/protect/android/src/test/java/com/pingidentity/rnprotect/RNPingProtectTest.kt-85-108 (1)

85-108: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

These tests pass on a rejection.

Both tests assert promise.await() and assertNull(promise.rejectCode). TestPromise.reject(throwable) at line 321 counts the latch down and sets only rejectThrowable, leaving rejectCode null. A rejection through that overload therefore satisfies both assertions, and the test does not detect the failure.

Assert that the promise resolved.

🐛 Proposed fix
     assertTrue(promise.await())
     assertNull(promise.rejectCode)
+    assertNull(promise.rejectThrowable)
+    assertNull(promise.rejectUserInfo)

Apply the same assertions to resumeBehavioralDataResolvesSuccessfully.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/protect/android/src/test/java/com/pingidentity/rnprotect/RNPingProtectTest.kt`
around lines 85 - 108, Strengthen both pauseBehavioralDataResolvesSuccessfully
and resumeBehavioralDataResolvesSuccessfully to assert that TestPromise was
resolved, not merely completed without a rejectCode. Use the promise’s
resolved-state assertion or equivalent existing TestPromise field, while
retaining the await and rejection checks.
PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts-169-176 (1)

169-176: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear protectError before each collection retry.

A failed collection sets protectError. A later successful collection does not clear it; only onStart clears the value. The controller can therefore expose a stale error after a successful retry. Clear the error before the first collectProtect call.

Suggested change
     try {
+      setProtectError(null);
       for (let index = 0; index < protectFields.length; index++) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts` around
lines 169 - 176, In the collection retry flow containing the protectFields loop,
clear the existing protectError state immediately before the first
collectProtect call. Keep the current catch behavior that records failures and
rethrows, so a successful retry leaves no stale error visible.
packages/protect/ios/RNPingProtectCommon.swift-203-215 (1)

203-215: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a dedicated behavioral-data error code for pause and resume failures. Both native platforms currently emit PROTECT_INITIALIZE_ERROR. Add PROTECT_BEHAVIORAL_DATA_ERROR to the Swift enum, JS ProtectErrorCode, and Android ProtectErrorCodes, then use it for both methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/ios/RNPingProtectCommon.swift` around lines 203 - 215,
Replace the initialization error code used by the pause/resume behavioral-data
failure paths with a new dedicated PROTECT_BEHAVIORAL_DATA_ERROR value. Add the
corresponding value to the Swift error enum, JavaScript ProtectErrorCode, and
Android ProtectErrorCodes, then update both pauseBehavioralData and
resumeBehavioralData handlers to use it while preserving existing error
handling.
packages/protect/CHANGELOG.md-7-7 (1)

7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the release-note API name.

The release note advertises createProtectClient().collectForDaVinci(...). The new public DaVinci API is daVinci.collectProtect(). List the exported Protect lifecycle functions separately if this entry must cover the full initial release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/CHANGELOG.md` at line 7, Update the initial-release entry in
CHANGELOG.md to advertise the public DaVinci API as daVinci.collectProtect()
instead of createProtectClient().collectForDaVinci(...). If documenting the
complete initial release, list the exported Protect lifecycle functions
separately.
packages/davinci/src/types/node.types.ts-360-362 (1)

360-362: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the supported collection API.

createProtectClient().collectForDaVinci(...) does not match the API described by this PR. Document await daVinci.collectProtect() instead. This prevents consumers from implementing a nonexistent integration path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/src/types/node.types.ts` around lines 360 - 362, Update the
DaVinci collection documentation in the nearby node type comments to reference
await daVinci.collectProtect() as the supported API, replacing
createProtectClient().collectForDaVinci(daVinci). Keep the guidance to invoke it
before daVinci.next({}) unchanged.
🧹 Nitpick comments (15)
packages/protect/ios/Tests/RNPingProtectImplTests.swift (2)

81-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report an unexpected resolve as a test failure.

When collectForDaVinci resolves, the helper returns "UNEXPECTED_RESOLVE" in the code position. The calling test then fails on a code mismatch, which hides the real cause. Call XCTFail("Expected rejection, got resolve") in the resolve closure before resuming the continuation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/ios/Tests/RNPingProtectImplTests.swift` around lines 81 -
101, Update invokeCollectForDaVinci’s resolve closure to call XCTFail("Expected
rejection, got resolve") before resuming the continuation, while preserving the
existing continuation return behavior.

70-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the ErrorType enum instead of the raw string "auth_error".

Line 76 compares against a string literal. Lines 43, 49, and 55 use ErrorType.argumentError.rawValue for the same field. If the raw value of the error type changes, this test keeps passing against a stale literal. Use the matching ErrorType case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/ios/Tests/RNPingProtectImplTests.swift` around lines 70 -
77, Update testCollectForDaVinciRejectsWhenCollectFails to compare type using
the matching ErrorType enum case’s rawValue instead of the hard-coded
"auth_error" string, consistent with the other tests.
packages/davinci/android/src/test/java/com/pingidentity/rndavinci/RNPingDavinciCommonTest.kt (1)

529-541: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the out-of-range test distinct from the empty-collector test.

The node is created with actions = emptyList(). Index 5 and index 0 therefore follow the identical branch. This test duplicates collectProtect_rejectsWithStateErrorWhenNoProtectCollector and does not verify index bounds. Populate the node with at least one Protect collector, then request index 5.

Note: this depends on the Protect SDK being available at test runtime. See the comment on lines 543-557.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/davinci/android/src/test/java/com/pingidentity/rndavinci/RNPingDavinciCommonTest.kt`
around lines 529 - 541, Update
collectProtect_rejectsWithStateErrorWhenIndexOutOfRange to create a
DummyContinueNode containing at least one Protect collector before requesting
index 5, so the test exercises index bounds rather than the empty-collector
path. Follow the Protect SDK runtime setup noted near the adjacent test while
preserving the existing rejection assertions.
packages/davinci/ios/Tests/DaVinciClientFactoryTests.swift (1)

50-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the duplicate test with a populated protect payload case.

testBuildWithNullProtectPayloadDoesNotThrow is identical to testBuildSucceedsWithRequiredFieldsOnly at lines 24-48. Both build the same payload with protect: nil and assert the same result.

The Android suite covers both cases. See packages/davinci/android/src/test/java/com/pingidentity/rndavinci/factory/DaVinciClientFactoryTest.kt lines 126-155, which tests a null payload and a populated ProtectLifecyclePayload. Change this test to supply a populated Protect payload so the iOS suite matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/ios/Tests/DaVinciClientFactoryTests.swift` around lines 50 -
74, Replace the duplicate nil-protect case in
testBuildWithNullProtectPayloadDoesNotThrow with a populated
ProtectLifecyclePayload, while keeping the build-and-non-nil assertion. Update
only the protect input so this test covers the populated payload scenario
alongside testBuildSucceedsWithRequiredFieldsOnly.
packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt (1)

131-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Neither config parser suite asserts the parsed Protect logger id. ProtectLifecyclePayload carries a loggerId field, and the PR objective states that Protect logging falls back to the DaVinci logger when no dedicated logger is configured. That fallback depends on the parsed value, which no test covers on either platform.

  • packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt#L131-L182: assert protect.loggerId is null in parseProtectDefaultsAreMapped, and assert the mapped value in parseProtectAllFieldsMapped after adding a loggerId entry to the protect map.
  • packages/davinci/ios/Tests/DaVinciConfigParserTests.swift#L92-L140: assert protect.loggerId is nil in testParseProtectDefaultsWhenEmpty, and assert the mapped value in testParseProtectAllFieldsMapped after adding a loggerId entry to the protect dictionary.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt`
around lines 131 - 182, Update the Protect parser tests in
packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt:131-182
to assert protect.loggerId is null by default and mapped when a loggerId entry
is provided. Apply the same assertions in
packages/davinci/ios/Tests/DaVinciConfigParserTests.swift:92-140, using nil for
the empty configuration and adding loggerId to the populated protect dictionary.
PingSampleApp/ui/davinci/components/molecules/DaVinciTextField.tsx (1)

28-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep multiline mode stable during editing.

isMultiline uses the live text length. This changes TextInput.multiline when the value crosses 60 characters and changes it back when the value falls below 61. Base the mode on collector metadata, or latch it for the current field, instead of changing the native input mode on every edit.

Also applies to: 42-42

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingSampleApp/ui/davinci/components/molecules/DaVinciTextField.tsx` at line
28, Update the isMultiline calculation in DaVinciTextField so
TextInput.multiline is latched for the current field or derived from stable
collector metadata rather than recalculated from live stringValue length on
every edit. Preserve the selected mode while the field is being edited,
including when the text crosses the 60-character threshold in either direction.
packages/davinci/src/davinci.ts (1)

417-427: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the collectProtect outcome at info level.

Every other client method logs its success outcome with logInfo. collectProtect logs success with logDebug. The coding guidelines require outcomes at info.

♻️ Proposed change
     async collectProtect(options?: { index?: number }) {
       const id = await ensureConfigured();
       logDebug('DaVinci collectProtect requested', { davinciId: id });
       try {
         await collectProtectForDaVinci(id, options ?? {});
-        logDebug('DaVinci collectProtect succeeded', { davinciId: id });
+        logInfo('DaVinci collectProtect succeeded', { davinciId: id });
       } catch (error) {

As per coding guidelines: "log entry at debug, outcomes at info, and failures at error".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/src/davinci.ts` around lines 417 - 427, Update the success
log in the collectProtect method to use logInfo instead of logDebug, while
keeping the request entry at debug and failure logging at error.

Source: Coding guidelines

packages/protect/src/index.tsx (1)

14-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use export type * for the type re-exports.

The index re-exports types with a named export type { ... } list. The repository guideline requires export type * for types in a package index.

♻️ Proposed change
-export type {
-  ProtectCollectOptions,
-  ProtectConfig,
-  ProtectErrorCode,
-} from './types';
+export type * from './types';

Confirm that ./types does not re-export internal helpers before you widen the export.

As per coding guidelines: "The package index must contain public re-exports only; use export type * for types, named exports for values".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/src/index.tsx` around lines 14 - 18, Update the package
index type re-export near ProtectCollectOptions, ProtectConfig, and
ProtectErrorCode to use export type * from './types' instead of a named type
list. First confirm ./types exposes only public types and no internal helpers,
preserving the package index’s public API boundary.

Source: Coding guidelines

packages/protect/src/NativeRNPingProtect.ts (2)

70-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the TSDoc block onto getNativeModule.

The doc block on Lines 70-76 describes getNativeModule, but it is attached to the _nativeModule variable declaration. getNativeModule on Line 82 has no doc comment. Move the block directly above getNativeModule.

♻️ Proposed change
+let _nativeModule: Spec | null = null;
+
+/** `@internal` — resets the module cache for testing only. */
+export function _resetNativeModuleForTesting(): void {
+  _nativeModule = null;
+}
+
 /**
  * Resolves the native module by probing TurboModule first, then falling back to the classic bridge module.
  * Result is cached — the native module does not change at runtime.
  *
  * `@returns` Native module implementation for the current architecture.
  * `@throws` Error when no native module is registered.
  */
-let _nativeModule: Spec | null = null;
-/** `@internal` — resets the module cache for testing only. */
-export function _resetNativeModuleForTesting(): void {
-  _nativeModule = null;
-}
 export function getNativeModule(): Spec {

As per coding guidelines: "Use TSDoc on all exported declarations".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/src/NativeRNPingProtect.ts` around lines 70 - 83, Move the
existing TSDoc block from the _nativeModule declaration to directly above the
exported getNativeModule function, leaving the cache variable and
_resetNativeModuleForTesting documentation unchanged.

Source: Coding guidelines


109-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the double casts with typed mapping functions.

toNativeCollectOptions and toNativeConfig use as unknown as Record<string, unknown>. The double cast removes all type checking between the public types and the native payload. toNativeProtectConfig below already builds an explicit payload. Use the same explicit approach, or type the parameters as objects with index-compatible shapes.

This is a suggestion, not a blocker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/src/NativeRNPingProtect.ts` around lines 109 - 122, Replace
the double casts in toNativeCollectOptions and toNativeConfig with explicit
typed mapping functions that construct Record<string, unknown> payloads from the
respective public types. Follow the existing toNativeProtectConfig pattern and
preserve all supported option and config fields without bypassing type checking.
packages/protect/ios/RNPingProtect.mm (1)

33-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the main-thread hop into one helper.

The four exported methods repeat the same isMainThread check and dispatch_async block. RNPingProtectClassic.mm already uses a withSwiftImpl: helper for the identical pattern. Use the same helper here to remove the duplication.

♻️ Proposed refactor
-- (RNPingProtectImpl *)swiftImpl
-{
-  return [RNPingProtectImpl shared];
-}
+- (void)withSwiftImpl:(void (^)(RNPingProtectImpl *impl))block
+{
+  if ([NSThread isMainThread]) {
+    block([RNPingProtectImpl shared]);
+    return;
+  }
+  dispatch_async(dispatch_get_main_queue(), ^{
+    block([RNPingProtectImpl shared]);
+  });
+}
 
 - (void)collectForDaVinci:(NSString *)davinciId
                    options:(NSDictionary *)options
                     config:(NSDictionary *)config
                    resolve:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)rejecter
 {
-  if ([NSThread isMainThread]) {
-    [[self swiftImpl] collectForDaVinci:davinciId options:options config:config resolve:resolve rejecter:rejecter];
-    return;
-  }
-
-  dispatch_async(dispatch_get_main_queue(), ^{
-    [[self swiftImpl] collectForDaVinci:davinciId options:options config:config resolve:resolve rejecter:rejecter];
-  });
+  [self withSwiftImpl:^(RNPingProtectImpl *impl) {
+    [impl collectForDaVinci:davinciId options:options config:config resolve:resolve rejecter:rejecter];
+  }];
 }

Apply the same change to initialize, pauseBehavioralData, and resumeBehavioralData.

As per coding guidelines: "Follow existing package patterns, SOLID, DRY".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/ios/RNPingProtect.mm` around lines 33 - 99, Extract the
repeated main-thread dispatch logic from collectForDaVinci, initialize,
pauseBehavioralData, and resumeBehavioralData into a shared withSwiftImpl:
helper, matching the existing pattern in RNPingProtectClassic.mm. Update all
four methods to invoke the helper while preserving their current Swift
implementation calls and arguments.

Source: Coding guidelines

packages/davinci/src/types/client.types.ts (1)

116-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Complete the public collectProtect API documentation.

  • packages/davinci/src/types/client.types.ts#L116-L137: Add an @returns tag for successful completion.
  • packages/davinci/ios/RNPingDavinciCommon.swift#L419-L484: Add Returns, Throws, and Note sections.
  • packages/davinci/ios/RNPingDavinciImpl.swift#L178-L193: Add Returns, Throws, and Note sections.

As per coding guidelines, “Use /// documentation on all public and internal Swift declarations with Parameters, Returns, Throws, and Note sections,” and TypeScript exports require TSDoc return tags.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/src/types/client.types.ts` around lines 116 - 137, Complete
the public collectProtect documentation: in
packages/davinci/src/types/client.types.ts lines 116-137, add a TSDoc `@returns`
tag describing successful completion; in
packages/davinci/ios/RNPingDavinciCommon.swift lines 419-484 and
packages/davinci/ios/RNPingDavinciImpl.swift lines 178-193, add /// Returns,
Throws, and Note sections for the corresponding declarations, preserving the
documented behavior that collection succeeds with no return value and throws
when Protect is unavailable or collection fails.

Source: Coding guidelines

packages/protect/android/src/main/java/com/pingidentity/rnprotect/RNPingProtectCommon.kt (1)

128-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply the required logger contract.

Use debug logging when an operation starts. Use info logging when an operation succeeds. Log a failure at error level before the bridge rejects it. Resolve the configured logger to a module-level no-op logger when no logger ID is available.

As per coding guidelines, logger entry events use debug, outcomes use info, failures use error, and optional loggers default to a module-level noop logger.

Also applies to: 163-176, 193-198, 214-219

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/protect/android/src/main/java/com/pingidentity/rnprotect/RNPingProtectCommon.kt`
around lines 128 - 144, The collectForDaVinci flow and the additionally
referenced operation blocks must use the configured logger, falling back to the
module-level no-op logger when no logger ID exists. Change operation-start logs
to debug, successful completion logs to info, and collector failures to error
before bridge rejection; preserve the existing rejection behavior and messages.

Source: Coding guidelines

packages/protect/android/src/main/java/com/pingidentity/rnprotect/ProtectErrorCodes.kt (1)

9-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add structured KDoc to the new Kotlin declarations.

The new Kotlin files use prose-only comments. Add the required @param, @return, and @throws sections where applicable.

  • packages/protect/android/src/main/java/com/pingidentity/rnprotect/ProtectErrorCodes.kt#L9-L18: document the public error-code declarations with the required KDoc structure.
  • packages/protect/android/src/main/java/com/pingidentity/rnprotect/RNPingProtectCommon.kt#L30-L67: add structured KDoc for the shared object and internal configuration declarations.
  • packages/protect/android/src/newarch/java/com/pingidentity/rnprotect/RNPingProtectModule.kt#L14-L74: add structured KDoc for the module and bridge methods.
  • packages/protect/android/src/newarch/java/com/pingidentity/rnprotect/RNPingProtectPackage.kt#L16-L50: add structured KDoc for the package and React Native registration methods.

As per coding guidelines, all public and internal Kotlin declarations require KDoc with @param, @return, and @throws.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/protect/android/src/main/java/com/pingidentity/rnprotect/ProtectErrorCodes.kt`
around lines 9 - 18, Apply structured KDoc to all public and internal Kotlin
declarations covered by this comment: ProtectErrorCodes.kt lines 9-18,
RNPingProtectCommon.kt lines 30-67, RNPingProtectModule.kt lines 14-74, and
RNPingProtectPackage.kt lines 16-50. Document each applicable parameter, return
value, and thrown exception with `@param`, `@return`, and `@throws` tags, including
the error-code declarations and bridge/package methods; preserve the existing
APIs and behavior.

Source: Coding guidelines

packages/davinci/android/src/main/java/com/pingidentity/rndavinci/factory/DaVinciClientFactory.kt (1)

81-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the required logger levels for Protect lifecycle events.

  • When ProtectLifecycle is unavailable, log the caught NoClassDefFoundError at error level before skipping the module.
  • Keep the collectProtect request log at debug level and change the successful collection log to info level.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/davinci/android/src/main/java/com/pingidentity/rndavinci/factory/DaVinciClientFactory.kt`
around lines 81 - 83, Update the Protect lifecycle logging in
DaVinciClientFactory.kt at lines 81-83 to log the caught NoClassDefFoundError at
error level before skipping the module. In RNPingDavinciCommon.kt at lines
524-526, keep the collectProtect request log at debug level and change the
successful collection log to info level.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt`:
- Around line 173-174: Remove the unconditional PROTECT exclusion in the
mapper’s field-processing logic. Let PROTECT fields proceed to the existing
registeredKeys.contains(key) check so fields without an instantiated
ProtectCollector are reported through unsupportedFields, while fields with a
native collector continue through the normal mapping path.

In
`@packages/davinci/android/src/main/java/com/pingidentity/rndavinci/RNPingDavinciCommon.kt`:
- Around line 541-551: Update parseCollectorIndex to default to 0 only when
options or the index key is absent; otherwise validate the supplied value as a
non-negative integer. Reject fractional numbers, malformed strings, negative
values, and unsupported types by throwing GenericError with
ErrorType.ARGUMENT_ERROR, rather than coercing them to 0 before collector
selection.

In `@packages/davinci/ios/Models/DaVinciPayloads.swift`:
- Around line 74-75: Update the configuration dictionary construction in
RNPingDavinci.mm to forward modules.protect through the TurboModule bridge,
using the parser field names expected by ProtectLifecyclePayload. Preserve the
existing field-by-field mapping and ensure DaVinciPayloads.protect receives the
mapped configuration instead of remaining nil.

In `@packages/davinci/ios/RNPingDavinciCommon.swift`:
- Around line 492-499: Update parseCollectorIndex and its caller to distinguish
an absent index from invalid input: accept only non-negative integer values,
reject invalid strings and NSNumber values that are not exact integers, and
propagate a GenericError of type argumentError before collector.collect() is
invoked. Preserve the default index of 0 only when the index is absent.

In `@packages/davinci/src/NativeRNPingDavinci.ts`:
- Around line 147-157: Update the exported collectProtect declaration’s TSDoc to
add an `@returns` tag documenting successful void resolution and an `@throws` tag
documenting rejection with DAVINCI_PROTECT_COLLECT_ERROR when the Protect SDK is
unavailable or collection fails.

In `@packages/protect/android/build.gradle`:
- Line 29: Change the PingOne Protect SDK dependency declaration from
compileOnly to implementation so Protect classes used by RNPingProtectCommon.kt
are packaged transitively for consuming apps. Keep the existing SDK version and
Android Gradle plugin configuration unchanged.
- Line 100: Document the required Android Protect SDK dependency in the package
installation instructions, specifying
implementation("com.pingidentity.sdks:protect:2.0.1") for consuming apps. Keep
the compileOnly declaration in the Android build configuration unchanged unless
the installation guidance is intentionally replaced by changing it to
implementation.

In
`@packages/protect/android/src/main/java/com/pingidentity/rnprotect/RNPingProtectCommon.kt`:
- Around line 318-324: Update parseCollectorIndex to accept only finite,
nonnegative integer values from the options map, rejecting nonnumeric strings,
fractional numbers, negative values, and non-finite numbers instead of
defaulting or truncating. For invalid input, throw GenericError with type
ARGUMENT_ERROR before resolving the native collector; preserve the existing
default only when the index is absent or explicitly null.

In
`@packages/protect/android/src/test/java/com/pingidentity/rnprotect/RNPingProtectTest.kt`:
- Around line 238-256: Move the public top-level ShadowProtectArguments object
and its createMap/createArray implementations from RNPingProtectTest.kt into a
same-package file named ShadowProtectArguments.kt. Keep the object public so
Robolectric can resolve it reflectively, and remove the duplicate declaration
from RNPingProtectTest.kt.

In `@packages/protect/ios/RNPingProtectCommon.swift`:
- Around line 78-84: Update the Task isolation handling around the bridge
methods so synchronous SDK work, including Protect.initialize(), Protect.data(),
Protect.pauseBehavioralData(), and Protect.resumeBehavioralData(), does not
execute on `@MainActor`. Move the blocking collection and initialization logic
into a nonisolated async helper, and update the Task closures to invoke that
helper while preserving PromiseBridge resolution and rejection behavior.

In `@packages/protect/ios/Tests/RNPingProtectCommonTests.swift`:
- Around line 88-116: Update testPauseBehavioralDataRejectsWhenSDKNotInitialized
and testResumeBehavioralDataRejectsWhenSDKNotInitialized to be async tests, and
replace blocking wait(for:timeout:) calls with await fulfillment(of:timeout:).
Preserve the existing rejection assertions and expectation behavior.

In `@packages/protect/src/protect.ts`:
- Around line 39-52: Add `@returns` documentation stating that the Promise
resolves to void on each public lifecycle API: startProtect,
pauseBehavioralData, and resumeBehavioralData. Update the TSDoc for each
exported declaration while preserving its existing parameter, throws, example,
and visibility tags.
- Around line 23-35: Update withLogging so the operation entry log uses
logger.debug instead of logger.info, and the successful outcome log uses
logger.info instead of logger.debug. Keep the existing failure logging and error
propagation unchanged.

In `@packages/protect/src/types/protect.types.ts`:
- Around line 114-123: Document the exported ProtectError constructor and static
from method with TSDoc. Include parameter descriptions for message, code, type,
optional status, and error, plus the from method’s returned ProtectError using
the applicable required tags; keep the implementation unchanged.

In `@packages/protect/turbo.json`:
- Around line 11-12: Update packages/protect/turbo.json at lines 11-12 and 32-33
to replace the non-recursive src/*.ts and src/*.tsx inputs in both platform task
hashes with the same recursive source pattern, ensuring nested files such as
src/types/protect.types.ts invalidate build:android and build:ios caches.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts`:
- Around line 188-192: Update the catch following onProtectCollect in the
submission flow to accept the caught error and log it at error level through the
configured logger, preserving the hook’s existing error state. Ensure failures
from both onProtectCollect and next are no longer silently consumed.
- Around line 188-192: Ensure every submission path in the controller, including
onFlowAction-triggered auto-submits, runs onProtectCollect before calling
next(plan.input). Centralize this behavior in a shared Protect-aware submission
helper and use it from both onSubmit and onFlowAction, preserving the hook’s
existing error update behavior.
- Around line 160-164: Update onProtectCollect in the missing davinciClient path
to fail closed: set protectError and reject with a GenericError containing type,
error, and message instead of returning successfully. Ensure onSubmit cannot
call next when Protect collection is unavailable, and preserve the requirement
that every caught error is logged or rethrown.

In `@PingTestRunner/jest.setup.js`:
- Around line 286-293: Add the missing toNativeProtectConfig mock export to the
NativeRNPingProtect shared mock, matching the existing pass-through behavior of
toNativeConfig, so startProtect() can call it before initialize without a
TypeError.

---

Outside diff comments:
In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts`:
- Around line 180-193: Update onSubmit to use a local in-flight ref or state
guard covering both onProtectCollect and next, setting it before starting the
asynchronous operation and returning early when already active. Clear the guard
in a finally handler so it resets after success or failure, and include any new
dependencies required by the hook.

---

Minor comments:
In
`@packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt`:
- Around line 790-808: The nil-result tests currently use indistinguishable
fixtures, leaving the existing-form/missing-field branch untested. In
packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt:790-808,
update resolvedFormFieldTypeReturnsNullWhenFieldMissing to provide a form whose
fields omit the collector key, while
resolvedFormFieldTypeReturnsNullWhenNoFormPresent must use input without form;
make the equivalent changes in
packages/davinci/ios/Tests/DaVinciNodeMapperTests.swift:707-719 for
testResolvedFormFieldTypeReturnsNilWhenFieldMissing and keep input: [:] only in
the no-form test.

In `@packages/davinci/src/types/node.types.ts`:
- Around line 360-362: Update the DaVinci collection documentation in the nearby
node type comments to reference await daVinci.collectProtect() as the supported
API, replacing createProtectClient().collectForDaVinci(daVinci). Keep the
guidance to invoke it before daVinci.next({}) unchanged.

In
`@packages/protect/android/src/test/java/com/pingidentity/rnprotect/RNPingProtectTest.kt`:
- Around line 85-108: Strengthen both pauseBehavioralDataResolvesSuccessfully
and resumeBehavioralDataResolvesSuccessfully to assert that TestPromise was
resolved, not merely completed without a rejectCode. Use the promise’s
resolved-state assertion or equivalent existing TestPromise field, while
retaining the await and rejection checks.

In `@packages/protect/CHANGELOG.md`:
- Line 7: Update the initial-release entry in CHANGELOG.md to advertise the
public DaVinci API as daVinci.collectProtect() instead of
createProtectClient().collectForDaVinci(...). If documenting the complete
initial release, list the exported Protect lifecycle functions separately.

In `@packages/protect/ios/RNPingProtectCommon.swift`:
- Around line 203-215: Replace the initialization error code used by the
pause/resume behavioral-data failure paths with a new dedicated
PROTECT_BEHAVIORAL_DATA_ERROR value. Add the corresponding value to the Swift
error enum, JavaScript ProtectErrorCode, and Android ProtectErrorCodes, then
update both pauseBehavioralData and resumeBehavioralData handlers to use it
while preserving existing error handling.

In `@packages/protect/src/__tests__/index.test.tsx`:
- Around line 113-120: Strengthen the error-wrapping assertions in the three
tests for startProtect, pauseBehavioralData, and resumeBehavioralData at
packages/protect/src/__tests__/index.test.tsx lines 113-120, 194-204, and
238-248. Capture each rejected value and assert its GenericError contract fields
type, error, and message, rather than checking only the native error message;
all three sites require direct test updates.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts`:
- Around line 169-176: In the collection retry flow containing the protectFields
loop, clear the existing protectError state immediately before the first
collectProtect call. Keep the current catch behavior that records failures and
rethrows, so a successful retry leaves no stale error visible.

---

Nitpick comments:
In
`@packages/davinci/android/src/main/java/com/pingidentity/rndavinci/factory/DaVinciClientFactory.kt`:
- Around line 81-83: Update the Protect lifecycle logging in
DaVinciClientFactory.kt at lines 81-83 to log the caught NoClassDefFoundError at
error level before skipping the module. In RNPingDavinciCommon.kt at lines
524-526, keep the collectProtect request log at debug level and change the
successful collection log to info level.

In
`@packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt`:
- Around line 131-182: Update the Protect parser tests in
packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt:131-182
to assert protect.loggerId is null by default and mapped when a loggerId entry
is provided. Apply the same assertions in
packages/davinci/ios/Tests/DaVinciConfigParserTests.swift:92-140, using nil for
the empty configuration and adding loggerId to the populated protect dictionary.

In
`@packages/davinci/android/src/test/java/com/pingidentity/rndavinci/RNPingDavinciCommonTest.kt`:
- Around line 529-541: Update
collectProtect_rejectsWithStateErrorWhenIndexOutOfRange to create a
DummyContinueNode containing at least one Protect collector before requesting
index 5, so the test exercises index bounds rather than the empty-collector
path. Follow the Protect SDK runtime setup noted near the adjacent test while
preserving the existing rejection assertions.

In `@packages/davinci/ios/Tests/DaVinciClientFactoryTests.swift`:
- Around line 50-74: Replace the duplicate nil-protect case in
testBuildWithNullProtectPayloadDoesNotThrow with a populated
ProtectLifecyclePayload, while keeping the build-and-non-nil assertion. Update
only the protect input so this test covers the populated payload scenario
alongside testBuildSucceedsWithRequiredFieldsOnly.

In `@packages/davinci/src/davinci.ts`:
- Around line 417-427: Update the success log in the collectProtect method to
use logInfo instead of logDebug, while keeping the request entry at debug and
failure logging at error.

In `@packages/davinci/src/types/client.types.ts`:
- Around line 116-137: Complete the public collectProtect documentation: in
packages/davinci/src/types/client.types.ts lines 116-137, add a TSDoc `@returns`
tag describing successful completion; in
packages/davinci/ios/RNPingDavinciCommon.swift lines 419-484 and
packages/davinci/ios/RNPingDavinciImpl.swift lines 178-193, add /// Returns,
Throws, and Note sections for the corresponding declarations, preserving the
documented behavior that collection succeeds with no return value and throws
when Protect is unavailable or collection fails.

In
`@packages/protect/android/src/main/java/com/pingidentity/rnprotect/ProtectErrorCodes.kt`:
- Around line 9-18: Apply structured KDoc to all public and internal Kotlin
declarations covered by this comment: ProtectErrorCodes.kt lines 9-18,
RNPingProtectCommon.kt lines 30-67, RNPingProtectModule.kt lines 14-74, and
RNPingProtectPackage.kt lines 16-50. Document each applicable parameter, return
value, and thrown exception with `@param`, `@return`, and `@throws` tags, including
the error-code declarations and bridge/package methods; preserve the existing
APIs and behavior.

In
`@packages/protect/android/src/main/java/com/pingidentity/rnprotect/RNPingProtectCommon.kt`:
- Around line 128-144: The collectForDaVinci flow and the additionally
referenced operation blocks must use the configured logger, falling back to the
module-level no-op logger when no logger ID exists. Change operation-start logs
to debug, successful completion logs to info, and collector failures to error
before bridge rejection; preserve the existing rejection behavior and messages.

In `@packages/protect/ios/RNPingProtect.mm`:
- Around line 33-99: Extract the repeated main-thread dispatch logic from
collectForDaVinci, initialize, pauseBehavioralData, and resumeBehavioralData
into a shared withSwiftImpl: helper, matching the existing pattern in
RNPingProtectClassic.mm. Update all four methods to invoke the helper while
preserving their current Swift implementation calls and arguments.

In `@packages/protect/ios/Tests/RNPingProtectImplTests.swift`:
- Around line 81-101: Update invokeCollectForDaVinci’s resolve closure to call
XCTFail("Expected rejection, got resolve") before resuming the continuation,
while preserving the existing continuation return behavior.
- Around line 70-77: Update testCollectForDaVinciRejectsWhenCollectFails to
compare type using the matching ErrorType enum case’s rawValue instead of the
hard-coded "auth_error" string, consistent with the other tests.

In `@packages/protect/src/index.tsx`:
- Around line 14-18: Update the package index type re-export near
ProtectCollectOptions, ProtectConfig, and ProtectErrorCode to use export type *
from './types' instead of a named type list. First confirm ./types exposes only
public types and no internal helpers, preserving the package index’s public API
boundary.

In `@packages/protect/src/NativeRNPingProtect.ts`:
- Around line 70-83: Move the existing TSDoc block from the _nativeModule
declaration to directly above the exported getNativeModule function, leaving the
cache variable and _resetNativeModuleForTesting documentation unchanged.
- Around line 109-122: Replace the double casts in toNativeCollectOptions and
toNativeConfig with explicit typed mapping functions that construct
Record<string, unknown> payloads from the respective public types. Follow the
existing toNativeProtectConfig pattern and preserve all supported option and
config fields without bypassing type checking.

In `@PingSampleApp/ui/davinci/components/molecules/DaVinciTextField.tsx`:
- Line 28: Update the isMultiline calculation in DaVinciTextField so
TextInput.multiline is latched for the current field or derived from stable
collector metadata rather than recalculated from live stringValue length on
every edit. Preserve the selected mode while the field is being edited,
including when the text crosses the 60-character threshold in either direction.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8276c540-5cb4-4fe8-86d0-41c16680a46c

📥 Commits

Reviewing files that changed from the base of the PR and between 2670c57 and 05c1711.

⛔ Files ignored due to path filters (4)
  • .yarn/install-state.gz is excluded by !**/.yarn/**, !**/*.gz
  • PingSampleApp/ios/Podfile.lock is excluded by !**/*.lock
  • PingTestRunner/ios/Podfile.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (91)
  • .github/workflows/js-unit-tests.yml
  • AGENTS.md
  • PingSampleApp/.env.example
  • PingSampleApp/android/app/build.gradle
  • PingSampleApp/package.json
  • PingSampleApp/src/clients.ts
  • PingSampleApp/src/styles/componentStyles.ts
  • PingSampleApp/ui/components/atoms/PingTextInput.tsx
  • PingSampleApp/ui/davinci/components/molecules/DaVinciFieldRenderer.tsx
  • PingSampleApp/ui/davinci/components/molecules/DaVinciTextField.tsx
  • PingSampleApp/ui/davinci/components/organisms/DaVinciContinueNodePanel.tsx
  • PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts
  • PingTestRunner/__tests__/integration/davinci.test.ts
  • PingTestRunner/__tests__/integration/protect.test.ts
  • PingTestRunner/android/settings.gradle
  • PingTestRunner/ios/PingTestRunner.xcodeproj/xcshareddata/xcschemes/RNPackagesTests.xcscheme
  • PingTestRunner/ios/Podfile
  • PingTestRunner/jest.config.js
  • PingTestRunner/jest.setup.js
  • PingTestRunner/package.json
  • PingTestRunner/scripts/test-native-android.sh
  • PingTestRunner/scripts/test-native-ios.sh
  • packages/davinci/android/build.gradle
  • packages/davinci/android/src/main/java/com/pingidentity/rndavinci/RNPingDavinciCommon.kt
  • packages/davinci/android/src/main/java/com/pingidentity/rndavinci/config/DaVinciConfigParser.kt
  • packages/davinci/android/src/main/java/com/pingidentity/rndavinci/config/ProtectLifecyclePayload.kt
  • packages/davinci/android/src/main/java/com/pingidentity/rndavinci/error/DaVinciErrorCodes.kt
  • packages/davinci/android/src/main/java/com/pingidentity/rndavinci/factory/DaVinciClientFactory.kt
  • packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt
  • packages/davinci/android/src/newarch/java/com/pingidentity/rndavinci/RNPingDavinciModule.kt
  • packages/davinci/android/src/oldarch/java/com/pingidentity/rndavinci/RNPingDavinciClassicModule.kt
  • packages/davinci/android/src/test/java/com/pingidentity/rndavinci/RNPingDavinciCommonTest.kt
  • packages/davinci/android/src/test/java/com/pingidentity/rndavinci/config/DaVinciConfigParserTest.kt
  • packages/davinci/android/src/test/java/com/pingidentity/rndavinci/factory/DaVinciClientFactoryTest.kt
  • packages/davinci/android/src/test/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapperTest.kt
  • packages/davinci/ios/Config/DaVinciConfigParser.swift
  • packages/davinci/ios/Error/DaVinciErrorCodes.swift
  • packages/davinci/ios/Factory/DaVinciClientFactory.swift
  • packages/davinci/ios/Mapper/DaVinciNodeMapper.swift
  • packages/davinci/ios/Models/DaVinciPayloads.swift
  • packages/davinci/ios/RNPingDavinci.mm
  • packages/davinci/ios/RNPingDavinciClassic.mm
  • packages/davinci/ios/RNPingDavinciCommon.swift
  • packages/davinci/ios/RNPingDavinciImpl.swift
  • packages/davinci/ios/Tests/DaVinciClientFactoryTests.swift
  • packages/davinci/ios/Tests/DaVinciConfigParserTests.swift
  • packages/davinci/ios/Tests/DaVinciNodeMapperTests.swift
  • packages/davinci/src/NativeRNPingDavinci.ts
  • packages/davinci/src/__tests__/createDaVinciClient.test.ts
  • packages/davinci/src/collectorHelpers.ts
  • packages/davinci/src/davinci.ts
  • packages/davinci/src/davinciMethods.ts
  • packages/davinci/src/types/client.types.ts
  • packages/davinci/src/types/config.types.ts
  • packages/davinci/src/types/node.types.ts
  • packages/davinci/src/useDavinci.tsx
  • packages/protect/CHANGELOG.md
  • packages/protect/LICENSE
  • packages/protect/README.md
  • packages/protect/RNPingProtect.podspec
  • packages/protect/android/build.gradle
  • packages/protect/android/src/main/AndroidManifest.xml
  • packages/protect/android/src/main/java/com/pingidentity/rnprotect/ProtectErrorCodes.kt
  • packages/protect/android/src/main/java/com/pingidentity/rnprotect/RNPingProtectCommon.kt
  • packages/protect/android/src/newarch/java/com/pingidentity/rnprotect/RNPingProtectModule.kt
  • packages/protect/android/src/newarch/java/com/pingidentity/rnprotect/RNPingProtectPackage.kt
  • packages/protect/android/src/oldarch/java/com/pingidentity/rnprotect/RNPingProtectClassicModule.kt
  • packages/protect/android/src/oldarch/java/com/pingidentity/rnprotect/RNPingProtectPackage.kt
  • packages/protect/android/src/test/java/com/pingidentity/rnprotect/RNPingProtectTest.kt
  • packages/protect/babel.config.js
  • packages/protect/eslint.config.mjs
  • packages/protect/ios/RNPingProtect.h
  • packages/protect/ios/RNPingProtect.mm
  • packages/protect/ios/RNPingProtectClassic.mm
  • packages/protect/ios/RNPingProtectCommon.swift
  • packages/protect/ios/RNPingProtectImpl.swift
  • packages/protect/ios/Tests/RNPingProtectCommonTests.swift
  • packages/protect/ios/Tests/RNPingProtectImplTests.swift
  • packages/protect/jest.config.js
  • packages/protect/jest.setup.js
  • packages/protect/package.json
  • packages/protect/src/NativeRNPingProtect.ts
  • packages/protect/src/__tests__/index.test.tsx
  • packages/protect/src/__tests__/native-module.test.tsx
  • packages/protect/src/index.tsx
  • packages/protect/src/protect.ts
  • packages/protect/src/types/index.ts
  • packages/protect/src/types/protect.types.ts
  • packages/protect/tsconfig.build.json
  • packages/protect/tsconfig.json
  • packages/protect/turbo.json

Comment on lines +173 to +174
// PROTECT fields are handled in mapCollectorPayload via form-field type lookup.
if (resolvedType == PROTECT) continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not hide PROTECT fields without a native collector.

Line 173 excludes every PROTECT field before Line 176 checks registeredKeys. If rn-protect is unavailable, no ProtectCollector is instantiated. JavaScript then receives neither a Protect collector nor an unsupportedFields entry.

Remove this unconditional exclusion. The existing registeredKeys.contains(key) check already excludes fields that have an instantiated collector.

Proposed fix
-                // PROTECT fields are handled in mapCollectorPayload via form-field type lookup.
-                if (resolvedType == PROTECT) continue
-
                 // A field is supported when the SDK instantiated a collector for its key.
                 if (registeredKeys.contains(key)) continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// PROTECT fields are handled in mapCollectorPayload via form-field type lookup.
if (resolvedType == PROTECT) continue
// A field is supported when the SDK instantiated a collector for its key.
if (registeredKeys.contains(key)) continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt`
around lines 173 - 174, Remove the unconditional PROTECT exclusion in the
mapper’s field-processing logic. Let PROTECT fields proceed to the existing
registeredKeys.contains(key) check so fields without an instantiated
ProtectCollector are reported through unsupportedFields, while fields with a
native collector continue through the normal mapping path.

Comment on lines +541 to +551
/**
* Parses the collector index from bridge options, defaulting to 0.
*/
private fun parseCollectorIndex(options: ReadableMap?): Int {
if (options == null || !options.hasKey("index") || options.isNull("index")) return 0
return when (options.getType("index")) {
com.facebook.react.bridge.ReadableType.Number -> options.getDouble("index").toInt()
com.facebook.react.bridge.ReadableType.String -> options.getString("index")?.toIntOrNull() ?: 0
else -> 0
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject invalid supplied collector indexes.

Line 547 truncates fractional numbers. Line 548 and Line 549 convert malformed supplied values to index 0. A caller can therefore collect from the first Protect collector instead of receiving an error.

Default only when index is absent. Reject negative, fractional, malformed, and unsupported supplied values with GenericError(type = ErrorType.ARGUMENT_ERROR, ...) before collector selection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/davinci/android/src/main/java/com/pingidentity/rndavinci/RNPingDavinciCommon.kt`
around lines 541 - 551, Update parseCollectorIndex to default to 0 only when
options or the index key is absent; otherwise validate the supplied value as a
non-negative integer. Reject fractional numbers, malformed strings, negative
values, and unsupported types by throwing GenericError with
ErrorType.ARGUMENT_ERROR, rather than coercing them to 0 before collector
selection.

Source: Coding guidelines

Comment on lines +74 to +75
/// Optional protect lifecycle module configuration. Present only when modules.protect is provided.
let protect: ProtectLifecyclePayload?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Forward Protect configuration through the TurboModule bridge.

packages/davinci/ios/RNPingDavinci.mm Lines 42-118 builds the configuration dictionary field by field but does not copy modules.protect. TurboModule clients therefore produce payload.protect == nil, and DaVinciClientFactory skips the lifecycle configuration at Lines 80-95. Map the Protect module payload into that dictionary with the parser field names. The classic bridge already forwards the raw configuration dictionary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/ios/Models/DaVinciPayloads.swift` around lines 74 - 75,
Update the configuration dictionary construction in RNPingDavinci.mm to forward
modules.protect through the TurboModule bridge, using the parser field names
expected by ProtectLifecyclePayload. Preserve the existing field-by-field
mapping and ensure DaVinciPayloads.protect receives the mapped configuration
instead of remaining nil.

Comment on lines +492 to +499
private static func parseCollectorIndex(_ options: NSDictionary) -> Int {
if let value = options["index"] as? NSNumber {
return value.intValue
}
if let value = options["index"] as? String, let parsed = Int(value) {
return parsed
}
return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject invalid collector indexes.

An absent index can default to 0. An invalid string also defaults to 0, and non-integer NSNumber values are truncated. For example, "invalid" can collect from collector 0 instead of rejecting the caller input.

Validate that a supplied index is a non-negative integer. Reject invalid values with a GenericError of type argumentError before calling collector.collect().

As per coding guidelines, “validate inputs before native calls; throw argument_error for caller mistakes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/ios/RNPingDavinciCommon.swift` around lines 492 - 499,
Update parseCollectorIndex and its caller to distinguish an absent index from
invalid input: accept only non-negative integer values, reject invalid strings
and NSNumber values that are not exact integers, and propagate a GenericError of
type argumentError before collector.collect() is invoked. Preserve the default
index of 0 only when the index is absent.

Source: Coding guidelines

Comment on lines +147 to +157
/**
* Run PingOne Protect data collection against the active PROTECT collector in the flow.
*
* @remarks
* Requires `@ping-identity/rn-protect` to be installed. Rejects with
* `DAVINCI_PROTECT_COLLECT_ERROR` when the Protect SDK is absent or collection fails.
*
* @param davinciId - Native DaVinci instance identifier.
* @param options - Per-call options (e.g. `index` for multi-collector nodes).
*/
collectProtect(davinciId: string, options: Object): Promise<void>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add @returns and @throws tags to collectProtect.

The remarks describe failure behavior, but the exported declaration does not have the required TSDoc tags. Document the void resolution and DAVINCI_PROTECT_COLLECT_ERROR rejection explicitly.

As per coding guidelines, “Use TSDoc on all exported declarations with required parameter, return, throws, example, platform-difference, and visibility tags as applicable.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci/src/NativeRNPingDavinci.ts` around lines 147 - 157, Update
the exported collectProtect declaration’s TSDoc to add an `@returns` tag
documenting successful void resolution and an `@throws` tag documenting rejection
with DAVINCI_PROTECT_COLLECT_ERROR when the Protect SDK is unavailable or
collection fails.

Source: Coding guidelines

Comment on lines +114 to +123
export class ProtectError extends PingError {
constructor(message: string, code: string, type: string, status?: number) {
super(message, code, type, status);
this.name = 'ProtectError';
Object.setPrototypeOf(this, new.target.prototype);
}

static from(error: unknown): ProtectError {
return PingError.fromAs(error, ProtectError);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document the public ProtectError API.

Add TSDoc for the constructor and ProtectError.from(). Document constructor parameters and document the error parameter and returned ProtectError for from().

As per coding guidelines, “Use TSDoc on all exported declarations with required parameter, return, throws, example, platform-difference, and visibility tags as applicable.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/src/types/protect.types.ts` around lines 114 - 123, Document
the exported ProtectError constructor and static from method with TSDoc. Include
parameter descriptions for message, code, type, optional status, and error, plus
the from method’s returned ProtectError using the applicable required tags; keep
the implementation unchanged.

Source: Coding guidelines

Comment on lines +11 to +12
"src/*.ts",
"src/*.tsx",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Track nested source files in both platform task hashes.

src/*.ts and src/*.tsx do not match files under src/types/. A change to packages/protect/src/types/protect.types.ts can reuse a stale build:android or build:ios cache result.

  • packages/protect/turbo.json#L11-L12: replace the direct-file patterns with a recursive source pattern.
  • packages/protect/turbo.json#L32-L33: replace the direct-file patterns with the same recursive source pattern.
Proposed fix
-        "src/*.ts",
-        "src/*.tsx",
+        "src/**",
📍 Affects 1 file
  • packages/protect/turbo.json#L11-L12 (this comment)
  • packages/protect/turbo.json#L32-L33
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/protect/turbo.json` around lines 11 - 12, Update
packages/protect/turbo.json at lines 11-12 and 32-33 to replace the
non-recursive src/*.ts and src/*.tsx inputs in both platform task hashes with
the same recursive source pattern, ensuring nested files such as
src/types/protect.types.ts invalidate build:android and build:ios caches.

Comment on lines +160 to +164
const onProtectCollect = useCallback(async (): Promise<void> => {
const davinciClient = davinciContext?.client;
if (!davinciClient) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when the Protect client is unavailable.

If Protect fields exist and davinciClient is missing, onProtectCollect resolves successfully. onSubmit then calls next, so a Protect node can advance without collection. Set protectError and reject the submission instead of returning.

As per coding guidelines, use GenericError with type, error, and message, and log or rethrow every caught error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts` around
lines 160 - 164, Update onProtectCollect in the missing davinciClient path to
fail closed: set protectError and reject with a GenericError containing type,
error, and message instead of returning successfully. Ensure onSubmit cannot
call next when Protect collection is unavailable, and preserve the requirement
that every caught error is logged or rethrown.

Source: Coding guidelines

Comment on lines +188 to +192
onProtectCollect()
.then(() => next(plan.input))
.catch(() => {
// `error` is already updated by the hook.
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not swallow submission failures.

The empty catch consumes both Protect collection failures and next failures without logging or rethrowing. Use the configured logger at error level and preserve the existing error state. Do not leave the catch empty.

As per coding guidelines, log or rethrow every caught error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts` around
lines 188 - 192, Update the catch following onProtectCollect in the submission
flow to accept the caught error and log it at error level through the configured
logger, preserving the hook’s existing error state. Ensure failures from both
onProtectCollect and next are no longer silently consumed.

Source: Coding guidelines


🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Run Protect collection before every submission path.

The new gate wraps only onSubmit. onFlowAction calls form.setValue, and the code documents that this auto-submits through next. A node containing both a flow collector and a Protect collector can therefore advance without collectProtect. Route all submission paths through one Protect-aware helper, or add a pre-submit callback to useDaVinciForm.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingSampleApp/ui/davinci/hooks/useDaVinciClientPanelController.ts` around
lines 188 - 192, Ensure every submission path in the controller, including
onFlowAction-triggered auto-submits, runs onProtectCollect before calling
next(plan.input). Centralize this behavior in a shared Protect-aware submission
helper and use it from both onSubmit and onFlowAction, preserving the hook’s
existing error update behavior.

Comment on lines +286 to +293
jest.mock('../packages/protect/src/NativeRNPingProtect', () => ({
__esModule: true,
getNativeModule: jest.fn(() => ({
collectForDaVinci: jest.fn(async () => undefined),
})),
toNativeCollectOptions: jest.fn((options) => options),
toNativeConfig: jest.fn((config) => config),
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add toNativeProtectConfig to the shared mock.

startProtect() calls toNativeProtectConfig(config) before it calls initialize. This mock does not export that function. A test that uses this shared mock and invokes startProtect will fail with TypeError instead of testing the Protect bridge.

Proposed fix
   toNativeCollectOptions: jest.fn((options) => options),
   toNativeConfig: jest.fn((config) => config),
+  toNativeProtectConfig: jest.fn((config) => config),
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock('../packages/protect/src/NativeRNPingProtect', () => ({
__esModule: true,
getNativeModule: jest.fn(() => ({
collectForDaVinci: jest.fn(async () => undefined),
})),
toNativeCollectOptions: jest.fn((options) => options),
toNativeConfig: jest.fn((config) => config),
}));
jest.mock('../packages/protect/src/NativeRNPingProtect', () => ({
__esModule: true,
getNativeModule: jest.fn(() => ({
collectForDaVinci: jest.fn(async () => undefined),
})),
toNativeCollectOptions: jest.fn((options) => options),
toNativeConfig: jest.fn((config) => config),
toNativeProtectConfig: jest.fn((config) => config),
}));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingTestRunner/jest.setup.js` around lines 286 - 293, Add the missing
toNativeProtectConfig mock export to the NativeRNPingProtect shared mock,
matching the existing pass-through behavior of toNativeConfig, so startProtect()
can call it before initialize without a TypeError.

pingidentity-gaurav and others added 2 commits August 11, 2026 09:09
…tolinking in PingTestRunner

- Skip testCollectForDaVinciRejectsWhenCollectFails: ProtectCollector(with:)
  accesses Bundle.main which crashes in the xctest agent (no host app bundle)
- Add PingTestRunner/react-native.config.js excluding @ping-identity/rn-protect
  from autolinking on both platforms — it is only needed for Robolectric unit
  tests, not the BrowserStack E2E APK build; autolinking was failing the CMake
  step because codegen JNI output does not exist until the package is built

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…PingProtectSpec.h

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PingTestRunner/react-native.config.js`:
- Line 10: Update the dependency comment in react-native.config.js to state that
`@ping-identity/rn-protect` is used by the integration test protect.test.ts and
that its Android project is manually included through settings.gradle, replacing
the incorrect Robolectric-only explanation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b8360be6-1c1f-4ba3-8e05-42247a1b9c9c

📥 Commits

Reviewing files that changed from the base of the PR and between e13703a and 835a546.

📒 Files selected for processing (1)
  • PingTestRunner/react-native.config.js


module.exports = {
dependencies: {
// rn-protect is included in package.json for Robolectric unit tests only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the dependency comment.

@ping-identity/rn-protect is also used by PingTestRunner/__tests__/integration/protect.test.ts. PingTestRunner/android/settings.gradle manually includes its Android project. The comment should describe the integration-test dependency and the manual Android linking rationale instead of limiting usage to Robolectric tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PingTestRunner/react-native.config.js` at line 10, Update the dependency
comment in react-native.config.js to state that `@ping-identity/rn-protect` is
used by the integration test protect.test.ts and that its Android project is
manually included through settings.gradle, replacing the incorrect
Robolectric-only explanation.

c.isConsoleLogEnabled = initConfig.isConsoleLogEnabled
c.deviceAttributesToIgnore = initConfig.deviceAttributesToIgnore
}
try await Protect.initialize()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to consider here, native Protect.initialize() no-ops once already initialized, so it seems that without a cleanup()/invalidate() hook (like RNPingJourneyImpl.swift has), a JS reload with new config would silently do nothing.

*
* Handled entirely by `@ping-identity/rn-protect` — appears as `executionMode:
* 'integration_required'` and `kind: 'integration'` in normalized collectors.
* Call `createProtectClient().collectForDaVinci(daVinci)` before `daVinci.next({})`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Call `createProtectClient().collectForDaVinci(daVinci)` before `daVinci.next({})`.
* Call `createProtectClient().collectProtect()` before `daVinci.next({})`.

handlers.reject(
GenericError(
type: .authError,
error: ProtectErrorCode.initializeError.rawValue,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit pick. How about we use a dedicated error code here, or possibly reuse collect_error, for pause/resume failures? This error suggests an initialization issue, and separating them would make handling clearer for callers.

resumeBehavioralDataOnStart = protect.resumeBehavioralDataOnStart
}
} catch (_: NoClassDefFoundError) {
// Protect SDK absent — rn-protect not installed; skip lifecycle module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding a debug or warning log here?

@tsdamas

tsdamas commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Great work! Left minor comments.

@rodrigoareis rodrigoareis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall implementation looks good. Left some comments

import PingOidc
import PingOrchestrate
import RNPingCore
#if canImport(PingOneProtect)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding these #if canImport(PingOneProtect) / compileOnly (Android) items individually, I think we should settle the pattern — because rn-core already defines one, and this PR contains both the compliant and the non-compliant implementation.

CoreRuntime.kt:50-54 and CoreRuntime.swift:114, 135 already say exactly what we're trying to do:

"Plugin packages (external-idp, protect) cannot depend on rn-davinci directly — this indirection lets DaVinci inject its collector lookup at init time without creating a circular dependency."

The contract is: the host (rn-davinci) publishes an opaque List<Any> handle resolver into rn-core, and the plugin (rn-protect) owns the SDK dependency and does its own filterIsInstance. Nothing SDK-typed crosses the boundary — which means the host needs no compileOnly, no canImport, and no NoClassDefFoundError guard.

rn-protect already implements this correctlyRNPingProtectCommon.kt:310-313 and protect/ios/RNPingProtectCommon.swift:86 both go through CoreRuntime.resolveDaVinciCollectors. That code is currently dead. The path that actually ships (daVinci.collectProtect()RNPingDavinciCommon.kt:492 / .swift:430) inverts the dependency, and that inversion is the direct cause of the canImport and compileOnly problems flagged above.

The JS-layer precedent is external-idp/src/externalIdp.ts:194authorizeForDaVinci lives on the plugin client and takes the DaVinci client as an argument (PingSampleApp/.../useDaVinciClientPanelController.ts:217).

Suggest mirroring that:

// instead of daVinci.collectProtect()
 await collectProtect(davinciClient, { index });   // exported from @ping-identity/rn-protect

That deletes collectProtect from the davinci native module on both platforms, removes all 4 #if canImport blocks, keeps PingOneProtect out of RNPingDavinci.podspec, resolves the stale PingSampleApp/ios/Podfile.lock:2581 entry, and makes the already-written collectForDaVinci the single implementation. If we'd rather keep daVinci.collectProtect() for DX, it can be a thin JS re-export — but the native call should still land on the protect module.

Happy to go with the tactical fix for this PR (declare the deps properly, make the guard log) if a broader refactor is out of scope — but could we at least (a) route collectProtect through the plugin so no new canImport is introduced, and (b) open a follow-up ticket for the serializer/module registries covering rn-davinci, rn-external-idp, rn-journey and rn-fido?

resumeBehavioralDataOnStart = protect.resumeBehavioralDataOnStart
}
} catch (_: NoClassDefFoundError) {
// Protect SDK absent — rn-protect not installed; skip lifecycle module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Protect SDK absent — rn-protect not installed; skip lifecycle module.
// Protect SDK absent — rn-protect not installed; skip lifecycle module.
logger?.e("modules.protect was configured but the PingOne Protect SDK is not on the classpath; ProtectLifecycle was NOT registered. Add com.pingidentity.sdks:protect.", e)

* Call this after a successful authentication flow. Requires `startProtect()` first.
*
* @param options - Optional logger instance.
* @throws {@link ProtectError} when the SDK is not initialized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So pauseBehavioralData() without a prior startProtect() rejects on iOS and silently succeeds on Android. That makes this doc only true on iOS.

The following tests document a platform divergence. Did we create a ticket to fix this on the native? For now I would recommend add a TODO to fix it

AndroidRNPingProtectTest.kt:86pauseBehavioralDataResolvesSuccessfully (resolves with no prior initialize())

iOSRNPingProtectCommonTests.swift:88testPauseBehavioralDataRejectsWhenSDKNotInitialized (rejects)

*
* @defaultValue `false`
*/
pauseBehavioralDataOnSuccess?: boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this?

let collectorIndex = parseCollectorIndex(options)

// `@objc` methods cannot be declared `async`, so a Task is required to
// enter the async context. The body runs off the main actor so that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not valid, the unstructured Task {} inherits the actor isolation of its enclosing context, and collectForDaVinci is annotated @MainActor (line 55). So the closure body is main-actor-isolated and collector.collect() runs on the main thread. Android correctly uses Dispatchers.IO.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants