diff --git a/PingSampleApp/ios/Podfile.lock b/PingSampleApp/ios/Podfile.lock
index c90092f75..5d686fc4a 100644
--- a/PingSampleApp/ios/Podfile.lock
+++ b/PingSampleApp/ios/Podfile.lock
@@ -3615,61 +3615,61 @@ SPEC CHECKSUMS:
RCTTypeSafety: d877728097547d0a37786cc9130c43ad71739ac3
React: 4b0b9cb962e694611e5e8a697c1b0300a2510c21
React-callinvoker: 70f125c17c7132811a6b473946ac5e7ae93b5e57
- React-Core: bab40f5b1f46fe0c5896895a6f333e861a821a81
- React-CoreModules: 05647d952e521113c128360633896ba7ba652e82
- React-cxxreact: 2b4bac1ec6eecc6288ac8a6caea6afb42585740e
+ React-Core: 7cbc3118df2334b2ef597d9a515938b02c82109f
+ React-CoreModules: 7d8c14ecb889e7786a04637583b55b7d8f246baf
+ React-cxxreact: f32be07cba236c2f20f4e05ca200577ba5358e78
React-debug: deb3a146ef717fa3e8f4c23e0288369fe53199b7
- React-defaultsnativemodule: 11e2948787a15d3cf1b66d7f29f13770a177bff7
- React-domnativemodule: 2f4b279acdb2963736fb5de2f585811dd90070b5
- React-Fabric: 6f8d1a303c96f1d078c14d74c4005bf457e5b782
- React-FabricComponents: b106410970e9a0c4e592da656c7a7e0947306c23
- React-FabricImage: 1abaf230dfce9b58fdf53c4128f3f40c6e64af6a
- React-featureflags: f7ef58d91079efde3ad223bcca6d197e845d5bcf
- React-featureflagsnativemodule: ae5abc9849d1696f4f8f11ee3744bf5715e032cf
- React-graphics: b306856c6ed9aac32f717a229550406a53b28a6d
- React-hermes: b6edce8fa19388654b1aea30844497cbeade83bc
- React-idlecallbacksnativemodule: cb386712842cb9e479c89311edb234d529b64db4
- React-ImageManager: 8ce94417853eaa22faaad1f4cc1952dd3f8e2275
- React-jserrorhandler: ab827d67dc270a9c8703eef524230baeafaf6876
- React-jsi: 545342ec5c78ab1277af5f0dbe8d489e7e73db14
- React-jsiexecutor: 20210891c7c77255c16dec6762faf68b373f9f74
- React-jsinspector: 4e73460e488132d70d2b4894e5578cc856f2cb74
- React-jsinspectorcdp: 8b2bcb5779289cb2b9ca517f2965ed23eb2fd3e0
- React-jsinspectornetwork: b5e0cb9e488d294eed2d8209dc3dc0f9587210c1
- React-jsinspectortracing: f3c4036e7b984405ac910f878576d325dd9f2834
- React-jsitooling: 75bbfd221b6173a5e848ca5a6680506bac064a56
- React-jsitracing: 11ed7d821864dd988c159d4943e0a1e0937c11b1
- React-logger: 984ebd897afad067555d081deaf03f57c4315723
- React-Mapbuffer: 0c045c844ce6d85cde53e85ab163294c6adad349
- React-microtasksnativemodule: d9499269ad1f484ae71319bac1d9231447f2094e
- react-native-config: adc7d6e70302363192ed8f2eae027864d218ff71
- react-native-safe-area-context: 01f6d357d42395422eed7c3bfe715614728f2989
- React-NativeModulesApple: 983f3483ef0a3446b56d490f09d579fba2442e17
+ React-defaultsnativemodule: 2c13a4240c5f96c42d069d1ba2392de6b4145bbd
+ React-domnativemodule: 91349b0b1cb20310cec1341b87cdd461aaa85e57
+ React-Fabric: bdfc7ec2481f26d7a9b8f59461f29ba4d903c549
+ React-FabricComponents: 47898469543d1bfb4528a9846419ec5568be89b1
+ React-FabricImage: ac8fc85ef452e5e9ae935c41118814651bd9e7f3
+ React-featureflags: 793b911e4c53e680db4a7d9965d0d6dc87b2fa88
+ React-featureflagsnativemodule: 25c9516d0dd004493c9bbafeb97da20bf9bde7dc
+ React-graphics: e07281690425dd9eeba3875d1faad28bc1f6da3b
+ React-hermes: bc1440d0e0662cc813bbf1c5ffbf9e0db2993a0f
+ React-idlecallbacksnativemodule: a2a3bb4a1793280b34d06d00169153b094be8c16
+ React-ImageManager: c9fa7461f3cab08e7bc98cbf55455b499e71c8b3
+ React-jserrorhandler: 15e591702040afed99cfcd088cf2337a8d09d807
+ React-jsi: 512ab3a1a628bc8824c41de8bcbbb81b2ac6fa8d
+ React-jsiexecutor: 653ccd2dee1e5ea558eecaf2f27b8bba0f09add8
+ React-jsinspector: 9121ccd2676a3f7c079ac01c9f90183422e3190e
+ React-jsinspectorcdp: 5c723ff2a09d73f2fdc496a545fb7003e7fdc079
+ React-jsinspectornetwork: 9cb0173f69e8405cef33fc79030fad26bbc3c073
+ React-jsinspectortracing: 65dc04125dc2392d85a82b6916f8cb088ea77566
+ React-jsitooling: 21af93cc98f760dd88d65b06b9317e0d4849fbbc
+ React-jsitracing: 4cc1b7de8087ae41c61a0eeee2593bc3362908b6
+ React-logger: 2f0d40bc8e648fbb1ff3b6580ad54189a8753290
+ React-Mapbuffer: 9a7c65078c6851397c1999068989e4fc239d0c80
+ React-microtasksnativemodule: 4f1ef719ba6c7ebbd2d75346ffa2916f9b4771c9
+ react-native-config: 85858ab5e598762e783a43ac592561ad110c35a9
+ react-native-safe-area-context: d446989793f96dc2f44c33c42dbfb316d983f24e
+ React-NativeModulesApple: f6f696e510b9d89c3c06b7764f56947dc13ae922
React-oscompat: 114036cd8f064558c9c1a0c04fc9ae5e1453706a
- React-perflogger: e7287fee27c16e3c8bd4d470f2361572b63be16b
- React-performancetimeline: 8ebbaa31d2d0cea680b0a2a567500d3cab8954fc
+ React-perflogger: 4b2f88ae059b600daf268528a4a83366338eef05
+ React-performancetimeline: e15fd9798123436f99e46898422fe921fecf506b
React-RCTActionSheet: 68c68b0a7a5d2b0cfc255c64889b6e485974e988
- React-RCTAnimation: d6c5c728b888a967ce9aff1ff71a8ed71a68d069
- React-RCTAppDelegate: 0fc048666bda159cd469a6fb9befb04b3fa62be4
- React-RCTBlob: 12d8c699a1f906840113ee8d8bb575e69a05509f
- React-RCTFabric: 01e815845ebc185f44205dcbf50eeb712fec23fe
- React-RCTFBReactNativeSpec: f57927fb0af6ce2f25c19f8b894e2986138aa89f
- React-RCTImage: a82518168f4ee407913b23ca749ca79ef51959f3
- React-RCTLinking: 7f343b584c36f024f390fea563483568fe763ef6
- React-RCTNetwork: 3165eb757ceb62a7cde4cdad043d63314122e8a3
- React-RCTRuntime: feee590c459c4cb6aaa7a00f3abc8c04709b536f
- React-RCTSettings: 6bad0ae45d8d872c873059f332f586f99875621f
- React-RCTText: 657d60f35983062de8f0cea67c279aa7a3ea9858
- React-RCTVibration: 78f4770515141efb7f55f9b27c49dda95319c3a8
+ React-RCTAnimation: 6bf502c89c53076f92cd1a254f5ec8d63ee263de
+ React-RCTAppDelegate: c90f5732784684c3dd226d812eccb578cd954ad7
+ React-RCTBlob: d2905f01749b80efd6d3b86fb15e30ed26d5450b
+ React-RCTFabric: 435b3ffaad113fb1f274c2f2a677c9fcc9b5cf55
+ React-RCTFBReactNativeSpec: a3178b419f42af196e90ca4bf07710dce5d68301
+ React-RCTImage: 8f5ffa03461339180a68820ea452af6e20ace2c7
+ React-RCTLinking: 1151646834d31f97580d8a75d768a84b2533b7f9
+ React-RCTNetwork: 52008724d0db90a540f4058ed0de0e41c4b7943c
+ React-RCTRuntime: 10ce9a7cb27ba307544d29a2a04e6202dc7b3e9a
+ React-RCTSettings: f724cacbd892ee18f985e1aebdd97386e49c76f5
+ React-RCTText: 6e1b95d9126d808410dfa96e09bc4441ec6f36f7
+ React-RCTVibration: 862a4e5b36d49e6299c8cbfb86486fc31f86f6fa
React-rendererconsistency: f7baab26c6d0cd5b2eb7afcecfd2d8b957017b18
- React-renderercss: bdd2f83a4a054c3e4321fd61305c202b848e471b
- React-rendererdebug: 9f8865ee038127a9d99d4b034c9da4935d204993
+ React-renderercss: 62acb8f010a062309e3bd0e203aa14636162e3b3
+ React-rendererdebug: 3a89ac44f15c7160735264d585a29525655238d2
React-rncore: f7438473c4c71ee1963fb06a8635bb96013c9e1c
- React-RuntimeApple: 4d2ab9f72b9193da86eceded128a67254fc18aeb
- React-RuntimeCore: 5fd73030438d094975ca0f549d162dd97746ae38
+ React-RuntimeApple: 81f0a9ba81ce7eb203529b0471dc69bf18f5f637
+ React-RuntimeCore: 6356e89b2518ba66a989c39a2adb18122a5e3b7b
React-runtimeexecutor: 17c70842d5e611130cb66f91e247bc4a609c3508
- React-RuntimeHermes: 3c88e6e1ea7ea0899dcffc77c10d61ea46688cfd
- React-runtimescheduler: 024500621c7c93d65371498abb4ee26d34f5d47d
+ React-RuntimeHermes: 0a1d7ce2fe08cf182235de1a9330b51aa6b935cd
+ React-runtimescheduler: 10ae98e1417eff159be5df8fdc8fcdaac557aba6
React-timing: c3c923df2b86194e1682e01167717481232f1dc7
React-utils: 7791a96e194eec85cb41dc98a2045b5f07839598
ReactAppDependencyProvider: ba631a31783569c13056dd57ff39e19764abdd6f
@@ -3680,7 +3680,7 @@ SPEC CHECKSUMS:
RNPingBinding: 1731d5254b7d026df05ee056ab7802f4888537e9
RNPingBrowser: b6afbf0fa46c9ae0a10daeb46927f05eca5c7c6d
RNPingCore: 3926e39f9ab60d83fb4d58225303864ca0d8171b
- RNPingDavinci: 81122638cb43d20c7e24e66eea5bd8700fd48aae
+ RNPingDavinci: 605780d605c87d00216376bb2b3f4f5ee72817e8
RNPingDeviceClient: 0ff89a604e97d1168eff3aba4b1d889139619bd7
RNPingDeviceId: 6eb3e9fc68a78e1aee87652e025f6e1ec6d5d67e
RNPingDeviceProfile: 3086f14fc5024196a64290afbbf7917432dd5900
@@ -3696,10 +3696,10 @@ SPEC CHECKSUMS:
RNSVG: e8fb86f41fccd7b67c4480bb9e179e0ad5785b80
RNVectorIcons: 54df27a2e90ddeb674c7237d76060ec9762d0bc5
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- VisionCamera: e82024ee7ce0fb6168a4c468ef43c711f4486baa
- VisionCameraBarcodeScanner: a65d708fd3672e6d43e94cce39b548465851752b
+ VisionCamera: 3914b29387fc40c64f7022830cfdc604f94eb966
+ VisionCameraBarcodeScanner: 0b6fe2e3c52ca68a0dbd5d25719c7523c68d807f
Yoga: daa1e4de4b971b977b23bc842aaa3e135324f1f3
PODFILE CHECKSUM: 35159028c49678100eea2347153c0cf4cd45998f
-COCOAPODS: 1.16.2
+COCOAPODS: 1.15.2
diff --git a/PingSampleApp/ui/davinci/components/molecules/DaVinciBooleanField.tsx b/PingSampleApp/ui/davinci/components/molecules/DaVinciBooleanField.tsx
new file mode 100644
index 000000000..d814a0c9e
--- /dev/null
+++ b/PingSampleApp/ui/davinci/components/molecules/DaVinciBooleanField.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+import React from 'react';
+import { Pressable, Switch, Text, View } from 'react-native';
+import type { BooleanCollector } from '@ping-identity/rn-davinci';
+import { davinciFieldStyles } from '../../../../src/styles/davinciStyles';
+import RichTextLabel from './RichTextLabel';
+import type { DaVinciCollectorRendererProps } from './types';
+
+/**
+ * Renders a {@link BooleanCollector} as a checkbox row or a switch toggle,
+ * depending on the collector's `appearance` field.
+ *
+ * @param props Renderer props.
+ * @returns Boolean field element.
+ */
+export default function DaVinciBooleanField(
+ props: DaVinciCollectorRendererProps,
+): React.ReactElement {
+ const { collector, value, onChange } = props;
+ const booleanCollector = collector as BooleanCollector;
+ const checked = typeof value === 'boolean' ? value : booleanCollector.value;
+ const showError =
+ booleanCollector.required && !checked && booleanCollector.errorMessage;
+
+ const label = booleanCollector.required
+ ? `${booleanCollector.label} *`
+ : booleanCollector.label;
+
+ if (booleanCollector.appearance === 'SWITCH') {
+ return (
+
+
+
+ onChange(next)}
+ accessibilityLabel={booleanCollector.label}
+ />
+
+ {showError ? (
+
+ {booleanCollector.errorMessage}
+
+ ) : null}
+
+ );
+ }
+
+ return (
+
+ onChange(!checked)}
+ accessibilityRole="checkbox"
+ accessibilityState={{ checked }}
+ accessibilityLabel={booleanCollector.label}
+ >
+
+ {checked ? ✓ : null}
+
+
+
+ {showError ? (
+
+ {booleanCollector.errorMessage}
+
+ ) : null}
+
+ );
+}
+
+const styles = {
+ switchRow: {
+ flexDirection: 'row' as const,
+ alignItems: 'center' as const,
+ justifyContent: 'space-between' as const,
+ },
+ switchLabel: {
+ flex: 1,
+ marginBottom: 0,
+ marginRight: 8,
+ },
+ checkboxRow: {
+ flexDirection: 'row' as const,
+ alignItems: 'center' as const,
+ },
+ checkmark: {
+ color: '#fff',
+ fontSize: 12,
+ fontWeight: '700' as const,
+ },
+};
diff --git a/PingSampleApp/ui/davinci/components/molecules/DaVinciFieldRenderer.tsx b/PingSampleApp/ui/davinci/components/molecules/DaVinciFieldRenderer.tsx
index 52b086463..cf941e848 100644
--- a/PingSampleApp/ui/davinci/components/molecules/DaVinciFieldRenderer.tsx
+++ b/PingSampleApp/ui/davinci/components/molecules/DaVinciFieldRenderer.tsx
@@ -6,6 +6,7 @@
*/
import React, { memo } from 'react';
+import DaVinciBooleanField from './DaVinciBooleanField';
import DaVinciDeviceField from './DaVinciDeviceField';
import DaVinciFlowButton from './DaVinciFlowButton';
import DaVinciIdpField from './DaVinciIdpField';
@@ -13,6 +14,7 @@ import DaVinciLabelField from './DaVinciLabelField';
import DaVinciMultiSelectField from './DaVinciMultiSelectField';
import DaVinciPasswordField from './DaVinciPasswordField';
import DaVinciPhoneNumberField from './DaVinciPhoneNumberField';
+import DaVinciReadOnlyTextField from './DaVinciReadOnlyTextField';
import DaVinciSingleSelectField from './DaVinciSingleSelectField';
import DaVinciSubmitButton from './DaVinciSubmitButton';
import DaVinciTextField from './DaVinciTextField';
@@ -42,8 +44,12 @@ function DaVinciFieldRenderer(
case 'FLOW_BUTTON':
case 'FLOW_LINK':
return ;
+ case 'SINGLE_CHECKBOX':
+ return ;
case 'LABEL':
return ;
+ case 'READ_ONLY_TEXT':
+ return ;
case 'SINGLE_SELECT':
case 'DROPDOWN':
case 'RADIO':
diff --git a/PingSampleApp/ui/davinci/components/molecules/DaVinciReadOnlyTextField.tsx b/PingSampleApp/ui/davinci/components/molecules/DaVinciReadOnlyTextField.tsx
new file mode 100644
index 000000000..c0705abcd
--- /dev/null
+++ b/PingSampleApp/ui/davinci/components/molecules/DaVinciReadOnlyTextField.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+import React from 'react';
+import { Text, View } from 'react-native';
+import type { ReadOnlyTextCollector } from '@ping-identity/rn-davinci';
+import { davinciFieldStyles } from '../../../../src/styles/davinciStyles';
+import type { DaVinciCollectorRendererProps } from './types';
+
+/**
+ * Renders a {@link ReadOnlyTextCollector} as a read-only content block,
+ * with an optional title when `titleEnabled` is `true`.
+ *
+ * @param props Renderer props.
+ * @returns Read-only text element.
+ */
+export default function DaVinciReadOnlyTextField(
+ props: DaVinciCollectorRendererProps,
+): React.ReactElement {
+ const { collector } = props;
+ const readOnlyCollector = collector as ReadOnlyTextCollector;
+
+ return (
+
+ {readOnlyCollector.titleEnabled && readOnlyCollector.title ? (
+
+ {readOnlyCollector.title}
+
+ ) : null}
+
+ {readOnlyCollector.content}
+
+
+ );
+}
diff --git a/PingSampleApp/ui/davinci/components/molecules/RichTextLabel.tsx b/PingSampleApp/ui/davinci/components/molecules/RichTextLabel.tsx
new file mode 100644
index 000000000..c070deea8
--- /dev/null
+++ b/PingSampleApp/ui/davinci/components/molecules/RichTextLabel.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+import React from 'react';
+import { Linking, Text } from 'react-native';
+import type { RichContent } from '@ping-identity/rn-davinci';
+import { colors } from '../../../../src/styles/colors';
+
+type Props = {
+ /** Plain-text fallback when no richContent is provided. */
+ text: string;
+ /** Optional rich content with link replacements. */
+ richContent?: RichContent;
+ style?: object;
+};
+
+/**
+ * Renders a string with optional inline hyperlinks from {@link RichContent}.
+ *
+ * Splits `richContent.content` on `{{token}}` placeholders and replaces each
+ * with a tappable `Text` span that opens `replacement.href` via `Linking`.
+ * Falls back to plain `text` when `richContent` is absent.
+ */
+export default function RichTextLabel({
+ text,
+ richContent,
+ style,
+}: Props): React.ReactElement {
+ if (!richContent || !richContent.replacements) {
+ return {text};
+ }
+
+ const parts = splitRichContent(richContent);
+
+ return (
+
+ {parts.map((part, index) =>
+ part.href ? (
+ {
+ if (part.href) {
+ Linking.openURL(part.href).catch(() => {});
+ }
+ }}
+ accessibilityRole="link"
+ accessibilityLabel={part.value}
+ >
+ {part.value}
+
+ ) : (
+ {part.value}
+ ),
+ )}
+
+ );
+}
+
+type ContentPart = { value: string; href?: string };
+
+function splitRichContent(richContent: RichContent): ContentPart[] {
+ const { content, replacements } = richContent;
+ const parts: ContentPart[] = [];
+ const tokenPattern = /\{\{(\w+)\}\}/g;
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+
+ while ((match = tokenPattern.exec(content)) !== null) {
+ if (match.index > lastIndex) {
+ parts.push({ value: content.slice(lastIndex, match.index) });
+ }
+ const token = match[1];
+ const replacement = replacements[token];
+ if (replacement) {
+ parts.push({ value: replacement.value, href: replacement.href });
+ } else {
+ parts.push({ value: match[0] });
+ }
+ lastIndex = match.index + match[0].length;
+ }
+
+ if (lastIndex < content.length) {
+ parts.push({ value: content.slice(lastIndex) });
+ }
+
+ return parts;
+}
+
+const linkStyle = {
+ color: colors.primary,
+ textDecorationLine: 'underline' as const,
+};
diff --git a/PingTestRunner/android/build.gradle b/PingTestRunner/android/build.gradle
index 231b98e5c..50a95a761 100644
--- a/PingTestRunner/android/build.gradle
+++ b/PingTestRunner/android/build.gradle
@@ -12,7 +12,7 @@ buildscript {
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
- kotlinVersion = "2.2.10"
+ kotlinVersion = "2.2.20"
}
repositories {
google()
diff --git a/PingTestRunner/ios/Podfile.lock b/PingTestRunner/ios/Podfile.lock
index fb77b2f84..87da52d56 100644
--- a/PingTestRunner/ios/Podfile.lock
+++ b/PingTestRunner/ios/Podfile.lock
@@ -2404,6 +2404,7 @@ PODS:
- hermes-engine
- PingDavinci (= 2.1.0)
- PingDavinciPlugin (= 2.1.0)
+ - PingExternalIdP (= 2.1.0)
- PingLogger (= 2.1.0)
- PingOidc (= 2.1.0)
- PingOrchestrate (= 2.1.0)
@@ -2440,6 +2441,7 @@ PODS:
- hermes-engine
- PingDavinci (= 2.1.0)
- PingDavinciPlugin (= 2.1.0)
+ - PingExternalIdP (= 2.1.0)
- PingLogger (= 2.1.0)
- PingOidc (= 2.1.0)
- PingOrchestrate (= 2.1.0)
@@ -3588,7 +3590,7 @@ SPEC CHECKSUMS:
RNPingBinding: 1731d5254b7d026df05ee056ab7802f4888537e9
RNPingBrowser: b6afbf0fa46c9ae0a10daeb46927f05eca5c7c6d
RNPingCore: 3926e39f9ab60d83fb4d58225303864ca0d8171b
- RNPingDavinci: 81122638cb43d20c7e24e66eea5bd8700fd48aae
+ RNPingDavinci: 605780d605c87d00216376bb2b3f4f5ee72817e8
RNPingDeviceClient: 0ff89a604e97d1168eff3aba4b1d889139619bd7
RNPingDeviceId: 6eb3e9fc68a78e1aee87652e025f6e1ec6d5d67e
RNPingDeviceProfile: 3086f14fc5024196a64290afbbf7917432dd5900
@@ -3605,4 +3607,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 06fe91365819bf22b320c91bdb09a02aa2b3c437
-COCOAPODS: 1.16.2
+COCOAPODS: 1.15.2
diff --git a/packages/davinci/README.md b/packages/davinci/README.md
index 22f9ed43d..cbf74005a 100644
--- a/packages/davinci/README.md
+++ b/packages/davinci/README.md
@@ -296,7 +296,9 @@ The following collector types are supported on Android and iOS:
| `ACTION` | Action button that advances the flow immediately. | Immediate |
| `FLOW_BUTTON` | Flow button that advances the flow immediately. | Immediate |
| `FLOW_LINK` | Flow link that advances the flow immediately. | Immediate |
+| `SINGLE_CHECKBOX` | Single checkbox or toggle (boolean field). | Manual input |
| `LABEL` | Read-only display content. | Output-only |
+| `READ_ONLY_TEXT` | Read-only text / agreement content. | Output-only |
Integration-dependent collectors (for example, social IdP, FIDO, or PingOne Protect) are
surfaced in node payloads and require client-side integration before submission
diff --git a/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/collector/DaVinciCollectorValueApplier.kt b/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/collector/DaVinciCollectorValueApplier.kt
index 06e947644..2ca5fcaf0 100644
--- a/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/collector/DaVinciCollectorValueApplier.kt
+++ b/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/collector/DaVinciCollectorValueApplier.kt
@@ -9,6 +9,7 @@ package com.pingidentity.rndavinci.collector
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
+import com.pingidentity.davinci.collector.BooleanCollector
import com.pingidentity.davinci.collector.Device
import com.pingidentity.davinci.collector.DeviceAuthenticationCollector
import com.pingidentity.davinci.collector.DeviceRegistrationCollector
@@ -128,6 +129,9 @@ internal object DaVinciCollectorValueApplier {
)
collector.value = device
}
+ is BooleanCollector -> {
+ collector.value = asBoolean(value, key)
+ }
else -> {
// All remaining collectors (TextCollector, PasswordCollector, SingleValueCollector,
// SubmitCollector, FlowCollector) extend SingleValueCollector which has setValue(String).
@@ -187,6 +191,14 @@ internal object DaVinciCollectorValueApplier {
return if (isIntegerValued) truncatedToLong else bridgedNumber
}
+ private fun asBoolean(value: Any?, fieldName: String): Boolean {
+ return when (value) {
+ is Boolean -> value
+ is String -> value.equals("true", ignoreCase = true)
+ else -> throw IllegalArgumentException("$fieldName expects a boolean value, got: ${value?.javaClass?.simpleName}")
+ }
+ }
+
private fun asString(value: Any?, fieldName: String): String {
return when (value) {
is String -> value
diff --git a/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt b/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt
index ada43075d..b6f156cf8 100644
--- a/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt
+++ b/packages/davinci/android/src/main/java/com/pingidentity/rndavinci/mapper/DaVinciNodeMapper.kt
@@ -11,6 +11,7 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import kotlinx.serialization.json.Json
import com.pingidentity.davinci.collector.Device
+import com.pingidentity.davinci.collector.BooleanCollector
import com.pingidentity.davinci.collector.DeviceAuthenticationCollector
import com.pingidentity.davinci.collector.DeviceRegistrationCollector
import com.pingidentity.davinci.collector.FlowCollector
@@ -19,6 +20,7 @@ import com.pingidentity.davinci.collector.MultiSelectCollector
import com.pingidentity.davinci.collector.PasswordCollector
import com.pingidentity.davinci.collector.PasswordPolicy
import com.pingidentity.davinci.collector.PhoneNumberCollector
+import com.pingidentity.davinci.collector.ReadOnlyTextCollector
import com.pingidentity.davinci.collector.SingleSelectCollector
import com.pingidentity.davinci.collector.SubmitCollector
import com.pingidentity.davinci.collector.TextCollector
@@ -198,6 +200,8 @@ internal object DaVinciNodeMapper {
is PhoneNumberCollector -> mapPhoneNumberCollector(collector)
is DeviceRegistrationCollector -> mapDeviceRegistrationCollector(collector)
is DeviceAuthenticationCollector -> mapDeviceAuthenticationCollector(collector)
+ is BooleanCollector -> mapBooleanCollector(collector)
+ is ReadOnlyTextCollector -> mapReadOnlyTextCollector(collector)
else -> {
logWarning(
logger,
@@ -433,6 +437,56 @@ internal object DaVinciNodeMapper {
return map
}
+ /**
+ * Serialize a [BooleanCollector] to a payload map.
+ *
+ * @param collector Native boolean collector.
+ * @return Serialized boolean collector map.
+ */
+ private fun mapBooleanCollector(collector: BooleanCollector): Map {
+ val map = baseCollectorMap(collector)
+ map["value"] = collector.value
+ map["appearance"] = collector.appearance.value
+ map["errorMessage"] = collector.errorMessage
+ collector.richContent?.let { rc ->
+ map["richContent"] = mapOf(
+ "content" to rc.content,
+ "replacements" to rc.replacements.mapValues { (_, r) ->
+ buildMap {
+ put("value", r.value)
+ put("href", r.href)
+ put("type", r.type)
+ put("target", r.target)
+ }
+ }
+ )
+ }
+ return map
+ }
+
+ /**
+ * Serialize a [ReadOnlyTextCollector] to a payload map.
+ *
+ * @remarks
+ * The `type` field is normalized to `"READ_ONLY_TEXT"` (the stable inputType) rather than the
+ * server-varying `type` field (e.g. `"AGREEMENT"`), matching the TypeScript contract.
+ *
+ * @param collector Native read-only text collector.
+ * @return Serialized read-only text collector map.
+ */
+ private fun mapReadOnlyTextCollector(collector: ReadOnlyTextCollector): Map {
+ return linkedMapOf(
+ "key" to collector.key,
+ "type" to "READ_ONLY_TEXT",
+ "content" to collector.content,
+ "title" to collector.title,
+ "titleEnabled" to collector.titleEnabled,
+ "enabled" to collector.enabled,
+ "agreementId" to collector.agreementId,
+ "useDynamicAgreement" to collector.useDynamicAgreement
+ )
+ }
+
private fun mapOptions(options: List): List