SDKS-5031: Add Authenticator app to Sample apps.#112
Conversation
📝 WalkthroughWalkthroughAdds 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. ChangesAndroid Authenticator App
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winAvoid 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 winPotential launcher background mismatch (color vs drawable).
Line 3 references
@color/ic_launcher_background, while a dedicated@drawable/ic_launcher_backgroundvector 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 winUpdate 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.0kotlin = "2.3.21"— Current and up-to-date ✓composeBom = "2025.08.01"— Latest stable is 2026.05.00Consider 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 winAdd 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 winFix 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 winVerify "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 winUse string resource for consistency.
Line 180 has a hardcoded content description
"Location information available", while line 172 correctly usesstringResource(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 winAdd
tapToRevealEnabledtoremember()dependency keys.At line 85,
tapToRevealEnabledis used in the lambda but missing from the dependency keys. When this parameter changes,isRevealedwon'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 winMove user-visible strings into
strings.xml.All
SettingItemtitles 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 throughres/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 winLocalize 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 tostrings.xmlfor 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 winRender 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 winDrop the auth-method icon's
contentDescriptionif it's purely decorative, or keep it consistent.You pass the icon's label as both the icon and the
Textcontent of an adjacentTextcomposable. Screen readers will then read the label twice. Either setcontentDescription = nullon the icon (TalkBack will rely on the neighboring text) or remove the adjacentTextto 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()forsentAt/respondedAtshows raw ISO; use a localized formatter.
sentAt.toString()(andrespondedAt.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 usingDateTimeFormatter/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 winRace between
LaunchedEffectandfinish(); also flashes loading UI before finishing.When
notificationIdis missing,onCreatecallsfinish()early (L59) — good. But whenpushClientinit fails or biometric isn't available (L86, L93), the code setserrorMessage, setsisLoading = false, then immediately callsfinish(). 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 delayfinish()), or remove the error state and justfinish()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 winEdit flow shares the deletion's lack of error handling.
Same concern as the deletion handler: if
updateOathCredential/updatePushCredentialthrows partway through,accountToEditis not cleared (line 364 unreached) and the user gets no error feedback. Consider wrapping intry/catch/finallyand 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 winDon't report a missing
PushClientas 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, orResult.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 winAlways invoke the callback on the error path.
Unlike
getOathBackupFiles()andgetPushBackupFiles(), thiscatchblock only updatesuiState.errorand never callscallback. 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
📒 Files selected for processing (83)
android/kotlin-authenticatorapp/.gitignoreandroid/kotlin-authenticatorapp/README.mdandroid/kotlin-authenticatorapp/app/.gitignoreandroid/kotlin-authenticatorapp/app/build.gradle.ktsandroid/kotlin-authenticatorapp/app/google-services.jsonandroid/kotlin-authenticatorapp/app/proguard-rules.proandroid/kotlin-authenticatorapp/app/src/main/AndroidManifest.xmlandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/AuthenticatorApp.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/BackupModels.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/DiagnosticLogger.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/LoginViewModel.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UserPreferences.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/AccountGroupingManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/JourneyManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/OathManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/TestAccountFactory.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationActionReceiver.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationHelper.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/PushNotificationActivity.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/LocationService.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/PushNotificationService.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AboutScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountDetailScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/DiagnosticLogsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/InitializationErrorScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/LoginScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/TestScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountAvatar.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/BackNavigationTopAppBar.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CallbackRenderers.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CircularProgressTimer.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/DetailRow.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditAccountDialog.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditableAccountItem.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EmptyStateMessage.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/ErrorAlertDialog.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/InfoCard.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/LoadingIndicator.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationCard.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/SettingItem.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/StatusIndicator.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Color.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Theme.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Type.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/DateUtils.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/NavigationAnimations.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/QrCodeAnalyzer.ktandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_check.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_close.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_background.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_foreground.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_notification.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ping_logo.xmlandroid/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlandroid/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values-night/themes.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values/ic_launcher_background.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values/strings.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values/themes.xmlandroid/kotlin-authenticatorapp/build.gradle.ktsandroid/kotlin-authenticatorapp/gradle.propertiesandroid/kotlin-authenticatorapp/gradle/gradle-daemon-jvm.propertiesandroid/kotlin-authenticatorapp/gradle/libs.versions.tomlandroid/kotlin-authenticatorapp/gradle/wrapper/gradle-wrapper.propertiesandroid/kotlin-authenticatorapp/gradlewandroid/kotlin-authenticatorapp/gradlew.batandroid/kotlin-authenticatorapp/settings.gradle.kts
| <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> |
There was a problem hiding this comment.
🧩 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/ -iRepository: ForgeRock/sdk-sample-apps
Length of output: 2151
🏁 Script executed:
cat -n android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.ktRepository: 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.ktRepository: 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.ktRepository: ForgeRock/sdk-sample-apps
Length of output: 1319
🏁 Script executed:
rg -nP "intent\." android/kotlin-authenticatorapp/app/src/main/kotlin --type=kotlin -C3Repository: 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 -C3Repository: 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 -100Repository: 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 -A5Repository: ForgeRock/sdk-sample-apps
Length of output: 51
🏁 Script executed:
rg -nP "data:|deeplink" android/kotlin-authenticatorapp/app/src/main --type=xml --type=kotlinRepository: ForgeRock/sdk-sample-apps
Length of output: 51
🏁 Script executed:
wc -l android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.ktRepository: ForgeRock/sdk-sample-apps
Length of output: 186
🏁 Script executed:
cat -n android/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.ktRepository: 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.ktRepository: ForgeRock/sdk-sample-apps
Length of output: 205
🏁 Script executed:
rg -nP "getData\(\)|intent\.data" android/kotlin-authenticatorapp/app/src/main --type=kotlinRepository: 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 -20Repository: 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.
| 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) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winURL-encode label and query values when building the otpauth URI.
As flagged in the previous review,
issuer,accountName,secret, andalgorithmare 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 byaddOathCredentialFromUri(...).🛠️ 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 winValidate 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 winTrim whitespace from input fields before building the URI.
User input often contains accidental leading/trailing whitespace. For
issuerandaccountName, this will appear in the authenticator UI. Forsecret, 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 winConsider 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 winExtract 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 winDefine 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 winDuplicated grouping logic and misleading display-name comment.
The OATH/Push branches in both the
shouldCombine=falseandshouldCombine=truepaths are near-identical. A small generic helper that takes the issuer/account/display fields plus anAccountGroup-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/displayAccountNameare 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 winReplace string-based
pushTypedetection 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 awhenexpression instead:Example refactor
val requiresChallenge = notification.pushType == PushType.CHALLENGE val requiresBiometric = notification.pushType == PushType.BIOMETRICor 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
📒 Files selected for processing (83)
android/kotlin-authenticatorapp/.gitignoreandroid/kotlin-authenticatorapp/README.mdandroid/kotlin-authenticatorapp/app/.gitignoreandroid/kotlin-authenticatorapp/app/build.gradle.ktsandroid/kotlin-authenticatorapp/app/google-services.jsonandroid/kotlin-authenticatorapp/app/proguard-rules.proandroid/kotlin-authenticatorapp/app/src/main/AndroidManifest.xmlandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/AuthenticatorApp.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/MainActivity.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/AuthenticatorViewModel.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/BackupModels.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/DiagnosticLogger.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/LoginViewModel.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UiModels.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/data/UserPreferences.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/AccountGroupingManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/JourneyManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/OathManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/PushManager.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/managers/TestAccountFactory.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/BiometricPromptActivity.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationActionReceiver.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/NotificationHelper.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/notification/PushNotificationActivity.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/LocationService.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/service/PushNotificationService.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AboutScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountDetailScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AccountsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/AuthenticatorNavHost.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/DiagnosticLogsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/EditAccountsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/InitializationErrorScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/LoginScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/ManualEntryScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/NotificationResponseScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/PushNotificationsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/QrScannerScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/SettingsScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/TestScreen.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountAvatar.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/AccountGroupItem.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/BackNavigationTopAppBar.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CallbackRenderers.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/CircularProgressTimer.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/DetailRow.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditAccountDialog.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EditableAccountItem.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/EmptyStateMessage.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/ErrorAlertDialog.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/InfoCard.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/LoadingIndicator.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationCard.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/NotificationHistoryCard.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/SettingItem.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/components/StatusIndicator.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Color.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Theme.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/ui/theme/Type.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/DateUtils.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/NavigationAnimations.ktandroid/kotlin-authenticatorapp/app/src/main/kotlin/com/pingidentity/authenticatorapp/util/QrCodeAnalyzer.ktandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_check.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_close.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_fingerprint.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_background.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_launcher_foreground.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ic_notification.xmlandroid/kotlin-authenticatorapp/app/src/main/res/drawable/ping_logo.xmlandroid/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlandroid/kotlin-authenticatorapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values-night/themes.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values/ic_launcher_background.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values/strings.xmlandroid/kotlin-authenticatorapp/app/src/main/res/values/themes.xmlandroid/kotlin-authenticatorapp/build.gradle.ktsandroid/kotlin-authenticatorapp/gradle.propertiesandroid/kotlin-authenticatorapp/gradle/gradle-daemon-jvm.propertiesandroid/kotlin-authenticatorapp/gradle/libs.versions.tomlandroid/kotlin-authenticatorapp/gradle/wrapper/gradle-wrapper.propertiesandroid/kotlin-authenticatorapp/gradlewandroid/kotlin-authenticatorapp/gradlew.batandroid/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
| // Determine notification status | ||
| val status = when { | ||
| notification.approved -> NotificationStatus.APPROVED | ||
| (notification.expired && notification.pending) -> NotificationStatus.EXPIRED | ||
| notification.pending -> NotificationStatus.PENDING | ||
| else -> NotificationStatus.DENIED | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://developer.pingidentity.com/reference/sdks/android/api-reference/forgerock-authenticator/forgerock-authenticator/org.forgerock.android.auth/-push-notification/index.html
- 2: https://developer.pingidentity.com/reference/sdks/android/api-reference-4.7.0/forgerock-authenticator/forgerock-authenticator/org.forgerock.android.auth/-push-notification/index.html
- 3: https://developer.pingidentity.com/reference/sdks/android/api-reference-4.7.0/forgerock-authenticator/forgerock-authenticator/org.forgerock.android.auth/-push-mechanism/index.html
- 4: https://docs.pingidentity.com/sdks/latest/_attachments/android/api-reference/forgerock-authenticator/forgerock-authenticator/org.forgerock.android.auth/-push-notification/-push-notification-builder/set-pending.html
- 5: https://docs.pingidentity.com/sdks/latest/release-notes/changelogs/changelog_android.html
- 6: https://developer.pingidentity.com/orchsdks/journey/use-cases/push/android/05_implement_push_client.html
- 7: https://github.com/pingidentity/pingone-mobile-sdk-android/
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.
| 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" | ||
| } | ||
| } |
There was a problem hiding this comment.
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, andEdg/(notEdge). - 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.
| 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.
| 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" | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Version regex misses IE 11 (rv:) and modern Edge (Edg/).
- The IE pattern
"MSIE (\\S+);|rv:(\\S+)"uses alternation, but onlygroups[1]is read. IE 11 reports its version viarv:which lands ingroups[2], so 11.x always returnsnull. UsematchResult.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 handleEdg/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.
| @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) | ||
| } |
There was a problem hiding this comment.
🧩 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=kotlinRepository: 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 5Repository: 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 -20Repository: 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/nullRepository: 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:
- 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
- 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
- 4: https://developer.pingidentity.com/pingone-api/mfa/mfa-settings.html
- 5: https://developer.pingidentity.com/pingone-api/mfa/device-authentication-policy.html
🌐 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:
- 1: https://docs.pingidentity.com/sdks/latest/sdks/index.html
- 2: https://docs.pingidentity.com/sdks/latest/sdks/use-cases/how-to-device-info.html
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.
| @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.
| private lateinit var authenticatorViewModel: AuthenticatorViewModel | ||
| private lateinit var loginViewModel: LoginViewModel |
There was a problem hiding this comment.
🛠️ 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.
| 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() | ||
| } | ||
| } |
There was a problem hiding this comment.
Remove duplicate onScanComplete() invocations.
The onScanComplete() callback is invoked twice for each successful scan:
- Immediately in the analyzer callback (lines 173, 179, 185) after calling the viewModel method
- Again in the
LaunchedEffectblocks (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") }, |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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.
| - **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 |
There was a problem hiding this comment.
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.
| - **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".
Jira
SDKS-5031
Description
This ticket introduces the authenticator sample app for testing HOTP/TOTP, Journey and Push accounts.
Summary by CodeRabbit
New Features
Documentation