Skip to content

SDKS-5065 Standardize SDK configuration#207

Closed
vibhorgoswami wants to merge 2 commits into
developfrom
SDKS-5065
Closed

SDKS-5065 Standardize SDK configuration#207
vibhorgoswami wants to merge 2 commits into
developfrom
SDKS-5065

Conversation

@vibhorgoswami

@vibhorgoswami vibhorgoswami commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

JIRA Ticket

SDKS-5065

Description

Standardizing the configuration of Journey, DaVinci and OidcWeb

Summary by CodeRabbit

Release Notes

  • New Features

    • Standardized SDK configuration using JSON-based config files for the sample app, supporting Journey, DaVinci, and OIDC authentication flows.
    • Redesigned configuration management with persistent storage and improved user interface.
  • Documentation

    • Updated CHANGELOG with configuration standardization details.
  • Tests

    • Added comprehensive test coverage for configuration parsing, validation, and management.
  • Chores

    • Updated build dependencies and project configuration.

Replace the sample app's hardcoded-presets + 30-key DataStore config
model with a standardized JSON-file-driven configuration that mirrors
the iOS sibling implementation. Introduces a unified Configuration data
class, ConfigType enum, ConfigurationParser with validation, and a
ConfigurationManager singleton backed by StateFlow and DataStore.
EnvViewModel shrinks from ~600 to ~30 lines. All 9 consumer call sites
updated. 29 unit tests added.

Phases:
- Phase 1: bundle assets/ping_sdk_config.json + kotlinx-serialization plugin
- Phase 2: Configuration model + ConfigurationParser + 18 unit tests
- Phase 3: ConfigurationManager + DataStore persistence + SDK builders
- Phase 4: replace EnvViewModel, rewire Compose UI, update all call sites

Refs: SDKS-5065
@vibhorgoswami vibhorgoswami self-assigned this Jun 2, 2026
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR standardizes SDK configuration in the Ping sample app by introducing a centralized ConfigurationManager singleton that manages persisted configurations, SDK instance creation, and runtime selection. A new JSON-based Configuration model and parser replace hardcoded presets. The configuration screen is refactored to a unified generic form, and all feature view models are updated to obtain SDK clients from the manager.

Changes

Sample App Configuration System Refactoring

Layer / File(s) Summary
Configuration Data Models and Serialization
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Configuration.kt, samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationTest.kt
ConfigType enum with four flow types and @SerialName mappings, and Configuration @Serializable data class with required (clientId, scopes, discoveryEndpoint, etc.) and flow-specific optional fields (cookieName/serverUrl for Journey, realm/acrValues for DaVinci/OIDC, par flag).
Configuration Exception Hierarchy
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationException.kt
Sealed exception base class with four typed subclasses for malformed JSON, missing fields, invalid field values, and duplicate names, each carrying specific error context.
Configuration Parser and Validation
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationParser.kt, samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationParserTest.kt
JSON decoder with strict parsing, semantic validation of URIs and scopes, Journey-specific field requirements, duplicate-name detection, and comprehensive error-case test coverage.
Configuration Defaults and Asset Loading
samples/pingsampleapp/src/main/assets/ping_sdk_config.json, samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationDefaults.kt
Bundled ping_sdk_config.json asset with sample Journey/DaVinci/OIDC configurations, and ConfigurationDefaults singleton that loads, caches, and provides default-ness checks.
Configuration Manager Singleton
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt, samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationManagerTest.kt
Core singleton managing combined configurations (defaults + user), per-type selections via StateFlow, live SDK instances, and mutex-guarded mutations (initialize, select, add, update, delete) with DataStore persistence. Includes JSON serialization helpers, per-SDK builders mapping configurations to Flow/DaVinci/OIDC parameters, and fallback-tolerant DataStore read/write.
Application Startup Integration
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/PingSampleApplication.kt
Initializes ConfigurationManager on app startup via async call instead of the previous initConfigs() helper.
Configuration Screen UI and State Management
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt
Refactors from hardcoded Journey vs OIDC variants to unified generic form. Introduces ConfigFormState and ConfigEditSheet with conditional field visibility by ConfigType, locked type dropdown while editing, dynamic configuration card rendering by iterating ConfigType.entries, and edit/delete actions gated by default-ness.
ViewModel Delegation to ConfigurationManager
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/EnvViewModel.kt
Simplified to a thin ViewModel exposing configurations and selections from ConfigurationManager and four command methods (select, add, update, delete) that delegate. Removes all prior config models, presets, persistence, JSON, and SDK builder logic from this file.
Feature View Models Updated to Use ConfigurationManager
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/journey/JourneyViewModel.kt, davinci/DaVinciViewModel.kt, oidc/CentralizeLoginViewModel.kt, token/TokenViewModel.kt, userprofile/UserProfileViewModel.kt, logout/LogoutViewModel.kt, devicemanagement/DeviceManagementViewModel.kt, journey/callback/IdPCallback.kt, davinci/collector/SocialLoginButton.kt
All view models and callbacks updated to obtain SDK clients (Journey, DaVinci, OidcWebClient, OidcDeviceClient) and configured redirect URIs from ConfigurationManager instead of module-level imports, enabling dynamic configuration selection.
Build Configuration and Documentation Updates
samples/pingsampleapp/build.gradle.kts, .gitignore, CHANGELOG.md
Adds kotlinx serialization plugin, serialization-json, kotlin-test, and coroutines-test dependencies. Excludes .sdk/ directory from version control. Documents standardized configuration approach in CHANGELOG under Unreleased/Changed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ForgeRock/ping-android-sdk#181: Prior refactoring of sample app configuration from hardcoded presets to dynamic selection; main PR consolidates this into centralized ConfigurationManager.
  • ForgeRock/ping-android-sdk#199: Introduces RFC 8628 device authorization client (OidcDeviceClient), which main PR now integrates and persists via ConfigurationManager.
  • ForgeRock/ping-android-sdk#176: Implements OIDC library PAR flag support; main PR adds PAR configuration field and plumbs it through SDK setup.

Suggested reviewers

  • spetrov
  • witrisna
  • george-bafaloukas-forgerock

Poem

🐰 A rabbit hops through config trees,
No presets left, just JSON breeze,
One manager to bind them all,
From Journey paths to OIDC's call,
The sample app now standardized with glee! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the required JIRA ticket link and a brief overview of the change, though it lacks detail on specific implementation approach and testing guidance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: standardizing SDK configuration across Journey, DaVinci, and OidcWeb by replacing hardcoded presets with a unified JSON-driven model.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SDKS-5065

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.

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 42.16%. Comparing base (3b02b1b) to head (640f1af).

Additional details and impacted files
@@            Coverage Diff             @@
##             develop     #207   +/-   ##
==========================================
  Coverage      42.16%   42.16%           
  Complexity      1287     1287           
==========================================
  Files            315      315           
  Lines           9605     9605           
  Branches        1447     1447           
==========================================
  Hits            4050     4050           
  Misses          4999     4999           
  Partials         556      556           
Flag Coverage Δ
integration-tests 24.62% <ø> (ø)
unit-tests 25.69% <ø> (ø)

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (1)
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfileViewModel.kt (1)

90-103: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear per-tab state when the configured client is missing.

When ConfigurationManager.* is null, these blocks do nothing and keep old user/error values, so the UI can show stale profile data after config/session changes. Please reset that tab’s state before returning when client/user is unavailable.

Suggested fix
 private fun journeyUserInfo() {
     viewModelScope.launch {
-        ConfigurationManager.journey?.journeyUser()?.let { user ->
-            when (val result = user.userinfo(false)) {
-                is Result.Failure ->
-                    state.update { s ->
-                        s.copy(journeyUser = null, journeyError = result.value)
-                    }
-
-                is Result.Success -> {
-                    state.update { s ->
-                        s.copy(journeyUser = result.value, journeyError = null)
-                    }
-                }
-            }
-        }
+        val user = ConfigurationManager.journey?.journeyUser()
+        if (user == null) {
+            state.update { s -> s.copy(journeyUser = null, journeyError = null) }
+            return@launch
+        }
+        when (val result = user.userinfo(false)) {
+            is Result.Failure ->
+                state.update { s ->
+                    s.copy(journeyUser = null, journeyError = result.value)
+                }
+            is Result.Success ->
+                state.update { s ->
+                    s.copy(journeyUser = result.value, journeyError = null)
+                }
+        }
     }
 }

Apply the same pattern to daVinciUserInfo() and oidcUserInfo().

Also applies to: 116-129, 142-155

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfileViewModel.kt`
around lines 90 - 103, When ConfigurationManager.journey (and similarly
ConfigurationManager.daVinci and ConfigurationManager.oidc) is null the existing
branches do nothing and leave stale state; update the null-case to explicitly
reset that tab’s state by calling state.update to set the corresponding user
field (journeyUser/daVinciUser/oidcUser) to null and the corresponding error
field (journeyError/daVinciError/oidcError) to a cleared value before returning.
Locate the blocks using ConfigurationManager.journey?.journeyUser()?.let { user
-> ... } and the daVinciUserInfo()/oidcUserInfo() usages and add the fallback
state.update calls mirroring the success/failure resets so each tab is cleared
when its client/user is unavailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 2: The changelog has a heading level jump: replace the "#### Changed"
heading under "## [Unreleased]" with "### Changed" so the heading increments by
one level and satisfies MD001; update the heading text where "#### Changed"
appears in CHANGELOG.md to "### Changed".

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Configuration.kt`:
- Around line 1-6: Update the copyright header in the new file Configuration.kt
to use only the current year (2026) instead of a range; replace "2024 - 2026"
with "2026" in the file header comment so it reads "Copyright (c) 2026 Ping
Identity Corporation".

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationDefaults.kt`:
- Line 65: ConfigurationDefaults.isDefault currently only checks
Configuration.name which lets a user config reuse a bundled default name and be
misclassified; update the logic so default/user identity is based on a stronger
key (e.g., check both Configuration.type and Configuration.name in
ConfigurationDefaults.isDefault or add a persisted origin/source flag on
Configuration and make isDefault consult that), and ensure ConfigurationParser
rejects user-provided configs whose (type, name) collide with any bundled
default; also update Env.kt UI checks (currently relying on isDefault) and
ConfigurationManager.add/update/delete to use the new identity so edit/delete
behavior is correct.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt`:
- Around line 185-215: The add(Configuration) and update(oldName, Configuration)
paths currently write userConfigs directly to _configurations and
writeUserConfigs without validating or enforcing uniqueness, risking duplicates
or invalid entries that break deserializeUserConfigs; before modifying state in
add and before mapping/replacing in update, run the same validation and
uniqueness checks used by ConfigurationParser (validate structure, required
fields, and that names are unique among userConfigs and do not collide with
ConfigurationDefaults), reject or normalize invalid configs, and only
persist/writeUserConfigs and update _configurations/_selections (and call
rebuildInstance) when validation passes.
- Around line 1-6: The file header uses a ranged copyright year; update the
header in ConfigurationManager.kt to use the single current year "2026" instead
of "2024 - 2026" — locate the top-of-file comment block (the file-level header
above the ConfigurationManager class) and replace the ranged year with the
single year 2026 so the new-file header matches the repo convention.
- Around line 236-248: When removing a selection because no fallback exists,
also clear/tear down the active SDK client so the app doesn't keep using the
deleted config; in the else branch where you call
newSelections.remove(config.type) and update _selections.value, invoke the same
teardown used elsewhere (e.g. call rebuildInstance(null) or the client-destroy
method used by rebuildInstance) and persist the removal via writeSelection if
applicable (use appContext?.let { writeSelection(it, config.type, null) } or
equivalent) so the live client is cleared when fallback == null.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt`:
- Around line 142-145: openAdd currently resets sheetFormState to a plain
ConfigFormState() which defaults the form to JOURNEY; change it to seed the new
form with the card's actual ConfigType so the modal opens with the right type.
Specifically, in openAdd (and the other openAdd occurrences referenced), keep
setting editingOldName = AddMode but initialize sheetFormState with the card's
ConfigType (e.g., new ConfigFormState(configType = currentCard.configType) or
equivalent) so the Add sheet defaults to the invoking card's type rather than
JOURNEY.
- Around line 156-163: canSave and onSave diverge from the real configuration
contract and allow/persist values ConfigurationParser would reject; update
canSave to reuse the same validation logic as ConfigurationParser (e.g., call
the parser's validate/parse method or a shared isValidConfiguration function) to
enforce non-empty scopes, well-formed redirectUri/discoveryEndpoint, and
required Journey fields (cookieName unless parser allows omission, and allow
omission of realm if parser does), and modify onSave to run the same
parser/validator before envViewModel.add or envViewModel.update (reject/save
only if parser returns valid Configuration or convert form via the parser) so
the UI cannot persist configurations the SDK builders would reject; reference
ConfigFormState.toConfiguration(), ConfigurationParser, canSave, onSave, and
envViewModel.add/update when making the changes.
- Around line 156-163: Reject saving when the new Configuration.name would
collide with any existing user configuration or a bundled default: in onSave
(use ConfigFormState.toConfiguration() and editingOldName) validate the
candidate name before calling envViewModel.add(config) or
envViewModel.update(old, config); allow the save if the name is unchanged from
editingOldName, otherwise check ConfigurationManager (or the envViewModel list)
and ConfigurationDefaults.isDefault(name) and surface an error/abort the save if
either returns true. Apply the same validation to the other save/edit handler
that uses the same add/update flow so edits cannot create ambiguous keys.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/EnvViewModel.kt`:
- Around line 1-6: Update the file header in EnvViewModel.kt to use the original
creation-year-to-current-year range (e.g., replace the single year "2026" with
the original start year followed by "-2026"); edit the top block comment so the
copyright line reads "Copyright (c) <original-start-year>-2026 Ping Identity
Corporation. All rights reserved." to match the repo convention for existing
Kotlin/Java files and leave the rest of the header unchanged.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/journey/JourneyViewModel.kt`:
- Around line 36-39: When ConfigurationManager.journey is null in
JourneyViewModel.start (the block with "val next =
ConfigurationManager.journey?.start(journeyName) ?: run { ... }"), set the view
state's error property as DaVinciViewModel.start does and then set loading to
false and return; specifically assign the correct JourneyState error field (use
the actual property name on JourneyState rather than a mismatched identifier)
with a message like "Journey not configured" so the UI shows an error instead of
silently doing nothing.

In
`@samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationManagerTest.kt`:
- Around line 190-221: The tests are not invoking the actual helpers under test;
update the three cases to call
ConfigurationManager.selectedConfig(ConfigType.JOURNEY) and
ConfigurationManager.hasConfiguration(ConfigType) (or whichever public helper
names exist) and assert their return values instead of using local Map/List
lookups; for the selectedConfig test ensure the ConfigurationManager is reset or
populated appropriately before asserting null, and for the hasConfiguration
tests populate ConfigurationManager with the sample configurations
(davinciConfig, journeyConfig) then assert
ConfigurationManager.hasConfiguration(ConfigType.JOURNEY) is true,
hasConfiguration(ConfigType.DAVINCI) is true, and
hasConfiguration(ConfigType.DEVICE) is false so the tests validate the actual
helpers.
- Around line 1-6: Replace the ranged copyright header at the top of
ConfigurationManagerTest.kt with the new-file header that uses only the current
year; change the "2024 - 2026" range to the single year "2026" in the file
header comment so the header for this new Kotlin file reflects the current year.

---

Outside diff comments:
In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfileViewModel.kt`:
- Around line 90-103: When ConfigurationManager.journey (and similarly
ConfigurationManager.daVinci and ConfigurationManager.oidc) is null the existing
branches do nothing and leave stale state; update the null-case to explicitly
reset that tab’s state by calling state.update to set the corresponding user
field (journeyUser/daVinciUser/oidcUser) to null and the corresponding error
field (journeyError/daVinciError/oidcError) to a cleared value before returning.
Locate the blocks using ConfigurationManager.journey?.journeyUser()?.let { user
-> ... } and the daVinciUserInfo()/oidcUserInfo() usages and add the fallback
state.update calls mirroring the success/failure resets so each tab is cleared
when its client/user is unavailable.
🪄 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: 47c62f39-d9dd-403b-9c97-beeb40106417

📥 Commits

Reviewing files that changed from the base of the PR and between 3b02b1b and 536b3e2.

📒 Files selected for processing (24)
  • .gitignore
  • CHANGELOG.md
  • samples/pingsampleapp/build.gradle.kts
  • samples/pingsampleapp/src/main/assets/ping_sdk_config.json
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/PingSampleApplication.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Configuration.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationDefaults.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationException.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationParser.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/EnvViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/DaVinciViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/davinci/collector/SocialLoginButton.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/devicemanagement/DeviceManagementViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/journey/JourneyViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/journey/callback/IdPCallback.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/logout/LogoutViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/oidc/CentralizeLoginViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/TokenViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfileViewModel.kt
  • samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationManagerTest.kt
  • samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationParserTest.kt
  • samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationTest.kt

Comment thread CHANGELOG.md Outdated
Comment on lines +1 to +6
/*
* 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.
*/

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 | ⚡ Quick win

Copyright header should use the current year only for new files.

This is a newly added file, so the header should read Copyright (c) 2026 Ping Identity Corporation rather than a 2024 - 2026 range.

📝 Proposed fix
-/*
- * Copyright (c) 2024 - 2026 Ping Identity Corporation. All rights reserved.
+/*
+ * Copyright (c) 2026 Ping Identity Corporation. All rights reserved.

Based on learnings: for newly added files use only the current year (e.g., "Copyright (c) 2026 Ping Identity Corporation").

📝 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) 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.
*/
/*
* 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.
*/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Configuration.kt`
around lines 1 - 6, Update the copyright header in the new file Configuration.kt
to use only the current year (2026) instead of a range; replace "2024 - 2026"
with "2026" in the file header comment so it reads "Copyright (c) 2026 Ping
Identity Corporation".

*
* @param config The configuration to check.
*/
fun isDefault(config: Configuration): Boolean = entries.any { it.name == config.name }

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how ConfigurationManager combines defaults + user configs and whether
# global name uniqueness is enforced, plus how isDefault gates edit/delete in UI.
fd -e kt 'ConfigurationManager|Env' | xargs rg -nC3 'isDefault|ConfigurationDefaults|user_configurations|\.name =='

Repository: ForgeRock/ping-android-sdk

Length of output: 14118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show ConfigurationDefaults.kt around isDefault + how entries/defaults are built
echo "---- ConfigurationDefaults.kt (around isDefault) ----"
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationDefaults.kt")
txt = p.read_text(encoding="utf-8")
for m in re.finditer(r'fun isDefault\s*\(', txt):
    start = max(0, txt.rfind('\n', 0, m.start())-400)
    end = min(len(txt), m.start()+400)
    chunk = txt[start:end]
    print(chunk)
    break
PY

# 2) Show ConfigurationManager.kt: how defaults+user configs are merged; and any uniqueness checks
echo "---- ConfigurationManager.kt (merge/load + add/update/delete) ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt")
txt = p.read_text(encoding="utf-8")
patterns = [
    r'init\s*\{',
    r'readUserConfigs',
    r'writeUserConfigs',
    r'load',
    r'val\s+defaults',
    r'val\s+userConfigs',
    r'suspend fun add',
    r'suspend fun update',
    r'suspend fun delete',
    r'build.*Instance',
    r'getAll|all\.firstOrNull'
]
for pat in patterns:
    m = re.search(pat, txt)
    if not m: 
        continue
    s = max(0, txt.rfind('\n', 0, m.start())-200)
    e = min(len(txt), m.start()+600)
    print("\n### Match:", pat, "at", m.start())
    print(txt[s:e])
PY

# 3) Locate where user configs are read/parsed and whether names are deduped/validated there
echo "---- Search for 'unique' / dedupe / duplicate / name checks in ConfigurationManager.kt ----"
rg -n --no-heading -S "unique|dedup|duplicate|dupe|enforce|require|check.*name|name.*check|distinct\\(|groupBy\\(\\s*\\{\\s*it\\.name" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt

# 4) UI gating context (Env.kt) - confirm isDefault solely gates edit/delete
echo "---- Env.kt (around edit/delete gating) ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt")
txt = p.read_text(encoding="utf-8")
# print small window around where onEdit/onDelete are set
for m in re.finditer(r'onEdit\s*=\s*if\s*\(!isDefault\)', txt):
    start = txt.rfind('\n', 0, m.start())-200
    end = txt.find('\n', m.end())+200
    print(txt[start:end])
    break
PY

Repository: ForgeRock/ping-android-sdk

Length of output: 9333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- ConfigurationManager.kt: readUserConfigs() body ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt")
txt = p.read_text(encoding="utf-8")
m = re.search(r'fun\s+readUserConfigs\s*\(', txt)
if not m:
    m = re.search(r'suspend\s+fun\s+readUserConfigs\s*\(', txt)
if not m:
    print("readUserConfigs not found")
    raise SystemExit(0)

start = txt.rfind('\n', 0, m.start())
# naive brace matching from first '{' after signature
brace_start = txt.find('{', m.end())
depth=0
i=brace_start
while i < len(txt):
    if txt[i]=='{': depth+=1
    elif txt[i]=='}':
        depth-=1
        if depth==0:
            end=i+1
            break
    i+=1
else:
    end=min(len(txt), m.end()+4000)

print(txt[start:end])
PY

echo "---- ConfigurationManager.kt: any dedupe/name checks in initialize/add/update/delete ----"
rg -n --no-heading -S "distinct\\(|groupBy\\(|associateBy\\(|Map<|\\bcontains\\(|\\bcheck\\b|require\\(|enforce|duplicate|dedup" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt

echo "---- ConfigurationDefaults.kt: load()/entries definition ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationDefaults.kt")
txt = p.read_text(encoding="utf-8")
for pat in [r'fun\s+load\s*\(', r'private\s+.*entries', r'assets/ping_sdk_config\.json', r'ConfigurationParser\.parse']:
    m = re.search(pat, txt)
    if not m:
        print("pattern not found:", pat)
        continue
    s = max(0, txt.rfind('\n', 0, m.start())-200)
    e = min(len(txt), txt.find('\n', m.end())+800)
    print("\n### match:", pat)
    print(txt[s:e])
PY

echo "---- Find and inspect ConfigurationParser implementation ----"
fd -e kt 'ConfigurationParser' samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config | head -n 50

fd -e kt 'ConfigurationParser' samples/pingsampleapp/src/main/java | head -n 50

Repository: ForgeRock/ping-android-sdk

Length of output: 4096


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- ConfigurationParser.kt (look for DuplicateName / uniqueness rules) ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationParser.kt")
txt = p.read_text(encoding="utf-8")
# print file with line numbers for key sections only
for term in ["DuplicateName", "unique", "duplicate", "name", "parse", "validate", "entries"]:
    if term in txt:
        pass
print("File length (lines):", txt.count("\\n")+1)
PY

rg -n --no-heading -S "DuplicateName|Duplicate|duplicate|DuplicateName|distinct|distinctBy|groupBy\\(|associateBy\\(|validate|throw" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationParser.kt

echo "---- ConfigurationParser.kt around parse/validation ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationParser.kt")
txt = p.read_text(encoding="utf-8")
# Extract around parse function
m = re.search(r'fun\s+parse\s*\(', txt)
if not m:
    raise SystemExit("parse() not found")
start = txt.rfind('\n', 0, m.start())
# find next 500 lines chunk by braces
brace_start = txt.find('{', m.end())
depth=0; i=brace_start
while i < len(txt):
    if txt[i]=='{': depth+=1
    elif txt[i]=='}':
        depth-=1
        if depth==0:
            end=i+1
            break
    i+=1
else:
    end=min(len(txt), m.end()+6000)
print(txt[max(0,start-200):min(len(txt),end+2000)])
PY

echo "---- ConfigurationManager.kt: deserializeUserConfigs() ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt")
txt = p.read_text(encoding="utf-8")
m = re.search(r'deserializeUserConfigs\s*\(', txt)
if not m:
    m = re.search(r'fun\s+deserializeUserConfigs\s*\(', txt)
if not m:
    raise SystemExit("deserializeUserConfigs not found")
# show around method
start = txt.rfind('\n', 0, m.start())
brace_start = txt.find('{', m.end())
depth=0; i=brace_start
while i < len(txt):
    if txt[i]=='{': depth+=1
    elif txt[i]=='}':
        depth-=1
        if depth==0:
            end=i+1
            break
    i+=1
else:
    end=min(len(txt), m.end()+4000)
print(txt[max(0,start-200):min(len(txt),end+800)])
PY

echo "---- ConfigurationManager.kt: any gating of edit/delete other than isDefault ----"
rg -n --no-heading -S "onEdit|onDelete|isDefault\\(" samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt

Repository: ForgeRock/ping-android-sdk

Length of output: 8347


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Where user config JSON is created/written
rg -n --no-heading -S "serializeUserConfigs|writeUserConfigs|USER_CONFIGS_KEY" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt

# Look for any name-collision validation in UI/view-model when adding/updating configs
rg -n --no-heading -S "DuplicateName|duplicate name|name.*already|isDefault\\(|ConfigurationDefaults\\.|match.*defaults" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config | head -n 200

# Find config add/update/edit entrypoints in Env/ViewModel
rg -n --no-heading -S "add\\(|update\\(|delete\\(|openAdd|openEdit|Config.*ViewModel|EnvViewModel" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp | head -n 200

Repository: ForgeRock/ping-android-sdk

Length of output: 13897


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect EnvViewModel and dialog / config editor code for name validation/collision checks
rg -n --no-heading -S "class EnvViewModel|openAdd\\(|openEdit\\(|add\\(config: Configuration\\)|update\\(oldName: String" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config

rg -n --no-heading -S "Configuration.*Dialog|Edit.*Configuration|Add.*Configuration|Config.*Form|EditConfig|AddConfig|Journey|DaVinci|Oidc|DeviceClient|discoveryEndpoint|redirectUri|scopes" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config

# Look for validation that references name equality or compares against defaults
rg -n --no-heading -S "ConfigurationDefaults\\.entries|isDefault\\(|ping_sdk_config\\.json|assets/ping_sdk_config\\.json|DuplicateName|already exists|name.*taken|existing.*name" \
  samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config

# Print EnvViewModel.kt and Env.kt sections around add/edit handlers
python3 - <<'PY'
import pathlib, re
base = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config")
for fn in ["EnvViewModel.kt","Env.kt","EnvView.kt","ConfigRow.kt","ConfigurationEdit.kt","EditEnv.kt"]:
    p = base/fn
    if p.exists():
        print("\n====", p, "====")
        txt = p.read_text(encoding="utf-8")
        # print file head or the relevant add/update/delete methods
        m = re.search(r'fun\s+(openAdd|openEdit|add|update|delete)\b', txt)
        if m:
            s = max(0, txt.rfind("\n", 0, m.start())-200)
            e = min(len(txt), m.start()+1200)
            print(txt[s:e])
        else:
            print(txt[:500])
PY

Repository: ForgeRock/ping-android-sdk

Length of output: 23569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show how Env.kt builds the per-type config list that's iterated (and therefore whether isDefault is applied across types).
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt")
txt = p.read_text(encoding="utf-8")
# find the configs.forEach block (where isDefault is computed)
m = re.search(r'configs\\.forEach\\s*\\{\\s*config\\s*->', txt)
if not m:
    # fallback: find "val isDefault" near that area
    m = re.search(r'val isDefault\\s*=\\s*ConfigurationDefaults\\.isDefault\\(config\\)', txt)
    print("fallback match at", m.start() if m else None)
else:
    print("configs.forEach match at", m.start())

start = txt.rfind('\n', 0, (m.start() if m else 0)) - 250 if m else 0
start = max(0, start)
end = txt.find('\n', (m.start() if m else 0) ) + 450 if m else 450
end = min(len(txt), end+600)
print(txt[start:end])
PY

echo "---- Env.kt: ConfigFormState + toConfiguration() + save enablement around onSave/modal ----"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt")
txt = p.read_text(encoding="utf-8")
# print ConfigFormState definition + toConfiguration
m1 = re.search(r'data class ConfigFormState', txt)
m2 = re.search(r'fun\\s+toConfiguration\\(', txt)
# print block around save enablement (where it checks form fields)
m3 = re.search(r'form\\.[a-zA-Z0-9_]+\\.isNotBlank\\(\\)', txt)
# print around sheet confirm button validation
m4 = re.search(r'text = if \\(isEdit\\) "Edit Configuration" else "Add Configuration"', txt)

# function to print around an index
def around(idx, before=200, after=600):
    s = max(0, txt.rfind('\n', 0, idx-before))
    e = min(len(txt), idx+after)
    return txt[s:e]

if m1:
    print("\n==== ConfigFormState block ====")
    print(around(m1.start(), before=0, after=260))
if m2:
    print("\n==== ConfigFormState.toConfiguration() block ====")
    print(around(m2.start(), before=0, after=320))
if m3:
    print("\n==== Save enablement block (first isNotBlank occurrence) ====")
    print(around(m3.start(), before=120, after=420))
if m4:
    print("\n==== Modal save button block (around label) ====")
    print(around(m4.start(), before=160, after=420))
PY

Repository: ForgeRock/ping-android-sdk

Length of output: 2321


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt"

echo "---- Line numbers of key spots in Env.kt ----"
rg -n --no-heading "val isDefault\s*=\s*ConfigurationDefaults\.isDefault\(config\)" "$FILE" || true
rg -n --no-heading "onSave\(form" "$FILE" || true
rg -n --no-heading "data class ConfigFormState" "$FILE" || true
rg -n --no-heading "fun\s+toConfiguration\(" "$FILE" || true
rg -n --no-heading "isNotBlank\(\)" "$FILE" || true
rg -n --no-heading "Text\\(\"Add Configuration\"|Edit Configuration" "$FILE" || true

# print small ranges around the isDefault usage
LINE_ISDEFAULT=$(rg -n --no-heading "val isDefault\s*=\s*ConfigurationDefaults\.isDefault\(config\)" "$FILE" | head -n1 | cut -d: -f1)
if [ -n "${LINE_ISDEFAULT:-}" ]; then
  echo "---- Env.kt around isDefault (edit/delete gating) ----"
  sed -n "$((LINE_ISDEFAULT-25)),$((LINE_ISDEFAULT+25))p" "$FILE"
fi

# print save button enabled/disabled logic (first isNotBlank block)
LINE_NOTBLANK=$(rg -n --no-heading "isNotBlank\(\)" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE_NOTBLANK:-}" ]; then
  echo "---- Env.kt around isNotBlank checks (save enablement) ----"
  sed -n "$((LINE_NOTBLANK-80)),$((LINE_NOTBLANK+120))p" "$FILE"
fi

# print config form state + mapping
LINE_FORM=$(rg -n --no-heading "data class ConfigFormState" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE_FORM:-}" ]; then
  echo "---- Env.kt around ConfigFormState ----"
  sed -n "$((LINE_FORM-20)),$((LINE_FORM+80))p" "$FILE"
fi

LINE_TO=$(rg -n --no-heading "fun\s+toConfiguration\(" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE_TO:-}" ]; then
  echo "---- Env.kt around toConfiguration() ----"
  sed -n "$((LINE_TO-20)),$((LINE_TO+60))p" "$FILE"
fi

Repository: ForgeRock/ping-android-sdk

Length of output: 13863


isDefault is name-only; a user config can reuse a bundled default name and then be treated as a default (edit/delete disabled and update/delete misbehave).
ConfigurationDefaults.isDefault returns true when any bundled default has the same Configuration.name, and Env.kt uses that to disable onEdit/onDelete for those rows. The add/edit sheet only checks form.name.isNotBlank() (no check against bundled default names), and ConfigurationParser enforces DuplicateName only within each parsed JSON list (defaults asset vs user JSON), not across them—so a persisted user config can collide with a default. Because ConfigurationManager.add/update/delete also rely on isDefault, the colliding user entry will be misclassified as “default” and won’t be editable/deletable (and update can skip replacing it).
Fix: make the default/user distinction use a stronger identity (e.g., (type, name)) or track a persisted “origin/source” flag and base isDefault on that; alternatively, reject user configs whose (type,) name collides with any bundled default.

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationDefaults.kt`
at line 65, ConfigurationDefaults.isDefault currently only checks
Configuration.name which lets a user config reuse a bundled default name and be
misclassified; update the logic so default/user identity is based on a stronger
key (e.g., check both Configuration.type and Configuration.name in
ConfigurationDefaults.isDefault or add a persisted origin/source flag on
Configuration and make isDefault consult that), and ensure ConfigurationParser
rejects user-provided configs whose (type, name) collide with any bundled
default; also update Env.kt UI checks (currently relying on isDefault) and
ConfigurationManager.add/update/delete to use the new identity so edit/delete
behavior is correct.

Comment on lines +1 to +6
/*
* 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.
*/

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 | ⚡ Quick win

Use the new-file header format here.

This file is new, so the copyright header should use 2026 instead of a ranged 2024 - 2026. Based on learnings: new Kotlin/Java files in this repo should use only the current year in the copyright header.

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt`
around lines 1 - 6, The file header uses a ranged copyright year; update the
header in ConfigurationManager.kt to use the single current year "2026" instead
of "2024 - 2026" — locate the top-of-file comment block (the file-level header
above the ConfigurationManager class) and replace the ranged year with the
single year 2026 so the new-file header matches the repo convention.

Comment on lines +185 to +215
suspend fun add(config: Configuration) {
mutex.withLock {
val current = _configurations.value
val userConfigs = current.filter { !ConfigurationDefaults.isDefault(it) } + config
_configurations.value = current.filter { ConfigurationDefaults.isDefault(it) } + userConfigs
appContext?.let { writeUserConfigs(it, userConfigs) }
}
}

/**
* Replaces the user-added configuration whose [Configuration.name] equals [oldName] with
* [config]. If the replaced entry was the active selection for its type, the SDK instance
* is rebuilt with the new config.
*/
suspend fun update(oldName: String, config: Configuration) {
mutex.withLock {
val defaults = _configurations.value.filter { ConfigurationDefaults.isDefault(it) }
val userConfigs = _configurations.value
.filter { !ConfigurationDefaults.isDefault(it) }
.map { if (it.name == oldName) config else it }
_configurations.value = defaults + userConfigs

appContext?.let { writeUserConfigs(it, userConfigs) }

// If this config was selected, update the selection and rebuild.
val currentSelection = _selections.value[config.type]
if (currentSelection?.name == oldName) {
_selections.value = _selections.value + (config.type to config)
appContext?.let { writeSelection(it, config.type, config) }
rebuildInstance(config)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate configs before add() / update() persist them.

These mutation paths bypass ConfigurationParser, but the rest of the manager treats name as a stable identifier for selection, update, and delete. A duplicate or otherwise invalid config saved here can corrupt later mutations and, on the next launch, make deserializeUserConfigs() drop the entire user-config list. Please enforce the same validation/uniqueness rules here before committing state.

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/ConfigurationManager.kt`
around lines 185 - 215, The add(Configuration) and update(oldName,
Configuration) paths currently write userConfigs directly to _configurations and
writeUserConfigs without validating or enforcing uniqueness, risking duplicates
or invalid entries that break deserializeUserConfigs; before modifying state in
add and before mapping/replacing in update, run the same validation and
uniqueness checks used by ConfigurationParser (validate structure, required
fields, and that names are unique among userConfigs and do not collide with
ConfigurationDefaults), reject or normalize invalid configs, and only
persist/writeUserConfigs and update _configurations/_selections (and call
rebuildInstance) when validation passes.

Comment on lines +156 to +163
fun onSave(form: ConfigFormState) {
val config = form.toConfiguration()
val old = editingOldName
if (old == null) {
envViewModel.add(config)
} else {
envViewModel.update(old, config)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Share the same validation rules as ConfigurationParser.

canSave has drifted from the real configuration contract: it allows empty scopes, malformed redirectUri / discoveryEndpoint, and Journey configs without cookieName, while also blocking valid Journey configs that omit realm. onSave then persists that Configuration unchanged, so the UI can save values the parser would reject and the SDK builders will consume later.

Also applies to: 393-398

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt`
around lines 156 - 163, canSave and onSave diverge from the real configuration
contract and allow/persist values ConfigurationParser would reject; update
canSave to reuse the same validation logic as ConfigurationParser (e.g., call
the parser's validate/parse method or a shared isValidConfiguration function) to
enforce non-empty scopes, well-formed redirectUri/discoveryEndpoint, and
required Journey fields (cookieName unless parser allows omission, and allow
omission of realm if parser does), and modify onSave to run the same
parser/validator before envViewModel.add or envViewModel.update (reject/save
only if parser returns valid Configuration or convert form via the parser) so
the UI cannot persist configurations the SDK builders would reject; reference
ConfigFormState.toConfiguration(), ConfigurationParser, canSave, onSave, and
envViewModel.add/update when making the changes.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject duplicate configuration names before save.

Configuration.name is the persistence key, ConfigurationDefaults.isDefault() matches defaults by name, and ConfigurationManager.update() / delete() also target user configs by name. A collision with another user config or a bundled default makes edits/deletes ambiguous and can leave a user config effectively undeletable.

Also applies to: 291-300

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt`
around lines 156 - 163, Reject saving when the new Configuration.name would
collide with any existing user configuration or a bundled default: in onSave
(use ConfigFormState.toConfiguration() and editingOldName) validate the
candidate name before calling envViewModel.add(config) or
envViewModel.update(old, config); allow the save if the name is unchanged from
editingOldName, otherwise check ConfigurationManager (or the envViewModel list)
and ConfigurationDefaults.isDefault(name) and surface an error/abort the save if
either returns true. Apply the same validation to the other save/edit handler
that uses the same add/update flow so edits cannot create ambiguous keys.

Comment on lines 1 to 6
/*
* Copyright (c) 2026 - 2026 Ping Identity Corporation. All rights reserved.
* 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 | ⚡ Quick win

Restore the original year range in this modified file header.

This is an existing file, so the header should keep its original creation year and extend it through 2026 instead of resetting to a single year. Based on learnings: existing Kotlin/Java files in this repo should use a creation-year-to-current-year range in the copyright header.

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/EnvViewModel.kt`
around lines 1 - 6, Update the file header in EnvViewModel.kt to use the
original creation-year-to-current-year range (e.g., replace the single year
"2026" with the original start year followed by "-2026"); edit the top block
comment so the copyright line reads "Copyright (c) <original-start-year>-2026
Ping Identity Corporation. All rights reserved." to match the repo convention
for existing Kotlin/Java files and leave the rest of the header unchanged.

Comment on lines +36 to +39
val next = ConfigurationManager.journey?.start(journeyName) ?: run {
loading.update { false }
return@launch
}

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 | ⚡ Quick win

Surface an error state when Journey is not configured.

Unlike DaVinciViewModel.start() (which sets error = "DaVinci not configured" when the SDK is null), here a null ConfigurationManager.journey only flips loading to false and returns. The screen is left with no node and no error message, giving the user no feedback about why nothing happened. Consider mirroring the DaVinci behavior for consistency.

🩹 Suggested change
         viewModelScope.launch {
             val next = ConfigurationManager.journey?.start(journeyName) ?: run {
+                state.update { it.copy(error = "Journey not configured") }
                 loading.update { false }
                 return@launch
             }

Adjust the field name to match JourneyState's actual error property.

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

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/journey/JourneyViewModel.kt`
around lines 36 - 39, When ConfigurationManager.journey is null in
JourneyViewModel.start (the block with "val next =
ConfigurationManager.journey?.start(journeyName) ?: run { ... }"), set the view
state's error property as DaVinciViewModel.start does and then set loading to
false and return; specifically assign the correct JourneyState error field (use
the actual property name on JourneyState rather than a mismatched identifier)
with a message like "Journey not configured" so the UI shows an error instead of
silently doing nothing.

Comment on lines +1 to +6
/*
* 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.
*/

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 | ⚡ Quick win

Use the new-file header format here.

This file is new, so the copyright header should use 2026 instead of a ranged 2024 - 2026. Based on learnings: new Kotlin/Java files in this repo should use only the current year in the copyright header.

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

In
`@samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationManagerTest.kt`
around lines 1 - 6, Replace the ranged copyright header at the top of
ConfigurationManagerTest.kt with the new-file header that uses only the current
year; change the "2024 - 2026" range to the single year "2026" in the file
header comment so the header for this new Kotlin file reflects the current year.

Comment on lines +190 to +221
@Test
fun `selectedConfig returns null when no configuration has been selected for that type`() {
// Access the in-memory StateFlow values directly; no Context needed.
// After construction (before initialize), selections is empty.
val selection = ConfigurationManager.selectedConfig(ConfigType.JOURNEY)
// The object may have been previously initialized in other tests (it's a singleton),
// so we test the helper's behaviour with an explicit map lookup.
val emptyMap = emptyMap<ConfigType, Configuration>()
val result = emptyMap[ConfigType.JOURNEY]
assertEquals(null, result)
}

@Test
fun `hasConfiguration returns false when configurations list is empty`() {
val emptyList = emptyList<Configuration>()
val result = emptyList.any { it.type == ConfigType.JOURNEY }
assertEquals(false, result)
}

@Test
fun `hasConfiguration returns true when matching type exists`() {
val configs = listOf(
davinciConfig(name = "DaVinci A"),
journeyConfig(name = "Journey A"),
)
val hasJourney = configs.any { it.type == ConfigType.JOURNEY }
val hasDaVinci = configs.any { it.type == ConfigType.DAVINCI }
val hasDevice = configs.any { it.type == ConfigType.DEVICE }

assertTrue(hasJourney)
assertTrue(hasDaVinci)
assertEquals(false, hasDevice)

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 | ⚡ Quick win

These tests are not asserting the ConfigurationManager helpers.

selectedConfig() is called and then ignored, and the hasConfiguration() cases only assert against local Map/List.any expressions. These tests will still pass if either helper regresses, so they are not covering the behavior their names claim to verify.

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

In
`@samples/pingsampleapp/src/test/kotlin/com/pingidentity/samples/pingsampleapp/config/ConfigurationManagerTest.kt`
around lines 190 - 221, The tests are not invoking the actual helpers under
test; update the three cases to call
ConfigurationManager.selectedConfig(ConfigType.JOURNEY) and
ConfigurationManager.hasConfiguration(ConfigType) (or whichever public helper
names exist) and assert their return values instead of using local Map/List
lookups; for the selectedConfig test ensure the ConfigurationManager is reset or
populated appropriately before asserting null, and for the hasConfiguration
tests populate ConfigurationManager with the sample configurations
(davinciConfig, journeyConfig) then assert
ConfigurationManager.hasConfiguration(ConfigType.JOURNEY) is true,
hasConfiguration(ConfigType.DAVINCI) is true, and
hasConfiguration(ConfigType.DEVICE) is false so the tests validate the actual
helpers.

@vibhorgoswami vibhorgoswami changed the title [feat] SDKS-5065 Standardize SDK configuration SDKS-5065 Standardize SDK configuration Jun 2, 2026
@vibhorgoswami vibhorgoswami deleted the SDKS-5065 branch June 4, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant