Skip to content

Commit 6be87ba

Browse files
committed
SDKS-4679 & SDKS-4681: Add Polling and QR Code collectors for DaVinci 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.
1 parent cb96135 commit 6be87ba

9 files changed

Lines changed: 1437 additions & 4 deletions

File tree

davinci/src/main/kotlin/com/pingidentity/davinci/CollectorRegistry.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2024 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -15,6 +15,8 @@ import com.pingidentity.davinci.collector.LabelCollector
1515
import com.pingidentity.davinci.collector.MultiSelectCollector
1616
import com.pingidentity.davinci.collector.PasswordCollector
1717
import com.pingidentity.davinci.collector.PhoneNumberCollector
18+
import com.pingidentity.davinci.collector.PollingCollector
19+
import com.pingidentity.davinci.collector.QRCodeCollector
1820
import com.pingidentity.davinci.collector.SingleSelectCollector
1921
import com.pingidentity.davinci.collector.SubmitCollector
2022
import com.pingidentity.davinci.collector.TextCollector
@@ -54,5 +56,8 @@ internal class CollectorRegistry : ModuleInitializer() {
5456
CollectorFactory.register("DEVICE_REGISTRATION", ::DeviceRegistrationCollector)
5557
CollectorFactory.register("DEVICE_AUTHENTICATION", ::DeviceAuthenticationCollector)
5658
CollectorFactory.register("PHONE_NUMBER", ::PhoneNumberCollector)
59+
60+
CollectorFactory.register("POLLING", ::PollingCollector)
61+
CollectorFactory.register("QR_CODE", ::QRCodeCollector)
5762
}
5863
}

davinci/src/main/kotlin/com/pingidentity/davinci/collector/PollingCollector.kt

Lines changed: 408 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
8+
package com.pingidentity.davinci.collector
9+
10+
import android.graphics.Bitmap
11+
import android.graphics.BitmapFactory
12+
import com.pingidentity.davinci.plugin.Collector
13+
import kotlinx.serialization.json.JsonObject
14+
import kotlinx.serialization.json.jsonPrimitive
15+
import kotlin.io.encoding.Base64
16+
17+
/**
18+
* A collector that handles QR code display in DaVinci authentication flows.
19+
*
20+
* The QRCodeCollector is used to display QR codes to users for authentication scenarios such as:
21+
* - Device pairing
22+
* - Multi-factor authentication setup
23+
* - Out-of-band authentication
24+
* - Cross-device authentication flows
25+
*
26+
* The QR code content is typically provided as a Base64-encoded image string that can be
27+
* decoded and displayed as a bitmap.
28+
*
29+
* ## Content Format
30+
*
31+
* The [content] field typically contains a data URI string with Base64-encoded image data:
32+
* ```
33+
* data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
34+
* ```
35+
*
36+
* @see Collector
37+
* @see bitmap
38+
*/
39+
class QRCodeCollector : Collector<Nothing> {
40+
41+
/**
42+
* The QR code content as a Base64-encoded data URI string.
43+
*
44+
* This typically contains the full data URI including the MIME type and Base64 prefix:
45+
* `data:image/png;base64,{base64-encoded-data}`
46+
*
47+
* The [bitmap] method extracts and decodes this content to create a displayable bitmap.
48+
*/
49+
lateinit var content: String
50+
private set
51+
52+
/**
53+
* Fallback text to display when the QR code cannot be rendered.
54+
*
55+
* This text provides an alternative way for users to complete the authentication
56+
* if the QR code cannot be displayed or scanned. It may contain:
57+
* - A manual entry code
58+
* - Instructions for alternative authentication methods
59+
* - Error or help information
60+
*/
61+
lateinit var fallbackText: String
62+
private set
63+
64+
/**
65+
* Initializes the QRCodeCollector with configuration from the input JSON.
66+
*
67+
* Extracts and sets the following parameters:
68+
* - [content]: QR code content as Base64-encoded data URI (default: "")
69+
* - [fallbackText]: Alternative text if QR code cannot be displayed (default: "")
70+
*
71+
* @param input JSON object containing the collector configuration with fields:
72+
* - `content`: The Base64-encoded QR code image data
73+
* - `fallbackText`: Alternative text for display
74+
* @return This QRCodeCollector instance for method chaining
75+
*
76+
* @see Collector.init
77+
*/
78+
override fun init(input: JsonObject): QRCodeCollector {
79+
super.init(input)
80+
content = input["content"]?.jsonPrimitive?.content ?: ""
81+
fallbackText = input["fallbackText"]?.jsonPrimitive?.content ?: ""
82+
return this
83+
}
84+
85+
/**
86+
* Converts the Base64-encoded QR code content to a displayable Bitmap.
87+
*
88+
* This method:
89+
* 1. Extracts the Base64 data from the [content] string (after "base64,")
90+
* 2. Decodes the Base64 string to a byte array
91+
* 3. Converts the byte array to a Bitmap using BitmapFactory
92+
*
93+
* ## Content Format
94+
*
95+
* The method expects [content] to be in data URI format:
96+
* ```
97+
* data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
98+
* ```
99+
*
100+
* The substring after "base64," is extracted and decoded.
101+
*
102+
* @return A Bitmap representation of the QR code, or `null` if decoding fails
103+
*
104+
* @see content
105+
* @see fallbackText
106+
*/
107+
fun bitmap(): Bitmap? {
108+
return try {
109+
// Decode Base64 content and convert to Bitmap
110+
val decodedBytes = Base64.decode(content.substringAfter("base64,"))
111+
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
112+
} catch (e: Exception) {
113+
// Return null if decoding fails
114+
null
115+
}
116+
}
117+
}

davinci/src/test/kotlin/com/pingidentity/davinci/CollectorRegistryTest.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
/*
2-
* Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2024 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
77

88
import com.pingidentity.davinci.CollectorRegistry
9+
import com.pingidentity.davinci.collector.DeviceAuthenticationCollector
10+
import com.pingidentity.davinci.collector.DeviceRegistrationCollector
911
import com.pingidentity.davinci.collector.FlowCollector
1012
import com.pingidentity.davinci.collector.LabelCollector
1113
import com.pingidentity.davinci.collector.MultiSelectCollector
1214
import com.pingidentity.davinci.collector.PasswordCollector
15+
import com.pingidentity.davinci.collector.PhoneNumberCollector
16+
import com.pingidentity.davinci.collector.PollingCollector
17+
import com.pingidentity.davinci.collector.QRCodeCollector
1318
import com.pingidentity.davinci.collector.SingleSelectCollector
1419
import com.pingidentity.davinci.collector.SubmitCollector
1520
import com.pingidentity.davinci.collector.TextCollector
@@ -61,9 +66,15 @@ class CollectorRegistryTest {
6166
add(buildJsonObject { put("inputType", "SINGLE_SELECT") })
6267
add(buildJsonObject { put("inputType", "MULTI_SELECT") })
6368
add(buildJsonObject { put("inputType", "MULTI_SELECT") })
69+
add(buildJsonObject { put("inputType", "DEVICE_REGISTRATION") })
70+
add(buildJsonObject { put("inputType", "DEVICE_AUTHENTICATION") })
71+
add(buildJsonObject { put("inputType", "PHONE_NUMBER") })
72+
add(buildJsonObject { put("type", "POLLING") })
73+
add(buildJsonObject { put("type", "QR_CODE") })
6474
}
6575

6676
val collectors = CollectorFactory.collector(mockk(), jsonArray)
77+
assertEquals(16, collectors.size)
6778
assertEquals(TextCollector::class.java, collectors[0]::class.java)
6879
assertEquals(PasswordCollector::class.java, collectors[1]::class.java)
6980
assertEquals(PasswordCollector::class.java, collectors[2]::class.java)
@@ -75,6 +86,11 @@ class CollectorRegistryTest {
7586
assertEquals(SingleSelectCollector::class.java, collectors[8]::class.java)
7687
assertEquals(MultiSelectCollector::class.java, collectors[9]::class.java)
7788
assertEquals(MultiSelectCollector::class.java, collectors[10]::class.java)
89+
assertEquals(DeviceRegistrationCollector::class.java, collectors[11]::class.java)
90+
assertEquals(DeviceAuthenticationCollector::class.java, collectors[12]::class.java)
91+
assertEquals(PhoneNumberCollector::class.java, collectors[13]::class.java)
92+
assertEquals(PollingCollector::class.java, collectors[14]::class.java)
93+
assertEquals(QRCodeCollector::class.java, collectors[15]::class.java)
7894
}
7995

8096
@TestRailCase(21280)
@@ -88,10 +104,11 @@ class CollectorRegistryTest {
88104
add(buildJsonObject { put("type", "SUBMIT_BUTTON") })
89105
add(buildJsonObject { put("inputType", "ACTION") })
90106
add(buildJsonObject { put("type", "UNKNOWN") })
107+
add(buildJsonObject { put("type", "QR_CODE") })
91108
}
92109

93110
val collectors = CollectorFactory.collector(mockk(), jsonArray)
94-
assertEquals(4, collectors.size)
111+
assertEquals(5, collectors.size)
95112
}
96113

97114
}

0 commit comments

Comments
 (0)