Skip to content

Feature/pingonemfa integration#205

Open
EvgeniyMish wants to merge 14 commits into
ForgeRock:developfrom
EvgeniyMish:feature/pingonemfa-integration
Open

Feature/pingonemfa integration#205
EvgeniyMish wants to merge 14 commits into
ForgeRock:developfrom
EvgeniyMish:feature/pingonemfa-integration

Conversation

@EvgeniyMish
Copy link
Copy Markdown

@EvgeniyMish EvgeniyMish commented Jun 1, 2026

JIRA Ticket

https://pingidentity.atlassian.net/browse/P14C-86660

Description

Summary

Integrates the pingonemfa wrapper module with the Orchestration SDK and wires it into pingsampleapp as a fully-functional reference implementation. This is Phase 1 — wrapper APIs only, no Collector integration.

What's in this PR

pingonemfa module (updated)

Full coroutine wrapper over the native PingOneMFA SDK
PushApprovalService foreground service for background banner approvals/denials
Geo enum mapping public regions to native SDK regions without leaking the native type
Unit tests
Full KDoc on all public API surface
README.md aligned with other documents in this repo

pingsampleapp integration (new)

5 screens: QR pairing scanner, accounts list, OTP with live countdown, mobile payload with copy-to-clipboard,
Push notification screen: handles DEFAULT (approve/deny), CHALLENGE (number options or free-form), and DRY (dismiss), with AlertDialog for success/error outcomes
Background push: PingOneNotificationHelper builds system banners; PingOneNotificationActionReceiver handles banner button taps; PingOnePushNotificationActivity handles foreground full-screen flow
Server-side cancellation: incoming cancel push dismisses the system banner and, if the activity is visible, closes it immediately via LocalBroadcastManager
All UI strings extracted to strings.xml; all AlertDialog feedback uses existing app theme
PingSampleApplication initializes PingOneMFA at startup and registers the FCM token
README.md updated to contain PingOneMFA information

Testing

Set-up native application in PingOne environment

Build with a valid google-services.json
Pairing: tap QR Code Registration → scan a PingOne pairing QR → success dialog → accounts list shows the paired account
OTP: tap One-Time Passcode → code displays with countdown; refreshes automatically at zero
Payload: tap Mobile Payload → payload shown; Copy button writes to clipboard
Push (foreground): trigger a push from PingOne while app is open → approval screen appears; approve/deny shows success dialog
Push (background): trigger a push while app is backgrounded → system banner with Allow/Deny buttons → tapping either completes the request
Cancellation: approve on a second device while the approval screen is open → screen dismisses automatically

Summary by CodeRabbit

  • New Features

    • Added PingOne MFA module: device pairing (QR/manual), OTP with countdown, mobile payloads, push approval flows (default/challenge/dry), system banners, in-app push UI, notification actions, and FCM integration.
  • UI / App Integration

    • Sample app updated with screens, ViewModel, navigation, notification helpers, foreground activity, and single-slot push notification store.
  • Documentation

    • Comprehensive module README and sample-app integration guide.
  • Tests

    • Extensive unit tests covering MFA, push handling, parsing, and service/banner wiring.

EvgeniyMish and others added 4 commits February 2, 2026 09:53
…ingOneMFA SDK into the orchestration SDK.

* initial commit POC
PingOneMFA Android SDK is wrapped in the DV SDK, created new sample app

* Introduce the first phase of a proof of concept for integrating the PingOneMFA
SDK into the orchestration SDK.

This change adds:
- A pingonemfa module that wraps the PingOneMFA SDK APIs required for core flows:
  - Account pairing
  - Retrieval of paired accounts
  - Push authentication
  - OTP authentication
- A PingOneMFApp: sample application demonstrating the supported flows

In this initial phase, all flows operate against the PingOne MFA SSO policy.

* Introduce the first phase of a proof of concept for integrating the PingOneMFA
SDK into the orchestration SDK.

This change adds:
- A pingonemfa module that wraps the PingOneMFA SDK APIs required for core flows:
  - Account pairing
  - Retrieval of paired accounts
  - Push authentication
  - OTP authentication
- A PingOneMFApp: sample application demonstrating the supported flows

In this initial phase, all flows operate against the PingOne MFA SSO policy.

* Introduce the first phase of a proof of concept for integrating the PingOneMFA
SDK into the orchestration SDK.

This change adds:
- A pingonemfa module that wraps the PingOneMFA SDK APIs required for core flows:
  - Account pairing
  - Retrieval of paired accounts
  - Push authentication
  - OTP authentication
- A PingOneMFApp: sample application demonstrating the supported flows

In this initial phase, all flows operate against the PingOne MFA SSO policy.

* added comments and backward compatibility support

* added comments and backward compatibility support

---------

Signed-off-by: Evgeniy Mishustin <33718049+EvgeniyMish@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new pingonemfa Android library (coroutine wrapper over PingOne MFA SDK) with push approval/background service, models, parsers, and tests; integrates it into the sample app with startup initialization, navigation, ViewModel, Compose screens, QR scanner, notification helpers, and push-routing.

Changes

PingOne MFA Library Module

Layer / File(s) Summary
Library build, manifest and README
pingonemfa/README.md, pingonemfa/build.gradle.kts, pingonemfa/.gitignore, pingonemfa/src/main/AndroidManifest.xml
Build configuration, module README, .gitignore, and manifest adding foreground service permissions and PushApprovalService.
Public API types and models
pingonemfa/src/main/java/.../Geo.kt, PingOneMFAException.kt, PingOneMfaAccount.kt, otp/OtpCodeInfo.kt, commons/Error.kt, push/PushType.kt
Region enum, PingOneMFAException, account and OTP data models, Error diagnostics model, and PushType enum with KDoc.
PingOneMFA wrapper and parsers
pingonemfa/src/main/java/.../PingOneMFA.kt, util/AccountParser.kt, util/ErrorParser.kt
Mutex-guarded coroutine wrapper exposing suspend APIs for initialize, setDeviceToken, pair, getDeviceInfo, getOneTimePasscode, processRemoteNotification, generateMobilePayload, plus AccountParser and ErrorParser converters.
Push wrapper and approval service
pingonemfa/src/main/java/.../push/PushNotification.kt, PushApprovalService.kt
Parcelable PushNotification wrapper with approve/deny suspend helpers and PushApprovalService performing background approve/deny calls while holding a foreground notification.
Library unit tests
pingonemfa/src/test/kotlin/.../*
Unit tests covering Geo mapping, PingOneMFA flows (success/error/exception), push approval service, PushNotification wrapper, AccountParser, ErrorParser, and PingOneMFAException behavior.

PingSampleApp Integration

Layer / File(s) Summary
Build, docs, strings and manifest
samples/pingsampleapp/README.md, samples/pingsampleapp/build.gradle.kts, samples/pingsampleapp/src/main/AndroidManifest.xml, samples/pingsampleapp/src/main/res/values/strings.xml
README additions for PingOne MFA, app Gradle dependency, manifest entries for notification receiver/activity, and new UI strings.
App init and navigation
samples/pingsampleapp/src/main/java/.../PingSampleApplication.kt, home/HomeApp.kt, navigation/Navigation.kt
Calls PingOneMFA.initialize during startup, adds home-screen tiles and navigation routes/destinations for PingOne accounts/OTP/payload/QR scanner.
ViewModel and UI state
samples/pingsampleapp/src/main/java/.../pingonemfa/PingOneMFAState.kt, PingOneMFAViewModel.kt
State container and ViewModel orchestrating loadAccounts, pair, collectOtp with countdown, collectPayload, and transient error/message handling.
Compose screens and components
samples/pingsampleapp/src/main/java/.../pingonemfa/ui/*, components/*
New Compose screens: accounts list, OTP screen with countdown, payload viewer, push-approval UI (default/challenge/dry), QR scanner with manual entry, and reusable UI components (ApproveDenyRow, NumberChallengeOptions, ManualNumberChallenge).
Notification store and helpers
samples/pingsampleapp/src/main/java/.../pingonemfa/notification/*
PushNotificationStore (single-slot), PingOneNotificationHelper (banner + actions), PingOneNotificationActionReceiver, PingOnePushNotificationActivity with cancellation broadcast.
Push service integration
samples/pingsampleapp/src/main/java/.../authenticator/service/PushNotificationService.kt
Extends existing PushNotificationService to forward FCM token to PingOneMFA, detect PingOne messages, call PingOneMFA.processRemoteNotification, and display/broadcast notifications or full-screen activity with cancellation handling.

Sequence Diagram(s)

sequenceDiagram
  participant App as PingSampleApp
  participant FCM as Firebase (FCM)
  participant PushSvc as PushNotificationService
  participant PingLib as PingOneMFA (library)
  participant UI as PingOneNotificationHelper / Activity
  App->>FCM: device registers token
  FCM->>App: remote message (PingOne data)
  App->>PushSvc: onMessageReceived(remoteMessage)
  PushSvc->>PingLib: processRemoteNotification(remoteMessage)
  PingLib-->>PushSvc: PushNotification result
  alt cancel
    PushSvc->>UI: remove notification via NotificationManager & broadcast cancel
  else present
    PushSvc->>UI: showPushNotification or start Activity
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • spetrov
  • rodrigoareis
  • brando-dill

"🐰 I hopped into repo trees today,
New MFA code to guide the way.
QR scans, push, and OTP—so neat,
Notifications and screens complete.
Hooray, the sample app can play!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

… into develop

# Conflicts:
#	gradle/libs.versions.toml
#	pingonemfa/.gitignore
#	samples/pingonemfapp/build.gradle.kts
#	samples/pingonemfapp/src/main/AndroidManifest.xml
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/DiagnosticLogger.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/UserPreferences.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/BiometricPromptActivity.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/NotificationHelper.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/service/PushNotificationService.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AboutScreen.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/DiagnosticLogsScreen.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/LoginScreen.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/QrScannerScreen.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/SettingsScreen.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/AccountAvatar.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/BackNavigationTopAppBar.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/EmptyStateMessage.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/ErrorAlertDialog.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/LoadingIndicator.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/SettingItem.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/theme/Color.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/theme/Theme.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/theme/Type.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/util/NavigationAnimations.kt
#	samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/util/QrCodeAnalyzer.kt
#	samples/pingonemfapp/src/main/res/drawable/ic_check.xml
#	samples/pingonemfapp/src/main/res/drawable/ic_close.xml
#	samples/pingonemfapp/src/main/res/drawable/ic_fingerprint.xml
#	samples/pingonemfapp/src/main/res/drawable/ic_launcher_foreground.xml
#	samples/pingonemfapp/src/main/res/drawable/ic_notification.xml
#	samples/pingonemfapp/src/main/res/drawable/ping_logo.xml
#	samples/pingonemfapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
#	samples/pingonemfapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
#	samples/pingonemfapp/src/main/res/values/ic_launcher_background.xml
#	samples/pingonemfapp/src/main/res/values/strings.xml
#	samples/pingonemfapp/src/main/res/values/themes.xml
#	settings.gradle.kts
@EvgeniyMish EvgeniyMish marked this pull request as ready for review June 1, 2026 09:22
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: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
protect/src/main/kotlin/com/pingidentity/protect/Protect.kt (1)

2-2: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the Kotlin file copyright year range.

This file was modified in 2026, so the header should be updated to a range (for example, 2025-2026) instead of a single year.

Based on learnings: existing modified Kotlin/Java files should use a year range from original creation year through the current year.

🤖 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 `@protect/src/main/kotlin/com/pingidentity/protect/Protect.kt` at line 2,
Update the file header in Protect.kt to use a year range instead of a single
year: change the copyright year from "2025" to "2025-2026" in the top-of-file
comment so the header reflects the file was modified in 2026.
🧹 Nitpick comments (25)
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePushNotificationScreen.kt (1)

133-139: 💤 Low value

Hardcoded contentDescription not localized.

This PR extracts UI strings to strings.xml, but the back-button description is a literal "Back". Screen readers will read an unlocalized string. Consider routing it through a string resource for consistency.

♻️ Suggested change
                         Icon(
                             imageVector = Icons.AutoMirrored.Filled.ArrowBack,
-                            contentDescription = "Back",
+                            contentDescription = stringResource(R.string.back),
                         )
🤖 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/pingonemfa/ui/PingOnePushNotificationScreen.kt`
around lines 133 - 139, The hardcoded contentDescription in
PingOnePushNotificationScreen's navigationIcon (inside the IconButton/Icon
block) should be replaced with a localized string resource; update the Icon's
contentDescription to use stringResource(R.string.some_back_label) (or a
descriptive key like R.string.back_navigation) and add the corresponding entry
to strings.xml (and localized variants), ensuring accessibility uses the
resource rather than the literal "Back".
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/util/PingOneMFAQrCodeAnalyzer.kt (1)

52-70: ⚡ Quick win

Throttle is ineffective until a code is detected.

lastAnalyzedTimestamp is only updated inside the success branch (Line 65). Until a valid QR is found, the elapsed-time check always passes, so ML Kit process() runs on essentially every frame — the intended 1s throttle never applies during the common "scanning, nothing detected yet" case, increasing CPU/battery usage. Stamp the timestamp when a frame is actually submitted for processing instead.

♻️ Suggested fix
         if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
+            lastAnalyzedTimestamp = currentTimestamp
             imageProxy.image?.let { image ->
                 val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees)

                 scanner.process(inputImage)
                     .addOnSuccessListener { barcodes ->
                         val found = barcodes.find { barcode ->
                             barcode.format == Barcode.FORMAT_QR_CODE &&
                                     barcode.rawValue?.matchesPingOnePairingKeyScheme() == true
                         }
                         found?.rawValue?.let { pairingKey ->
-                            lastAnalyzedTimestamp = currentTimestamp
                             onQrCodeDetected(pairingKey)
                         }
                     }
🤖 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/pingonemfa/util/PingOneMFAQrCodeAnalyzer.kt`
around lines 52 - 70, The throttle is only updated when a QR is detected because
lastAnalyzedTimestamp is set inside the success branch; instead, update
lastAnalyzedTimestamp when you submit a frame for processing so frames are
rate-limited even if no QR is found. In the block where you check
TimeUnit.SECONDS.toMillis(1) and before calling scanner.process(inputImage)
(referencing lastAnalyzedTimestamp, currentTimestamp, scanner.process,
inputImage, imageProxy, and onQrCodeDetected), assign lastAnalyzedTimestamp =
currentTimestamp immediately after passing the elapsed-time check and before
invoking scanner.process, leaving the existing success/failure handlers
otherwise unchanged.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/authenticator/service/PushNotificationService.kt (1)

102-133: ⚡ Quick win

Make PingOne and generic push routing mutually exclusive.

For a PingOne MFA message, remoteMessage.data is non-empty, so after the PingOne branch (Lines 103-117) the generic block at Lines 119-133 also runs and feeds the PingOne payload into pushClient?.processNotification(...). This double-processes the message and will likely emit a misleading "Error processing notification" diagnostic for every PingOne push. Route to the generic path only when the PingOne marker is absent.

♻️ Proposed guard
         // Check if the message received from PingOne MFA Push by looking for expected key
         if (remoteMessage.data.containsKey("PingOne")) {
             diagnosticLogger.d("Received PingOne MFA Push message")
             scope.launch {
                 // Process the notification using PingOneMFA's processRemoteNotification method, which handles decryption and validation
                 PingOneMFA.processRemoteNotification(remoteMessage)
                     .onSuccess {
                         diagnosticLogger.d("Successfully collected PingOne MFA push notification")
                         // Handle the notification (this will display a system notification or full-screen notification based on app state)
                         handlePingOneNotification(it)
                     }
                     .onFailure { error ->
                         diagnosticLogger.e("Failed to collect PingOne MFA push notification: ${error.message}")
                     }
             }
+            return
         }
         // Handle the message data payload
         if (remoteMessage.data.isNotEmpty()) {
🤖 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/authenticator/service/PushNotificationService.kt`
around lines 102 - 133, The PingOne branch and the generic branch both run for
PingOne pushes because remoteMessage.data is non-empty; change the control flow
so they are mutually exclusive by checking for the PingOne marker before running
the generic path (e.g., use an else branch or return after invoking
PingOneMFA.processRemoteNotification), ensuring that when
PingOneMFA.processRemoteNotification(remoteMessage) (and
handlePingOneNotification(...)) is invoked you do not also call
pushClient?.processNotification(remoteMessage.data ...) or
handleNotification(...).
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AboutScreen.kt (1)

117-121: ⚡ Quick win

Use fillMaxWidth() instead of fillMaxSize() here.

This Card sits inside a Column with verticalScroll, so the height constraint is unbounded and fillMaxHeight (part of fillMaxSize) is a no-op. The intent is to fill width.

♻️ Proposed change
             Card(
                 modifier = Modifier
-                    .fillMaxSize()
+                    .fillMaxWidth()
                     .padding(horizontal = 8.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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AboutScreen.kt`
around lines 117 - 121, In the AboutScreen Card declaration replace the
Modifier.fillMaxSize() call with Modifier.fillMaxWidth() because the Card is
inside a vertically scrolling Column (unbounded height) so fillMaxHeight is a
no-op; update the modifier on the Card in AboutScreen (Card { ... } modifier
usage) to use fillMaxWidth(). Ensure you only change the modifier chain (keep
.padding(horizontal = 8.dp)) and remove the fillMaxSize reference.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/NotificationResponseScreen.kt (1)

285-331: ⚡ Quick win

Remove the large commented-out block (and the smaller ones at Lines 119-121, 149).

This dead/commented code (the locked-credential branch and credential/imageURL snippets) adds noise and references symbols not present in this module. Drop it; version control preserves the history if needed later.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/NotificationResponseScreen.kt`
around lines 285 - 331, Delete the large commented-out locked-credential UI
block in NotificationResponseScreen.kt along with the smaller commented
credential/imageURL snippets; remove references to symbols inside the comments
(credential, isLocked, lockingPolicy, BiometricAvailablePolicy,
DeviceTamperingPolicy, lockMessage, imageURL) so the file contains only active
code—keep no traces of these commented branches since VCS preserves history.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt (5)

162-162: ⚡ Quick win

Extract hardcoded strings to string resources.

Lines 162 ("Settings") and 267 ("No accounts added yet") contain hardcoded strings that should be extracted to strings.xml for consistency and localization support. Line 179 correctly uses stringResource(id = R.string.menu_about) for similar text.

🌐 Proposed fix
                                         Text("Settings")
+                                        Text(stringResource(id = R.string.menu_settings))
                     EmptyStateMessage(
-                        title = "No accounts added yet",
+                        title = stringResource(id = R.string.accounts_empty_state_title),
                         subtitle = stringResource(id = R.string.accounts_empty_state_subtitle)

Note: R.string.menu_settings and R.string.accounts_empty_state_title already exist in strings.xml.

Also applies to: 267-267

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt`
at line 162, Replace hardcoded UI strings in the AccountsScreen composable by
using string resources: change the Text("Settings") instance to use
stringResource(id = R.string.menu_settings) and change the Text("No accounts
added yet") instance to use stringResource(id =
R.string.accounts_empty_state_title); ensure you import
androidx.compose.ui.res.stringResource and reference the existing R.string IDs
(menu_settings, accounts_empty_state_title) in the AccountsScreen function so
all displayed text comes from strings.xml.

193-234: ⚡ Quick win

Clarify or remove redundant secondary FAB animation.

The showFabMenu state and AnimatedVisibility wrapper (lines 194-218) suggest a multi-action FAB menu, but both the animated secondary FAB (line 207: onScanQrCode()) and the primary FAB (line 224: onScanQrCode()) perform the same action. Additionally, showFabMenu is set to false immediately when clicked (line 206, 223), so the secondary FAB never actually animates in. This setup appears incomplete or unnecessary.

🧹 Proposed fix

If only one action is needed, remove the animated secondary FAB:

         floatingActionButton = {
-            Column(horizontalAlignment = Alignment.End) {
-                AnimatedVisibility(
-                    visible = showFabMenu,
-                    enter = fadeIn(),
-                    exit = fadeOut()
-                ) {
-                    Column(
-                        horizontalAlignment = Alignment.End,
-                        verticalArrangement = Arrangement.spacedBy(8.dp)
-                    ) {
-                        // Scan QR code option
-                        FloatingActionButton(
-                            onClick = {
-                                showFabMenu = false
-                                onScanQrCode()
-                            },
-                            modifier = Modifier.size(48.dp),
-                            containerColor = MaterialTheme.colorScheme.secondaryContainer
-                        ) {
-                            Icon(
-                                imageVector = Icons.Default.QrCodeScanner,
-                                contentDescription = "Scan QR Code"
-                            )
-                        }
-                    }
-                }
-                
-                // Primary FAB
-                FloatingActionButton(
+            FloatingActionButton(
                     onClick = {
-                        showFabMenu = false
                         onScanQrCode()
                     },
                     containerColor = MaterialTheme.colorScheme.primaryContainer,
                     contentColor = MaterialTheme.colorScheme.onPrimaryContainer
                 ) {
                     Icon(
                         imageVector = Icons.Default.Add,
                         contentDescription = stringResource(id = R.string.content_description_add_account)
                     )
                 }
-            }
         },

Also remove the unused showFabMenu state declaration (line 101).

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt`
around lines 193 - 234, The AnimatedVisibility block and the showFabMenu state
are redundant because both the secondary and primary FloatingActionButton call
onScanQrCode() and showFabMenu is immediately set to false, so remove the entire
AnimatedVisibility column (the secondary FloatingActionButton that calls
onScanQrCode()) and delete the showFabMenu state declaration; keep only the
primary FloatingActionButton (the one using Icons.Default.Add and
stringResource(id = R.string.content_description_add_account)) so a single FAB
performs the scan action.

280-285: 💤 Low value

Fix key generation comment to match implementation.

The comment claims the key uses "issuer, account name, and all credential IDs" but the actual implementation uses userId and deviceId. Update the comment to reflect the actual key composition.

📝 Proposed fix
                         key = { account ->
-                            // Create a unique key using issuer, account name, and all credential IDs
+                            // Create a unique key using user ID and device ID
                             val userId = account.id
                             val deviceId = account.deviceId
🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt`
around lines 280 - 285, Update the misleading comment above the key lambda so it
accurately describes the implemented key composition: replace the text claiming
it uses "issuer, account name, and all credential IDs" with a concise note that
the key is built from account.id and account.deviceId (variables userId and
deviceId) used in the key = { account -> ... } block.

254-264: ⚡ Quick win

Consolidate redundant loading state checks.

Both isInitialLoading and isLoadingPingOneAccounts display identical LoadingIndicator messages. This duplication can be simplified.

♻️ Proposed refactor
             when {
-                uiState.isInitialLoading -> {
-                    LoadingIndicator(
-                        message = stringResource(id = R.string.loading_accounts)
-                    )
-                }
-                uiState.isLoadingPingOneAccounts -> {
+                uiState.isInitialLoading || uiState.isLoadingPingOneAccounts -> {
                     LoadingIndicator(
                         message = stringResource(id = R.string.loading_accounts)
                     )
🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt`
around lines 254 - 264, The two identical branches in the when block of
AccountsScreen (checking uiState.isInitialLoading and
uiState.isLoadingPingOneAccounts) should be consolidated into a single branch
that checks the combined condition (e.g., uiState.isInitialLoading ||
uiState.isLoadingPingOneAccounts) and renders LoadingIndicator once; update the
when in the AccountsScreen composable to replace the duplicate branches with one
branch using the combined predicate so LoadingIndicator (message =
stringResource(id = R.string.loading_accounts)) is only declared once.

300-304: ⚡ Quick win

Replace non-null assertion with safe error handling.

Using !! on uiState.error (line 302) can crash the app if the state is unexpectedly null. The null-check on line 300 guards this, but using a let block is safer and more idiomatic.

🛡️ Proposed fix
             // Error handling
-            if (uiState.error != null) {
-                ErrorAlertDialog(
-                    errorMessage = uiState.error!!,
-                    onDismiss = { viewModel.clearError() }
-                )
-            }
+            uiState.error?.let { errorMessage ->
+                ErrorAlertDialog(
+                    errorMessage = errorMessage,
+                    onDismiss = { viewModel.clearError() }
+                )
+            }
🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt`
around lines 300 - 304, The current AccountsScreen uses a non-null assertion
uiState.error!! when calling ErrorAlertDialog which risks an NPE; replace this
pattern by using a safe let scope on uiState.error (e.g., uiState.error?.let {
error -> ErrorAlertDialog(errorMessage = error, onDismiss = {
viewModel.clearError() }) }) so ErrorAlertDialog only receives a non-null String
and you can remove the explicit null-check + !!; keep the onDismiss calling
viewModel.clearError() as-is.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/OtpScreen.kt (1)

44-48: 💤 Low value

Extract the loading message to strings.xml.

The PR objective notes UI strings are externalized; this literal should use stringResource(...) for consistency and localization.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/OtpScreen.kt`
around lines 44 - 48, The hard-coded loading text in OtpScreen's state.isLoading
branch should be moved to resources: add a new string entry (e.g.,
name="loading_otp_code") in strings.xml, then replace the literal "Loading OTP
code..." in the LoadingIndicator call with
stringResource(R.string.loading_otp_code). Update imports to include
androidx.compose.ui.res.stringResource if needed and ensure the change is made
inside the OtpScreen composable where state.isLoading is handled.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/ExpiringOtpCode.kt (1)

42-42: 💤 Low value

Stale comment.

The comment says "Green -> Yellow -> Red", but the interpolation is PingLightBluePingRed. Update or remove to avoid confusion.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/ExpiringOtpCode.kt`
at line 42, The inline comment above the color interpolation in
ExpiringOtpCode.kt is stale ("Green -> Yellow -> Red") and should be updated or
removed to match the actual interpolation (PingLightBlue → PingRed); locate the
comment near the interpolation logic in the ExpiringOtpCode component and either
replace the text with an accurate description like "Interpolate between
PingLightBlue -> PingRed" or delete the comment entirely to avoid confusion.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/SettingItem.kt (1)

96-101: 💤 Low value

Avoid a hardcoded content description.

The chevron's contentDescription = "Navigate" is a hardcoded literal. Since it's a decorative affordance for an already-labeled row, prefer null, or source from strings.xml if it should be announced.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/SettingItem.kt`
around lines 96 - 101, The Icon in SettingItem (inside the hasNavigation &&
onNavigate != null branch) uses a hardcoded contentDescription "Navigate";
change it to null (or load a localized string resource if it must be announced)
so the decorative chevron isn't redundantly announced — update the Icon call in
SettingItem.kt where hasNavigation and onNavigate are checked to set
contentDescription = null (or use stringResource(...) if you decide to provide
an accessible label).
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/SettingsScreen.kt (1)

70-107: 💤 Low value

Extract hardcoded UI strings to strings.xml.

This screen hardcodes user-facing text ("Settings", "Theme", the theme description, "Enable diagnostic logging", "View diagnostic logs", etc.), whereas BackNavigationTopAppBar already sources its content description via stringResource. Since the app already maintains a strings.xml, move these literals there for consistency and localization.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/SettingsScreen.kt`
around lines 70 - 107, Replace hardcoded UI strings in SettingsScreen.kt with
string resources: move literals like "Settings", "Theme", the theme description,
"Enable diagnostic logging", and "View diagnostic logs" into strings.xml and use
stringResource(...) in the UI. Update BackNavigationTopAppBar title to use
stringResource(R.string.settings), replace SettingItem title/description
arguments to use stringResource(R.string.theme) and
stringResource(R.string.theme_description, getThemeDisplayName(themeMode)) for
the interpolated theme text, and similarly use stringResource for the
diagnosticLogging titles/descriptions; keep the same calls to SettingItem,
getThemeDisplayName(themeMode), viewModel.setDiagnosticLogging, and the
diagnosticLogging conditional but substitute the string literals with
stringResource references.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/util/QrCodeAnalyzer.kt (1)

56-59: 💤 Low value

Replace printStackTrace() with structured logging.

Scan failures are silently dumped to stderr via printStackTrace(), which is invisible in production diagnostics. The app already has a DiagnosticLogger; route failures there instead so they’re captured consistently.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/util/QrCodeAnalyzer.kt`
around lines 56 - 59, In QrCodeAnalyzer replace the use of
exception.printStackTrace() inside the addOnFailureListener lambda with a call
to the app's DiagnosticLogger (e.g., DiagnosticLogger.error or the equivalent
logging method used elsewhere) so scan failures are recorded in structured logs;
include a clear contextual message (e.g., "QR scan failed") and pass the
exception object to the logger to preserve stack/kv details.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/QrScannerScreen.kt (1)

108-117: 💤 Low value

Remove commented-out dead code.

These commented snackbar/lastAddedOathCredential lines are leftover scaffolding and add noise. Delete them before merge.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/QrScannerScreen.kt`
around lines 108 - 117, Remove the commented-out dead code block related to
showing a snackbar for lastAddedOathCredential: delete the LaunchedEffect block
and its references to uiState.lastAddedOathCredential,
snackbarHostState.showSnackbar(...), viewModel.clearLastAddedOathCredential(),
and onScanComplete() in QrScannerScreen.kt so only active code remains and no
leftover scaffold comments are left behind.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/MainViewModel.kt (1)

101-108: 💤 Low value

No-op copy on success path.

it.copy(pingOneMfaAccounts = it.pingOneMfaAccounts, error = null) reassigns the accounts list to itself; only error = null has effect (account updates flow in via setupStateFlows). The redundant field is harmless but misleading — drop it for clarity.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/MainViewModel.kt`
around lines 101 - 108, In loadPingOneMfaAccounts(), the success branch calls
_uiState.update with a no-op copy that reassigns pingOneMfaAccounts to itself;
remove that redundant field so the update only clears error (e.g.,
_uiState.update { it.copy(error = null) }), leaving pingOneMfaAccounts to be
updated by the existing setupStateFlows; keep pingOneMfaAccountsLoaded = true
and the accountsManager.loadAccounts().onFailure block unchanged.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/MainActivity.kt (1)

107-126: 🏗️ Heavy lift

ViewModel is constructed directly, so it won't survive configuration changes.

Instantiating PingOneMFAViewModel with its constructor inside lifecycleScope rebuilds it (and reloads accounts) on every onCreate, including rotation, defeating the purpose of a ViewModel. Obtain it through ViewModelProvider/viewModels() with a factory so its state is retained.

🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/MainActivity.kt`
around lines 107 - 126, setupViewModels currently constructs PingOneMFAViewModel
directly inside lifecycleScope which causes a new instance on every onCreate;
instead create a PingOneMFAViewModelFactory that accepts Application,
UserPreferences, AccountsManager, OTPManager (or providers for async init) and
use ViewModelProvider / by viewModels(factory) to obtain the ViewModel so it
survives configuration changes; move client creation into the factory or provide
singleton managers (AccountsManager, OTPManager) that are created once and
passed into the factory, and replace the direct constructor call in
setupViewModels with ViewModelProvider(this,
factory).get(PingOneMFAViewModel::class.java) (or equivalent viewModels usage).
samples/pingonemfapp/build.gradle.kts (1)

62-65: 💤 Low value

Remove composeOptions.kotlinCompilerExtensionVersion since the module uses the Kotlin Compose compiler Gradle plugin

  • In samples/pingsampleapp/build.gradle.kts (lines 58-60), composeOptions { kotlinCompilerExtensionVersion = "1.5.9" } is likely redundant/misleading because the module applies alias(libs.plugins.compose.compiler) (which is org.jetbrains.kotlin.plugin.compose versioned by the Kotlin version in gradle/libs.versions.toml).
composeOptions {
    kotlinCompilerExtensionVersion = "1.5.9"
}

Remove the composeOptions block to avoid implying that 1.5.9 controls the Compose compiler.

🤖 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/pingonemfapp/build.gradle.kts` around lines 62 - 65, The
composeOptions block setting kotlinCompilerExtensionVersion is redundant because
this module applies the Kotlin Compose compiler plugin
(alias(libs.plugins.compose.compiler) / org.jetbrains.kotlin.plugin.compose)
which controls the compiler version; remove the composeOptions {
kotlinCompilerExtensionVersion = "1.5.8" } block from the build.gradle.kts
(eliminate the composeOptions/kotlinCompilerExtensionVersion entry) so the
plugin-managed version from gradle/libs.versions.toml is used instead.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/NotificationHelper.kt (1)

84-84: 💤 Low value

Variable shadows parameter with same name.

val title = title ?: ... shadows the function parameter title, which can be confusing. Use a different name for clarity.

Suggested fix
-        val title = title ?: context.getString(R.string.system_notification_title)
+        val notificationTitle = title ?: context.getString(R.string.system_notification_title)
         val content = when {
             body != null -> body
             else -> context.getString(R.string.system_notification_content)
         }
 
         // Build the notification
         val builder = NotificationCompat.Builder(context, CHANNEL_ID)
             .setSmallIcon(R.drawable.ic_notification)
-            .setContentTitle(title)
+            .setContentTitle(notificationTitle)
🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/NotificationHelper.kt`
at line 84, The parameter name title in the relevant function is being shadowed
by a local val (val title = title ?: ...); rename the local variable to a
non-conflicting name (for example resolvedTitle or fallbackTitle) and update
subsequent uses in the function to reference that new name; ensure the parameter
name remains unchanged and the fallback logic uses
context.getString(R.string.system_notification_title) when title is null.
samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/managers/OTPManager.kt (1)

23-23: ⚡ Quick win

Public mutable Job field allows external interference.

otpRefreshJob is a public var, allowing callers to reassign it and break the single-job invariant enforced by startAutoRefresh. Consider making it private or exposing only a read-only view.

Suggested fix
-    var otpRefreshJob: Job? = null
+    private var otpRefreshJob: Job? = null
🤖 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/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/managers/OTPManager.kt`
at line 23, otpRefreshJob is a public mutable var which lets callers reassign or
cancel it; make it private to preserve the single-job invariant enforced by
startAutoRefresh and only expose a safe read-only view. Change otpRefreshJob to
a private var otpRefreshJob: Job? and, if external visibility is required, add a
public read-only accessor (e.g., val isAutoRefreshActive: Boolean get() =
otpRefreshJob?.isActive == true or val currentOtpRefreshJob: Job? get() =
otpRefreshJob) and update startAutoRefresh / any cancelAutoRefresh logic to use
the private field.
pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt (1)

26-33: ⚡ Quick win

Preserve the original cause for better diagnostics.

internal constructor(cause: Exception) only forwards cause.message; the original throwable is discarded, so its stack trace won't be chained in logs/crash reports. Thread the cause through to Exception(message, cause).

♻️ Proposed change to retain the cause chain
-class PingOneMFAException(message: String?) : Exception(message) {
+class PingOneMFAException(message: String?, cause: Throwable? = null) : Exception(message, cause) {
 
     // Internal factory constructors — accept native SDK type but keep them off the public API surface entirely.
     internal constructor(error: PingOneSDKError) : this(
         "Code=${error.code} \"${error.message}\" UserInfo=${error.userInfo}"
     )
 
-    internal constructor(cause: Exception) : this(cause.message ?: "Unknown error")
+    internal constructor(cause: Exception) : this(cause.message ?: "Unknown error", cause)
 }
🤖 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
`@pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt`
around lines 26 - 33, The secondary constructor in PingOneMFAException discards
the original cause; update the exception class to preserve cause chaining by
adding a cause parameter to the primary constructor and passing it to the
superclass (e.g., change class PingOneMFAException(message: String?) :
Exception(message) to accept an optional cause and call Exception(message,
cause)), then change internal constructor(cause: Exception) to delegate to the
primary constructor with this(cause.message ?: "Unknown error", cause) so the
original throwable is preserved in the stack trace.
pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt (2)

97-130: ⚡ Quick win

Reuse the existing PushNotification suspend wrappers instead of re-implementing approve/deny.

PushNotification already exposes approveNotification(context, authenticationMethod, numberChallenge) and denyNotification(context) that bridge the same callbacks into Result. These two private helpers duplicate that logic. Calling the existing functions keeps a single source of truth for the callback bridging and error mapping.

♻️ Suggested reuse
         scope.launch {
             try {
                 if (userAction.equals("approve", ignoreCase = true)) {
-                    approveNotificationWithAppInBackground(notificationObject, authMethod)
-                }else{
-                    denyNotificationWithAppInBackground(notificationObject)
+                    notificationObject.approveNotification(this@PushApprovalService, authMethod)
+                        .getOrThrow()
+                } else {
+                    notificationObject.denyNotification(this@PushApprovalService)
+                        .getOrThrow()
                 }
             } catch (e: Exception) {
🤖 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
`@pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt`
around lines 97 - 130, The two private suspend helpers
approveNotificationWithAppInBackground and denyNotificationWithAppInBackground
duplicate existing suspend wrappers on PushNotification; replace their internal
implementations (and call sites) to delegate to
PushNotification.approveNotification(context, authenticationMethod,
numberChallenge) and PushNotification.denyNotification(context) respectively,
then remove the duplicate callback-to-coroutine bridging code so the single
source of truth in PushNotification handles error/result mapping.

84-91: 💤 Low value

Hardcoded, non-localized notification strings in a library.

The channel name and notification title/text are hardcoded English literals. Since this is a reusable library surface shown to end users, consider sourcing them from string resources so consumers can localize.

🤖 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
`@pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt`
around lines 84 - 91, The notification channel name and notification title/text
are hardcoded in PushApprovalService when creating the NotificationChannel and
building the NotificationCompat.Builder; replace those literals with string
resources and load them via getString(...) (e.g., use
getString(R.string.mfa_approval_channel_name) for the channel name and
getString(R.string.approving_login_title) /
getString(R.string.contacting_server_text) for the notification title and text)
so consumers can localize; update any related XML string resource files with
appropriate keys and ensure NotificationChannel creation and
NotificationCompat.Builder use the resource strings.
pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt (1)

331-349: ⚡ Quick win

Deduplicate and harden the aps JSON parsing.

These two helpers parse the same message.data["aps"] string independently (parsed twice per notification at Lines 240–241), and Json.parseToJsonElement throws SerializationException on malformed input. If the SDK callback runs off the registering thread, that throw escapes the try in processRemoteNotification. Consider parsing once into a nullable model with runCatching and extracting both fields.

♻️ Suggested consolidation
-                                title = getTitleFromRemoteMessageData(message.data["aps"]),
-                                message = getBodyFromRemoteMessageData(message.data["aps"])
+                                title = parseAps(message.data["aps"])?.title,
+                                message = parseAps(message.data["aps"])?.body
-    private fun getTitleFromRemoteMessageData(data: String?): String? =
-        data?.let {
-            Json.parseToJsonElement(it)
-                .jsonObject["alert"]
-                ?.jsonObject
-                ?.get("title")
-                ?.jsonPrimitive
-                ?.contentOrNull
-        }
-
-    private fun getBodyFromRemoteMessageData(data: String?): String? =
-        data?.let {
-            Json.parseToJsonElement(it)
-                .jsonObject["alert"]
-                ?.jsonObject
-                ?.get("body")
-                ?.jsonPrimitive
-                ?.contentOrNull
-        }
+    private data class Aps(val title: String?, val body: String?)
+
+    private fun parseAps(data: String?): Aps? = data?.let {
+        runCatching {
+            val alert = Json.parseToJsonElement(it).jsonObject["alert"]?.jsonObject
+            Aps(
+                title = alert?.get("title")?.jsonPrimitive?.contentOrNull,
+                body = alert?.get("body")?.jsonPrimitive?.contentOrNull,
+            )
+        }.getOrNull()
+    }
🤖 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 `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt`
around lines 331 - 349, The two helpers getTitleFromRemoteMessageData and
getBodyFromRemoteMessageData both re-parse the same JSON (message.data["aps"])
and can throw SerializationException; refactor by parsing the aps JSON once
(e.g., in processRemoteNotification or a new private parseApsModel function)
using runCatching to return a nullable parsed model (data class or JsonObject),
then derive title and body from that single parsed result rather than calling
Json.parseToJsonElement twice; ensure the parsing is wrapped in
runCatching/nullable handling so malformed input doesn't throw out of
processRemoteNotification and update callers to use the single parsed result to
populate title/body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5be0659a-a2ef-44fc-9f2e-ce7c4f9dfe7a

📥 Commits

Reviewing files that changed from the base of the PR and between 2f10c6f and 6bff127.

📒 Files selected for processing (95)
  • README.md
  • gradle/libs.versions.toml
  • pingonemfa/.gitignore
  • pingonemfa/README.md
  • pingonemfa/build.gradle.kts
  • pingonemfa/src/main/AndroidManifest.xml
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/Geo.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMfaAccount.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/otp/OtpCodeInfo.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushNotification.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushType.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/AccountParser.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/GeoTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFATest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushApprovalServiceTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/AccountParserTest.kt
  • protect/src/main/kotlin/com/pingidentity/protect/Protect.kt
  • samples/pingonemfapp/.gitignore
  • samples/pingonemfapp/README.md
  • samples/pingonemfapp/build.gradle.kts
  • samples/pingonemfapp/src/main/AndroidManifest.xml
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/MainActivity.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/PingOneMFApp.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/DiagnosticLogger.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/MainViewModel.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/UiModels.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/data/UserPreferences.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/managers/AccountsManager.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/managers/OTPManager.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/BiometricPromptActivity.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/NotificationActionReceiver.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/NotificationHelper.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/notification/PushNotificationActivity.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/service/PushNotificationService.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AboutScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AccountsScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/AuthenticatorNavHost.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/DiagnosticLogsScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/LoginScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/NotificationResponseScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/OtpScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/QrScannerScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/SettingsScreen.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/AccountAvatar.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/AccountCard.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/BackNavigationTopAppBar.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/EmptyStateMessage.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/ErrorAlertDialog.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/ExpiringOtpCode.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/LoadingIndicator.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/components/SettingItem.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/theme/Color.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/theme/Theme.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/ui/theme/Type.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/util/NavigationAnimations.kt
  • samples/pingonemfapp/src/main/kotlin/com/pingidentity/pingonemfapp/util/QrCodeAnalyzer.kt
  • samples/pingonemfapp/src/main/res/drawable/ic_check.xml
  • samples/pingonemfapp/src/main/res/drawable/ic_close.xml
  • samples/pingonemfapp/src/main/res/drawable/ic_fingerprint.xml
  • samples/pingonemfapp/src/main/res/drawable/ic_launcher_foreground.xml
  • samples/pingonemfapp/src/main/res/drawable/ic_notification.xml
  • samples/pingonemfapp/src/main/res/drawable/ping_logo.xml
  • samples/pingonemfapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • samples/pingonemfapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • samples/pingonemfapp/src/main/res/values/ic_launcher_background.xml
  • samples/pingonemfapp/src/main/res/values/strings.xml
  • samples/pingonemfapp/src/main/res/values/themes.xml
  • samples/pingsampleapp/README.md
  • samples/pingsampleapp/build.gradle.kts
  • samples/pingsampleapp/src/main/AndroidManifest.xml
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/PingSampleApplication.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/authenticator/service/PushNotificationService.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/home/HomeApp.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/navigation/Navigation.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/PingOneMFAState.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/PingOneMFAViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/notification/PingOneNotificationActionReceiver.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/notification/PingOneNotificationHelper.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/notification/PingOnePushNotificationActivity.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/notification/PushNotificationStore.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneMFAAccountsScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneOTPScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePayloadScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePushNotificationScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneQrScannerScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/components/ApproveDenyRow.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/components/ManualNumberChallenge.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/components/NumberChallengeOptions.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/util/PingOneMFAQrCodeAnalyzer.kt
  • samples/pingsampleapp/src/main/res/values/strings.xml
  • settings.gradle.kts

Comment thread pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 25.90%. Comparing base (2f10c6f) to head (15f729a).

❗ There is a different number of reports uploaded between BASE (2f10c6f) and HEAD (15f729a). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (2f10c6f) HEAD (15f729a)
integration-tests 1 0
Additional details and impacted files
@@              Coverage Diff               @@
##             develop     #205       +/-   ##
==============================================
- Coverage      42.64%   25.90%   -16.75%     
+ Complexity      1287      932      -355     
==============================================
  Files            312      306        -6     
  Lines           9447     9199      -248     
  Branches        1403     1369       -34     
==============================================
- Hits            4029     2383     -1646     
- Misses          4862     6500     +1638     
+ Partials         556      316      -240     
Flag Coverage Δ
integration-tests ?
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.

Caution

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

⚠️ Outside diff range comments (1)
pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt (1)

152-160: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the method result, not the mock field directly.

Line 159 currently checks push.notificationObject.numberMatchingOptions, which bypasses getNumbersChallenge() and weakens the test’s stated intent. Assert the method output instead.

Proposed fix
-        assertTrue(push.notificationObject.numberMatchingOptions == null || push.notificationObject.numberMatchingOptions.isEmpty())
+        val result = push.getNumbersChallenge()
+        assertTrue(result == null || result.isEmpty())
🤖 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
`@pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt`
around lines 152 - 160, The test currently asserts the mock field directly
instead of the method under test; change the assertion to call
PushNotification.getNumbersChallenge() on the push instance (created with the
mocked notificationObject and numberMatchingType returning null) and assert that
the returned collection/array is empty (or null according to the method's
contract), removing the direct check of
push.notificationObject.numberMatchingOptions so the test verifies
getNumbersChallenge() behavior.
🤖 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.

Outside diff comments:
In
`@pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt`:
- Around line 152-160: The test currently asserts the mock field directly
instead of the method under test; change the assertion to call
PushNotification.getNumbersChallenge() on the push instance (created with the
mocked notificationObject and numberMatchingType returning null) and assert that
the returned collection/array is empty (or null according to the method's
contract), removing the direct check of
push.notificationObject.numberMatchingOptions so the test verifies
getNumbersChallenge() behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cceea350-4450-4a18-af80-aa3445bcfcfb

📥 Commits

Reviewing files that changed from the base of the PR and between 6bff127 and 5f079fe.

📒 Files selected for processing (3)
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/PingOneMFAViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/PingOneMFAViewModel.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/push/PushApprovalService.kt

@EvgeniyMish EvgeniyMish marked this pull request as draft June 1, 2026 15:53
@EvgeniyMish EvgeniyMish marked this pull request as ready for review June 2, 2026 10:37
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/pingonemfa/ui/PingOneOTPScreen.kt (1)

96-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Misleading copy for the not-paired state.

The isOtpDeviceNotPaired branch reuses accounts_empty_state_title/subtitle, which describe an empty accounts list rather than "device not paired — pair a device to view its OTP." Consider a dedicated string so the message matches the actual condition.

🤖 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/pingonemfa/ui/PingOneOTPScreen.kt`
around lines 96 - 100, The not-paired branch for state.isOtpDeviceNotPaired
incorrectly reuses accounts_empty_state_title/subtitle; update the
EmptyStateMessage call in the isOtpDeviceNotPaired branch to use dedicated
string resources (e.g., otp_device_not_paired_title and
otp_device_not_paired_subtitle) and add those entries to the strings.xml with
copy that clearly instructs the user to pair a device to view OTPs; keep the
EmptyStateMessage usage and parameters (title/subtitle) unchanged except for the
new stringResource keys so the UI text matches the condition.
🧹 Nitpick comments (5)
pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt (2)

338-364: 💤 Low value

Extract duplicated intent-extra keys/builder.

approvePushNotificationFromBanner and denyPushNotificationFromBanner are identical except for the user_action value, and the extra keys ("notification", "auth_method", "user_action") are string literals shared with PushApprovalService. Diverging keys here would silently break the service contract. Consider shared constants and a single private helper parameterized by action.

🤖 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 `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt`
around lines 338 - 364, approvePushNotificationFromBanner and
denyPushNotificationFromBanner duplicate intent building and use hard-coded
extra keys that must match PushApprovalService; refactor by introducing shared
constants for the intent extras (e.g., NOTIFICATION_EXTRA, AUTH_METHOD_EXTRA,
USER_ACTION_EXTRA) and a single private helper method (e.g.,
sendPushApprovalFromBanner(action: String)) that constructs the Intent, sets the
extras using the constants, and calls ContextCompat.startForegroundService;
update approvePushNotificationFromBanner and denyPushNotificationFromBanner to
call this helper with "approve" and "deny" respectively and keep references to
ContextProvider.context and PushApprovalService unchanged.

267-290: ⚡ Quick win

A malformed aps payload fails an otherwise-valid notification.

getTitleFromRemoteMessageData / getBodyFromRemoteMessageData call Json.parseToJsonElement(...).jsonObject, which throws on non-JSON or non-object input. Because these run inside the notificationObject?.let { ... } success branch (Line 275-276) within the surrounding try, a parse failure is caught at Line 293 and turns a perfectly valid notificationObject into a Result.failure. Consider parsing title/body defensively (returning null on failure) so the notification still succeeds without a display title/body.

🤖 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 `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt`
around lines 267 - 290, The current logic in the PushNotification success branch
calls getTitleFromRemoteMessageData and getBodyFromRemoteMessageData which throw
when the "aps" payload is malformed, causing an otherwise-valid notification to
be turned into a failure; change those parsing helpers
(getTitleFromRemoteMessageData/getBodyFromRemoteMessageData) to parse
defensively and return null on parse errors (catch JSON parse/format exceptions
and return null), so the PushNotification construction in the
notificationObject?.let { ... } branch uses nullable title/message and still
returns Result.success when notificationObject is present.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneOTPScreen.kt (1)

116-120: ⚡ Quick win

Hardcoded countdown string.

"Refreshes in ${state.otpSecondsRemaining}s" is not localizable and is inconsistent with the strings-in-strings.xml approach used elsewhere. Prefer a parameterized string resource.

♻️ Use a string resource
                         Text(
-                            text = "Refreshes in ${state.otpSecondsRemaining}s",
+                            text = stringResource(R.string.text_pingone_mfa_otp_refreshes_in, state.otpSecondsRemaining),
                             style = MaterialTheme.typography.bodyMedium,
                             color = MaterialTheme.colorScheme.onSurfaceVariant,
                         )
🤖 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/pingonemfa/ui/PingOneOTPScreen.kt`
around lines 116 - 120, Replace the hardcoded countdown text in the
PingOneOTPScreen Text composable with a parameterized string resource: add a
string entry (e.g., "refreshes_in_seconds") to strings.xml that accepts an
integer placeholder, then use stringResource(R.string.refreshes_in_seconds,
state.otpSecondsRemaining) in the Text call (located in PingOneOTPScreen.kt
where the Text currently shows "Refreshes in ${state.otpSecondsRemaining}s");
ensure the proper import for stringResource is present and remove the inline "s"
suffix so the localization string controls formatting.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneMFAAccountsScreen.kt (1)

156-163: ⚡ Quick win

Move the "Region:" / "ID:" labels into strings.xml.

These literals are hardcoded while the rest of the screen uses stringResource. This is inconsistent with the PR's "UI strings moved to strings.xml" goal and isn't localizable.

♻️ Use string resources
                                     Text(
-                                        text = "Region: ${account.region}",
+                                        text = stringResource(R.string.text_pingone_mfa_account_region, account.region),
                                         style = MaterialTheme.typography.bodyMedium,
                                         color = MaterialTheme.colorScheme.onSurfaceVariant,
                                     )
                                     Text(
-                                        text = "ID: ${account.id}",
+                                        text = stringResource(R.string.text_pingone_mfa_account_id, account.id),
                                         style = MaterialTheme.typography.bodyMedium,
                                         color = MaterialTheme.colorScheme.onSurfaceVariant,
                                     )
🤖 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/pingonemfa/ui/PingOneMFAAccountsScreen.kt`
around lines 156 - 163, The hardcoded "Region: ${account.region}" and "ID:
${account.id}" Text usages in PingOneMFAAccountsScreen.kt should be moved to
string resources and accessed via stringResource with placeholders; add entries
like ping_region_label="%1$s: %2$s" or "Region: %1$s"/or better "Region: %s" and
ping_id_label="ID: %s" in strings.xml, then replace the Text calls to use
stringResource(R.string.ping_region_label, "Region", account.region) or
(preferably) define the full localized label with a single placeholder and call
stringResource(R.string.ping_region_label, account.region) and
stringResource(R.string.ping_id_label, account.id) so the UI matches the rest of
the screen and is localizable.
samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePayloadScreen.kt (1)

130-137: ⚡ Quick win

Flag the copied MFA payload as sensitive

Place a sensitive flag on the ClipData’s ClipDescription so the system can hide/obfuscate clipboard previews (this is a UI rendering hint, not extra protection for the underlying clipboard contents).

🛡️ Flag clip as sensitive
                                 scope.launch {
-                                    clipboardManager.setClipEntry(
-                                        ClipEntry(ClipData.newPlainText("payload", state.payload))
-                                    )
+                                    val clip = ClipData.newPlainText("payload", state.payload).apply {
+                                        description.extras = PersistableBundle().apply {
+                                            putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
+                                        }
+                                    }
+                                    clipboardManager.setClipEntry(ClipEntry(clip))
                                 }

LocalClipboard / suspend Clipboard.setClipEntry (ClipEntry(ClipData)) are available in androidx.compose.ui 1.11.0-rc01+—so ensure this module resolves to that version or newer.

🤖 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/pingonemfa/ui/PingOnePayloadScreen.kt`
around lines 130 - 137, The clipboard copy currently uses
ClipData.newPlainText(...) without marking the clip as sensitive; update the
Button handler to build a ClipDescription with the MIMETYPE_TEXT_PLAIN and the
ClipDescription.FLAG_IS_SENSITIVE flag and create the ClipData using that
description (or otherwise ensure the ClipData’s ClipDescription has
FLAG_IS_SENSITIVE) before calling
clipboardManager.setClipEntry(ClipEntry(clipData)); modify the code around
ClipEntry, ClipData.newPlainText(...) and clipboardManager.setClipEntry(...) so
the MFA payload is flagged as sensitive (requires androidx.compose.ui
1.11.0-rc01+).
🤖 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
`@pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt`:
- Around line 73-77: The secondary constructor internal constructor(errors:
Array<PingOneSDKError>) can NPE when errors[0] is actually null; change it to
pick the first non-null PingOneSDKError (e.g. errors.firstOrNull { it != null }
or errors.filterNotNull().firstOrNull()) and use its message via a safe call
with a fallback (e.g. firstNonNull?.message ?: "Unknown error"); keep
internalErrorsList = ErrorParser.fromPingOneSDKErrors(errors) unchanged.

In `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/AccountParser.kt`:
- Line 51: The username property in AccountParser (username: String?) is
currently treated as required by kotlinx.serialization because it lacks an
explicit default; update the AccountParser data/class declaration to give
username a default value (e.g., username: String? = null) so deserialization
does not throw MissingFieldException when the "username" key is absent, matching
the pattern used for sibling nullable fields.

---

Outside diff comments:
In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneOTPScreen.kt`:
- Around line 96-100: The not-paired branch for state.isOtpDeviceNotPaired
incorrectly reuses accounts_empty_state_title/subtitle; update the
EmptyStateMessage call in the isOtpDeviceNotPaired branch to use dedicated
string resources (e.g., otp_device_not_paired_title and
otp_device_not_paired_subtitle) and add those entries to the strings.xml with
copy that clearly instructs the user to pair a device to view OTPs; keep the
EmptyStateMessage usage and parameters (title/subtitle) unchanged except for the
new stringResource keys so the UI text matches the condition.

---

Nitpick comments:
In `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt`:
- Around line 338-364: approvePushNotificationFromBanner and
denyPushNotificationFromBanner duplicate intent building and use hard-coded
extra keys that must match PushApprovalService; refactor by introducing shared
constants for the intent extras (e.g., NOTIFICATION_EXTRA, AUTH_METHOD_EXTRA,
USER_ACTION_EXTRA) and a single private helper method (e.g.,
sendPushApprovalFromBanner(action: String)) that constructs the Intent, sets the
extras using the constants, and calls ContextCompat.startForegroundService;
update approvePushNotificationFromBanner and denyPushNotificationFromBanner to
call this helper with "approve" and "deny" respectively and keep references to
ContextProvider.context and PushApprovalService unchanged.
- Around line 267-290: The current logic in the PushNotification success branch
calls getTitleFromRemoteMessageData and getBodyFromRemoteMessageData which throw
when the "aps" payload is malformed, causing an otherwise-valid notification to
be turned into a failure; change those parsing helpers
(getTitleFromRemoteMessageData/getBodyFromRemoteMessageData) to parse
defensively and return null on parse errors (catch JSON parse/format exceptions
and return null), so the PushNotification construction in the
notificationObject?.let { ... } branch uses nullable title/message and still
returns Result.success when notificationObject is present.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneMFAAccountsScreen.kt`:
- Around line 156-163: The hardcoded "Region: ${account.region}" and "ID:
${account.id}" Text usages in PingOneMFAAccountsScreen.kt should be moved to
string resources and accessed via stringResource with placeholders; add entries
like ping_region_label="%1$s: %2$s" or "Region: %1$s"/or better "Region: %s" and
ping_id_label="ID: %s" in strings.xml, then replace the Text calls to use
stringResource(R.string.ping_region_label, "Region", account.region) or
(preferably) define the full localized label with a single placeholder and call
stringResource(R.string.ping_region_label, account.region) and
stringResource(R.string.ping_id_label, account.id) so the UI matches the rest of
the screen and is localizable.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneOTPScreen.kt`:
- Around line 116-120: Replace the hardcoded countdown text in the
PingOneOTPScreen Text composable with a parameterized string resource: add a
string entry (e.g., "refreshes_in_seconds") to strings.xml that accepts an
integer placeholder, then use stringResource(R.string.refreshes_in_seconds,
state.otpSecondsRemaining) in the Text call (located in PingOneOTPScreen.kt
where the Text currently shows "Refreshes in ${state.otpSecondsRemaining}s");
ensure the proper import for stringResource is present and remove the inline "s"
suffix so the localization string controls formatting.

In
`@samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePayloadScreen.kt`:
- Around line 130-137: The clipboard copy currently uses
ClipData.newPlainText(...) without marking the clip as sensitive; update the
Button handler to build a ClipDescription with the MIMETYPE_TEXT_PLAIN and the
ClipDescription.FLAG_IS_SENSITIVE flag and create the ClipData using that
description (or otherwise ensure the ClipData’s ClipDescription has
FLAG_IS_SENSITIVE) before calling
clipboardManager.setClipEntry(ClipEntry(clipData)); modify the code around
ClipEntry, ClipData.newPlainText(...) and clipboardManager.setClipEntry(...) so
the MFA payload is flagged as sensitive (requires androidx.compose.ui
1.11.0-rc01+).
🪄 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: 060b7f5b-a676-4095-b5e2-d3be557fadba

📥 Commits

Reviewing files that changed from the base of the PR and between 5f079fe and c61644d.

📒 Files selected for processing (12)
  • pingonemfa/README.md
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFA.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/AccountParser.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFATest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/AccountParserTest.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/PingOneMFAViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneMFAAccountsScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOneOTPScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePayloadScreen.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePushNotificationScreen.kt
✅ Files skipped from review due to trivial changes (1)
  • pingonemfa/README.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/AccountParserTest.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/PingOneMFAViewModel.kt
  • samples/pingsampleapp/src/main/java/com/pingidentity/samples/pingsampleapp/pingonemfa/ui/PingOnePushNotificationScreen.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/push/PushNotificationTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFATest.kt

Comment thread pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/AccountParser.kt Outdated
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 (4)
pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFAExceptionTest.kt (1)

147-152: 💤 Low value

Remove the leftover println debug statement. Line 149 is a debug artifact that pollutes test output.

🧹 Proposed cleanup
         val errors = arrayOf(PingOneSDKError(10003, null))
-        println("SDK error message: ${errors[0].message}")
         val e = PingOneMFAException(errors)
         assertEquals("Unknown error", e.message)
🤖 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
`@pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFAExceptionTest.kt`
around lines 147 - 152, In PingOneMFAExceptionTest.remove the leftover debug
print by deleting the println("SDK error message: ${errors[0].message}") call in
the test method named `constructor with error array defaults message when first
error message is null`; keep the rest of the test intact so it still constructs
`errors`, instantiates `PingOneMFAException(errors)`, and asserts the message
equals "Unknown error".
pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/ErrorParserTest.kt (1)

42-52: 💤 Low value

The test name says "when SDK userInfo is null", but as the inline comment notes, PingOneSDKError.userInfo defaults to an empty map and is never null here — so this exercises the empty (not null) path. Consider renaming to reflect what's actually verified, or explicitly set userInfo = null if the SDK type allows it, to cover the ?: emptyMap() branch.

🤖 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
`@pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/ErrorParserTest.kt`
around lines 42 - 52, Rename the test or modify the setup so it actually covers
the null-guard branch: either (A) rename the test function `fromPingOneSDKError
returns empty userInfo when SDK userInfo is null` to reflect that it currently
verifies an empty-map case (e.g., `...when SDK userInfo is empty`), or (B) if
the SDK type allows, instantiate `PingOneSDKError` with `userInfo = null` before
calling `parser.fromPingOneSDKError(sdkError)` to exercise the `?: emptyMap()`
path; update the assertion on `result.first().userInfo` accordingly. Ensure
changes reference the existing test function and the
`PingOneSDKError`/`parser.fromPingOneSDKError` usage so the intent is clear.
pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/ErrorParser.kt (1)

13-16: 💤 Low value

Use a KDoc block (/**) for the class doc. This comment uses [PingOneSDKError] / [Error] doc-link syntax but is opened with /*, so it won't be parsed as KDoc and the links won't resolve.

📝 Proposed fix
-/*
+/**
  * Converts native [PingOneSDKError] objects from the `pingidsdkv2` AAR into the
  * wrapper [Error] type, so callers never need a direct dependency on the native SDK.
  */
🤖 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 `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/ErrorParser.kt`
around lines 13 - 16, Update the top-of-file block comment to a proper KDoc
block (/** ... */) so the bracketed doc links like [PingOneSDKError] and [Error]
are parsed and resolved; replace the existing /* ... */ comment above the
converter (the class or top-level mapper in ErrorParser.kt that converts
PingOneSDKError to the wrapper Error type) with a KDoc comment preserving the
text and links.
pingonemfa/build.gradle.kts (1)

9-16: ⚡ Quick win

Remove redundant Android/Kotlin plugin aliases in pingonemfa

pingonemfa/build.gradle.kts applies id("com.pingidentity.convention.android.library"), and build-logic/convention/src/main/kotlin/com/pingidentity/convention/AndroidLibraryConventionPlugin.kt already does pluginManager.apply("com.android.library"), so alias(libs.plugins.androidLibrary) is redundant.

It also applies id("com.pingidentity.convention.centralPublish"), and build-logic/convention/src/main/kotlin/com/pingidentity/convention/MavenCentralPublishConventionPlugin.kt already does pluginManager.apply("kotlin-android"), so alias(libs.plugins.kotlinAndroid) is redundant in this module (though it’s unlikely to break since Gradle won’t need to apply the same plugin twice).

plugins {
    id("com.pingidentity.convention.android.library")
    id("com.pingidentity.convention.centralPublish")
    id("kotlin-parcelize")
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.kotlinAndroid)
    alias(libs.plugins.kotlinSerialization)
}
🤖 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 `@pingonemfa/build.gradle.kts` around lines 9 - 16, Remove the redundant plugin
aliases from the plugins block in pingonemfa/build.gradle.kts: delete
alias(libs.plugins.androidLibrary) and alias(libs.plugins.kotlinAndroid) because
id("com.pingidentity.convention.android.library") (see
AndroidLibraryConventionPlugin) already applies "com.android.library" and
id("com.pingidentity.convention.centralPublish") (see
MavenCentralPublishConventionPlugin) already applies "kotlin-android"; keep the
remaining plugins such as id("kotlin-parcelize") and
alias(libs.plugins.kotlinSerialization).
🤖 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 `@pingonemfa/build.gradle.kts`:
- Around line 9-16: Remove the redundant plugin aliases from the plugins block
in pingonemfa/build.gradle.kts: delete alias(libs.plugins.androidLibrary) and
alias(libs.plugins.kotlinAndroid) because
id("com.pingidentity.convention.android.library") (see
AndroidLibraryConventionPlugin) already applies "com.android.library" and
id("com.pingidentity.convention.centralPublish") (see
MavenCentralPublishConventionPlugin) already applies "kotlin-android"; keep the
remaining plugins such as id("kotlin-parcelize") and
alias(libs.plugins.kotlinSerialization).

In `@pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/ErrorParser.kt`:
- Around line 13-16: Update the top-of-file block comment to a proper KDoc block
(/** ... */) so the bracketed doc links like [PingOneSDKError] and [Error] are
parsed and resolved; replace the existing /* ... */ comment above the converter
(the class or top-level mapper in ErrorParser.kt that converts PingOneSDKError
to the wrapper Error type) with a KDoc comment preserving the text and links.

In
`@pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFAExceptionTest.kt`:
- Around line 147-152: In PingOneMFAExceptionTest.remove the leftover debug
print by deleting the println("SDK error message: ${errors[0].message}") call in
the test method named `constructor with error array defaults message when first
error message is null`; keep the rest of the test intact so it still constructs
`errors`, instantiates `PingOneMFAException(errors)`, and asserts the message
equals "Unknown error".

In
`@pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/ErrorParserTest.kt`:
- Around line 42-52: Rename the test or modify the setup so it actually covers
the null-guard branch: either (A) rename the test function `fromPingOneSDKError
returns empty userInfo when SDK userInfo is null` to reflect that it currently
verifies an empty-map case (e.g., `...when SDK userInfo is empty`), or (B) if
the SDK type allows, instantiate `PingOneSDKError` with `userInfo = null` before
calling `parser.fromPingOneSDKError(sdkError)` to exercise the `?: emptyMap()`
path; update the assertion on `result.first().userInfo` accordingly. Ensure
changes reference the existing test function and the
`PingOneSDKError`/`parser.fromPingOneSDKError` usage so the intent is clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 01c90658-2245-4052-a02f-aa57313301eb

📥 Commits

Reviewing files that changed from the base of the PR and between c61644d and 15f729a.

📒 Files selected for processing (8)
  • pingonemfa/build.gradle.kts
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/Error.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/AccountParser.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/ErrorParser.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/ErrorTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/commons/PingOneMFAExceptionTest.kt
  • pingonemfa/src/test/kotlin/com/pingidentity/pingonemfa/util/ErrorParserTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/commons/PingOneMFAException.kt
  • pingonemfa/src/main/java/com/pingidentity/pingonemfa/util/AccountParser.kt

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