Skip to content

SDKS-4937: App parity with iOS#203

Draft
vibhorgoswami wants to merge 3 commits into
developfrom
SDKS-4937
Draft

SDKS-4937: App parity with iOS#203
vibhorgoswami wants to merge 3 commits into
developfrom
SDKS-4937

Conversation

@vibhorgoswami
Copy link
Copy Markdown
Contributor

@vibhorgoswami vibhorgoswami commented May 22, 2026

JIRA Ticket

SDKS-4937

Description

Match Android and iOS Sample Apps

Summary by CodeRabbit

  • New Features
    • Redesigned Token screen with tab-driven navigation, card-based token views, top-bar icon actions, clipboard copy for token fields, expiry countdown, and dedicated error/empty cards.
    • Added “Duplicate” action for Journey and OIDC configs in the Env screen (UI + ViewModel actions).
  • Refactor
    • User profile view reorganized to field-based layout with improved labeling, fallbacks, and raw-info rendering.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

Token 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.

Changes

Token Screen Redesign

Layer / File(s) Summary
ViewModel state and formatting cleanup
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/TokenViewModel.kt
Removed JSON-based token formatting and the public formattedToken flow; retained state MutableStateFlow and existing token operation methods; added provider imports.
Screen imports and TokenScreen initialization
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt
Updated imports to include clipboard support and icon assets. TokenScreen now collects only tokenState and triggers tokenViewModel.loadAllTokens() via LaunchedEffect.
Scaffold, top bar actions, and tab-driven content
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt
TopAppBar uses icon action buttons (access, refresh, revoke, clear). Main content replaced by a TabRow selecting Journey/DaVinci/OIDC and rendering TokenCard, ErrorCard, or EmptyCard per tab.
Token, expiry, field, error, and empty card composables
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt
Added TokenCard (fields + expiry), ExpiryCountdownCard (binary-search expiry threshold and formatted countdown), TokenFieldCard (labeled value with optional truncation and inline clipboard-copy), ErrorCard, and EmptyCard.

Env config duplication

Layer / File(s) Summary
Env wiring and prop changes
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt
Env passes onDuplicate into JourneyCard and OidcCard; onEdit signatures adjusted to Int? to distinguish presets (null) from custom entries.
Journey/OIDC card wiring updates
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt
Preset rows use onTap to edit with index=null and disable edit/delete; custom rows provide index-aware onEdit/onDelete and the new onDuplicate handler.
ConfigRow long-press and duplicate menu
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/Env.kt
ConfigRow now uses combinedClickable (tap + long-press), manages dropdown state, and shows a DropdownMenu with a “Duplicate” item that invokes onDuplicate.
EnvViewModel duplicate actions
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/config/EnvViewModel.kt
Added public methods duplicateJourneyConfig, duplicateDaVinciConfig, and duplicateWebConfig that append copies (with display = "Copy of ...") to custom config lists and persist them on the IO dispatcher.

UserProfile field-driven card

Layer / File(s) Summary
UserProfile imports
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfile.kt
Added height and JsonObject imports used by the refactored UserInfoCard and helpers.
UserInfoCard and UserInfoField refactor
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/userprofile/UserProfile.kt
Reworked UserInfoCard to render UserInfoField rows from JsonObject keys; labels are uppercase, null values are skipped, blank values show \"N/A\", dividers added, and raw-info toggle rendering adjusted.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • spetrov
  • rodrigoareis

Poem

🐰 In tabs the tokens hop and gleam,

Expiry counts like a tiny stream.
Cards hold secrets, clipped with care,
A rabbit nudges them to share.
🥕 Quick hops, soft bytes, UI trimmed fair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% 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
Title check ✅ Passed The title accurately reflects the main objective of the pull request—achieving parity between the Android and iOS sample apps—which is the central theme unifying all the file changes.
Description check ✅ Passed The pull request description includes the required JIRA ticket link and a brief description, though the description is minimal and could provide more detail about specific changes for reviewer context.
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.

✏️ 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-4937

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt (1)

298-311: 💤 Low value

Minor capitalization inconsistency with "DaVinci" tab.

The tab displays "DaVinci" (Line 137), but EmptyCard will show "Davinci" due to replaceFirstChar { 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

📥 Commits

Reviewing files that changed from the base of the PR and between 02ca67e and 1af0521.

📒 Files selected for processing (2)
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/token/Token.kt
  • samples/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
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 42.64%. Comparing base (2f10c6f) to head (11f076b).

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           
Flag Coverage Δ
integration-tests 25.08% <ø> (ø)
unit-tests 25.90% <ø> (ø)

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 win

Update 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

📥 Commits

Reviewing files that changed from the base of the PR and between c3d7cc3 and 11f076b.

📒 Files selected for processing (3)
  • 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/userprofile/UserProfile.kt

Comment on lines +405 to +408
.combinedClickable(
onClick = onTap,
onLongClick = { menuExpanded = true },
)
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

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.

Comment on lines +209 to +213
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('"'))
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

🧩 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:


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.

Suggested change
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.

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