-
Notifications
You must be signed in to change notification settings - Fork 3
SDKS-4679 & SDKS-4681: Add Polling and QR Code collectors for DaVinci flows #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| package com.pingidentity.davinci.collector | ||
|
|
||
| import android.graphics.Bitmap | ||
| import android.graphics.BitmapFactory | ||
| import com.pingidentity.davinci.plugin.Collector | ||
| import kotlinx.serialization.json.JsonObject | ||
| import kotlinx.serialization.json.jsonPrimitive | ||
| import kotlin.io.encoding.Base64 | ||
|
|
||
| /** | ||
| * A collector that handles QR code display in DaVinci authentication flows. | ||
| * | ||
| * The QRCodeCollector is used to display QR codes to users for authentication scenarios such as: | ||
| * - Device pairing | ||
| * - Multi-factor authentication setup | ||
| * - Out-of-band authentication | ||
| * - Cross-device authentication flows | ||
| * | ||
| * The QR code content is typically provided as a Base64-encoded image string that can be | ||
| * decoded and displayed as a bitmap. | ||
| * | ||
| * ## Content Format | ||
| * | ||
| * The [content] field typically contains a data URI string with Base64-encoded image data: | ||
| * ``` | ||
| * data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA... | ||
| * ``` | ||
| * | ||
| * @see Collector | ||
| * @see bitmap | ||
| */ | ||
| class QRCodeCollector : Collector<Nothing> { | ||
|
|
||
| /** | ||
| * The QR code content as a Base64-encoded data URI string. | ||
| * | ||
| * This typically contains the full data URI including the MIME type and Base64 prefix: | ||
| * `data:image/png;base64,{base64-encoded-data}` | ||
| * | ||
| * The [bitmap] method extracts and decodes this content to create a displayable bitmap. | ||
| */ | ||
| lateinit var content: String | ||
| private set | ||
|
|
||
| /** | ||
| * Fallback text to display when the QR code cannot be rendered. | ||
| * | ||
| * This text provides an alternative way for users to complete the authentication | ||
| * if the QR code cannot be displayed or scanned. It may contain: | ||
| * - A manual entry code | ||
| * - Instructions for alternative authentication methods | ||
| * - Error or help information | ||
| */ | ||
| lateinit var fallbackText: String | ||
| private set | ||
|
|
||
| /** | ||
| * Initializes the QRCodeCollector with configuration from the input JSON. | ||
| * | ||
| * Extracts and sets the following parameters: | ||
| * - [content]: QR code content as Base64-encoded data URI (default: "") | ||
| * - [fallbackText]: Alternative text if QR code cannot be displayed (default: "") | ||
| * | ||
| * @param input JSON object containing the collector configuration with fields: | ||
| * - `content`: The Base64-encoded QR code image data | ||
| * - `fallbackText`: Alternative text for display | ||
| * @return This QRCodeCollector instance for method chaining | ||
| * | ||
| * @see Collector.init | ||
| */ | ||
| override fun init(input: JsonObject): QRCodeCollector { | ||
| super.init(input) | ||
| content = input["content"]?.jsonPrimitive?.content ?: "" | ||
| fallbackText = input["fallbackText"]?.jsonPrimitive?.content ?: "" | ||
| return this | ||
| } | ||
|
|
||
| /** | ||
| * Converts the Base64-encoded QR code content to a displayable Bitmap. | ||
| * | ||
| * This method: | ||
| * 1. Extracts the Base64 data from the [content] string (after "base64,") | ||
| * 2. Decodes the Base64 string to a byte array | ||
| * 3. Converts the byte array to a Bitmap using BitmapFactory | ||
| * | ||
| * ## Content Format | ||
| * | ||
| * The method expects [content] to be in data URI format: | ||
| * ``` | ||
| * data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA... | ||
| * ``` | ||
| * | ||
| * The substring after "base64," is extracted and decoded. | ||
| * | ||
| * @return A Bitmap representation of the QR code, or `null` if decoding fails | ||
| * | ||
| * @see content | ||
| * @see fallbackText | ||
| */ | ||
| fun bitmap(): Bitmap? { | ||
| return try { | ||
| // Decode Base64 content and convert to Bitmap | ||
| val decodedBytes = Base64.decode(content.substringAfter("base64,")) | ||
| BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size) | ||
| } catch (e: Exception) { | ||
| // Return null if decoding fails | ||
| null | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /* | ||
| * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. | ||
| * Copyright (c) 2024 - 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. | ||
|
|
@@ -11,8 +11,10 @@ import com.pingidentity.davinci.collector.Form | |
| import com.pingidentity.davinci.plugin.Collector | ||
| import com.pingidentity.davinci.plugin.CollectorFactory | ||
| import com.pingidentity.davinci.plugin.DaVinci | ||
| import com.pingidentity.davinci.plugin.collectors | ||
| import com.pingidentity.exception.ApiException | ||
| import com.pingidentity.oidc.exception.AuthorizeException | ||
| import com.pingidentity.orchestrate.ContinueNode | ||
| import com.pingidentity.orchestrate.ErrorNode | ||
| import com.pingidentity.orchestrate.FailureNode | ||
| import com.pingidentity.orchestrate.FlowContext | ||
|
|
@@ -116,6 +118,22 @@ private fun transform( | |
| ) | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
|
Comment on lines
+121
to
+135
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -40Repository: 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 -50Repository: 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 -100Repository: 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 The val existing = context.flowContext.getValue<ContinueNode>(CONTINUE_NODE) as? Connector
?: return FailureNode(IllegalStateException("Rewind state to last rendered UI failed."))Additionally, confirm that reusing 🤖 Prompt for AI Agents |
||
|
|
||
| val collectors = mutableListOf<Collector<*>>() | ||
| if ("form" in json) collectors.addAll(Form.parse(daVinci, json)) | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.