Skip to content

SDKS-4679 & SDKS-4681: Add Polling and QR Code collectors for DaVinci flows#152

Open
witrisna wants to merge 1 commit intodevelopfrom
SDKS-4679
Open

SDKS-4679 & SDKS-4681: Add Polling and QR Code collectors for DaVinci flows#152
witrisna wants to merge 1 commit intodevelopfrom
SDKS-4679

Conversation

@witrisna
Copy link
Copy Markdown
Contributor

@witrisna witrisna commented Jan 19, 2026

JIRA Ticket

SDKS-4679
SDKS-4681

Description

  • Implement PollingCollector to handle asynchronous operations like push notifications and OOB authentication, supporting both simple delays and active challenge status polling.
  • Implement QRCodeCollector to handle Base64-encoded QR code images for device pairing and MFA setup.
  • Add Compose UI components (Polling.kt, QRCode.kt) and integrate them into ContinueNode.
  • Register new collectors in CollectorRegistry.
  • Add comprehensive unit tests for PollingCollector and QRCodeCollector logic.

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced polling collector with configurable retry logic and real-time status tracking
    • Added QR code display capability with automatic decoding and fallback text support
    • Implemented state rewind feature enabling navigation back to previously rendered UI states
    • Enhanced sample apps with polling progress indicator and QR code rendering components

@witrisna witrisna self-assigned this Jan 19, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 19, 2026

Codecov Report

❌ Patch coverage is 58.92857% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.16%. Comparing base (a8272e6) to head (458da40).

Files with missing lines Patch % Lines
...pingidentity/davinci/collector/PollingCollector.kt 62.06% 3 Missing and 30 partials ⚠️
...otlin/com/pingidentity/davinci/module/Transform.kt 0.00% 9 Missing and 2 partials ⚠️
.../pingidentity/davinci/collector/QRCodeCollector.kt 83.33% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #152      +/-   ##
=============================================
+ Coverage      43.98%   44.16%   +0.17%     
- Complexity      1231     1252      +21     
=============================================
  Files            306      308       +2     
  Lines           9255     9367     +112     
  Branches        1326     1375      +49     
=============================================
+ Hits            4071     4137      +66     
- Misses          4638     4650      +12     
- Partials         546      580      +34     
Flag Coverage Δ
integration-tests 28.17% <1.78%> (-0.38%) ⬇️
unit-tests 25.45% <58.92%> (+0.41%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@vibhorgoswami vibhorgoswami left a comment

Choose a reason for hiding this comment

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

One comment related to bitmap extraction.

Copy link
Copy Markdown
Contributor

@spetrov spetrov left a comment

Choose a reason for hiding this comment

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

LGTM! 👍🏻

Comment thread davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt Outdated
@witrisna witrisna force-pushed the SDKS-4679 branch 5 times, most recently from 79e1f3c to 8adc3d7 Compare April 1, 2026 16:16
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

Warning

Rate limit exceeded

@witrisna has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 9 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 45 minutes and 9 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d3f8d140-d673-47da-96dd-0a61419a45e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4647395 and 458da40.

📒 Files selected for processing (17)
  • davinci/src/main/kotlin/com/pingidentity/davinci/CollectorRegistry.kt
  • davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt
  • davinci/src/main/kotlin/com/pingidentity/davinci/collector/QRCodeCollector.kt
  • davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/CollectorRegistryTest.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.Response.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/ContinueNode.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Polling.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/QRCode.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/DaVinci.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/DaVinciContinueNode.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/QRCode.kt
📝 Walkthrough

Walkthrough

This PR introduces two new DaVinci collectors—PollingCollector for status polling with configurable intervals and retries, and QRCodeCollector for rendering QR codes from Base64-encoded data. It registers both collectors, adds their UI components to sample applications, and enhances state management to handle rewind events that reset UI to previously rendered states.

Changes

Cohort / File(s) Summary
Collector Registration
davinci/src/main/kotlin/com/pingidentity/davinci/CollectorRegistry.kt, davinci/src/test/kotlin/com/pingidentity/davinci/CollectorRegistryTest.kt
Registered POLLING and QR_CODE collectors in registry; updated copyright year to 2026; test assertions expanded to cover five new collector types with size validation updated to 16 total.
Polling Collector
davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt
New collector implementing SingleValueCollector, Submittable, ContinueNodeAware, DaVinciAware, and Closeable. Supports challenge-status HTTP polling with retries/delays or simple delay mode, emitting PollingStatus sealed class variants (Continue, TimedOut, Expired, Error, Complete).
QR Code Collector
davinci/src/main/kotlin/com/pingidentity/davinci/collector/QRCodeCollector.kt
New collector implementing Collector<Nothing>. Extracts content and fallbackText from JSON config; provides bitmap() method to decode Base64-encoded QR code data into Android Bitmap with graceful null fallback on decode errors.
State Management
davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt
Added handling for rewindStateToLastRenderedUI and rewindStateToSpecificRenderedUI events. Retrieves existing ContinueNode from flow context and reuses its context/collectors instead of creating new ones. Updated copyright year to 2026.
Polling Tests
davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt
Comprehensive test suite covering initialization, simple polling, challenge-status polling with HTTP mocking, retry logic, non-200 response handling, JSON parsing errors, and bypasses when polling is disabled.
QR Code Tests
davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt
Test suite validating JSON initialization, default value handling, bitmap() null returns on invalid Base64 or non-image data, and method chaining support.
Rewind Test Helpers
davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.Response.kt, davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.kt
Added test response generators and two @Test cases validating rewind event handling, confirming new ContinueNode instances are created with matching identity but different object references.
Sample App UI (Main)
samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt, samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/DaVinci.kt
Wrapped ContinueNode rendering with Compose key() to control recomposition identity. Updated copyright year to 2026. Removed unused import.
Sample App Collectors
samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/ContinueNode.kt, samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/DaVinciContinueNode.kt
Extended collector dispatch to render Polling(it, onNext) for PollingCollector and QRCode(it) for QRCodeCollector.
Sample App Polling UI
samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Polling.kt, samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt
New Compose component managing polling lifecycle: starts async polling on composition, updates status message on Continue, calls onNext() on terminal states (Complete, TimedOut, Error, Expired), and cancels polling on disposal. Displays CircularProgressIndicator and status text while polling.
Sample App QR Code UI
samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/QRCode.kt, samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/QRCode.kt
New Compose component memoizing QR code bitmap generation, rendering centered image at fixed size (256.dp) when bitmap is non-null, with optional fallback text beneath.

Sequence Diagram(s)

sequenceDiagram
    participant Client as PollingCollector
    participant Delay as Async Delay
    participant Server as HTTP Server
    participant Flow as Flow<PollingStatus>

    Client->>Delay: Wait pollInterval ms
    Delay-->>Client: Delay complete
    Client->>Server: POST challenge status (if pollChallengeStatus)
    Server-->>Client: Response (200 or error)
    alt Non-200 Response
        Client->>Flow: emit Expired
        Client->>Client: Stop polling
    else 200 OK, isChallengeComplete=true
        Client->>Flow: emit Complete(status)
        Client->>Client: Stop polling
    else 200 OK, isChallengeComplete=false
        Client->>Flow: emit Continue(retryCount, maxRetries)
        Client->>Client: Decrement retriesAllowed
        alt Retries Exhausted
            Client->>Flow: emit TimedOut
            Client->>Client: Stop polling
        else Retries Remaining
            Client->>Delay: Wait pollInterval ms (loop)
        end
    else Exception
        Client->>Flow: emit Error(exception)
        Client->>Client: Stop polling
    end
Loading
sequenceDiagram
    participant Client as Client
    participant Handler as Transform
    participant Context as FlowContext
    participant Factory as CollectorFactory
    participant Node as ContinueNode

    Client->>Handler: POST (rewindStateToLastRenderedUI)
    Handler->>Context: Retrieve existing ContinueNode via CONTINUE_NODE key
    alt ContinueNode Exists
        Handler->>Handler: Extract context, daVinci, input, collectors from existing node
        Handler->>Handler: Create new Connector(reused fields)
        Handler->>Factory: inject(Connector, new collectors)
        Factory-->>Handler: Connector ready
        Handler-->>Client: Return new ContinueNode
    else ContinueNode Missing
        Handler-->>Client: Fail with IllegalStateException
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • spetrov
  • rodrigoareis

Poem

🐰 Hop! New collectors hop on by,
Polling waits and QR codes fly,
Rewind states to moments past,
Each keystroke makes the magic last!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding two new collector types (Polling and QR Code) to DaVinci flows, directly reflecting the primary objective.
Description check ✅ Passed The description follows the template structure with both JIRA tickets linked and a comprehensive bulleted list explaining implementation scope, including collectors, UI components, registration, and testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SDKS-4679

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (4)
davinci/src/main/kotlin/com/pingidentity/davinci/collector/QRCodeCollector.kt (1)

1-6: ⚠️ Potential issue | 🟡 Minor

Copyright header should cover 2025–2026.

As per coding guidelines ("Every new source file must include the MIT license header with copyright 2025-2026 Ping Identity Corporation").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/QRCodeCollector.kt`
around lines 1 - 6, Update the file header in QRCodeCollector.kt so the MIT
license copyright line covers "2025–2026 Ping Identity Corporation" instead of
just 2026; edit the top comment block (the file header comment) to list the
years 2025–2026 to comply with the project guideline that every new source file
includes the 2025–2026 header.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/QRCode.kt (1)

1-6: ⚠️ Potential issue | 🟡 Minor

Copyright header should cover 2025–2026.

Same header concern as the samples/app copy. As per coding guidelines ("Every new source file must include the MIT license header with copyright 2025-2026 Ping Identity Corporation"), use 2025 - 2026.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/QRCode.kt`
around lines 1 - 6, Update the source file header in QRCode.kt to use the
required copyright range "2025 - 2026" instead of the current year; locate the
file/class QRCode.kt and replace the existing copyright line in the MIT license
header so it reads "Copyright (c) 2025 - 2026 Ping Identity Corporation. All
rights reserved." ensuring the rest of the license text remains unchanged.
samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Polling.kt (1)

1-6: ⚠️ Potential issue | 🟡 Minor

Copyright header should cover 2025–2026.

Same as other new files in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Polling.kt`
around lines 1 - 6, Update the file header in Polling.kt to use the correct
copyright range 2025–2026 instead of just 2026; edit the top comment block (the
file-level header comment) so the line that currently reads "Copyright (c) 2026
Ping Identity Corporation." becomes "Copyright (c) 2025–2026 Ping Identity
Corporation." to match other new files in the PR.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt (1)

1-6: ⚠️ Potential issue | 🟡 Minor

Copyright header should cover 2025–2026.

As per coding guidelines ("Every new source file must include the MIT license header with copyright 2025-2026 Ping Identity Corporation").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt`
around lines 1 - 6, Update the copyright header in Polling.kt to cover the
2025–2026 range: replace the current "2026" year in the top comment block with
"2025–2026" (use an en dash) so the MIT license header matches the project's
required range.
🧹 Nitpick comments (4)
davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt (1)

38-38: Prefer JUnit 4's @Before over kotlin.test.BeforeTest in JUnit-based tests.

Coding guidelines specify JUnit 4 for unit tests. kotlin.test.BeforeTest happens to resolve to @org.junit.Before on the JVM target, but directly using org.junit.Before is clearer and consistent with the rest of the JUnit 4 imports (org.junit.Test, org.junit.Assert.*) used in this file.

♻️ Suggested change
-import kotlin.test.BeforeTest
+import org.junit.Before
@@
-    `@BeforeTest`
+    `@Before`
     fun setUp() {

As per coding guidelines: "Use JUnit 4, MockK (io.mockk:mockk), and Robolectric for Android-specific classes in unit tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt` at
line 38, Replace the kotlin.test.BeforeTest import with JUnit 4's
org.junit.Before in the PollingCollectorTest; update any setup method annotated
with `@BeforeTest` (e.g., setUp in class PollingCollectorTest) to use `@Before` so
the file consistently uses JUnit 4 annotations alongside existing org.junit.Test
and Assert imports.
davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt (1)

214-226: pollRetries.toInt() can throw during init().

init already tolerates missing fields with string defaults, but if the server ever supplies a non-numeric pollRetries value, toInt() throws NumberFormatException during initialization — unlike the poll() body which uses toIntOrNull()/toLongOrNull() with fallback defaults. Consider using the same defensive conversion here for consistency.

♻️ Suggested change
-        retriesAllowed = pollRetries.toInt()
+        retriesAllowed = pollRetries.toIntOrNull() ?: 60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt`
around lines 214 - 226, The init method currently sets retriesAllowed =
pollRetries.toInt() which can throw NumberFormatException for non-numeric input;
change the conversion to a defensive form (use pollRetries.toIntOrNull() with a
fallback like 60) and assign retriesAllowed from that safe value so
initialization never throws. Update the PollingCollector.init function to parse
pollRetries defensively (mirror the poll() behavior) before assigning
retriesAllowed, keeping pollRetries as the original string and using the safe
parsed integer as the counter.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt (1)

44-83: Redundant coroutine + Job tracking; LaunchedEffect already handles cancellation.

LaunchedEffect(field) runs the block in a scope that is cancelled automatically when the composable leaves composition or when field changes, so the inner launch { … } wrapper and the mirroring DisposableEffect(field) { onDispose { pollingJob?.cancel() } } do not add cancellation guarantees — they add an extra coroutine layer and redundant state. Running field.poll().collect { … } directly inside the LaunchedEffect is simpler and has identical lifecycle semantics. Note also that statusMessage assignments inside the terminal branches are never observable because the whole Column is hidden once isPolling = false and onNext() navigates away.

Proposed simplification
-    var isPolling by remember { mutableStateOf(true) }
-    var statusMessage by remember { mutableStateOf("Polling...") }
-    var pollingJob: Job? by remember { mutableStateOf(null) }
-
-    LaunchedEffect(field) {
-        pollingJob = launch {
-            field.poll().collect { status ->
-                when (status) {
-                    is PollingStatus.Continue -> {
-                        statusMessage = "Polling... (${status.retryCount}/${status.maxRetries})"
-                    }
-                    is PollingStatus.Complete -> {
-                        isPolling = false
-                        onNext()
-                    }
-                    is PollingStatus.TimedOut -> {
-                        isPolling = false
-                        statusMessage = "Polling timedOut"
-                        onNext()
-                    }
-                    is PollingStatus.Error -> {
-                        isPolling = false
-                        statusMessage = "Error: ${status.exception.message}"
-                        onNext()
-                    }
-
-                    is PollingStatus.Expired -> {
-                        isPolling = false
-                        statusMessage = "Polling Expired"
-                        onNext()
-                    }
-
-                }
-            }
-        }
-    }
-
-    // Cancel polling when the composable is disposed
-    DisposableEffect(field) {
-        onDispose {
-            pollingJob?.cancel()
-            isPolling = false
-        }
-    }
+    var isPolling by remember { mutableStateOf(true) }
+    var statusMessage by remember { mutableStateOf("Polling...") }
+
+    LaunchedEffect(field) {
+        field.poll().collect { status ->
+            when (status) {
+                is PollingStatus.Continue ->
+                    statusMessage = "Polling... (${status.retryCount}/${status.maxRetries})"
+                is PollingStatus.Complete,
+                is PollingStatus.TimedOut,
+                is PollingStatus.Error,
+                is PollingStatus.Expired -> {
+                    isPolling = false
+                    onNext()
+                }
+            }
+        }
+    }

Imports for Job, launch, and DisposableEffect can then be dropped.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt`
around lines 44 - 83, Remove the redundant inner coroutine and manual Job
tracking: inside the LaunchedEffect(field) block call field.poll().collect { ...
} directly instead of launching a new coroutine and assigning pollingJob; drop
the pollingJob variable and the DisposableEffect(field) cleanup since
LaunchedEffect already cancels on disposal or key changes. Keep the same
handling for PollingStatus branches (Continue, Complete, TimedOut, Error,
Expired) but remove the external isPolling/pollingJob cancellation logic and any
imports only used for Job/launch/DisposableEffect; leave onNext(), statusMessage
and isPolling assignments as-is where needed.
davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt (1)

14-17: Switch to JUnit 4 and Robolectric for proper bitmap decoding tests.

This file uses kotlin.test but should use JUnit 4 with @RunWith(RobolectricTestRunner::class) per coding guidelines. More critically, QRCodeCollector.bitmap() calls android.graphics.BitmapFactory.decodeByteArray(). On a plain JVM, this unmocked Android stub throws RuntimeException("Stub!"), which the try-catch in bitmap() swallows and returns null. This causes the "valid Base64 but invalid image data" and "only prefix" test cases to pass silently without actually exercising the real decode path.

Migrate to Robolectric to shadow BitmapFactory and make these assertions meaningful. Consider adding a positive test that decodes a real PNG byte sequence and asserts a non-null Bitmap is returned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt`
around lines 14 - 17, Replace kotlin.test usage with JUnit4 + Robolectric:
annotate the test class with `@RunWith`(RobolectricTestRunner::class) and use
org.junit.Test and org.junit.Assert methods instead of kotlin.test; this ensures
android.graphics.BitmapFactory is shadowed. Update imports and test runner for
QRCodeCollectorTest so QRCodeCollector.bitmap() calls hit Robolectric's
BitmapFactory rather than the JVM stub. Add a positive test that supplies a
valid PNG byte array (real image bytes encoded as Base64) to
QRCodeCollector.bitmap() and assert the returned Bitmap is non-null using
Assert.assertNotNull, and keep the existing negative tests but ensure they now
exercise the real decode path. Ensure unique references:
QRCodeCollector.bitmap(), QRCodeCollectorTest class, and the test method names
are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt`:
- Around line 1-6: Update the MIT license header year range in newly added
source files (e.g., PollingCollector.kt and PollingCollectorTest.kt) from "2026"
to "2025 - 2026"; find the top-of-file comment block and replace the single year
with the range "2025 - 2026" so every new source file follows the required
copyright header format.
- Around line 351-356: The catch block in PollingCollector.kt currently swallows
CancellationException (catch (e: Exception)) which prevents cooperative
coroutine cancellation; update the try/catch inside the polling loop (the block
around emit(PollingStatus.Error(e)) in class PollingCollector / function
handling the poll) to either rethrow when the caught exception is a
kotlinx.coroutines.CancellationException (if (e is CancellationException) throw
e) before logging/emitting, or narrow the catch to non-cancellation exception
types (e.g., IOException, JsonParseException); also add the import
kotlinx.coroutines.CancellationException if you choose the explicit rethrow
approach.
- Around line 303-307: The current PollingCollector code builds pollingUrl by
deriving baseUrl with selfHref.substringBefore("/davinci/connections"), which is
brittle and can return the full href when the delimiter is missing; replace this
with robust URL parsing (e.g., use java.net.URL) to extract the origin/authority
from selfHref (or explicitly validate that substringBefore found the delimiter
before using it) and then construct pollingUrl as
"<origin>/davinci/user/credentials/challenge/$challenge/status"; update
references to selfHref, baseUrl and pollingUrl in the PollingCollector code to
use the parsed origin or throw/log a clear error when selfHref is invalid.

In `@davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt`:
- Around line 121-135: Replace the unchecked cast on the retrieved ContinueNode
with a safe cast to Connector: change how you obtain existing (use as? Connector
with a null check returning FailureNode) so ClassCastException cannot be thrown;
update the Connector construction to either use the current active context
(context) if rewind should not reuse the old FlowContext, or keep
existing.context but add an explicit comment above the creation explaining the
intentional rewind semantics (preserving old cookies/headers/tokens) so the
behavior is clear; target symbols: CONTINUE_NODE, ContinueNode, Connector,
existing.context and CollectorFactory.inject.

In `@davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt`:
- Around line 1-6: Update the file header to use the correct copyright year
range "2025-2026" instead of "2026"; replace the first comment block in
davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt so the
MIT license header begins with "Copyright (c) 2025-2026 Ping Identity
Corporation. All rights reserved."; ensure the rest of the header text (license
reference lines) remains unchanged.

In
`@samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/QRCode.kt`:
- Around line 1-6: Update the file header in QRCode.kt to use the required
copyright year range "2025–2026" instead of "2026"; specifically change the
top-of-file MIT license header comment in QRCode.kt so the copyright line reads
"Copyright (c) 2025–2026 Ping Identity Corporation. All rights reserved." while
keeping the rest of the license text unchanged.

In `@samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt`:
- Around line 117-122: Replace the direct android.util.Log usage inside the
key(node) composition block in DaVinci.kt with the project's Logger interface
from foundation/logger: find the Log.e("DaVinci", ...) call and swap it for the
Logger instance (use its error method and preserve the same message/context),
ensuring imports are updated and no android.util.Log is referenced; also update
the file header copyright range string "2024-2026" to "2025-2026" to match the
new source file guideline.

---

Duplicate comments:
In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/QRCodeCollector.kt`:
- Around line 1-6: Update the file header in QRCodeCollector.kt so the MIT
license copyright line covers "2025–2026 Ping Identity Corporation" instead of
just 2026; edit the top comment block (the file header comment) to list the
years 2025–2026 to comply with the project guideline that every new source file
includes the 2025–2026 header.

In
`@samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Polling.kt`:
- Around line 1-6: Update the file header in Polling.kt to use the correct
copyright range 2025–2026 instead of just 2026; edit the top comment block (the
file-level header comment) so the line that currently reads "Copyright (c) 2026
Ping Identity Corporation." becomes "Copyright (c) 2025–2026 Ping Identity
Corporation." to match other new files in the PR.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt`:
- Around line 1-6: Update the copyright header in Polling.kt to cover the
2025–2026 range: replace the current "2026" year in the top comment block with
"2025–2026" (use an en dash) so the MIT license header matches the project's
required range.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/QRCode.kt`:
- Around line 1-6: Update the source file header in QRCode.kt to use the
required copyright range "2025 - 2026" instead of the current year; locate the
file/class QRCode.kt and replace the existing copyright line in the MIT license
header so it reads "Copyright (c) 2025 - 2026 Ping Identity Corporation. All
rights reserved." ensuring the rest of the license text remains unchanged.

---

Nitpick comments:
In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt`:
- Around line 214-226: The init method currently sets retriesAllowed =
pollRetries.toInt() which can throw NumberFormatException for non-numeric input;
change the conversion to a defensive form (use pollRetries.toIntOrNull() with a
fallback like 60) and assign retriesAllowed from that safe value so
initialization never throws. Update the PollingCollector.init function to parse
pollRetries defensively (mirror the poll() behavior) before assigning
retriesAllowed, keeping pollRetries as the original string and using the safe
parsed integer as the counter.

In `@davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt`:
- Line 38: Replace the kotlin.test.BeforeTest import with JUnit 4's
org.junit.Before in the PollingCollectorTest; update any setup method annotated
with `@BeforeTest` (e.g., setUp in class PollingCollectorTest) to use `@Before` so
the file consistently uses JUnit 4 annotations alongside existing org.junit.Test
and Assert imports.

In `@davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt`:
- Around line 14-17: Replace kotlin.test usage with JUnit4 + Robolectric:
annotate the test class with `@RunWith`(RobolectricTestRunner::class) and use
org.junit.Test and org.junit.Assert methods instead of kotlin.test; this ensures
android.graphics.BitmapFactory is shadowed. Update imports and test runner for
QRCodeCollectorTest so QRCodeCollector.bitmap() calls hit Robolectric's
BitmapFactory rather than the JVM stub. Add a positive test that supplies a
valid PNG byte array (real image bytes encoded as Base64) to
QRCodeCollector.bitmap() and assert the returned Bitmap is non-null using
Assert.assertNotNull, and keep the existing negative tests but ensure they now
exercise the real decode path. Ensure unique references:
QRCodeCollector.bitmap(), QRCodeCollectorTest class, and the test method names
are updated accordingly.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt`:
- Around line 44-83: Remove the redundant inner coroutine and manual Job
tracking: inside the LaunchedEffect(field) block call field.poll().collect { ...
} directly instead of launching a new coroutine and assigning pollingJob; drop
the pollingJob variable and the DisposableEffect(field) cleanup since
LaunchedEffect already cancels on disposal or key changes. Keep the same
handling for PollingStatus branches (Continue, Complete, TimedOut, Error,
Expired) but remove the external isPolling/pollingJob cancellation logic and any
imports only used for Job/launch/DisposableEffect; leave onNext(), statusMessage
and isPolling assignments as-is where needed.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3dde2656-fa58-4e9a-ad32-ac809a6ff41f

📥 Commits

Reviewing files that changed from the base of the PR and between a8272e6 and 4647395.

📒 Files selected for processing (17)
  • davinci/src/main/kotlin/com/pingidentity/davinci/CollectorRegistry.kt
  • davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt
  • davinci/src/main/kotlin/com/pingidentity/davinci/collector/QRCodeCollector.kt
  • davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/CollectorRegistryTest.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.Response.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt
  • davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/ContinueNode.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Polling.kt
  • samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/QRCode.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/DaVinci.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/DaVinciContinueNode.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/Polling.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/QRCode.kt

Comment on lines +1 to +6
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
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.

⚠️ Potential issue | 🟡 Minor

License header year range should be 2025 - 2026.

As per coding guidelines: "Every new source file must include the MIT license header with copyright 2025-2026 Ping Identity Corporation". The same issue applies to the new PollingCollectorTest.kt and any other newly added files.

📝 Proposed header fix
 /*
- * Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
+ * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
  *
  * This software may be modified and distributed under the terms
  * of the MIT license. See the LICENSE file for details.
  */
📝 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
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
/*
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt`
around lines 1 - 6, Update the MIT license header year range in newly added
source files (e.g., PollingCollector.kt and PollingCollectorTest.kt) from "2026"
to "2025 - 2026"; find the top-of-file comment block and replace the single year
with the range "2025 - 2026" so every new source file follows the required
copyright header format.

Comment on lines +303 to +307
if (selfHref != null && interactionId != null) {
// Construct the polling endpoint URL
// Example: https://auth.pingone.ca/env-id/davinci/user/credentials/challenge/abc123/status
val baseUrl = selfHref.substringBefore("/davinci/connections")
val pollingUrl = "$baseUrl/davinci/user/credentials/challenge/$challenge/status"
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any server-provided polling/status link in sample responses and docs
rg -nP -C2 '"(poll|status|challenge)"\s*:\s*\{' 
rg -nP -C2 'davinci/user/credentials/challenge'

Repository: ForgeRock/ping-android-sdk

Length of output: 5348


🏁 Script executed:

#!/bin/bash
# Read the PollingCollector.kt to understand the context around lines 303-307
echo "=== PollingCollector.kt context (lines 280-320) ==="
sed -n '280,320p' davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt | cat -n

Repository: ForgeRock/ping-android-sdk

Length of output: 2531


🏁 Script executed:

#!/bin/bash
# Read the test file to see what selfHref values are being tested
echo "=== PollingCollectorTest.kt ==="
cat -n davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt | head -200

Repository: ForgeRock/ping-android-sdk

Length of output: 8588


🏁 Script executed:

#!/bin/bash
# Search for where selfHref comes from and how it's obtained
echo "=== Search for selfHref assignment/origin ==="
rg -n "selfHref\s*=" davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt -B5 -A2

Repository: ForgeRock/ping-android-sdk

Length of output: 612


🏁 Script executed:

#!/bin/bash
# Search for other test cases and selfHref patterns
echo "=== Other test cases with selfHref ==="
rg -n "href" davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt -B2 -A2

Repository: ForgeRock/ping-android-sdk

Length of output: 1606


🏁 Script executed:

#!/bin/bash
# Check full PollingCollectorTest to see all test cases
echo "=== Full PollingCollectorTest.kt ==="
wc -l davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt
tail -150 davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt

Repository: ForgeRock/ping-android-sdk

Length of output: 5717


🏁 Script executed:

#!/bin/bash
# Check for any URL parsing utilities or helpers in the codebase
echo "=== Look for URL parsing utilities ==="
rg -n "substringBefore|substringAfter|URL\.parse|URI|toURL" davinci/src/main/kotlin/ --type kt | head -20

Repository: ForgeRock/ping-android-sdk

Length of output: 152


🏁 Script executed:

#!/bin/bash
# Search for URL parsing utilities without the problematic type flag
echo "=== Look for URL parsing utilities ==="
rg -n "substringBefore|substringAfter|URL|URI" davinci/src/main/kotlin/com/pingidentity/davinci/ | head -30

Repository: ForgeRock/ping-android-sdk

Length of output: 2184


🏁 Script executed:

#!/bin/bash
# Check if there are any other selfHref patterns in actual API responses or docs
echo "=== Check for API response examples or documentation ==="
fd -e json -e md davinci/src/ | xargs grep -l "davinci/connections\|davinci/user/credentials" | head -10

Repository: ForgeRock/ping-android-sdk

Length of output: 497


🏁 Script executed:

#!/bin/bash
# Read the actual test to understand what URL the test expects after parsing
echo "=== Full test case showing URL construction ==="
sed -n '135,193p' davinci/src/test/kotlin/com/pingidentity/davinci/PollingCollectorTest.kt | cat -n

Repository: ForgeRock/ping-android-sdk

Length of output: 2869


Brittle base-URL extraction via substringBefore.

The selfHref.substringBefore("/davinci/connections") approach silently fails if the URL structure changes. If /davinci/connections is absent, substringBefore returns the entire href unchanged, producing a malformed polling URL. Use proper URL parsing (e.g., java.net.URL) to extract the origin/authority, or validate that the substring operation succeeded before constructing the polling endpoint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt`
around lines 303 - 307, The current PollingCollector code builds pollingUrl by
deriving baseUrl with selfHref.substringBefore("/davinci/connections"), which is
brittle and can return the full href when the delimiter is missing; replace this
with robust URL parsing (e.g., use java.net.URL) to extract the origin/authority
from selfHref (or explicitly validate that substringBefore found the delimiter
before using it) and then construct pollingUrl as
"<origin>/davinci/user/credentials/challenge/$challenge/status"; update
references to selfHref, baseUrl and pollingUrl in the PollingCollector code to
use the parsed origin or throw/log a clear error when selfHref is invalid.

Comment on lines +351 to +356
} catch (e: Exception) {
// Handle network errors, JSON parsing errors, or other exceptions
davinci.config.logger.w("Error polling challenge status", e)
emit(PollingStatus.Error(e))
shouldContinuePolling = false // Exit the polling loop on exception
}
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.

⚠️ Potential issue | 🔴 Critical

catch (Exception) swallows CancellationException and breaks coroutine cancellation.

kotlinx.coroutines.CancellationException extends Exception, so this catch block will intercept cancellation, emit PollingStatus.Error, and suppress cooperative cancellation of the surrounding viewModelScope/lifecycleScope. If the user navigates away mid-poll, the flow will continue to leak an error emission instead of cancelling cleanly.

Either rethrow CancellationException explicitly or narrow the catch.

🔒 Proposed fix
-                    } catch (e: Exception) {
+                    } catch (e: CancellationException) {
+                        throw e
+                    } catch (e: Exception) {
                         // Handle network errors, JSON parsing errors, or other exceptions
                         davinci.config.logger.w("Error polling challenge status", e)
                         emit(PollingStatus.Error(e))
                         shouldContinuePolling = false // Exit the polling loop on exception
                     }

Remember to import kotlinx.coroutines.CancellationException.

📝 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
} catch (e: Exception) {
// Handle network errors, JSON parsing errors, or other exceptions
davinci.config.logger.w("Error polling challenge status", e)
emit(PollingStatus.Error(e))
shouldContinuePolling = false // Exit the polling loop on exception
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
// Handle network errors, JSON parsing errors, or other exceptions
davinci.config.logger.w("Error polling challenge status", e)
emit(PollingStatus.Error(e))
shouldContinuePolling = false // Exit the polling loop on exception
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt`
around lines 351 - 356, The catch block in PollingCollector.kt currently
swallows CancellationException (catch (e: Exception)) which prevents cooperative
coroutine cancellation; update the try/catch inside the polling loop (the block
around emit(PollingStatus.Error(e)) in class PollingCollector / function
handling the poll) to either rethrow when the caught exception is a
kotlinx.coroutines.CancellationException (if (e is CancellationException) throw
e) before logging/emitting, or narrow the catch to non-cancellation exception
types (e.g., IOException, JsonParseException); also add the import
kotlinx.coroutines.CancellationException if you choose the explicit rethrow
approach.

Comment on lines +121 to +135
val eventName = json["eventName"]?.jsonPrimitive?.content
if (eventName == "rewindStateToLastRenderedUI" || eventName == "rewindStateToSpecificRenderedUI") {
val existing = context.flowContext.getValue<ContinueNode>(CONTINUE_NODE)
?: return FailureNode(IllegalStateException("Rewind state to last rendered UI failed."))
// Create a new Connector instance with the same so that Jetpack Compose
// sees a different object reference and triggers recomposition and its collectors.
return Connector(
existing.context,
(existing as Connector).daVinci,
existing.input,
existing.collectors,
).apply {
CollectorFactory.inject(this)
}
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how CONTINUE_NODE is written and read, and whether any non-Connector ContinueNode is ever stored.
rg -nP --type=kotlin -C3 '\bCONTINUE_NODE\b'
rg -nP --type=kotlin -C3 'flowContext\[CONTINUE_NODE\]|getValue<ContinueNode>'
# Find all subclasses/producers of ContinueNode in the davinci module
ast-grep --pattern $'class $_ : ContinueNode($$$)'
ast-grep --pattern $'class $_($$$) : ContinueNode($$$)'

Repository: ForgeRock/ping-android-sdk

Length of output: 4933


🏁 Script executed:

# Find ContinueNode definition and all implementations
rg -nP --type=kotlin 'interface ContinueNode|class.*ContinueNode|sealed.*ContinueNode'

# Find Connector class definition and constructor
rg -nP --type=kotlin -B5 -A10 'class Connector'

# Find where daVinci is sourced in Transform.kt
rg -nP --type=kotlin -B10 -A10 'Transform.kt.*daVinci'

# Look at the Connector constructor specifically
fd -e kt -x grep -l "class Connector" {} \;

Repository: ForgeRock/ping-android-sdk

Length of output: 3307


🏁 Script executed:

# Read the Connector.kt node hook where CONTINUE_NODE is stored
cat -n davinci/src/main/kotlin/com/pingidentity/davinci/module/Connector.kt | sed -n '135,150p'

# Read the full Transform.kt context around the rewind logic
cat -n davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt | sed -n '110,140p'

# Find the Transform function signature to see where daVinci/workflow comes from
rg -nP --type=kotlin -B5 'fun transform' davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt | head -40

Repository: ForgeRock/ping-android-sdk

Length of output: 2022


🏁 Script executed:

# Get full Transform function signature
cat -n davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt | sed -n '104,115p'

# Check ContinueNode definition in orchestrate
cat -n foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/Node.kt | sed -n '32,50p'

# Search for all ContinueNode subclasses across the entire codebase
rg -nP --type=kotlin ': ContinueNode\(' --glob='!**/test/**'

Repository: ForgeRock/ping-android-sdk

Length of output: 1562


🏁 Script executed:

# Check if journey module defines its own CONTINUE_NODE
rg -nP --type=kotlin 'CONTINUE_NODE' journey/

# Verify that the CONTINUE_NODE key is davinci-specific
grep -r "CONTINUE_NODE" davinci/src/main/kotlin/ | grep -E 'const|val'

Repository: ForgeRock/ping-android-sdk

Length of output: 364


🏁 Script executed:

# Check how Connector.context is used internally
rg -nP --type=kotlin -A5 'this.context|existing.context' davinci/src/main/kotlin/com/pingidentity/davinci/module/Connector.kt

# Look at how ContinueNode.next() calls workflow.next() with its context
cat -n foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/Node.kt | sed -n '38,60p'

# Check what happens after a node is returned from transform — how is it used
rg -nP --type=kotlin -B3 -A3 'transform(context|Transform.transform' davinci/src/main/kotlin/com/pingidentity/davinci/module/ | head -50

Repository: ForgeRock/ping-android-sdk

Length of output: 877


🏁 Script executed:

# Find where transform() is called and how its return value is used
rg -nP --type=kotlin 'transform\(' davinci/src/main/kotlin/com/pingidentity/davinci/module/

# Check if there's any documentation or tests showing the rewind context intent
fd -e kt davinci/src/test -exec grep -l "rewind\|Rewind" {} \;

Repository: ForgeRock/ping-android-sdk

Length of output: 581


🏁 Script executed:

# Find where transform() is called and understand the flow
rg -nP --type=kotlin -B5 -A5 'return@transform transform' davinci/src/main/kotlin/

# Search for rewind in davinci test files
find davinci/src/test -name "*.kt" -type f -exec grep -l "rewind\|Rewind" {} \;

# Check what workflow.next() does when passed the node's context
rg -nP --type=kotlin 'fun next\(' foundation/orchestrate/ --glob='!**/test/**'

Repository: ForgeRock/ping-android-sdk

Length of output: 2018


🏁 Script executed:

# Check the test files for rewind behavior
cat davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.kt | head -100

# Also check the Response test file
cat davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.Response.kt | head -100

Repository: ForgeRock/ping-android-sdk

Length of output: 8223


🏁 Script executed:

# Verify the context usage: when a Connector calls next(), what context does it use?
rg -nP --type=kotlin -A10 'suspend fun next' foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/Node.kt

# Check how workflow.next() uses the context parameter
cat -n foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/Workflow.kt | sed -n '145,160p'

# Look for any tests involving rewind to understand intent
rg -nP --type=kotlin 'rewindState' davinci/src/test/

Repository: ForgeRock/ping-android-sdk

Length of output: 2713


🏁 Script executed:

# Read the rewind test to understand the expected behavior
cat -n davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.kt | sed -n '557,600p'

# Also read the second rewind test
cat -n davinci/src/test/kotlin/com/pingidentity/davinci/DaVinciTest.kt | sed -n '601,640p'

Repository: ForgeRock/ping-android-sdk

Length of output: 4514


Use safe cast for Connector instead of unchecked cast.

The existing as Connector cast will throw ClassCastException if CONTINUE_NODE ever holds a ContinueNode that isn't a Connector. While only Connector is currently stored in the davinci module, this is not enforced at compile time. Adopt the safe cast pattern already used in ErrorNode.kt (line 32) for consistency and defensiveness:

val existing = context.flowContext.getValue<ContinueNode>(CONTINUE_NODE) as? Connector
    ?: return FailureNode(IllegalStateException("Rewind state to last rendered UI failed."))

Additionally, confirm that reusing existing.context (the old FlowContext snapshot) is the intended behavior for rewind semantics. The new Connector is constructed with the previous context, which means subsequent next() calls will operate against that captured context state rather than the current active context. This intentionally preserves the old state (including cookies, headers, PAR tokens) at the cost of divergence. If this is by design, add an explicit comment explaining the rewind semantics; if not, pass context (the current active context) to the new Connector.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@davinci/src/main/kotlin/com/pingidentity/davinci/module/Transform.kt` around
lines 121 - 135, Replace the unchecked cast on the retrieved ContinueNode with a
safe cast to Connector: change how you obtain existing (use as? Connector with a
null check returning FailureNode) so ClassCastException cannot be thrown; update
the Connector construction to either use the current active context (context) if
rewind should not reuse the old FlowContext, or keep existing.context but add an
explicit comment above the creation explaining the intentional rewind semantics
(preserving old cookies/headers/tokens) so the behavior is clear; target
symbols: CONTINUE_NODE, ContinueNode, Connector, existing.context and
CollectorFactory.inject.

Comment on lines +1 to +6
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
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.

⚠️ Potential issue | 🟡 Minor

Copyright header year range should be 2025-2026.

As per coding guidelines, "Every new source file must include the MIT license header with copyright 2025-2026 Ping Identity Corporation".

📝 Proposed fix
-/*
- * Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
+/*
+ * Copyright (c) 2025-2026 Ping Identity Corporation. All rights reserved.
📝 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
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
/*
* Copyright (c) 2025-2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt`
around lines 1 - 6, Update the file header to use the correct copyright year
range "2025-2026" instead of "2026"; replace the first comment block in
davinci/src/test/kotlin/com/pingidentity/davinci/QRCodeCollectorTest.kt so the
MIT license header begins with "Copyright (c) 2025-2026 Ping Identity
Corporation. All rights reserved."; ensure the rest of the header text (license
reference lines) remains unchanged.

Comment on lines +1 to +6
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
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.

⚠️ Potential issue | 🟡 Minor

Copyright header should cover 2025–2026.

As per coding guidelines ("Every new source file must include the MIT license header with copyright 2025-2026 Ping Identity Corporation"), new files should use the 2025 - 2026 range.

Proposed fix
-/*
- * Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
+/*
+ * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
📝 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
/*
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
/*
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/QRCode.kt`
around lines 1 - 6, Update the file header in QRCode.kt to use the required
copyright year range "2025–2026" instead of "2026"; specifically change the
top-of-file MIT license header comment in QRCode.kt so the copyright line reads
"Copyright (c) 2025–2026 Ping Identity Corporation. All rights reserved." while
keeping the rest of the license text unchanged.

Comment on lines +117 to 122
key(node) {
Render(node = node, onNodeUpdated, onStart) {
onNext(node)
}
}
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm rewind reuses the original ContinueNode instance rather than building a new one
fd -t f 'Transform.kt' davinci --exec cat {}

Repository: ForgeRock/ping-android-sdk

Length of output: 6144


🏁 Script executed:

fd -t f -p 'samples/app' 'DaVinci.kt'

Repository: ForgeRock/ping-android-sdk

Length of output: 167


🏁 Script executed:

# Also check the structure to find the correct path
git ls-files | grep -i 'davinci.*\.kt' | head -20

Repository: ForgeRock/ping-android-sdk

Length of output: 1502


🏁 Script executed:

git ls-files | grep 'samples/app.*[Dd]a[Vv]inci'

Repository: ForgeRock/ping-android-sdk

Length of output: 2259


🏁 Script executed:

# Let's search more broadly for the samples app
git ls-files | grep 'samples.*\.kt$' | head -30

Repository: ForgeRock/ping-android-sdk

Length of output: 2443


🏁 Script executed:

cat -n samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt | sed -n '110,130p'

Repository: ForgeRock/ping-android-sdk

Length of output: 858


🏁 Script executed:

# Also check the copyright header
head -10 samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt

Repository: ForgeRock/ping-android-sdk

Length of output: 349


🏁 Script executed:

# Check the Connector class to understand its relationship to ContinueNode
cat samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/ContinueNode.kt

Repository: ForgeRock/ping-android-sdk

Length of output: 5265


Address logging violation and copyright year issue before approving.

The key(node) approach is correct for forcing recomposition on rewind. However, there are two issues:

  1. Line 125: Replace Log.e("DaVinci", ...) with the Logger interface from foundation/logger per guidelines (do not use android.util.Log directly).

  2. Copyright header: Update to "2025-2026" instead of "2024-2026" per coding guidelines for new source files.

The keying works because the rewind logic in Transform.kt deliberately creates a new ContinueNode instance (the comment states: "Create a new Connector instance... so that Jetpack Compose sees a different object reference and triggers recomposition"), so the key detects the changed reference and resets composition state as intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@samples/app/src/main/java/com/pingidentity/samples/app/davinci/DaVinci.kt`
around lines 117 - 122, Replace the direct android.util.Log usage inside the
key(node) composition block in DaVinci.kt with the project's Logger interface
from foundation/logger: find the Log.e("DaVinci", ...) call and swap it for the
Logger instance (use its error method and preserve the same message/context),
ensuring imports are updated and no android.util.Log is referenced; also update
the file header copyright range string "2024-2026" to "2025-2026" to match the
new source file guideline.

… flows

- Implement `PollingCollector` to handle asynchronous operations like push notifications and OOB authentication, supporting both simple delays and active challenge status polling.
- Implement `QRCodeCollector` to handle Base64-encoded QR code images for device pairing and MFA setup.
- Add Compose UI components (`Polling.kt`, `QRCode.kt`) and integrate them into `ContinueNode`.
- Register new collectors in `CollectorRegistry`.
- Add comprehensive unit tests for `PollingCollector` and `QRCodeCollector` logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants