Skip to content

SDKS-5031: Add Authenticator app to Sample apps.#112

Open
vibhorgoswami wants to merge 1 commit into
mainfrom
SDKS-5031
Open

SDKS-5031: Add Authenticator app to Sample apps.#112
vibhorgoswami wants to merge 1 commit into
mainfrom
SDKS-5031

Conversation

@vibhorgoswami
Copy link
Copy Markdown
Contributor

@vibhorgoswami vibhorgoswami commented May 7, 2026

Jira

SDKS-5031

Description

This ticket introduces the authenticator sample app for testing HOTP/TOTP, Journey and Push accounts.

Summary by CodeRabbit

  • New Features

    • Full Ping Authenticator Android app: OATH (TOTP/HOTP) and Push MFA, Journey-based enrollment, QR scanner, biometric lock/approval flows, notification handling with challenge/biometric support, backup/restore and test utilities, and a complete Compose UI (accounts, notifications, settings, diagnostic logs).
  • Documentation

    • Added comprehensive README with architecture, setup, QR formats, testing steps, and troubleshooting.

Review Change Stack

@vibhorgoswami vibhorgoswami requested review from spetrov and witrisna May 7, 2026 23:26
@vibhorgoswami vibhorgoswami self-assigned this May 7, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

📝 Walkthrough

Walkthrough

Adds a complete Android Kotlin Authenticator app: build and Gradle setup, application bootstrap/manifest, storages and SDK clients, managers for OATH/Push/Journey, FCM handling and notification UI, Jetpack Compose navigation and screens, shared UI components, theming, utilities, and app resources.

Changes

Android Authenticator App

Layer / File(s) Summary
Build & project configuration android/kotlin-authenticatorapp/*, gradle/*, gradle.properties, gradle/wrapper/*, gradlew*, gradle/libs.versions.toml — Gradle wrapper, version catalog, project properties, wrapper scripts, and top-level module plugin exposure.
Module build and app config app/build.gradle.kts, app/google-services.json, app/proguard-rules.pro, settings.gradle.kts, app/.gitignore
App bootstrap & Manifest app/src/main/AndroidManifest.xml, AuthenticatorApp.kt, MainActivity.kt
Core models & managers app/src/main/kotlin/.../data/*, managers/*
Services & notifications service/*, notification/*
Compose navigation & screens ui/AuthenticatorNavHost.kt, ui/*Screen.kt
UI components, theme & utils ui/components/*, ui/theme/*, util/*
Resources & drawables res/drawable/*, res/mipmap-*, res/values*/strings.xml, res/values/themes.xml

Sequence Diagram(s)

sequenceDiagram
  participant App as MainActivity
  participant Init as AuthenticatorApp
  participant OATH as OathManager
  participant PUSH as PushManager
  participant FCM as FirebaseMessaging
  App->>Init: await/init clients & storages
  Init->>OATH: createOathStorage / OathClient
  Init->>PUSH: createPushStorage / PushClient
  Init->>FCM: request token (if configured)
  App->>OATH: loadCredentials()
  App->>PUSH: loadNotifications()
  App->>App: start Compose NavHost with view models
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • witrisna
  • spetrov

Poem

I hopped through code from root to res,
Wove OATH and Push with gentle finesse.
QR to Journey, Compose in bloom—
Notifications ring, defeating gloom.
With fluffy logs and themes so bright,
This rabbit ships an app tonight. 🐇✨

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

@vibhorgoswami vibhorgoswami marked this pull request as ready for review May 11, 2026 19:35
Copy link
Copy Markdown

@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: 3

Note

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

🟡 Minor comments (17)
android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xml-6-9 (1)

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

Avoid hardcoded white for icon tint/fill

Line 6 and Line 8 hardcode #FFFFFF, which makes this icon fragile across themes/backgrounds. Use a theme/color resource so contrast stays correct in light/dark modes.

Suggested change
-    android:tint="#FFFFFF">
+    android:tint="@color/icon_tint">
   <path
-      android:fillColor="#FFFFFF"
+      android:fillColor="@color/icon_tint"
🤖 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 `@android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xml`
around lines 6 - 9, The icon XML currently hardcodes white in android:tint and
the path's android:fillColor (`#FFFFFF`); replace those literals with a
theme-aware color reference (e.g. use a theme attribute like
?attr/colorOnBackground or a color resource such as `@color/icon_tint` that maps
to the correct color in dark/light themes) so the fingerprint icon adapts to
theme/background; update the android:tint attribute and the path's
android:fillColor to the chosen reference (file ic_fingerprint.xml, element with
android:tint and the <path> element) and add/update the referenced color
resource or theme attribute in your colors/themes if it doesn't exist.
android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml-3-4 (1)

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

Potential launcher background mismatch (color vs drawable).

Line 3 references @color/ic_launcher_background, while a dedicated @drawable/ic_launcher_background vector exists. If the grid background is intended, it is currently not used.

Proposed fix
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/ic_launcher_background"/>
+    <background android:drawable="@drawable/ic_launcher_background"/>
     <foreground android:drawable="@drawable/ic_launcher_foreground"/>
 </adaptive-icon>

Also apply the same update in:
android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml`
around lines 3 - 4, The launcher XML uses a color resource for the background
but a vector drawable named ic_launcher_background exists; update the background
attribute in ic_launcher.xml to use
android:drawable="@drawable/ic_launcher_background" instead of
"@color/ic_launcher_background", and make the same change in
ic_launcher_round.xml so both use the drawable foreground/background pairing
(leave the foreground android:drawable="@drawable/ic_launcher_foreground"
as-is).
android/kotlin-authenticatorapp/gradle/libs.versions.toml-2-7 (1)

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

Update library versions for compatibility and maintenance.

Versions have been verified to exist and work together, but two are outdated:

  • agp = "9.1.1" — Latest is 9.2.0
  • kotlin = "2.3.21" — Current and up-to-date ✓
  • composeBom = "2025.08.01" — Latest stable is 2026.05.00

Consider updating AGP and Compose BOM to their latest stable versions for access to bug fixes and improvements.

🤖 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 `@android/kotlin-authenticatorapp/gradle/libs.versions.toml` around lines 2 -
7, Update the AGP and Compose BOM version entries in libs.versions.toml: change
the agp value from "9.1.1" to "9.2.0" and change composeBom from "2025.08.01" to
"2026.05.00" so the project uses the latest stable Android Gradle Plugin and
Compose BOM; leave other entries (kotlin, coreKtx, appcompat, serialization)
unchanged and run a quick build to confirm compatibility.
android/kotlin-authenticatorapp/README.md-69-69 (1)

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

Add hyphen for compound adjective: "lifecycle-conscious".

📝 Proposed fix
-- **ViewModel**: Architecture component for managing UI-related data in a lifecycle conscious way
+- **ViewModel**: Architecture component for managing UI-related data in a lifecycle-conscious way
🤖 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 `@android/kotlin-authenticatorapp/README.md` at line 69, Update the README line
describing ViewModel to use the hyphenated compound adjective
"lifecycle-conscious": change the phrase "lifecycle conscious" to
"lifecycle-conscious" in the README entry for "ViewModel" so it reads
"**ViewModel**: Architecture component for managing UI-related data in a
lifecycle-conscious way".
android/kotlin-authenticatorapp/README.md-63-63 (1)

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

Fix typo: "comunication" → "communication".

📝 Proposed fix
-# complexity to deal with MFA capabilities and comunication with Ping backend.
+# complexity to deal with MFA capabilities and communication with Ping backend.
🤖 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 `@android/kotlin-authenticatorapp/README.md` at line 63, The README contains a
typo in the SDK Layer bullet: change "comunication" to "communication" in the
line "- **SDK Layer**: Abstracts the complexity to deal with MFA capabilities
and comunication with Ping backend." so the sentence reads "...and communication
with Ping backend."; update the README.md accordingly where the "SDK Layer"
bullet appears.
android/kotlin-authenticatorapp/app/src/main/res/values/strings.xml-4-4 (1)

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

Verify "APush" is intentional.

The notification channel name is "APush Authentication" with a capital 'A' prefix. If this is a typo and should be "Push Authentication", please correct it. If "APush" is intentional branding or naming, please disregard.

🤖 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 `@android/kotlin-authenticatorapp/app/src/main/res/values/strings.xml` at line
4, The notification channel string value for notification_channel_name currently
reads "APush Authentication"—verify whether the leading "A" is a typo; if it
should be "Push Authentication" update the string value for
notification_channel_name accordingly, otherwise leave it as-is to preserve
intentional branding.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.kt-180-180 (1)

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

Use string resource for consistency.

Line 180 has a hardcoded content description "Location information available", while line 172 correctly uses stringResource(id = R.string.content_description_location_available). For consistency and i18n support, use the string resource here as well.

🌐 Proposed fix
-                            contentDescription = "Location information available",
+                            contentDescription = stringResource(id = R.string.content_description_location_available),
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.kt`
at line 180, Replace the hardcoded contentDescription "Location information
available" with the string resource used elsewhere; in
NotificationHistoryCard.kt update the contentDescription parameter to use
stringResource(id = R.string.content_description_location_available) (same
resource referenced at line where stringResource is already used) so the
component uses the centralized i18n string instead of a literal.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.kt-85-87 (1)

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

Add tapToRevealEnabled to remember() dependency keys.

At line 85, tapToRevealEnabled is used in the lambda but missing from the dependency keys. When this parameter changes, isRevealed won't recompute, causing stale reveal behavior to persist.

Proposed fix
-    var isRevealed by remember(firstOathCredential?.isLocked, firstOathCode?.code) { 
+    var isRevealed by remember(
+        tapToRevealEnabled,
+        firstOathCredential?.isLocked,
+        firstOathCode?.code
+    ) {
         mutableStateOf(!tapToRevealEnabled || (firstOathCredential?.isLocked == false && firstOathCode != 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.kt`
around lines 85 - 87, The remember block that initializes var isRevealed uses
tapToRevealEnabled inside its lambda but does not include it in the dependency
keys, so update the remember(...) call that defines isRevealed to include
tapToRevealEnabled alongside firstOathCredential?.isLocked and firstOathCode;
ensure the key list reads remember(firstOathCredential?.isLocked, firstOathCode,
tapToRevealEnabled) { mutableStateOf(...) } so isRevealed recomputes when
tapToRevealEnabled changes.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.kt-86-159 (1)

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

Move user-visible strings into strings.xml.

All SettingItem titles and descriptions (Lines 89-90, 100-101, 111-112, 122-123, 133-134, 143-144, 155-156), the app-bar title (Line 75), and the theme dialog strings (Lines 188, 199, 214, 225-227) are hardcoded English. Other screens in this PR pull from resources, so this is inconsistent and blocks localization. As per the AI summary, the rest of the app already routes copy through res/values/strings.xml.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.kt`
around lines 86 - 159, Several user-visible strings in SettingsScreen
(titles/descriptions passed to SettingItem, the app-bar title, and theme dialog
labels used with getThemeDisplayName/showThemeDialog) are hardcoded; extract
each into res/values/strings.xml and replace the literals with
stringResource(...) calls. Specifically, move the titles/descriptions for Copy
OTP, Tap to reveal, Theme (and the dynamic theme display label), Combine
accounts, Enable diagnostic logging, View diagnostic logs, Enable Test mode,
plus the app-bar title and theme dialog option strings into strings.xml, and
update the SettingsScreen composable to reference them (keep using SettingItem,
getThemeDisplayName, showThemeDialog, onNavigate/onToggle/viewModel methods as
before). Ensure interpolation for the theme display uses stringResource with
format args or build the final display string via stringResource for
localization.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.kt-60-66 (1)

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

Localize the top-app-bar title.

The other labels in this screen (push_notifications_empty_state, push_notifications_pending_requests, push_notifications_notification_history) use string resources, but the title "Push Notifications" is inlined. Move it to strings.xml for i18n consistency.

🌐 Proposed fix
-            BackNavigationTopAppBar(
-                title = "Push Notifications",
-                onBackClick = onDismiss
-            )
+            BackNavigationTopAppBar(
+                title = stringResource(id = R.string.push_notifications_title),
+                onBackClick = onDismiss
+            )
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.kt`
around lines 60 - 66, Replace the hard-coded "Push Notifications" title passed
to BackNavigationTopAppBar with a string resource; add a new entry in
strings.xml (e.g., push_notifications_title) and load it via
stringResource(R.string.push_notifications_title) in PushNotificationsScreen.kt
where BackNavigationTopAppBar(title = ...) is set so the screen title is
localized consistently with the other labels.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt-157-186 (1)

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

Render a fallback when the notification lookup returns null.

authenticatorViewModel.getNotificationItemById(notificationId)?.let { ... } silently renders nothing when the lookup fails (e.g., the user taps a stale notification from the tray after it was already approved/expired/deleted, or after process death rehydration). The user lands on a blank screen with no way forward except system back. Either pop the back stack automatically or show an "unavailable" UI in the null branch.

🧭 Suggested fallback
-            authenticatorViewModel.getNotificationItemById(notificationId)?.let { notificationItem ->
-                NotificationResponseScreen( ... )
-            }
+            val notificationItem = authenticatorViewModel.getNotificationItemById(notificationId)
+            if (notificationItem == null) {
+                LaunchedEffect(notificationId) { navController.popBackStack() }
+            } else {
+                NotificationResponseScreen( /* …existing args… */ )
+            }
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt`
around lines 157 - 186, The null-check around
authenticatorViewModel.getNotificationItemById(notificationId) currently renders
nothing when the notification is missing; update the composable to handle the
null branch by either immediately popping the nav stack (use
navController.popBackStack()) or rendering a simple "notification unavailable"
fallback UI that offers a dismiss action; locate the block where
getNotificationItemById(notificationId)?.let { ... } is used in the
AuthenticatorNavHost composable and add the null-case handling (referencing
authenticatorViewModel.getNotificationItemById, NotificationResponseScreen,
navController, and BiometricPromptActivity/EXTRA_NOTIFICATION_ID if needed) so
users don’t land on a blank screen.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt-291-303 (1)

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

Drop the auth-method icon's contentDescription if it's purely decorative, or keep it consistent.

You pass the icon's label as both the icon and the Text content of an adjacent Text composable. Screen readers will then read the label twice. Either set contentDescription = null on the icon (TalkBack will rely on the neighboring text) or remove the adjacent Text to avoid duplication.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt`
around lines 291 - 303, The Icon in NotificationResponseScreen.kt is passing the
same label used by the adjacent Text, causing screen readers to read it twice;
update the Icon usage in the composable (the Icon(...) call next to the
Text(...) in NotificationResponseScreen) to mark the icon as decorative by
setting its contentDescription to null, or alternatively remove the adjacent
Text so only one accessible label remains—ensure you change the Icon(...) call
(not the styling) so TalkBack reads the label only once.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt-247-270 (1)

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

Instant.toString() for sentAt/respondedAt shows raw ISO; use a localized formatter.

sentAt.toString() (and respondedAt.toString()) renders the underlying type's default string (e.g. 2026-05-11T12:34:56.000Z). For a user-facing timestamp, format with the device locale/timezone using DateTimeFormatter/DateFormat.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt`
around lines 247 - 270, The UI currently shows raw ISO strings via
sentAt.toString() and respondedAt.toString(); replace those calls with a
localized, timezone-aware formatted string by converting the Instant
(sentAt/respondedAt) to a ZonedDateTime using ZoneId.systemDefault() and
formatting it with a DateTimeFormatter (e.g.,
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault()))
or equivalent Android formatter; update the Text usages in
NotificationResponseScreen where sentAt and
notificationItem.notification.respondedAt are rendered to use the formatted
string instead of toString().
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.kt-71-97 (1)

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

Race between LaunchedEffect and finish(); also flashes loading UI before finishing.

When notificationId is missing, onCreate calls finish() early (L59) — good. But when pushClient init fails or biometric isn't available (L86, L93), the code sets errorMessage, sets isLoading = false, then immediately calls finish(). The error message is rendered for one frame and the activity tears down before the user can read it. Surface these errors via a snackbar in the launching screen (or briefly delay finish()), or remove the error state and just finish() with a result code the caller can interpret.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.kt`
around lines 71 - 97, The activity currently sets UI state
(isLoading/errorMessage) then immediately calls finish(), causing a one-frame
flash; update the two failure paths (the catch block around pushClient
initialization and the BiometricManager else branch inside LaunchedEffect) to
stop modifying UI state before finishing: instead package the error into an
Intent result (e.g., putExtra("AUTH_ERROR", "...")),
setResult(Activity.RESULT_CANCELED, resultIntent) and call finish(), or
alternatively post the error to the launcher via a shared ViewModel/Callback so
the caller can show a Snackbar; do this for the pushClient initialization catch
and the BiometricManager failure branch and remove the immediate
isLoading/errorMessage assignments so the UI does not flash.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.kt-344-366 (1)

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

Edit flow shares the deletion's lack of error handling.

Same concern as the deletion handler: if updateOathCredential/updatePushCredential throws partway through, accountToEdit is not cleared (line 364 unreached) and the user gets no error feedback. Consider wrapping in try/catch/finally and surfacing the result.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.kt`
around lines 344 - 366, The onConfirm coroutine in the onConfirm lambda (inside
coroutineScope.launch) currently updates OATH and Push creds via
viewModel.updateOathCredential and viewModel.updatePushCredential but lacks
error handling so accountToEdit may never be cleared and the user gets no
feedback; wrap the update loop in try/catch/finally: perform the credential
updates in try, catch exceptions and surface a user-visible error (e.g., via
ViewModel error state or a Snackbar/Toast), and move accountToEdit = null into
the finally block so it always clears regardless of success or failure; use the
existing coroutineScope.launch and the same update* method names when locating
where to add the try/catch/finally.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.kt-323-338 (1)

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

Don't report a missing PushClient as a successful token lookup.

Returning Result.success("Not available") sends the caller down the success path, so the UI can show “device token retrieved” even when Push was never initialized. Return a failure, or Result.success(null) if absence is expected and handled explicitly.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.kt`
around lines 323 - 338, The getDeviceToken function currently treats a missing
pushClient as a success by returning Result.success("Not available"); change
this to return either Result.success(null) if callers explicitly handle a null
token, or return Result.failure(Exception("PushClient not initialized")) to
surface an error; update the null/exception branch inside the
withContext(Dispatchers.IO) call (where pushClient?.getDeviceToken() ?: ...) and
ensure diagnosticLogger.d/e calls remain correct (use diagnosticLogger.d when
token != null and diagnosticLogger.e when returning failure).
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.kt-1073-1090 (1)

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

Always invoke the callback on the error path.

Unlike getOathBackupFiles() and getPushBackupFiles(), this catch block only updates uiState.error and never calls callback. Any screen waiting for database info can stay stuck indefinitely after a failure.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.kt`
around lines 1073 - 1090, The catch block in getDatabaseInfo currently only
updates _uiState.error and never calls the provided callback, which can leave
callers waiting; update the catch to invoke the callback with a deterministic
fallback DatabaseInfo (e.g., empty paths and zero sizes/backup counts) after
updating _uiState so callers always receive a DatabaseInfo instance. Locate
getDatabaseInfo and its catch, keep the _uiState.update(...) call, then call
callback(DatabaseInfo(oathDbPath = "", oathDbSize = 0, oathBackupCount = 0,
pushDbPath = "", pushDbSize = 0, pushBackupCount = 0)) (or use whatever
default/empty constructor DatabaseInfo provides) so callers of getDatabaseInfo
always get a response even when oathManager.getDatabaseInfo() or
pushManager.getDatabaseInfo() throws.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 78fb41f8-4c29-4e3a-8b64-d35edd78ac89

📥 Commits

Reviewing files that changed from the base of the PR and between 8206f33 and 50c7f78.

📒 Files selected for processing (83)
  • android/kotlin-authenticatorapp/.gitignore
  • android/kotlin-authenticatorapp/README.md
  • android/kotlin-authenticatorapp/app/.gitignore
  • android/kotlin-authenticatorapp/app/build.gradle.kts
  • android/kotlin-authenticatorapp/app/google-services.json
  • android/kotlin-authenticatorapp/app/proguard-rules.pro
  • android/kotlin-authenticatorapp/app/src/main/AndroidManifest.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/AuthenticatorApp.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/BackupModels.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/DiagnosticLogger.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/LoginViewModel.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UserPreferences.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/AccountGroupingManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/JourneyManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/OathManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/TestAccountFactory.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationActionReceiver.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationHelper.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/PushNotificationActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/LocationService.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/PushNotificationService.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AboutScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountDetailScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/DiagnosticLogsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/InitializationErrorScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/LoginScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/TestScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountAvatar.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/BackNavigationTopAppBar.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CallbackRenderers.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CircularProgressTimer.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/DetailRow.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditAccountDialog.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditableAccountItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EmptyStateMessage.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/ErrorAlertDialog.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/InfoCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/LoadingIndicator.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/SettingItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/StatusIndicator.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Color.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Theme.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Type.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/DateUtils.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/NavigationAnimations.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/QrCodeAnalyzer.kt
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_check.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_close.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_background.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_foreground.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_notification.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ping_logo.xml
  • android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values-night/themes.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/ic_launcher_background.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/strings.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/themes.xml
  • android/kotlin-authenticatorapp/build.gradle.kts
  • android/kotlin-authenticatorapp/gradle.properties
  • android/kotlin-authenticatorapp/gradle/gradle-daemon-jvm.properties
  • android/kotlin-authenticatorapp/gradle/libs.versions.toml
  • android/kotlin-authenticatorapp/gradle/wrapper/gradle-wrapper.properties
  • android/kotlin-authenticatorapp/gradlew
  • android/kotlin-authenticatorapp/gradlew.bat
  • android/kotlin-authenticatorapp/settings.gradle.kts

Comment on lines +38 to +47
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" android:host="totp" />
<data android:scheme="otpauth" android:host="hotp" />
<data android:scheme="pushauth" android:host="push" />
<data android:scheme="mfauth" android:host="totp" />
<data android:scheme="mfauth" android:host="hotp" />
</intent-filter>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 --type=kotlin '\b(otpauth|pushauth|mfauth)\b'
rg -nP -C3 --type=kotlin '\bintent\.(data|action)\b' -g 'kotlin-authenticatorapp/**'

Repository: ForgeRock/sdk-sample-apps

Length of output: 6239


🏁 Script executed:

fd -t f "MainActivity.kt" android/kotlin-authenticatorapp/

Repository: ForgeRock/sdk-sample-apps

Length of output: 171


🏁 Script executed:

rg -nP -C5 "intent\.data|getIntent\(\)" --type=kotlin android/kotlin-authenticatorapp/

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

rg -nP "fun.*Uri|validate|sanitize|parse.*uri" --type=kotlin android/kotlin-authenticatorapp/ -i

Repository: ForgeRock/sdk-sample-apps

Length of output: 2151


🏁 Script executed:

cat -n android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt

Repository: ForgeRock/sdk-sample-apps

Length of output: 12878


🏁 Script executed:

sed -n '88,120p' android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/OathManager.kt

Repository: ForgeRock/sdk-sample-apps

Length of output: 1351


🏁 Script executed:

sed -n '109,140p' android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.kt

Repository: ForgeRock/sdk-sample-apps

Length of output: 1319


🏁 Script executed:

rg -nP "intent\." android/kotlin-authenticatorapp/app/src/main/kotlin --type=kotlin -C3

Repository: ForgeRock/sdk-sample-apps

Length of output: 2415


🏁 Script executed:

rg -nP "ACTION_VIEW|deepLink|navDeepLink" android/kotlin-authenticatorapp/app/src/main/kotlin --type=kotlin -C3

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

cat android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt | head -100

Repository: ForgeRock/sdk-sample-apps

Length of output: 4152


🏁 Script executed:

rg -nP "onNewIntent|intent\.data|intent\.dataString" android/kotlin-authenticatorapp/app/src/main/kotlin --type=kotlin -B2 -A5

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

rg -nP "data:|deeplink" android/kotlin-authenticatorapp/app/src/main --type=xml --type=kotlin

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

wc -l android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt

Repository: ForgeRock/sdk-sample-apps

Length of output: 186


🏁 Script executed:

cat -n android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt

Repository: ForgeRock/sdk-sample-apps

Length of output: 12819


🏁 Script executed:

rg -nP "uri|data|Uri|scheme" android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt

Repository: ForgeRock/sdk-sample-apps

Length of output: 205


🏁 Script executed:

rg -nP "getData\(\)|intent\.data" android/kotlin-authenticatorapp/app/src/main --type=kotlin

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

grep -r "otpauth\|pushauth\|mfauth" android/kotlin-authenticatorapp/app/src/main/kotlin --include="*.kt" | grep -v "test\|Test\|comment\|uri\|Uri" | head -20

Repository: ForgeRock/sdk-sample-apps

Length of output: 1078


🏁 Script executed:

grep -A 10 -B 5 "intent-filter" android/kotlin-authenticatorapp/app/src/main/AndroidManifest.xml | grep -A 10 "otpauth\|pushauth\|mfauth"

Repository: ForgeRock/sdk-sample-apps

Length of output: 768


Remove unused deep-link intent filters from manifest or implement proper URI handling with validation.

The deep-link schemes defined in the intent filter (otpauth, pushauth, mfauth) are not currently handled by MainActivity or the navigation code. No code path extracts or processes intent.data from these schemes. Either implement deep-link handling with input validation (validate scheme/host/params, sanitize secrets, reject unknown algorithms before forwarding to OATH/Push SDK) or remove the unused intent filters from the manifest to eliminate the attack surface on the exported activity.

🤖 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 `@android/kotlin-authenticatorapp/app/src/main/AndroidManifest.xml` around
lines 38 - 47, The manifest declares deep-link intent-filters (schemes: otpauth,
pushauth, mfauth) that MainActivity does not handle, creating an unnecessary
attack surface; either remove those <intent-filter> entries from
AndroidManifest.xml or implement explicit handling in MainActivity (e.g.,
onCreate/onNewIntent) to parse intent.data and validate scheme/host/params,
sanitize/avoid logging secrets, reject unknown algorithms/parameter values, and
only forward validated data to the OATH/Push SDK; also ensure the activity
export settings are explicit (exported="false" if not needed) when removing or
keeping deep-links.

Comment on lines +258 to +288
private fun buildOtpauthUri(
issuer: String,
accountName: String,
secret: String,
oathType: OathType,
algorithm: OathAlgorithm,
digits: Int,
period: Int
): String {
return buildString {
append(UriScheme.OTPAUTH.value)
append(oathType.name.lowercase())
append("/")
append(issuer)
append(":")
append(accountName)
append("?secret=")
append(secret)
append("&issuer=")
append(issuer)
append("&algorithm=")
append(algorithm.name)
append("&digits=")
append(digits)

if (oathType == OathType.TOTP) {
append("&period=")
append(period)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

URL-encode label and query values when building the otpauth URI.

issuer, accountName, and secret (and to be safe, algorithm) are concatenated raw into both the path label and the query string. Any space, :, /, ?, &, #, +, or non-ASCII character in the user-entered issuer/account name will produce a malformed URI that either fails to parse or is parsed into the wrong fields by addOathCredentialFromUri(...). The Key URI Format requires URL-encoding for the label and query parameter values.

🛠️ Proposed fix using URL encoding
 private fun buildOtpauthUri(
     issuer: String,
     accountName: String,
     secret: String,
     oathType: OathType,
     algorithm: OathAlgorithm,
     digits: Int,
     period: Int
 ): String {
+    fun enc(value: String): String =
+        java.net.URLEncoder.encode(value, "UTF-8").replace("+", "%20")
+
     return buildString {
         append(UriScheme.OTPAUTH.value)
         append(oathType.name.lowercase())
         append("/")
-        append(issuer)
+        append(enc(issuer))
         append(":")
-        append(accountName)
+        append(enc(accountName))
         append("?secret=")
-        append(secret)
+        append(enc(secret))
         append("&issuer=")
-        append(issuer)
+        append(enc(issuer))
         append("&algorithm=")
         append(algorithm.name)
         append("&digits=")
         append(digits)

         if (oathType == OathType.TOTP) {
             append("&period=")
             append(period)
         }
     }
 }
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt`
around lines 258 - 288, In buildOtpauthUri, URL-encode the label and all query
parameter values instead of concatenating raw strings: encode the issuer and
accountName (the label portion used in the path), and encode secret, issuer,
algorithm, digits and period used in the query string so
spaces/special/non-ASCII characters don’t break parsing by
addOathCredentialFromUri; use Android’s Uri.encode (or URLEncoder with UTF-8)
when building the strings in buildOtpauthUri to produce a valid otpauth URI.

Comment thread android/kotlin-authenticatorapp/app/src/main/res/values-night/themes.xml Outdated
Copy link
Copy Markdown

@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: 11

♻️ Duplicate comments (2)
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt (2)

258-288: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

URL-encode label and query values when building the otpauth URI.

As flagged in the previous review, issuer, accountName, secret, and algorithm are concatenated raw into both the path label and query string. Any special characters (space, :, /, ?, &, #, +, or non-ASCII) in user-entered values will produce a malformed URI that either fails to parse or is parsed incorrectly by addOathCredentialFromUri(...).

🛠️ Proposed fix using URL encoding
 private fun buildOtpauthUri(
     issuer: String,
     accountName: String,
     secret: String,
     oathType: OathType,
     algorithm: OathAlgorithm,
     digits: Int,
     period: Int
 ): String {
+    fun enc(value: String): String =
+        java.net.URLEncoder.encode(value, "UTF-8").replace("+", "%20")
+
     return buildString {
         append(UriScheme.OTPAUTH.value)
         append(oathType.name.lowercase())
         append("/")
-        append(issuer)
+        append(enc(issuer))
         append(":")
-        append(accountName)
+        append(enc(accountName))
         append("?secret=")
-        append(secret)
+        append(enc(secret))
         append("&issuer=")
-        append(issuer)
+        append(enc(issuer))
         append("&algorithm=")
         append(algorithm.name)
         append("&digits=")
         append(digits)

         if (oathType == OathType.TOTP) {
             append("&period=")
             append(period)
         }
     }
 }
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt`
around lines 258 - 288, The otpauth URI is built by concatenating raw values
which can break with special characters; update buildOtpauthUri to
percent-encode the label and all query values instead of inserting them raw:
encode the label portion (the "issuer:accountName" path segment) and encode
secret, issuer, algorithm.name (and accountName if used in label), before
appending them to the StringBuilder used in buildOtpauthUri; keep digits and
period numeric but still encode any string query values, and handle the
OathType.TOTP branch the same way so the appended "&period=" value remains
valid. Use a standard encoder (e.g., android.net.Uri.encode(...) or URLEncoder
with UTF-8) when preparing issuer, accountName, secret and algorithm.name.

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

Validate that the secret is valid Base32.

The secret field accepts any string without validation. OATH secrets must be Base32-encoded per RFC 4648. Invalid secrets will cause runtime errors when the credential is added. Validating early provides clearer error messages and better UX.

🛡️ Proposed validation
+// Add helper function
+private fun isValidBase32(value: String): Boolean {
+    return value.all { it.uppercaseChar() in 'A'..'Z' || it in '2'..'7' || it == '=' }
+}
+
 // Secret key field
 OutlinedTextField(
     value = secret,
     onValueChange = { secret = it },
     label = { Text(stringResource(id = R.string.manual_entry_secret_key_label)) },
     modifier = Modifier.fillMaxWidth(),
-    singleLine = true
+    singleLine = true,
+    supportingText = {
+        if (secret.isNotBlank() && !isValidBase32(secret)) {
+            Text(
+                text = "Secret must be valid Base32 (A-Z, 2-7)",
+                color = MaterialTheme.colorScheme.error
+            )
+        }
+    },
+    isError = secret.isNotBlank() && !isValidBase32(secret)
 )

And update the submit button enabled condition:

 enabled = issuer.isNotBlank() && 
          accountName.isNotBlank() && 
          secret.isNotBlank() &&
+         isValidBase32(secret) &&
          digits.toIntOrNull()?.let { it == 6 || it == 8 } ?: false &&
          (oathType == OathType.HOTP || period.toIntOrNull()?.let { it > 0 } ?: false)
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt`
around lines 124 - 131, The secret input in ManualEntryScreen currently accepts
any string; add RFC4648 Base32 validation by implementing a helper (e.g.,
isValidBase32(secret: String)) that trims and uppercases the input and checks
characters are only A–Z and 2–7 (and optional '=' padding in valid positions)
and that the length/padding rules are satisfied; use that helper to set a
visible error state for the secret OutlinedTextField and prevent submission by
updating the submit button enabled condition (the same logic that reads the
secret variable) to require isValidBase32(secret) before enabling the submit
action.
🧹 Nitpick comments (6)
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt (1)

217-229: ⚡ Quick win

Trim whitespace from input fields before building the URI.

User input often contains accidental leading/trailing whitespace. For issuer and accountName, this will appear in the authenticator UI. For secret, whitespace will likely cause Base32 decoding to fail.

♻️ Proposed fix
 onClick = {
     // Create otpauth URI and add credential
     val uri = buildOtpauthUri(
-        issuer = issuer,
-        accountName = accountName,
-        secret = secret,
+        issuer = issuer.trim(),
+        accountName = accountName.trim(),
+        secret = secret.trim(),
         oathType = oathType,
         algorithm = algorithm,
         digits = digits.toIntOrNull() ?: 6,
         period = period.toIntOrNull() ?: 30
     )
     diagnosticLogger.d("ManualEntryScreen: Adding credential from URI")
     viewModel.addOathCredentialFromUri(uri)
 },
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt`
around lines 217 - 229, Inputs may contain leading/trailing whitespace causing
UI issues and Base32 decode failures; before calling buildOtpauthUri in
ManualEntryScreen's onClick handler, trim the user-provided strings (issuer,
accountName, secret) and use those trimmed values when constructing the URI and
passing it to viewModel.addOathCredentialFromUri(uri). Locate the onClick block
that calls buildOtpauthUri and replace usage of issuer/accountName/secret with
their trimmed equivalents (e.g., create local trimmed variables) so the URI
builder and subsequent processing receive sanitized input.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt (1)

197-218: ⚡ Quick win

Consider asynchronous camera initialization.

The ProcessCameraProvider.getInstance(context).get() call (line 199) blocks the main thread while waiting for the camera provider. While this is a common pattern and the blocking time is typically brief, consider using the async callback-based approach for better responsiveness:

♻️ Alternative async approach
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
    try {
        val cameraProvider = cameraProviderFuture.get()
        cameraProvider.unbindAll()
        cameraProvider.bindToLifecycle(
            lifecycleOwner,
            selector,
            preview,
            imageAnalysis
        )
    } catch (e: Exception) {
        diagnosticLogger.e("QrScannerScreen: Failed to bind camera use cases", e)
        viewModel.setError(context.getString(R.string.qr_scanner_error_camera_init, e.message))
    }
}, ContextCompat.getMainExecutor(context))
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt`
around lines 197 - 218, The camera provider is being retrieved synchronously via
ProcessCameraProvider.getInstance(context).get(), which can block the main
thread; change this to the async future-based pattern by storing the
ProcessCameraProvider.getInstance(context) result in a cameraProviderFuture,
call cameraProviderFuture.addListener(...) with
ContextCompat.getMainExecutor(context), and inside that listener call
cameraProviderFuture.get(), cameraProvider.unbindAll(), and
cameraProvider.bindToLifecycle(lifecycleOwner, selector, preview,
imageAnalysis), while preserving the existing exception handling that logs to
diagnosticLogger and calls viewModel.setError with the same string resource;
update the code in QrScannerScreen around the current try/catch to use the
cameraProviderFuture listener instead.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt (2)

127-148: ⚡ Quick win

Extract duplicated ViewModel creation logic.

The ViewModel creation code is duplicated three times (error path, success path, and catch block). This violates DRY and creates maintenance burden.

♻️ Proposed refactor to reduce duplication
+private fun createViewModels(
+    application: Application,
+    userPreferences: UserPreferences,
+    oathManager: OathManager,
+    pushManager: PushManager,
+    journeyManager: JourneyManager,
+    diagnosticLogger: DiagnosticLogger
+) {
+    authenticatorViewModel = AuthenticatorViewModel(
+        application = application,
+        userPreferences = userPreferences,
+        oathManager = oathManager,
+        pushManager = pushManager,
+        accountGroupingManager = AccountGroupingManager(userPreferences, diagnosticLogger),
+        testAccountFactory = TestAccountFactory()
+    )
+    
+    loginViewModel = LoginViewModel(
+        application = application,
+        journeyManager = journeyManager,
+        oathManager = oathManager,
+        pushManager = pushManager
+    )
+}

Then call createViewModels(...) in all three locations instead of duplicating the creation code.

Also applies to: 165-183, 191-212

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt`
around lines 127 - 148, Extract the duplicated ViewModel construction into a
single helper function (e.g., createViewModels) and call it from the three spots
noted instead of repeating the block that constructs AuthenticatorViewModel and
LoginViewModel; ensure the helper accepts required dependencies (application,
userPreferences, oathManager, pushManager, journeyManager, diagnosticLogger) or
constructs AccountGroupingManager(TestAccountFactory) locally, then have the
caller set authenticatorViewModel.setInitializationError(initError) when needed,
mark areViewModelsInitialized = true and return@launch as before.

238-244: ⚡ Quick win

Define constants for intent extra keys.

The intent extra keys "NAVIGATE_TO" and "NOTIFICATION_ID" are hard-coded strings. Defining them as constants improves maintainability and reduces typo risk.

♻️ Proposed refactor

Add to companion object or a Constants file:

companion object {
    const val EXTRA_NAVIGATE_TO = "NAVIGATE_TO"
    const val EXTRA_NOTIFICATION_ID = "NOTIFICATION_ID"
}

Then use:

-if (extras.containsKey("NAVIGATE_TO")) {
-    val destination = extras.getString("NAVIGATE_TO") ?: return "accounts"
+if (extras.containsKey(EXTRA_NAVIGATE_TO)) {
+    val destination = extras.getString(EXTRA_NAVIGATE_TO) ?: return "accounts"
     
-    if (destination == "notifications" && extras.containsKey("NOTIFICATION_ID")) {
-        val notificationId = extras.getString("NOTIFICATION_ID") ?: return "notifications"
+    if (destination == "notifications" && extras.containsKey(EXTRA_NOTIFICATION_ID)) {
+        val notificationId = extras.getString(EXTRA_NOTIFICATION_ID) ?: return "notifications"
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt`
around lines 238 - 244, Replace the hard-coded intent extra keys in
MainActivity.kt with named constants: add const val EXTRA_NAVIGATE_TO and const
val EXTRA_NOTIFICATION_ID (e.g., in MainActivity's companion object or a shared
Constants object) and then update usages in the navigation lookup (the
extras.containsKey and extras.getString calls around the
NAVIGATE_TO/NOTIFICATION_ID logic) to reference EXTRA_NAVIGATE_TO and
EXTRA_NOTIFICATION_ID instead of the string literals so all intent key usage is
centralized and less error-prone.
android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt (2)

178-259: ⚡ Quick win

Duplicated grouping logic and misleading display-name comment.

The OATH/Push branches in both the shouldCombine=false and shouldCombine=true paths are near-identical. A small generic helper that takes the issuer/account/display fields plus an AccountGroup-mutating lambda would eliminate ~70 lines of copy-paste, reducing the risk of the two branches drifting (this has already happened in other PRs of this shape).

Separately, the comment "Update display names if the new credential has them" (lines 225 and 247) does not match the code — displayIssuer/displayAccountName are reassigned unconditionally, so the last credential processed always wins regardless of whether its display names are populated. Either drop the conditional wording from the comment, or add a non-blank check before overwriting.

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 178 - 259, Refactor the duplicated grouping logic by extracting a
small helper (e.g., addGroupForCredential) that accepts issuer/account/display
fields plus a lambda or flags to merge into accountGroups/separateGroups and use
it for both oathCredentials and pushCredentials handling to eliminate the
repeated blocks (refer to accountGroups, separateGroups, AccountGroup,
oathCredentials, pushCredentials, shouldCombine). Also fix the misleading
comment and behavior around display names: either remove "if the new credential
has them" or change the assignment to only overwrite
displayIssuer/displayAccountName when the incoming credential's display fields
are non-blank (check credential.displayIssuer/credential.displayAccountName
before assigning). Ensure the helper updates oathCredentials or pushCredentials
appropriately when merging.

116-119: ⚡ Quick win

Replace string-based pushType detection with typed enum matching.

Lines 117-118 use pushType.toString().lowercase().contains(), which is fragile and breaks silently if the SDK's enum is renamed. Use a when expression instead:

Example refactor
val requiresChallenge = notification.pushType == PushType.CHALLENGE
val requiresBiometric = notification.pushType == PushType.BIOMETRIC

or if other logic is involved:

val (requiresChallenge, requiresBiometric) = when (notification.pushType) {
    PushType.CHALLENGE -> true to false
    PushType.BIOMETRIC -> false to true
    PushType.DEFAULT -> false to false
}

The codebase already demonstrates this pattern in NotificationHelper.kt (lines 104–117).

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 116 - 119, The current detection uses string matching on
notification.pushType (val pushTypeStr = notification.pushType.toString();
requiresChallenge/ requiresBiometric using .contains()), which is fragile;
change this to direct enum matching on notification.pushType (use equality
checks or a when expression against PushType values) to set requiresChallenge
and requiresBiometric accordingly (e.g., notification.pushType ==
PushType.CHALLENGE / PushType.BIOMETRIC or a when that returns the pair). Update
the variables requiresChallenge and requiresBiometric to derive from that enum
match and remove the pushTypeStr string logic.
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`:
- Around line 289-298: The getOs function incorrectly matches "Linux" before
mobile tokens, causing Android (and some iPadOS) UAs to be reported as desktop;
fix by reordering and strengthening the when-clauses in getOs: check for
"Android" first, then "iPhone" and "iPad" (so iPad is detected before
"Macintosh"), then check "Windows", "Macintosh", and "Linux"; optionally use
case-insensitive contains to be robust across UA casing.
- Around line 278-287: The getBrowser function mis-classifies modern Edge
because its UA contains "Chrome" and "Edg/" not "Edge"; update getBrowser to
check for Edge first by matching the "Edg/" token (and also legacy "Edge")
before checking "Chrome" and "Safari" (i.e., reorder checks to test Edge ->
Chrome -> Firefox -> Safari -> Internet Explorer -> else), and also update the
related getBrowserVersion branch for "Edge" to recognize the "Edg/(\\S+)" token
instead of "Edge/(\\S+)" so Edge versions are parsed correctly.
- Around line 300-315: In getBrowserVersion, update the browser-specific regexes
and extraction so IE11's rv: group and modern Edge's Edg/ are captured and
returned; for example change the "Internet Explorer" arm to a pattern that
matches both MSIE and rv: but do not assume the version is always in
groups[1]—instead use the matchResult.groupValues.drop(1).firstOrNull {
it.isNotEmpty() } approach to pick the first non-empty capture, and update the
"Edge" arm to include both "Edge/(\\S+)" and "Edg/(\\S+)" alternatives so modern
Edge versions are returned correctly.
- Around line 144-150: The status determination incorrectly requires both
notification.expired and notification.pending to be true, which never happens;
update the when expression that sets status so that NotificationStatus.EXPIRED
is selected when notification.expired is true (independent of pending), e.g.,
evaluate notification.approved first, then notification.expired, then
notification.pending, and fallback to NotificationStatus.DENIED; refer to the
variable status and properties notification.approved, notification.expired,
notification.pending and the enum NotificationStatus to locate and fix the
logic.
- Around line 391-398: The policy comparisons in getLockMessage use
lockingPolicy?.lowercase(), which prevents matching the mixed-case constants
(e.g., BiometricAvailablePolicy.POLICY_NAME and
DeviceTamperingPolicy.POLICY_NAME); change the subject of the when to the raw
lockingPolicy (remove .lowercase()) so the when compares the original string
values (keep the null branch intact), and apply the same fix to the analogous
comparison in NotificationResponseScreen.kt so those branches are reachable.

In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UserPreferences.kt`:
- Around line 189-196: The getAccountOrder() method currently splits a stored
string by ACCOUNT_ORDER_SEPARATOR which will break if any account key contains
the separator; update the persistence to use a robust serialization (e.g., JSON
array) instead: change the read path in getAccountOrder() to parse the stored
KEY_ACCOUNT_ORDER value as a JSON array (or use Gson/JSONArray) and return the
list, and update the corresponding write path (where you join/save account
order) to serialize the List<String> to JSON rather than joining with
ACCOUNT_ORDER_SEPARATOR; reference KEY_ACCOUNT_ORDER, ACCOUNT_ORDER_SEPARATOR,
and getAccountOrder() to locate both read and write code to replace
string-splitting/joining with JSON serialization/deserialization.

In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt`:
- Around line 49-50: The AuthenticatorViewModel and LoginViewModel are being
instantiated manually and will not survive configuration changes; create a
ViewModelProvider.Factory (e.g., AuthenticatorViewModelFactory) that accepts
required dependencies (Application, UserPreferences, OathManager, PushManager,
AccountGroupingManager, TestAccountFactory, etc.) and implement create() to
return AuthenticatorViewModel (and a similar factory if LoginViewModel has
deps), then obtain instances in onCreate via ViewModelProvider(this,
factory)[AuthenticatorViewModel::class.java] and ViewModelProvider(this,
factoryForLogin)[LoginViewModel::class.java] so the ViewModels persist across
rotations and avoid reinitialization/races.

In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt`:
- Around line 110-125: The analyzer callback currently calls onScanComplete()
immediately after invoking the ViewModel add methods, causing duplicate
invocations because the LaunchedEffect blocks (watching
uiState.lastAddedOathCredential and uiState.lastAddedPushCredential) also call
onScanComplete(); remove the onScanComplete() calls from the analyzer callback
(the calls near the analyzer function handling QR results) and keep only the
LaunchedEffect-based calls so navigation happens after the ViewModel updates and
the state change is observed (references: uiState.lastAddedOathCredential,
uiState.lastAddedPushCredential, viewModel.clearLastAddedOathCredential,
viewModel.clearLastAddedPushCredential, and onScanComplete).
- Line 271: Replace the hardcoded dialog title "Error" in the AlertDialog inside
QrScannerScreen.kt with a localized stringResource call (e.g., use
stringResource(R.string.error)) so the title is localized consistently with
other UI strings; ensure a corresponding R.string.error entry exists in
strings.xml and update the AlertDialog title parameter to use the stringResource
instead of the literal.

In `@android/kotlin-authenticatorapp/README.md`:
- Line 69: In the README.md ViewModel bullet, fix the grammar by hyphenating the
compound adjective: change the phrase "lifecycle conscious" to
"lifecycle-conscious" in the line that reads "**ViewModel**: Architecture
component for managing UI-related data in a lifecycle conscious way" so it
becomes "**ViewModel**: Architecture component for managing UI-related data in a
lifecycle-conscious way".
- Line 63: Replace the misspelled word "comunication" with "communication" in
the README sentence that starts with "SDK Layer" (the phrase currently reads
"Abstracts the complexity to deal with MFA capabilities and comunication with
Ping backend"); update that line so it reads "Abstracts the complexity to deal
with MFA capabilities and communication with Ping backend".

---

Duplicate comments:
In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt`:
- Around line 258-288: The otpauth URI is built by concatenating raw values
which can break with special characters; update buildOtpauthUri to
percent-encode the label and all query values instead of inserting them raw:
encode the label portion (the "issuer:accountName" path segment) and encode
secret, issuer, algorithm.name (and accountName if used in label), before
appending them to the StringBuilder used in buildOtpauthUri; keep digits and
period numeric but still encode any string query values, and handle the
OathType.TOTP branch the same way so the appended "&period=" value remains
valid. Use a standard encoder (e.g., android.net.Uri.encode(...) or URLEncoder
with UTF-8) when preparing issuer, accountName, secret and algorithm.name.
- Around line 124-131: The secret input in ManualEntryScreen currently accepts
any string; add RFC4648 Base32 validation by implementing a helper (e.g.,
isValidBase32(secret: String)) that trims and uppercases the input and checks
characters are only A–Z and 2–7 (and optional '=' padding in valid positions)
and that the length/padding rules are satisfied; use that helper to set a
visible error state for the secret OutlinedTextField and prevent submission by
updating the submit button enabled condition (the same logic that reads the
secret variable) to require isValidBase32(secret) before enabling the submit
action.

---

Nitpick comments:
In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`:
- Around line 178-259: Refactor the duplicated grouping logic by extracting a
small helper (e.g., addGroupForCredential) that accepts issuer/account/display
fields plus a lambda or flags to merge into accountGroups/separateGroups and use
it for both oathCredentials and pushCredentials handling to eliminate the
repeated blocks (refer to accountGroups, separateGroups, AccountGroup,
oathCredentials, pushCredentials, shouldCombine). Also fix the misleading
comment and behavior around display names: either remove "if the new credential
has them" or change the assignment to only overwrite
displayIssuer/displayAccountName when the incoming credential's display fields
are non-blank (check credential.displayIssuer/credential.displayAccountName
before assigning). Ensure the helper updates oathCredentials or pushCredentials
appropriately when merging.
- Around line 116-119: The current detection uses string matching on
notification.pushType (val pushTypeStr = notification.pushType.toString();
requiresChallenge/ requiresBiometric using .contains()), which is fragile;
change this to direct enum matching on notification.pushType (use equality
checks or a when expression against PushType values) to set requiresChallenge
and requiresBiometric accordingly (e.g., notification.pushType ==
PushType.CHALLENGE / PushType.BIOMETRIC or a when that returns the pair). Update
the variables requiresChallenge and requiresBiometric to derive from that enum
match and remove the pushTypeStr string logic.

In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt`:
- Around line 127-148: Extract the duplicated ViewModel construction into a
single helper function (e.g., createViewModels) and call it from the three spots
noted instead of repeating the block that constructs AuthenticatorViewModel and
LoginViewModel; ensure the helper accepts required dependencies (application,
userPreferences, oathManager, pushManager, journeyManager, diagnosticLogger) or
constructs AccountGroupingManager(TestAccountFactory) locally, then have the
caller set authenticatorViewModel.setInitializationError(initError) when needed,
mark areViewModelsInitialized = true and return@launch as before.
- Around line 238-244: Replace the hard-coded intent extra keys in
MainActivity.kt with named constants: add const val EXTRA_NAVIGATE_TO and const
val EXTRA_NOTIFICATION_ID (e.g., in MainActivity's companion object or a shared
Constants object) and then update usages in the navigation lookup (the
extras.containsKey and extras.getString calls around the
NAVIGATE_TO/NOTIFICATION_ID logic) to reference EXTRA_NAVIGATE_TO and
EXTRA_NOTIFICATION_ID instead of the string literals so all intent key usage is
centralized and less error-prone.

In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt`:
- Around line 217-229: Inputs may contain leading/trailing whitespace causing UI
issues and Base32 decode failures; before calling buildOtpauthUri in
ManualEntryScreen's onClick handler, trim the user-provided strings (issuer,
accountName, secret) and use those trimmed values when constructing the URI and
passing it to viewModel.addOathCredentialFromUri(uri). Locate the onClick block
that calls buildOtpauthUri and replace usage of issuer/accountName/secret with
their trimmed equivalents (e.g., create local trimmed variables) so the URI
builder and subsequent processing receive sanitized input.

In
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt`:
- Around line 197-218: The camera provider is being retrieved synchronously via
ProcessCameraProvider.getInstance(context).get(), which can block the main
thread; change this to the async future-based pattern by storing the
ProcessCameraProvider.getInstance(context) result in a cameraProviderFuture,
call cameraProviderFuture.addListener(...) with
ContextCompat.getMainExecutor(context), and inside that listener call
cameraProviderFuture.get(), cameraProvider.unbindAll(), and
cameraProvider.bindToLifecycle(lifecycleOwner, selector, preview,
imageAnalysis), while preserving the existing exception handling that logs to
diagnosticLogger and calls viewModel.setError with the same string resource;
update the code in QrScannerScreen around the current try/catch to use the
cameraProviderFuture listener instead.
🪄 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: d8c4362b-69a8-4779-9d23-c10f43ef51d0

📥 Commits

Reviewing files that changed from the base of the PR and between 50c7f78 and 00e35d4.

📒 Files selected for processing (83)
  • android/kotlin-authenticatorapp/.gitignore
  • android/kotlin-authenticatorapp/README.md
  • android/kotlin-authenticatorapp/app/.gitignore
  • android/kotlin-authenticatorapp/app/build.gradle.kts
  • android/kotlin-authenticatorapp/app/google-services.json
  • android/kotlin-authenticatorapp/app/proguard-rules.pro
  • android/kotlin-authenticatorapp/app/src/main/AndroidManifest.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/AuthenticatorApp.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/BackupModels.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/DiagnosticLogger.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/LoginViewModel.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UserPreferences.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/AccountGroupingManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/JourneyManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/OathManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/TestAccountFactory.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationActionReceiver.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationHelper.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/PushNotificationActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/LocationService.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/PushNotificationService.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AboutScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountDetailScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/DiagnosticLogsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/InitializationErrorScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/LoginScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/TestScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountAvatar.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/BackNavigationTopAppBar.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CallbackRenderers.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CircularProgressTimer.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/DetailRow.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditAccountDialog.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditableAccountItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EmptyStateMessage.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/ErrorAlertDialog.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/InfoCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/LoadingIndicator.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/SettingItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/StatusIndicator.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Color.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Theme.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Type.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/DateUtils.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/NavigationAnimations.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/QrCodeAnalyzer.kt
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_check.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_close.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_background.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_foreground.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_notification.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ping_logo.xml
  • android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values-night/themes.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/ic_launcher_background.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/strings.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/themes.xml
  • android/kotlin-authenticatorapp/build.gradle.kts
  • android/kotlin-authenticatorapp/gradle.properties
  • android/kotlin-authenticatorapp/gradle/gradle-daemon-jvm.properties
  • android/kotlin-authenticatorapp/gradle/libs.versions.toml
  • android/kotlin-authenticatorapp/gradle/wrapper/gradle-wrapper.properties
  • android/kotlin-authenticatorapp/gradlew
  • android/kotlin-authenticatorapp/gradlew.bat
  • android/kotlin-authenticatorapp/settings.gradle.kts
✅ Files skipped from review due to trivial changes (19)
  • android/kotlin-authenticatorapp/app/.gitignore
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_check.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Color.kt
  • android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • android/kotlin-authenticatorapp/app/src/main/res/values/ic_launcher_background.xml
  • android/kotlin-authenticatorapp/gradle.properties
  • android/kotlin-authenticatorapp/.gitignore
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_foreground.xml
  • android/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • android/kotlin-authenticatorapp/app/proguard-rules.pro
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_close.xml
  • android/kotlin-authenticatorapp/gradle/wrapper/gradle-wrapper.properties
  • android/kotlin-authenticatorapp/gradle/libs.versions.toml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xml
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_notification.xml
  • android/kotlin-authenticatorapp/gradle/gradle-daemon-jvm.properties
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/BackupModels.kt
  • android/kotlin-authenticatorapp/build.gradle.kts
  • android/kotlin-authenticatorapp/gradlew
🚧 Files skipped from review as they are similar to previous changes (57)
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/DateUtils.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/DetailRow.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Type.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/BackNavigationTopAppBar.kt
  • android/kotlin-authenticatorapp/app/src/main/res/values-night/themes.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EmptyStateMessage.kt
  • android/kotlin-authenticatorapp/app/src/main/res/values/themes.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/DiagnosticLogsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/InfoCard.kt
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_background.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/LoginScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/StatusIndicator.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditAccountDialog.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/res/drawable/ping_logo.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/LoadingIndicator.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/SettingItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/ErrorAlertDialog.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/LocationService.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.kt
  • android/kotlin-authenticatorapp/settings.gradle.kts
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CircularProgressTimer.kt
  • android/kotlin-authenticatorapp/app/google-services.json
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationActionReceiver.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/AuthenticatorApp.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountAvatar.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/JourneyManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Theme.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/NavigationAnimations.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/TestAccountFactory.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/AccountGroupingManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CallbackRenderers.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/PushNotificationActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountDetailScreen.kt
  • android/kotlin-authenticatorapp/app/build.gradle.kts
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/QrCodeAnalyzer.kt
  • android/kotlin-authenticatorapp/app/src/main/AndroidManifest.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationHelper.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/TestScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditableAccountItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/PushNotificationService.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AboutScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/DiagnosticLogger.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationCard.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountsScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/InitializationErrorScreen.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/OathManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.kt
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/LoginViewModel.kt
  • android/kotlin-authenticatorapp/app/src/main/res/values/strings.xml
  • android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.kt

Comment on lines +144 to +150
// Determine notification status
val status = when {
notification.approved -> NotificationStatus.APPROVED
(notification.expired && notification.pending) -> NotificationStatus.EXPIRED
notification.pending -> NotificationStatus.PENDING
else -> NotificationStatus.DENIED
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Ping Identity Android MFA SDK PushNotification expired pending approved fields semantics

💡 Result:

In Ping Identity's Android MFA SDKs, particularly the ForgeRock Authenticator SDK (integrated into Ping SDKs), the PushNotification class models push authentication notifications with fields tracking their lifecycle status [1][2]. - pending (Boolean): Determines if the PushNotification has been interacted with by the user. True if the authentication is pending user response (not yet handled); false otherwise. Used in methods like getPendingNotifications to list notifications awaiting approval or rejection [1][3][4]. - approved (Boolean): Determines if the PushNotification has been approved by the user, indicating successful authentication. Set via setApproved(true) after user approval [1][2]. - expired: Derived from timeExpired (Calendar, date the notification expires) and ttl (Long, time-to-live). isExpired returns true if past expiration, preventing response to stale notifications. Updated logic aligns Android with iOS for cross-platform consistency [5][1][2]. These fields enable apps to manage notification state: retrieve pending ones via pushClient.getPendingNotifications or mechanism.getPendingNotifications, approve/deny via handlers like sendApproval/sendDenial (Orchestration SDK) or approveNotification, and check expiration before processing [6][1][3]. In PingOne MFA SDK, notifications are processed similarly, presenting approve/deny options [7].

Citations:


Fix status logic to handle expired notifications without pending response.

When a notification expires without user action, the SDK sets pending=false per the official documentation ("pending…False otherwise"). The current logic only recognizes expiration when (expired && pending) both true—a contradictory state that won't occur. Expired notifications without response fall through to DENIED instead of EXPIRED.

Proposed fix
     val status = when {
         notification.approved -> NotificationStatus.APPROVED
-        (notification.expired && notification.pending) -> NotificationStatus.EXPIRED
+        notification.expired -> NotificationStatus.EXPIRED
         notification.pending -> NotificationStatus.PENDING
         else -> NotificationStatus.DENIED
     }
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 144 - 150, The status determination incorrectly requires both
notification.expired and notification.pending to be true, which never happens;
update the when expression that sets status so that NotificationStatus.EXPIRED
is selected when notification.expired is true (independent of pending), e.g.,
evaluate notification.approved first, then notification.expired, then
notification.pending, and fallback to NotificationStatus.DENIED; refer to the
variable status and properties notification.approved, notification.expired,
notification.pending and the enum NotificationStatus to locate and fix the
logic.

Comment on lines +278 to +287
private fun getBrowser(userAgent: String): String {
return when {
userAgent.contains("Chrome") -> "Chrome"
userAgent.contains("Firefox") -> "Firefox"
userAgent.contains("Safari") -> "Safari"
userAgent.contains("Edge") -> "Edge"
userAgent.contains("MSIE") || userAgent.contains("Trident") -> "Internet Explorer"
else -> "Unknown"
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Browser detection mis-classifies Edge (and modern Edge is never matched).

User-agent ordering matters because the strings overlap:

  • Chromium-based Edge UAs contain Chrome, Safari, and Edg/ (not Edge).
  • Chrome UAs contain Safari.
  • Only legacy/EdgeHTML browsers had the literal token Edge.

With the current order, every Edge browser is reported as Chrome, and the Edge branch effectively never fires on modern Edge. Reorder the checks (Edge before Chrome before Safari) and match the Edg/ token.

🐛 Proposed fix
 private fun getBrowser(userAgent: String): String {
     return when {
-        userAgent.contains("Chrome") -> "Chrome"
-        userAgent.contains("Firefox") -> "Firefox"
-        userAgent.contains("Safari") -> "Safari"
-        userAgent.contains("Edge") -> "Edge"
-        userAgent.contains("MSIE") || userAgent.contains("Trident") -> "Internet Explorer"
+        userAgent.contains("Edg/") || userAgent.contains("Edge/") -> "Edge"
+        userAgent.contains("Firefox") -> "Firefox"
+        userAgent.contains("Chrome") -> "Chrome"
+        userAgent.contains("Safari") -> "Safari"
+        userAgent.contains("MSIE") || userAgent.contains("Trident") -> "Internet Explorer"
         else -> "Unknown"
     }
 }

Note: getBrowserVersion will also need its "Edge" -> "Edge/(\\S+)" arm updated to handle Edg/ (see related comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun getBrowser(userAgent: String): String {
return when {
userAgent.contains("Chrome") -> "Chrome"
userAgent.contains("Firefox") -> "Firefox"
userAgent.contains("Safari") -> "Safari"
userAgent.contains("Edge") -> "Edge"
userAgent.contains("MSIE") || userAgent.contains("Trident") -> "Internet Explorer"
else -> "Unknown"
}
}
private fun getBrowser(userAgent: String): String {
return when {
userAgent.contains("Edg/") || userAgent.contains("Edge/") -> "Edge"
userAgent.contains("Firefox") -> "Firefox"
userAgent.contains("Chrome") -> "Chrome"
userAgent.contains("Safari") -> "Safari"
userAgent.contains("MSIE") || userAgent.contains("Trident") -> "Internet Explorer"
else -> "Unknown"
}
}
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 278 - 287, The getBrowser function mis-classifies modern Edge
because its UA contains "Chrome" and "Edg/" not "Edge"; update getBrowser to
check for Edge first by matching the "Edg/" token (and also legacy "Edge")
before checking "Chrome" and "Safari" (i.e., reorder checks to test Edge ->
Chrome -> Firefox -> Safari -> Internet Explorer -> else), and also update the
related getBrowserVersion branch for "Edge" to recognize the "Edg/(\\S+)" token
instead of "Edge/(\\S+)" so Edge versions are parsed correctly.

Comment on lines +289 to +298
private fun getOs(userAgent: String): String {
return when {
userAgent.contains("Windows") -> "Windows"
userAgent.contains("Macintosh") -> "macOS"
userAgent.contains("Linux") -> "Linux"
userAgent.contains("Android") -> "Android"
userAgent.contains("iPhone") || userAgent.contains("iPad") -> "iOS"
else -> "Unknown"
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

OS detection always reports Android as Linux.

Android user-agent strings include the token Linux (e.g. Mozilla/5.0 (Linux; Android 12; ...)), so the Linux arm fires before the Android arm is ever checked. Same applies to iOS — iPad UAs on recent iPadOS can also masquerade as Macintosh. Reorder mobile platforms before desktop ones.

🐛 Proposed fix
 private fun getOs(userAgent: String): String {
     return when {
-        userAgent.contains("Windows") -> "Windows"
-        userAgent.contains("Macintosh") -> "macOS"
-        userAgent.contains("Linux") -> "Linux"
-        userAgent.contains("Android") -> "Android"
-        userAgent.contains("iPhone") || userAgent.contains("iPad") -> "iOS"
+        userAgent.contains("Android") -> "Android"
+        userAgent.contains("iPhone") || userAgent.contains("iPad") -> "iOS"
+        userAgent.contains("Windows") -> "Windows"
+        userAgent.contains("Macintosh") -> "macOS"
+        userAgent.contains("Linux") -> "Linux"
         else -> "Unknown"
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun getOs(userAgent: String): String {
return when {
userAgent.contains("Windows") -> "Windows"
userAgent.contains("Macintosh") -> "macOS"
userAgent.contains("Linux") -> "Linux"
userAgent.contains("Android") -> "Android"
userAgent.contains("iPhone") || userAgent.contains("iPad") -> "iOS"
else -> "Unknown"
}
}
private fun getOs(userAgent: String): String {
return when {
userAgent.contains("Android") -> "Android"
userAgent.contains("iPhone") || userAgent.contains("iPad") -> "iOS"
userAgent.contains("Windows") -> "Windows"
userAgent.contains("Macintosh") -> "macOS"
userAgent.contains("Linux") -> "Linux"
else -> "Unknown"
}
}
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 289 - 298, The getOs function incorrectly matches "Linux" before
mobile tokens, causing Android (and some iPadOS) UAs to be reported as desktop;
fix by reordering and strengthening the when-clauses in getOs: check for
"Android" first, then "iPhone" and "iPad" (so iPad is detected before
"Macintosh"), then check "Windows", "Macintosh", and "Linux"; optionally use
case-insensitive contains to be robust across UA casing.

Comment on lines +300 to +315
private fun getBrowserVersion(userAgent: String, browser: String): String? {
return try {
val regex = when (browser) {
"Chrome" -> "Chrome/(\\S+)"
"Firefox" -> "Firefox/(\\S+)"
"Safari" -> "Version/(\\S+)"
"Edge" -> "Edge/(\\S+)"
"Internet Explorer" -> "MSIE (\\S+);|rv:(\\S+)"
else -> return null
}
val matchResult = Regex(regex).find(userAgent)
matchResult?.groups?.get(1)?.value
} catch (_: Exception) {
null
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Version regex misses IE 11 (rv:) and modern Edge (Edg/).

  • The IE pattern "MSIE (\\S+);|rv:(\\S+)" uses alternation, but only groups[1] is read. IE 11 reports its version via rv: which lands in groups[2], so 11.x always returns null. Use matchResult.groupValues.drop(1).firstOrNull { it.isNotEmpty() } (or two separate patterns).
  • If you fix the Edge detection in getBrowser, the "Edge" -> "Edge/(\\S+)" arm here also needs to handle Edg/ so modern Edge versions are captured.
🛡️ Proposed fix
 private fun getBrowserVersion(userAgent: String, browser: String): String? {
     return try {
         val regex = when (browser) {
             "Chrome" -> "Chrome/(\\S+)"
             "Firefox" -> "Firefox/(\\S+)"
             "Safari" -> "Version/(\\S+)"
-            "Edge" -> "Edge/(\\S+)"
+            "Edge" -> "Edg(?:e)?/(\\S+)"
             "Internet Explorer" -> "MSIE (\\S+);|rv:(\\S+)"
             else -> return null
         }
         val matchResult = Regex(regex).find(userAgent)
-        matchResult?.groups?.get(1)?.value
+        matchResult?.groupValues?.drop(1)?.firstOrNull { it.isNotEmpty() }
     } catch (_: Exception) {
         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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 300 - 315, In getBrowserVersion, update the browser-specific
regexes and extraction so IE11's rv: group and modern Edge's Edg/ are captured
and returned; for example change the "Internet Explorer" arm to a pattern that
matches both MSIE and rv: but do not assume the version is always in
groups[1]—instead use the matchResult.groupValues.drop(1).firstOrNull {
it.isNotEmpty() } approach to pick the first non-empty capture, and update the
"Edge" arm to include both "Edge/(\\S+)" and "Edg/(\\S+)" alternatives so modern
Edge versions are returned correctly.

Comment on lines +391 to +398
@Composable
fun getLockMessage(lockingPolicy: String?): String {
return when (lockingPolicy?.lowercase()) {
BiometricAvailablePolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_biometric_available)
DeviceTamperingPolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_device_tampering)
null -> stringResource(id = R.string.account_locked_unknown_policy)
else -> stringResource(id = R.string.account_locked_generic_policy, lockingPolicy)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep --pattern $'object BiometricAvailablePolicy { $$$ }'
ast-grep --pattern $'object DeviceTamperingPolicy { $$$ }'
rg -nP --type=kotlin -C2 'POLICY_NAME\s*='
rg -nP --type=kotlin -C2 '\blockingPolicy\b'

Repository: ForgeRock/sdk-sample-apps

Length of output: 5372


🏁 Script executed:

rg -nP 'import.*BiometricAvailablePolicy|import.*DeviceTamperingPolicy' --type=kotlin

Repository: ForgeRock/sdk-sample-apps

Length of output: 1136


🏁 Script executed:

fd -type f -name "*.kt" | xargs rg -l "BiometricAvailablePolicy|DeviceTamperingPolicy"

Repository: ForgeRock/sdk-sample-apps

Length of output: 583


🏁 Script executed:

rg -nP 'class BiometricAvailablePolicy|class DeviceTamperingPolicy|object BiometricAvailablePolicy|object DeviceTamperingPolicy' --type=kotlin -A 5

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

find . -name "build.gradle" -o -name "build.gradle.kts" | xargs grep -i "pingidentity\|com.pingidentity" | head -20

Repository: ForgeRock/sdk-sample-apps

Length of output: 1279


🏁 Script executed:

find . -name "gradle.properties" -o -name "*.gradle.kts" | xargs grep -i "version" | grep -i "mfa\|pingidentity"

Repository: ForgeRock/sdk-sample-apps

Length of output: 51


🏁 Script executed:

find . -name "libs.versions.toml" -o -name "gradle.properties" | xargs cat 2>/dev/null

Repository: ForgeRock/sdk-sample-apps

Length of output: 48533


🏁 Script executed:

find . -name "build.gradle.kts" -path "*/kotlin-authenticatorapp/*" -exec grep -A 20 "dependencies" {} \;

Repository: ForgeRock/sdk-sample-apps

Length of output: 1598


🌐 Web query:

Ping Identity MFA SDK 2.0.0 BiometricAvailablePolicy POLICY_NAME constant value

💡 Result:

The Ping Identity MFA/Authenticator policy name constant for BiometricAvailablePolicy (used in the SDK JSON as the key) is: biometricAvailable [1]. This value is shown as the Authenticator app’s default “Require biometrics” policy name in Ping’s SDK documentation, and it matches the policy’s JSON format used to pass policy parameters by policy name. [1]

Citations:


🌐 Web query:

Ping Identity MFA SDK locking policies DeviceTamperingPolicy documentation

💡 Result:

Ping Identity’s MFA/Authenticator “DeviceTamperingPolicy” is part of the ForgeRock Authenticator (Ping “Authenticator”/FRA SDK) device integrity policies, and its “locking” behavior happens when that policy is breached (device tampering likelihood score exceeds a configured threshold), causing the SDK to lock the registered account and block MFA. In PingOne (cloud) MFA, separate “lockout” settings exist (account lockout after incorrect MFA actions) and separate device/policy checks exist under “Device Authentication Policies”, but the exact DeviceTamperingPolicy name is tied to the Authenticator app policy module. 1) DeviceTamperingPolicy (Authenticator SDK) definition - DeviceTamperingPolicy checks the integrity of the device software/hardware, using FRRootDetector from the forgerock-core module, aggregating multiple tamper detectors into a score in the range 0.0 to 1.0, where the configured score is used as the threshold for determining whether the device is tampered [1]. - The JSON policy format example is: {"deviceTampering": {"score": 0.8}} [2]. 2) What “locking policy” means for DeviceTamperingPolicy (SDK behavior) - Ping’s SDK documentation explains that if authenticator app policy conditions are not met, the account cannot be registered in the Authenticator app, and if policy conditions are breached after successful registration, “the account is locked, and MFA is blocked” [2]. - The “Integrate authenticator app policies” SDK guide shows the mechanism: create a DeviceTamperingPolicy and then call FRAClient.lockAccount(account, policy) to lock the account when tampering is detected [3]. It also states that the policy evaluator’s default built-in policies include DeviceTamperingPolicy [3]. 3) “Lockout” / account-lock settings in PingOne MFA (cloud) These are distinct from the Authenticator SDK’s device tampering policy. PingOne exposes MFA lockout settings that lock the account after a maximum number of incorrect MFA authorization actions (e.g., incorrect OTP or denying a push) and keep the account locked for a configured duration. - MFA Settings document that lockout.failureCount is the maximum number of incorrect MFA authorization actions before the account is locked, and lockout.durationSeconds is the time to remain locked; you update via the MFA settings endpoint [4]. - PingOne also supports configuring “Device authentication policies” (MFA Policies) that let you configure different settings per MFA method, and includes an ignoreUserLock option in the device authentication policy data model [5]. So, DeviceTamperingPolicy’s “locking” is an Authenticator-app policy breach action (locks the registered account and blocks MFA), while PingOne’s MFA “locking” is typically user/account lockout after failed MFA actions (lockout.failureCount / lockout.durationSeconds) and can be influenced by policy checks such as ignoreUserLock. References: [1] https://developer.pingidentity.com/reference/sdks/android/api-reference-4.7.0/forgerock-authenticator/forgerock-authenticator/org.forgerock.android.auth.policy/-device-tampering-policy/index.html [4] https://developer.pingidentity.com/pingone-api/mfa/mfa-settings.html [5] https://developer.pingidentity.com/pingone-api/mfa/device-authentication-policy.html [2] https://docs.pingidentity.com/sdks/latest/authenticator/use-cases/how-to-apply-authenticator-policies.html [3] https://docs.pingidentity.com/sdks/latest/authenticator-module/use-cases/integrate-authenticator-policies.html

Citations:


🌐 Web query:

Ping Identity SDK DeviceTamperingPolicy POLICY_NAME "deviceTampering" constant

💡 Result:

Ping (ForgeRock) SDK: the “Device tampering detection” policy has policy name constant POLICY_NAME = "deviceTampering". [1] Where it’s documented: Ping’s “Secure the Authenticator app using policies” guide lists the available Authenticator app policies and explicitly shows “Policy name: deviceTampering” under the “Device tampering detection” policy. [2]

Citations:


Remove .lowercase() from the policy comparison to prevent targeted message branches from being unreachable.

The locking policy constants (BiometricAvailablePolicy.POLICY_NAME = "biometricAvailable", DeviceTamperingPolicy.POLICY_NAME = "deviceTampering") are mixed-case, but the code compares them against lockingPolicy?.lowercase(). This causes the string comparisons to always fail—every locked account incorrectly falls through to the generic account_locked_generic_policy branch.

🛡️ Proposed fix
 `@Composable`
 fun getLockMessage(lockingPolicy: String?): String {
-    return when (lockingPolicy?.lowercase()) {
-        BiometricAvailablePolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_biometric_available)
-        DeviceTamperingPolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_device_tampering)
+    return when (lockingPolicy) {
+        BiometricAvailablePolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_biometric_available)
+        DeviceTamperingPolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_device_tampering)
         null -> stringResource(id = R.string.account_locked_unknown_policy)
         else -> stringResource(id = R.string.account_locked_generic_policy, lockingPolicy)
     }
 }

Note: The same pattern appears in NotificationResponseScreen.kt (line 529).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Composable
fun getLockMessage(lockingPolicy: String?): String {
return when (lockingPolicy?.lowercase()) {
BiometricAvailablePolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_biometric_available)
DeviceTamperingPolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_device_tampering)
null -> stringResource(id = R.string.account_locked_unknown_policy)
else -> stringResource(id = R.string.account_locked_generic_policy, lockingPolicy)
}
`@Composable`
fun getLockMessage(lockingPolicy: String?): String {
return when (lockingPolicy) {
BiometricAvailablePolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_biometric_available)
DeviceTamperingPolicy.POLICY_NAME -> stringResource(id = R.string.account_locked_device_tampering)
null -> stringResource(id = R.string.account_locked_unknown_policy)
else -> stringResource(id = R.string.account_locked_generic_policy, lockingPolicy)
}
}
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.kt`
around lines 391 - 398, The policy comparisons in getLockMessage use
lockingPolicy?.lowercase(), which prevents matching the mixed-case constants
(e.g., BiometricAvailablePolicy.POLICY_NAME and
DeviceTamperingPolicy.POLICY_NAME); change the subject of the when to the raw
lockingPolicy (remove .lowercase()) so the when compares the original string
values (keep the null branch intact), and apply the same fix to the analogous
comparison in NotificationResponseScreen.kt so those branches are reachable.

Comment on lines +49 to +50
private lateinit var authenticatorViewModel: AuthenticatorViewModel
private lateinit var loginViewModel: LoginViewModel
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

ViewModels won't survive configuration changes.

The ViewModels are created manually rather than via ViewModelProvider, which means they'll be destroyed and recreated on configuration changes (e.g., screen rotation). This causes:

  • Loss of user state during rotation
  • Unnecessary reinitialization overhead
  • Potential race conditions if rotation occurs during async setup
♻️ Recommended approach using ViewModelProvider

Create a ViewModelProvider.Factory:

class AuthenticatorViewModelFactory(
    private val application: Application,
    private val userPreferences: UserPreferences,
    private val oathManager: OathManager,
    private val pushManager: PushManager,
    private val accountGroupingManager: AccountGroupingManager,
    private val testAccountFactory: TestAccountFactory
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(AuthenticatorViewModel::class.java)) {
            `@Suppress`("UNCHECKED_CAST")
            return AuthenticatorViewModel(
                application,
                userPreferences,
                oathManager,
                pushManager,
                accountGroupingManager,
                testAccountFactory
            ) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

Then use in onCreate:

authenticatorViewModel = ViewModelProvider(this, factory)[AuthenticatorViewModel::class.java]

This ensures ViewModels survive configuration changes per Android architecture guidelines.

Also applies to: 113-215

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.kt`
around lines 49 - 50, The AuthenticatorViewModel and LoginViewModel are being
instantiated manually and will not survive configuration changes; create a
ViewModelProvider.Factory (e.g., AuthenticatorViewModelFactory) that accepts
required dependencies (Application, UserPreferences, OathManager, PushManager,
AccountGroupingManager, TestAccountFactory, etc.) and implement create() to
return AuthenticatorViewModel (and a similar factory if LoginViewModel has
deps), then obtain instances in onCreate via ViewModelProvider(this,
factory)[AuthenticatorViewModel::class.java] and ViewModelProvider(this,
factoryForLogin)[LoginViewModel::class.java] so the ViewModels persist across
rotations and avoid reinitialization/races.

Comment on lines +110 to +125
LaunchedEffect(uiState.lastAddedOathCredential) {
if (uiState.lastAddedOathCredential != null) {
snackbarHostState.showSnackbar(context.getString(R.string.qr_scanner_account_added_successfully))
viewModel.clearLastAddedOathCredential()
onScanComplete()
}
}

// Also check for push credentials
LaunchedEffect(uiState.lastAddedPushCredential) {
if (uiState.lastAddedPushCredential != null) {
snackbarHostState.showSnackbar(context.getString(R.string.qr_scanner_account_added_successfully))
viewModel.clearLastAddedPushCredential()
onScanComplete()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove duplicate onScanComplete() invocations.

The onScanComplete() callback is invoked twice for each successful scan:

  1. Immediately in the analyzer callback (lines 173, 179, 185) after calling the viewModel method
  2. Again in the LaunchedEffect blocks (lines 114, 123) when the credential is successfully added

This could cause navigation issues or unexpected UI behavior. Remove the onScanComplete() calls from the analyzer callback (lines 173, 179, 185) and rely solely on the LaunchedEffect blocks to invoke the callback after successful state updates.

🔧 Proposed fix
                                 when {
                                     qrCodeResult.startsWith(UriScheme.OTPAUTH.value) -> {
                                         diagnosticLogger.d("QrScannerScreen: Detected OATH QR code")
                                         viewModel.addOathCredentialFromUri(qrCodeResult)
-                                        onScanComplete()
                                     }
 
                                     qrCodeResult.startsWith(UriScheme.PUSHAUTH.value) -> {
                                         diagnosticLogger.d("QrScannerScreen: Detected Push QR code")
                                         viewModel.addPushCredentialFromUri(qrCodeResult)
-                                        onScanComplete()
                                     }
 
                                     qrCodeResult.startsWith(UriScheme.MFAUTH.value) -> {
                                         diagnosticLogger.d("QrScannerScreen: Detected MFA QR code")
                                         viewModel.addMfaCredentialFromUri(qrCodeResult)
-                                        onScanComplete()
                                     }

Also applies to: 165-195

🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt`
around lines 110 - 125, The analyzer callback currently calls onScanComplete()
immediately after invoking the ViewModel add methods, causing duplicate
invocations because the LaunchedEffect blocks (watching
uiState.lastAddedOathCredential and uiState.lastAddedPushCredential) also call
onScanComplete(); remove the onScanComplete() calls from the analyzer callback
(the calls near the analyzer function handling QR results) and keep only the
LaunchedEffect-based calls so navigation happens after the ViewModel updates and
the state change is observed (references: uiState.lastAddedOathCredential,
uiState.lastAddedPushCredential, viewModel.clearLastAddedOathCredential,
viewModel.clearLastAddedPushCredential, and onScanComplete).

if (uiState.error != null) {
AlertDialog(
onDismissRequest = { viewModel.clearError() },
title = { Text("Error") },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the hardcoded error dialog title.

The error dialog title uses a hardcoded string "Error", while all other UI strings in this file use stringResource for localization. This breaks i18n/l10n consistency.

🌐 Proposed fix
                 AlertDialog(
                     onDismissRequest = { viewModel.clearError() },
-                    title = { Text("Error") },
+                    title = { Text(stringResource(id = R.string.error)) },
                     text = { Text(uiState.error!!) },

Note: Ensure R.string.error exists in your string resources.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title = { Text("Error") },
AlertDialog(
onDismissRequest = { viewModel.clearError() },
title = { Text(stringResource(id = R.string.error)) },
text = { Text(uiState.error!!) },
🤖 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
`@android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.kt`
at line 271, Replace the hardcoded dialog title "Error" in the AlertDialog
inside QrScannerScreen.kt with a localized stringResource call (e.g., use
stringResource(R.string.error)) so the title is localized consistently with
other UI strings; ensure a corresponding R.string.error entry exists in
strings.xml and update the AlertDialog title parameter to use the stringResource
instead of the literal.

- **Presentation Layer**: Android Activities/Fragments for user interaction.
- **Domain Layer**: Handles business logic, orchestrates feature flows, and manages state.
- **Data/Service Layer**: Integrates with Ping SDK modules (`push`, `oath`, `journey`) and other services.
- **SDK Layer**: Abstracts the complexity to deal with MFA capabilities and comunication with Ping backend.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix spelling error.

"comunication" should be "communication".

📝 Proposed fix
-- **SDK Layer**: Abstracts the complexity to deal with MFA capabilities and comunication with Ping backend.
+- **SDK Layer**: Abstracts the complexity to deal with MFA capabilities and communication with Ping backend.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **SDK Layer**: Abstracts the complexity to deal with MFA capabilities and comunication with Ping backend.
- **SDK Layer**: Abstracts the complexity to deal with MFA capabilities and communication with Ping backend.
🧰 Tools
🪛 LanguageTool

[grammar] ~63-~63: Ensure spelling is correct
Context: ...exity to deal with MFA capabilities and comunication with Ping backend. The application follows ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 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 `@android/kotlin-authenticatorapp/README.md` at line 63, Replace the misspelled
word "comunication" with "communication" in the README sentence that starts with
"SDK Layer" (the phrase currently reads "Abstracts the complexity to deal with
MFA capabilities and comunication with Ping backend"); update that line so it
reads "Abstracts the complexity to deal with MFA capabilities and communication
with Ping backend".


- **Kotlin**: 100% Kotlin codebase
- **Jetpack Compose**: Declarative UI toolkit for building native UI
- **ViewModel**: Architecture component for managing UI-related data in a lifecycle conscious way
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix grammar: hyphenate compound adjective.

"lifecycle conscious" should be "lifecycle-conscious" when used as a compound adjective.

📝 Proposed fix
-- **ViewModel**: Architecture component for managing UI-related data in a lifecycle conscious way
+- **ViewModel**: Architecture component for managing UI-related data in a lifecycle-conscious way
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **ViewModel**: Architecture component for managing UI-related data in a lifecycle conscious way
- **ViewModel**: Architecture component for managing UI-related data in a lifecycle-conscious way
🧰 Tools
🪛 LanguageTool

[grammar] ~69-~69: Use a hyphen to join words.
Context: ... managing UI-related data in a lifecycle conscious way - Coroutines: For asyn...

(QB_NEW_EN_HYPHEN)

🤖 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 `@android/kotlin-authenticatorapp/README.md` at line 69, In the README.md
ViewModel bullet, fix the grammar by hyphenating the compound adjective: change
the phrase "lifecycle conscious" to "lifecycle-conscious" in the line that reads
"**ViewModel**: Architecture component for managing UI-related data in a
lifecycle conscious way" so it becomes "**ViewModel**: Architecture component
for managing UI-related data in a lifecycle-conscious way".

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