SDKS-4937: App parity with iOS#203
Conversation
📝 WalkthroughWalkthroughToken screen refactored to a tab-driven card UI with top-bar icon actions and clipboard copy; Env adds a long-press Duplicate menu and corresponding ViewModel duplicate methods; UserProfile's info card is refactored to field-driven rows. ChangesToken Screen Redesign
Env config duplication
UserProfile field-driven card
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt (1)
298-311: 💤 Low valueMinor capitalization inconsistency with "DaVinci" tab.
The tab displays "DaVinci" (Line 137), but
EmptyCardwill show "Davinci" due toreplaceFirstChar { it.uppercase() }on"davinci". Consider using a display name property or mapping for consistency.💡 Suggested approach
`@Composable` private fun EmptyCard(tab: TokenType) { + val tabDisplayName = when (tab) { + TokenType.JOURNEY -> "Journey" + TokenType.DAVINCI -> "DaVinci" + TokenType.OIDC -> "OIDC" + } Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), ) { Text( - text = "No ${tab.name.lowercase().replaceFirstChar { it.uppercase() }} token available", + text = "No $tabDisplayName token available", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(16.dp), ) } }🤖 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/token/Token.kt` around lines 298 - 311, EmptyCard currently builds a display string from tab.name using lowercase + replaceFirstChar which yields "Davinci" for the DaVinci tab; instead add/use a canonical display name on the TokenType (e.g., a displayName property or a when-mapping) and use that in EmptyCard (replace uses of tab.name.lowercase().replaceFirstChar { it.uppercase() } with tab.displayName or a small mapping in EmptyCard that returns "DaVinci" for the DaVinci enum value and the usual capitalized form for others) so the casing matches the tab label exactly.
🤖 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.
Nitpick comments:
In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt`:
- Around line 298-311: EmptyCard currently builds a display string from tab.name
using lowercase + replaceFirstChar which yields "Davinci" for the DaVinci tab;
instead add/use a canonical display name on the TokenType (e.g., a displayName
property or a when-mapping) and use that in EmptyCard (replace uses of
tab.name.lowercase().replaceFirstChar { it.uppercase() } with tab.displayName or
a small mapping in EmptyCard that returns "DaVinci" for the DaVinci enum value
and the usual capitalized form for others) so the casing matches the tab label
exactly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c2dba3de-9f99-4d86-87da-400ae6eca3d8
📒 Files selected for processing (2)
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.ktsamples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/TokenViewModel.kt
💤 Files with no reviewable changes (1)
- samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/TokenViewModel.kt
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #203 +/- ##
==========================================
Coverage 42.64% 42.64%
Complexity 1287 1287
==========================================
Files 312 312
Lines 9447 9447
Branches 1403 1403
==========================================
Hits 4029 4029
Misses 4862 4862
Partials 556 556
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…urrent configuration.
There was a problem hiding this comment.
Actionable comments posted: 2
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/UserProfile.kt (1)
2-2:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate copyright header to modified-file year range.
This file appears to be an existing file with new edits, so the header should use a year range (original year–current year), not a single year.
Based on learnings: “For existing files that are being modified, use a year range from the original creation year through the current year (e.g.,
Copyright (c) 2025-2026 Ping Identity Corporation).”🤖 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/UserProfile.kt` at line 2, This file's header uses a single-year copyright; update the header comment to a year range reflecting the original creation year through the current year (e.g., change "Copyright (c) 2026 Ping Identity Corporation" to "Copyright (c) 2025-2026 Ping Identity Corporation") in the top-of-file comment of UserProfile.kt (package/com.pingidentity/samples/pingsampleapp/userprofile or class UserProfile) so the modified-file year range is used for existing files.
🤖 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
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt`:
- Around line 405-408: The long-press-only duplicate action uses
combinedClickable with onLongClick but lacks an accessibility label; update the
combinedClickable call(s) (the one with onClick = onTap, onLongClick = {
menuExpanded = true } and the similar occurrence later) to pass an
onLongClickLabel describing the action (e.g., "Open duplicate menu" or "Show
duplicate options") so screen readers expose the long-press affordance; ensure
the label string is localized like other UI text.
In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfile.kt`:
- Around line 209-213: The current UserInfoField initializations use
user?.get(...)? .toString()?.trim('"') which is fragile because
JsonElement.toString() returns quoted JSON; replace these with safe extraction
via the JsonPrimitive API: use
user?.get("fieldName")?.jsonPrimitive?.contentOrNull (e.g., for "given_name",
"family_name", "email", "preferred_username") so the unquoted String or null is
returned; update the four UserInfoField(...) calls (the UserInfoField(...) lines
for First Name, Family Name, Email, Username) and add the necessary
kotlinx.serialization.json import if missing.
---
Outside diff comments:
In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfile.kt`:
- Line 2: This file's header uses a single-year copyright; update the header
comment to a year range reflecting the original creation year through the
current year (e.g., change "Copyright (c) 2026 Ping Identity Corporation" to
"Copyright (c) 2025-2026 Ping Identity Corporation") in the top-of-file comment
of UserProfile.kt (package/com.pingidentity/samples/pingsampleapp/userprofile or
class UserProfile) so the modified-file year range is used for existing files.
🪄 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: 7d7c69a9-7797-49e5-8b77-773ebfed2bb3
📒 Files selected for processing (3)
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.ktsamples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/EnvViewModel.ktsamples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfile.kt
| .combinedClickable( | ||
| onClick = onTap, | ||
| onLongClick = { menuExpanded = true }, | ||
| ) |
There was a problem hiding this comment.
Add an accessibility label for the long-press duplicate action.
On Line 405, the duplicate entry point is long-press-only, but combinedClickable has no onLongClickLabel. This can make the action hard to access via assistive tech.
Suggested fix
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = onTap,
+ onLongClickLabel = "Show actions",
onLongClick = { menuExpanded = true },
)
.padding(vertical = 4.dp),Also applies to: 459-470
🤖 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 405 - 408, The long-press-only duplicate action uses
combinedClickable with onLongClick but lacks an accessibility label; update the
combinedClickable call(s) (the one with onClick = onTap, onLongClick = {
menuExpanded = true } and the similar occurrence later) to pass an
onLongClickLabel describing the action (e.g., "Open duplicate menu" or "Show
duplicate options") so screen readers expose the long-press affordance; ensure
the label string is localized like other UI text.
| UserInfoField("First Name", user?.get("given_name")?.toString()?.trim('"') | ||
| ?: user?.get("name")?.toString()?.trim('"')) | ||
| UserInfoField("Family Name", user?.get("family_name")?.toString()?.trim('"')) | ||
| UserInfoField("Email", user?.get("email")?.toString()?.trim('"')) | ||
| UserInfoField("Username", user?.get("preferred_username")?.toString()?.trim('"')) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In kotlinx-serialization-json, what is the behavior difference between JsonElement.toString() and JsonPrimitive.contentOrNull for string and JsonNull values?
💡 Result:
Difference between JsonElement.toString and JsonPrimitive.contentOrNull (for strings and JsonNull): 1) JsonElement.toString - For a JsonPrimitive string value, it prints a JSON-valid representation including quotes. For example the JsonElement representing the string "test" will toString to ""test"" (i.e., including the double quotes) [1][2]. - For a JsonNull value, JsonElement.toString prints the literal "null" (as JSON text) [1]. 2) JsonPrimitive.contentOrNull - For a JsonPrimitive string, contentOrNull returns the unquoted string content (not a quoted JSON string) [3]. - For JsonNull, contentOrNull returns null (it is designed specifically so callers get null for JsonNull instead of the string "null") [1][3]. So in practice: - String primitive: - toString -> """" (quoted) - contentOrNull -> "" (unquoted) - JsonNull: - toString -> "null" (JSON literal) - contentOrNull -> null (Kotlin null)
Citations:
- 1: https://github.com/Kotlin/kotlinx.serialization/blob/master/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt
- 2: The built-in String type (JsonPrimitive/JsonLiteral) puts double quotes around the string value Kotlin/kotlinx.serialization#2752
- 3: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-primitive/
Avoid toString().trim('"') when extracting string claims from JsonObject
JsonElement.toString() returns JSON-formatted text (quoted strings) and the literal "null" for JsonNull; trimming quotes is fragile. Prefer jsonPrimitive.contentOrNull to get the unquoted String or null.
Suggested fix
+import kotlinx.serialization.json.contentOrNull
+import kotlinx.serialization.json.jsonPrimitive
+
+private fun JsonObject.stringClaim(key: String): String? =
+ this[key]?.jsonPrimitive?.contentOrNull
+
`@Composable`
private fun UserInfoCard(
title: String,
user: JsonObject?,
@@
- UserInfoField("First Name", user?.get("given_name")?.toString()?.trim('"')
- ?: user?.get("name")?.toString()?.trim('"'))
- UserInfoField("Family Name", user?.get("family_name")?.toString()?.trim('"'))
- UserInfoField("Email", user?.get("email")?.toString()?.trim('"'))
- UserInfoField("Username", user?.get("preferred_username")?.toString()?.trim('"'))
+ UserInfoField("First Name", user?.stringClaim("given_name") ?: user?.stringClaim("name"))
+ UserInfoField("Family Name", user?.stringClaim("family_name"))
+ UserInfoField("Email", user?.stringClaim("email"))
+ UserInfoField("Username", user?.stringClaim("preferred_username"))📝 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.
| UserInfoField("First Name", user?.get("given_name")?.toString()?.trim('"') | |
| ?: user?.get("name")?.toString()?.trim('"')) | |
| UserInfoField("Family Name", user?.get("family_name")?.toString()?.trim('"')) | |
| UserInfoField("Email", user?.get("email")?.toString()?.trim('"')) | |
| UserInfoField("Username", user?.get("preferred_username")?.toString()?.trim('"')) | |
| import kotlinx.serialization.json.contentOrNull | |
| import kotlinx.serialization.json.jsonPrimitive | |
| private fun JsonObject.stringClaim(key: String): String? = | |
| this[key]?.jsonPrimitive?.contentOrNull | |
| `@Composable` | |
| private fun UserInfoCard( | |
| title: String, | |
| user: JsonObject?, | |
| ) { | |
| // ... other content ... | |
| UserInfoField("First Name", user?.stringClaim("given_name") ?: user?.stringClaim("name")) | |
| UserInfoField("Family Name", user?.stringClaim("family_name")) | |
| UserInfoField("Email", user?.stringClaim("email")) | |
| UserInfoField("Username", user?.stringClaim("preferred_username")) | |
| } |
🤖 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/UserProfile.kt`
around lines 209 - 213, The current UserInfoField initializations use
user?.get(...)? .toString()?.trim('"') which is fragile because
JsonElement.toString() returns quoted JSON; replace these with safe extraction
via the JsonPrimitive API: use
user?.get("fieldName")?.jsonPrimitive?.contentOrNull (e.g., for "given_name",
"family_name", "email", "preferred_username") so the unquoted String or null is
returned; update the four UserInfoField(...) calls (the UserInfoField(...) lines
for First Name, Family Name, Email, Username) and add the necessary
kotlinx.serialization.json import if missing.
JIRA Ticket
SDKS-4937
Description
Match Android and iOS Sample Apps
Summary by CodeRabbit