Compare commits

...

681 Commits

Author SHA1 Message Date
Patrick Honkonen
d8a9c596b2 🍒[PM-33394] fix: Surface CookieRedirectException message during sync-on-unlock (#6645) 2026-03-12 16:00:06 +00:00
Patrick Honkonen
e33c4df59c 🍒[PM-33394] debt: Add userFriendlyMessage extension and errorMessage to result types (#6644) 2026-03-12 15:27:55 +00:00
Patrick Honkonen
7f426f1037 🍒[PM-33394] fix: Propagate CookieRedirectException error message (#6640) 2026-03-11 18:37:45 +00:00
Patrick Honkonen
9bde261007 🍒[PM-33227] feat: Add Clear SSO Cookies button to debug menu (#6632) 2026-03-10 14:41:05 +00:00
Patrick Honkonen
37b336ee35 🍒[PM-32123] feat: Propagate informative cookie redirect error message (#6631) 2026-03-10 14:40:46 +00:00
Patrick Honkonen
c9d28941c6 🍒[PM-33262] feat: Add cookie support to Glide image requests (#6630) 2026-03-10 14:40:30 +00:00
Daniel James Smith
053ac28e38 Remove Gitter chat badge from README (#6612) 2026-03-04 17:54:54 +00:00
Patrick Honkonen
3400d5f875 llm: Add plan-android-work command and planning skills (#6597) 2026-03-04 13:35:57 +00:00
David Perez
9f274bbffa PM-33112: Avoid double announcement of BitwardenSwitch content description (#6611) 2026-03-04 00:48:15 +00:00
David Perez
cf1455a45a Add Authenticator app-lock timeout (#6609) 2026-03-03 20:14:35 +00:00
Patrick Honkonen
d0dc4200f8 [PM-21659] llm: Add workflow skills and finalize CLAUDE.md restructuring (#6575)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-03 06:57:57 +00:00
David Perez
8a2b46e81a Move the AppStateManager to the data module (#6593) 2026-03-02 20:53:22 +00:00
David Perez
3538ca54ca Update Compose to 2026.02.01 (#6607) 2026-03-02 19:21:24 +00:00
David Perez
5a61ba96f6 Update Firebase BOM (#6606) 2026-03-02 19:21:07 +00:00
David Perez
836233f4d5 Move FakeLifecycle to core module (#6608) 2026-03-02 17:48:20 +00:00
renovate[bot]
3b081faf65 [deps]: Update hilt to v2.59.2 (#6602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 16:35:30 +00:00
renovate[bot]
61517014a7 [deps]: Update com.google.devtools.ksp to v2.3.6 (#6601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 16:03:34 +00:00
renovate[bot]
4a1582b1e4 [deps]: Update org.junit:junit-bom to v6.0.3 (#6603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 16:03:09 +00:00
bw-ghapp[bot]
227224b6cb Crowdin Pull (#6600)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-03-02 15:05:12 +00:00
Patrick Honkonen
60bc6ee0ca [PM-32802] fix: 400 error when archiving/unarchiving org-owned ciphers (#6592) 2026-02-27 20:10:58 +00:00
Patrick Honkonen
e509d60af6 Replace test workflow with sharded parallel CI execution (#6582)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
2026-02-27 18:49:47 +00:00
Patrick Honkonen
1f9390a668 [PM-32658] Add skill routing to CLAUDE.md Quick Reference (#6574)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 18:13:46 +00:00
Patrick Honkonen
ed1abcac5b [PM-32657] Add build-test-verify skill and extract build/test/deploy sections (#6573) 2026-02-27 16:32:02 +00:00
bw-ghapp[bot]
209e216213 Update SDK to 2.0.0-5425-a6f4a233 (#6595)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-02-27 15:43:26 +00:00
bw-ghapp[bot]
7bde0ce716 Update SDK to 2.0.0-5422-26e2b107 (#6569)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-02-27 11:34:47 +00:00
Patrick Honkonen
a517b3f970 [PM-32656] Fix implementing-android-code skill annotations and formatting (#6572)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-26 20:00:19 +00:00
Álison Fernandes
c7d173cf9a [PM-32751] ci: Fix version name output in run summary (#6585) 2026-02-26 18:56:45 +00:00
Patrick Honkonen
38f3d3d720 [PM-32714] Add cookie domain-suffix resolution and fix cloud config path exclusion (#6589) 2026-02-26 16:35:03 +00:00
David Perez
487b163d38 BWA-235: Update Authenticator to use state-based navigation for top-level navigation (#6586) 2026-02-26 15:27:49 +00:00
Patrick Honkonen
52da80e0fc [PM-32780] Disable Claude Code attribution in commits and PRs (#6588) 2026-02-26 15:16:30 +00:00
Álison Fernandes
1abb640512 [PM-32758] ci: Improve CI cache to fix GitHub runners running out of memory (#6583) 2026-02-25 22:24:48 +00:00
David Perez
64a79ff108 PM-29870: Add explicit traversal order for scaffold (#6580) 2026-02-25 18:08:20 +00:00
David Perez
fd6d32ec09 PM-31772: Simplify origin for verified sources (#6577) 2026-02-25 17:13:47 +00:00
David Perez
4ca79bb8c7 Remove unnecessary opt-in annotations (#6581) 2026-02-25 17:11:38 +00:00
Patrick Honkonen
642456f2fe [PM-32655] Extract troubleshooting guide into docs/TROUBLESHOOTING.md (#6571)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-25 16:07:10 +00:00
André Bispo
7b1b519b0d [PM-30916] bug: Create passkey myitems (#6558) 2026-02-25 15:12:35 +00:00
David Perez
d51d6c7c54 PM-29867: Fix notifications announcement (#6570) 2026-02-24 21:59:53 +00:00
Patrick Honkonen
4adb46170d [PM-32566] Refactor cookie acquisition ViewModel and simplify tests (#6564)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 16:16:26 +00:00
bw-ghapp[bot]
3360999706 Update SDK to 2.0.0-5335-7a22aa7f (#6562)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-02-24 11:59:16 +00:00
Patrick Honkonen
b10568a3ae Add implementing-android-code skill and deduplicate CLAUDE.md (#6534)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-24 07:09:15 +00:00
David Perez
d9f6fe97ff PM-32607: Label headers for accesibility (#6567) 2026-02-23 22:08:32 +00:00
David Perez
89f70a6b18 PM-29871: Add external links announcements (#6566) 2026-02-23 17:48:35 +00:00
David Perez
8b2aaf9c79 PM-29866: Remove redundant content description in icon buttons (#6565) 2026-02-23 17:41:13 +00:00
bw-ghapp[bot]
c9f3afa851 Crowdin Pull (#6561)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-02-23 12:57:50 +00:00
bw-ghapp[bot]
5ef7482fae Update SDK to 2.0.0-5302-1693d4d4 (#6549)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-02-23 09:42:08 +00:00
David Perez
c69f3554c6 PM-30892: Fix radio button spacing (#6559) 2026-02-20 23:15:52 +00:00
David Perez
c6b4c490ca Replace ZonedDateTime with Instant (#6554) 2026-02-20 19:02:25 +00:00
David Perez
92664b6752 Fix incorrect apostrophe (#6557) 2026-02-20 16:48:30 +00:00
Hunter Wittenborn
06284a31df [PM-32356] Fix: Use soft logout for token refresh failures to preserve account (#6545)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 21:59:30 +00:00
aj-rosado
794781213e [PM-31835] feat: add generator copy password field on send (#6508) 2026-02-19 19:50:10 +00:00
aj-rosado
d1cf808e97 [PM-31810] Added logic to gate Send auth verification behind premium (#6556) 2026-02-19 19:10:59 +00:00
Álison Fernandes
4356156aad [PM-32200] ci: Add workflow to enforce PR labels (#6530) 2026-02-19 18:32:29 +00:00
David Perez
268be4210e PM-29863: Update segmented control font (#6555) 2026-02-19 17:47:05 +00:00
aj-rosado
4ee55111f4 [PM-32149] Send email verification error dialogs (#6535) 2026-02-19 15:30:18 +00:00
Patrick Honkonen
1a6936262c [PM-32122] Add cookie acquisition navigation (#6529)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-18 18:11:43 +00:00
David Perez
6f19ae534f Clean up ColorExtensions tests (#6551) 2026-02-18 13:26:07 +00:00
Patrick Honkonen
46a8236ef7 Update RootNavScreen docs (#6553) 2026-02-18 13:24:40 +00:00
Patrick Honkonen
f6f630ff8c [PM-32121] Add CookieAcquisition screen and ViewModel (#6523)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 20:44:44 +00:00
David Perez
bd0640e5b4 PM-32353: Archive and Unarchive buttons should honor MP reprompt (#6546) 2026-02-17 18:55:46 +00:00
Patrick Honkonen
436ae9333c [PM-29885] Implement SSO cookie vending authentication flow (#6522)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 18:41:07 +00:00
Patrick Honkonen
9b13cd4498 [PM-30703] Introduce CXF payload parser and update to alpha05 (#6347)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 18:20:52 +00:00
Ignacio
f6cd94485a [PM-32022] Fix browser autofill dialog showing for non-default browsers (#6514) 2026-02-17 16:57:12 +00:00
David Perez
222bc44c99 PM-32354: Filter out archived items from CXP (#6547) 2026-02-17 15:34:38 +00:00
github-actions[bot]
275d90bb61 Update Google privileged browsers list (#6538)
Co-authored-by: GitHub Actions Bot <actions@github.com>
2026-02-17 14:40:56 +00:00
David Perez
a23183597c PM-32252: Update View Item date information layout (#6544) 2026-02-17 14:30:00 +00:00
David Perez
e3ab4f3d68 Update AGP to v9.0.1 (#6543) 2026-02-17 14:27:58 +00:00
bw-ghapp[bot]
34a7c4455c Update SDK to 2.0.0-5210-4ffddfe5 (#6533)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-02-17 13:34:52 +00:00
renovate[bot]
4a68c2343d [deps]: Update com.google.devtools.ksp to v2.3.5 (#6541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 16:36:43 +00:00
André Bispo
fb9d16730e [PM-30870] Fix editing blocked autofill URIs (#6532) 2026-02-16 15:51:10 +00:00
renovate[bot]
5c348ac360 [deps]: Lock file maintenance (#6542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 15:47:20 +00:00
bw-ghapp[bot]
3985817c16 Crowdin Pull (#6539)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-02-16 15:45:56 +00:00
Patrick Honkonen
8664ce4614 [PM-32251] Decouple SDK token repository from network module (#6537)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 22:39:31 +00:00
David Perez
f3c746fd49 Update Anroidx dependencies (#6536) 2026-02-13 22:27:54 +00:00
aj-rosado
ce3f0acf74 [PM-31614] feat: Added new UI for the Email verification on sends (#6488) 2026-02-13 22:19:09 +00:00
bw-ghapp[bot]
b20622e7d6 Update SDK to 2.0.0-5131-c0c3ee5f (#6531)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-02-13 12:20:38 +00:00
David Perez
e939b20a82 PM-31664: Add new SnackbarRelay type specific for the View Screen (#6528) 2026-02-12 21:10:27 +00:00
David Perez
a8e77a5abc PM-32146: Add back 'parent' param to webAuthn url (#6527) 2026-02-12 18:57:41 +00:00
aj-rosado
afa9c28341 [PM-31615] feat: Updated Send network models to support email verification (#6519) 2026-02-12 16:43:05 +00:00
bw-ghapp[bot]
bb44586d76 Update SDK to 2.0.0-5087-3e8a45eb (#6521)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-02-12 11:40:57 +00:00
Patrick Honkonen
4cdd0b8422 [PM-32029] Implement SDK interfaces for cookie management (#6517)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <SaintPatrck@users.noreply.github.com>
2026-02-11 21:02:46 +00:00
David Perez
5a4973d678 PM-31925: Replace 'android' reference with logic in LibraryExtension (#6520) 2026-02-11 17:17:23 +00:00
Patrick Honkonen
a914d12e6f [PM-80371] Enhance CLAUDE.md using bitwarden-init plugin (#6368)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-11 17:03:11 +00:00
David Perez
e5875cd8fe PM-31922: Remove deprecated Android block where possible (#6512) 2026-02-11 15:55:33 +00:00
bw-ghapp[bot]
a3aefd369a Update SDK to 2.0.0-5064-8700dc73 (#6513)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2026-02-11 15:25:41 +00:00
Mick Letofsky
60a1265c5d Slim down and align with our current practices (#6518) 2026-02-11 13:07:02 +00:00
David Perez
95272d9692 Update Kover to v0.9.7 (#6516) 2026-02-10 23:31:48 +00:00
Patrick Honkonen
3be5bead89 [PM-32011] Add cookie callback flow to AuthRepository (#6510)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 22:33:32 +00:00
David Perez
31d480d6b4 PM-31953: Support multiple schemes for Duo, WebAuthn, and SSO callbacks (#6498) 2026-02-10 20:21:40 +00:00
bw-ghapp[bot]
43940102ff Update SDK to 2.0.0-5046-d59280a3 (#6511)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-02-10 18:41:45 +00:00
Patrick Honkonen
253f0d7ec4 [PM-31993] Add cookie vendor deep link intent filter (#6507)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 18:26:06 +00:00
David Perez
d7428a15bc PM-31924: Remove the 'android.dependency.useConstraints' gradle property (#6509) 2026-02-10 18:24:30 +00:00
Patrick Honkonen
5d84df9d31 [PM-31993] Add deep link utilities for cookie vendor callbacks (#6506)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 16:17:54 +00:00
Patrick Honkonen
d8c69a3243 [PM-31982] Add CookieDiskSource for cookie persistence (#6504)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <SaintPatrck@users.noreply.github.com>
2026-02-10 15:39:16 +00:00
Eran Boudjnah
f0837f7668 [PM-22523] PM-19476: Allow empty string as word separator (#5334) 2026-02-10 14:00:46 +00:00
Marc Nguyen
f094430d6c [PM-31980] Fix passkeys on some browsers by fixing JSON parsing (#6502) 2026-02-10 13:44:14 +00:00
Patrick Honkonen
cf3660a5aa [PM-31954] Add server communication models to ConfigResponseJson (#6500) 2026-02-10 13:17:34 +00:00
bw-ghapp[bot]
5300386ce3 Update SDK to 2.0.0-5021-f954d14b (#6495)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-02-10 12:20:00 +00:00
David Perez
eb24a50baa Update to Kotlin v2.3.10 (#6499) 2026-02-10 09:12:26 +00:00
David Perez
4d31dccc74 Update the gradlew Wrapper to v9.3.1 (#6496) 2026-02-09 22:20:54 +00:00
David Perez
8ee721c8ae PM-31927: Pre-emptively patch Brave browser Autofill bug (#6497) 2026-02-09 21:32:19 +00:00
David Perez
c0907b867b PM-31926: Add Autofill reminder for Vivaldi browser (#6494) 2026-02-09 21:26:04 +00:00
David Perez
6eba9ecd4b Update Firebase BOM to v34.9.0 (#6493) 2026-02-09 21:25:43 +00:00
David Perez
594cb507df Update the ZonedDateTimeSerializer to be more lenient when deserializing (#6489) 2026-02-09 14:58:09 +00:00
bw-ghapp[bot]
e615bdbea5 Update SDK to 2.0.0-5002-7f4059e7 (#6481)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-02-09 14:15:16 +00:00
bw-ghapp[bot]
071d3c8cd5 Crowdin Pull (#6491)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-02-09 14:08:41 +00:00
David Perez
ad3a9a6c2e Update AGP to v9.0.0 (#6479) 2026-02-06 20:55:11 +00:00
David Perez
cbe13d2015 PM-31735: Add the archivedDate property to the updateCipher API (#6483) 2026-02-05 20:23:18 +00:00
Patrick Honkonen
f728c15794 Configure Claude to use the Bitwarden marketplace (#6484) 2026-02-05 18:16:08 +00:00
David Perez
586f24ffec PM-31734: Add archived item filtering for passkeys (#6482) 2026-02-05 17:05:42 +00:00
Patrick Honkonen
8e8367a82f [PM-31775] Refactor popUpToCompleteRegistration to use type-safe KClass reference (#6480) 2026-02-05 16:44:01 +00:00
David Perez
47b9509062 Update build optimizations (#6433) 2026-02-04 20:08:15 +00:00
David Perez
29648e03c8 Update protobuf to v4.33.5 (#6478) 2026-02-04 17:36:20 +00:00
David Perez
15e217bc49 PM-31656, PM-31658, PM-31659: Address Archive feature bugs (#6473) 2026-02-04 17:33:29 +00:00
bw-ghapp[bot]
7ec4faf424 Update SDK to 2.0.0-4872-065ef30b (#6464)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-02-04 17:30:35 +00:00
Patrick Honkonen
e31fa46a73 [PM-30279] Extract credential provider handling to dedicated activity (#6472)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 22:05:28 +00:00
David Perez
aff8b0347b Update test tools (#6468) 2026-02-03 21:54:42 +00:00
renovate[bot]
f4d34e4649 [deps]: Update androidx.credentials:credentials to v1.6.0-rc01 (#6455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 21:35:42 +00:00
David Perez
2e18b079f8 Update Androidx dependencies (#6467) 2026-02-03 18:29:31 +00:00
aj-rosado
b0eea88af2 [PM-31613] Add send email verification feature flag (#6470) 2026-02-03 17:09:15 +00:00
Patrick Honkonen
4cac4d6a6e Add comprehensive tests for Unlock feature (#6426)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 14:33:58 +00:00
David Perez
a2ec99fb05 Remove the configuration cache to avoid play store build issues (#6466) 2026-02-02 19:10:20 +00:00
Patrick Honkonen
d49629de9e Add Android testing skill for Claude (#6370)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 18:42:01 +00:00
renovate[bot]
c85cbb70a1 [deps]: Lock file maintenance (#6460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 18:33:13 +00:00
github-actions[bot]
e482820201 Update Google privileged browsers list (#6452)
Co-authored-by: GitHub Actions Bot <actions@github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2026-02-02 17:59:57 +00:00
Shamim Shahrier Emon
74d45c3906 [PM-31393] Sends: UI/UX inconsistency of the password field (#6435) 2026-02-02 17:58:19 +00:00
Lucas
12eb42097c [PM-30259] Add iodéOS browser to community FIDO2 privileged list (#6298) 2026-02-02 17:34:29 +00:00
David Perez
0811d14606 PM-31603: Add toast when resetpassword succeeds (#6465) 2026-02-02 17:26:01 +00:00
Ruyut
365067e5be [PM-31583] Fix typos in authentication-related KDoc comments (#6461) 2026-02-02 15:29:31 +00:00
bw-ghapp[bot]
9652c7e049 Crowdin Pull (#6453)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-02-02 14:14:55 +00:00
bw-ghapp[bot]
6cc519bc3f Update SDK to 2.0.0-4835-5285d3fc (#6446)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-02-02 14:07:02 +00:00
aj-rosado
9f82b42e36 [BWA-182] Add mTLS support for Glide image loading (#6125)
Co-authored-by: David Perez <david@livefront.com>
2026-01-30 19:57:59 +00:00
Patrick Honkonen
5531b478d3 Add comprehensive tests for FileManagerImpl (#6425)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 19:18:53 +00:00
Ruyut
fe5b61bf25 [PM-31445] Fix minor KDoc typos and wording issues. (#6441) 2026-01-30 19:15:03 +00:00
Patrick Honkonen
92ba38c831 [PM-31446] fix:Append assetlinks.json path to DAL URLs (#6447) 2026-01-30 18:22:00 +00:00
Patrick Honkonen
675b346666 Add comprehensive tests for ExportViewModel (#6442)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 16:16:24 +00:00
Patrick Honkonen
0f087b7d15 Add comprehensive tests for AuthenticatorRepositoryImpl (#6424)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 15:41:21 +00:00
Álison Fernandes
99a6dd7647 [PM-31436] Consolidate Feature categories in release notes and add labels (#6439) 2026-01-30 14:01:08 +00:00
bw-ghapp[bot]
ea4df7dde9 Update SDK to 2.0.0-4818-c1e4bb66 (#6444)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-30 12:08:39 +00:00
Álison Fernandes
f541919d39 [PM-31292] ci: update renovate config to remove gradle group and ignore sdk updates (#6437) 2026-01-29 21:06:49 +00:00
Amy Galles
3d1f46983a use option to determine if release will be marked latest (#6417) 2026-01-29 18:41:36 +00:00
David Perez
b0084d2f1f Set cache problem to warning (#6436) 2026-01-29 16:35:10 +00:00
David Perez
0d0a5cb292 Item migration flow has been moved into a graph (#6427) 2026-01-29 15:16:02 +00:00
bw-ghapp[bot]
ebfe293c81 Update SDK to 2.0.0-4800-bed92cae (#6431)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-29 13:49:01 +00:00
Patrick Honkonen
254b2cd25b Add comprehensive tests for Import Parsers and UuidManager (#6423)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 21:55:06 +00:00
Patrick Honkonen
3d974d710c [PM-31370] Refactor stringToUri and consolidate FileManager (#6432)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:37:24 +00:00
bw-ghapp[bot]
7717a09c06 Update SDK to 2.0.0-4772-490c1be4 (#6395)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-01-28 18:55:02 +00:00
David Perez
674cff1c3c PM-31363: Fix crash caused by a duplicate ID (#6428) 2026-01-28 18:45:57 +00:00
Álison Fernandes
ca9ec45548 [PM-31343] Fix dependencies listed under Maintenance by adding a new fallback section to release.yml (#6420) 2026-01-28 14:59:34 +00:00
David Perez
009136ce1e Minor cleanup of the MigrateToMyItemsScreen (#6421) 2026-01-28 14:59:22 +00:00
David Perez
19a3697605 Remove intialization of NetworkConnectionManager from application class (#6419) 2026-01-28 14:57:10 +00:00
David Perez
954571ff4a Optimize build times (#6418) 2026-01-27 19:01:20 +00:00
David Perez
66316e4bd2 Cleanup organizations (#6391) 2026-01-27 17:28:09 +00:00
David Perez
9463cf646b Update Kotlin and associated dependencies (#6408) 2026-01-27 17:14:39 +00:00
David Perez
e81710c24f GradlewWrapper updates (#6415) 2026-01-27 17:14:24 +00:00
David Perez
71466405fa Update testing tools (#6407) 2026-01-27 15:21:52 +00:00
David Perez
618bdc7424 Update protobufs to v4.33.4 (#6414) 2026-01-27 15:21:30 +00:00
David Perez
0f05e30997 Update the Compose BOM to v2026.01.00 (#6401) 2026-01-27 15:21:13 +00:00
David Perez
006a13d5ac Update Sonarqube to v7.2.2.6593 (#6406) 2026-01-26 21:48:01 +00:00
David Perez
1d35004999 Update the Gradle Wrapper to the latest version (#6405) 2026-01-26 17:42:53 +00:00
David Perez
85249987aa Update app version name to 2026.2.0 (#6409) 2026-01-26 17:42:27 +00:00
bw-ghapp[bot]
f05cf773fb Crowdin Pull (#6412)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-01-26 14:54:34 +00:00
Patrick Honkonen
2e311b6c4a [PM-30899] Store account keys upon SSO user creation (#6384) 2026-01-23 19:51:25 +00:00
David Perez
ee5ed77bc1 Update to Junit v6.0.2 (#6402) 2026-01-23 18:43:33 +00:00
aj-rosado
04a3cd227e [PM-30644] Removing special circumstance validation from MigrateToMyItems route (#6358) 2026-01-23 17:06:31 +00:00
aj-rosado
ec28dde6d2 [PM-31081] Added snackbar when items are successfully migrated (#6394) 2026-01-23 16:51:41 +00:00
David Perez
319872ccf9 PM-29693: Add introducing archive action card to vault screen (#6390) 2026-01-23 16:50:43 +00:00
aj-rosado
9f1fad8be0 [PM-28990] Skipping vault migration on Network or Timeout error (#6393) 2026-01-23 16:06:17 +00:00
aj-rosado
0395d489c2 [PM-31069] Add OrganizationId support for Vault Migration operations (#6397) 2026-01-23 16:05:55 +00:00
David Perez
2acf429f67 PM-29696: Add action card for lapsed premium subscription (#6389) 2026-01-23 15:24:00 +00:00
David Perez
721fbbb82c PM-31162: Update copy on the snackbar for archive feature (#6399) 2026-01-23 15:07:27 +00:00
David Perez
6d198bd8c9 Update to Firebase v34.8.0 (#6396) 2026-01-23 15:07:09 +00:00
Álison Fernandes
8658f1d42c [PM-14880] ci: Address automated PR labeling workflow feedback (#6400) 2026-01-22 21:25:09 +00:00
Shamim Shahrier Emon
acc3e24d65 [PM-30664] Unlock with PIN doesn’t appear as enabled after enabling ‘Require master password on app restart’ (#6344) 2026-01-21 18:42:02 +00:00
bw-ghapp[bot]
40c8346bf7 Update SDK to 2.0.0-4676-0544ddec (#6388)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-21 17:24:18 +00:00
aj-rosado
a7badf8b0b [PM-28470] Implement revoke from organization (#6383)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-21 16:53:26 +00:00
David Perez
c52910e74a PM-31043: Add unarchive button to overflow menus (#6387) 2026-01-21 16:50:30 +00:00
David Perez
afc1ff4d7a PM-31042: Add overflow archive button (#6385) 2026-01-21 14:50:00 +00:00
bw-ghapp[bot]
8cb4fab1de Update SDK to 2.0.0-4672-b3e4ea24 (#6371)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-01-21 11:24:07 +00:00
David Perez
f79113aa7f Fix minor typos (#6386) 2026-01-20 21:49:38 +00:00
renovate[bot]
7d814df04e [deps]: Lock file maintenance (#6382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 18:45:36 +00:00
David Perez
49b208f013 PM-29697: Finish View and Edit Cipher UI for archive (#6377) 2026-01-20 18:43:14 +00:00
bw-ghapp[bot]
8d33e6660a Crowdin Pull (#6380)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-01-20 15:48:58 +00:00
David Perez
27a0f5172c Move Vault Listing Dialog clicks to VaultItemListingHandlers (#6375) 2026-01-16 19:50:26 +00:00
David Perez
3e470ebc25 PM-30868: Archive Banner on Edit Item Screen (#6367) 2026-01-16 19:48:15 +00:00
aj-rosado
eb18ca04a0 [PM-28471] Migrate individual vault to organization (#6352) 2026-01-16 19:11:43 +00:00
David Perez
759e0563a9 PM-30897: Add archive and unarchive button on Edit Cipher Screen (#6372) 2026-01-16 17:17:19 +00:00
David Perez
757f444493 PM-29694: Update archive empty state (#6369) 2026-01-15 18:29:16 +00:00
David Perez
98ba1690bf PM-30807: Add archived header to ViewItem Screen (#6362) 2026-01-15 15:45:01 +00:00
David Perez
44274a888e PM-30795: Update cipher filtering logic for archive (#6359) 2026-01-15 15:14:26 +00:00
bw-ghapp[bot]
77cc0d5fba Update SDK to 2.0.0-4524-513f18bf (#6361)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-15 15:13:40 +00:00
Álison Fernandes
026393384b [PM-30823] ci: Fix BWA Play Store publishing for rc cherry picks and update upload step names (#6360) 2026-01-15 14:42:45 +00:00
Patrick Honkonen
7daeaca63e refactor(claude): Refine reviewing-changes skill description for clarity and usage (#6366) 2026-01-15 13:59:58 +00:00
Gavin Gui
353e7e9a4e [PM-30394] PM-29960: Skip biometric prompt on Xiaomi HyperOS (#6316) 2026-01-14 16:08:57 +00:00
bw-ghapp[bot]
2d824f96f5 Update SDK to 2.0.0-4505-df9bd639 (#6355)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-14 16:00:14 +00:00
David Perez
a9b1623f8b PM-30774: Add archiving and unarchiving network requests (#6356) 2026-01-14 14:49:01 +00:00
David Perez
6d72d3a1c9 PM-30767: Add archive row to Vault Screen (#6354) 2026-01-13 21:41:45 +00:00
David Perez
f6edc19595 Remove the unused showDivider flag from BitwardenGroupItem (#6353) 2026-01-13 16:42:51 +00:00
David Perez
45125a94c2 Update archive string with noun suffix (#6351) 2026-01-13 16:38:05 +00:00
David Perez
66900f71df End subtext and end icon support to BitwardenGroupItem (#6349) 2026-01-13 15:27:32 +00:00
bw-ghapp[bot]
d12c546c9a Update SDK to 2.0.0-4498-7681828f (#6350)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-13 13:08:36 +00:00
David Perez
be365eec1c PM-30708: Add archive item navigation (#6348) 2026-01-12 21:30:04 +00:00
Álison Fernandes
d86959b375 [PM-14880] ci: Update feature labels (#6346) 2026-01-12 18:45:23 +00:00
bw-ghapp[bot]
282cce8ce0 Update SDK to 2.0.0-4479-ad9fb51d (#6345)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2026-01-12 17:00:34 +00:00
bw-ghapp[bot]
e8eaf4e68c Crowdin Pull (#6342)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-01-12 15:06:13 +00:00
Patrick Honkonen
41dfc2b6e8 Improve KDoc on StateFlowExtensions (#6338) 2026-01-09 16:38:43 +00:00
Patrick Honkonen
7bfd4b5a6c Document best practices for Clock/Time handling (#6340) 2026-01-09 14:53:38 +00:00
bw-ghapp[bot]
557b667dab Update SDK to 2.0.0-4441-c5a3b833 (#6333)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-09 14:37:39 +00:00
aj-rosado
eff4ce7abb [PM-28468] Updated validation and navigation for MigrateToMyItems (#6279)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2026-01-08 21:29:28 +00:00
David Perez
577e3c04e3 Add the Archive items feature flag (#6337) 2026-01-08 20:55:04 +00:00
David Perez
203313eb1d Improve clock usage patterns (#6336) 2026-01-08 20:54:33 +00:00
David Perez
5d308aa95f PM-30522: Add support for processing app links for Duo, WebAuthn, and SSO (#6332) 2026-01-07 19:45:04 +00:00
David Perez
c4a94cf5d1 Add concrete FlightRecorderDiskSource (#6281) 2026-01-07 19:30:53 +00:00
Lucas
5245a7a0c7 [PM-30258] Remove CalyxOS Chromium from the FIDO2 privileged list (#6297) 2026-01-07 17:13:59 +00:00
bw-ghapp[bot]
9432df6ff4 Update SDK to 2.0.0-4408-ef987b96 (#6331)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2026-01-07 16:33:15 +00:00
David Perez
461e1e1ff9 Update generated SSO uri to be typed (#6329) 2026-01-06 21:02:36 +00:00
David Perez
a8ef32ae76 Allow trailing commas in JSON (#6326) 2026-01-06 15:40:37 +00:00
Patrick Honkonen
769bfc83af [VULN-362] Move Compose tooling dependency to debugImplementation (#6327) 2026-01-06 15:25:20 +00:00
Patrick Honkonen
29d84d69f5 [PM-28271] Rename validatePin to validatePinUserKey and update SDK usage (#6323) 2026-01-05 22:08:21 +00:00
David Perez
05d003edb2 Update Firebase BOM to latest versions (#6324) 2026-01-05 21:22:59 +00:00
bw-ghapp[bot]
03562a8605 Update SDK to 2.0.0-4373-3c666766 (#6311)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2026-01-05 18:27:32 +00:00
Lucas
e6c46169fb [PM-30260] Add WebLibre to the FIDO2 privileged community list (#6299) 2026-01-05 18:24:21 +00:00
David Perez
7d4d7a25b5 Update Androidx dependencies (#6322) 2026-01-05 17:06:28 +00:00
renovate[bot]
1cb37b8458 [deps]: Lock file maintenance (#6320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 17:05:52 +00:00
David Perez
3c7b70f325 PM-30389: Allow for different auth tab schemes (#6315) 2026-01-05 16:36:17 +00:00
David Perez
9a8c504c8b Move TestHelpers to core test-fixtures module (#6314) 2026-01-05 15:57:13 +00:00
bw-ghapp[bot]
b07a92f7d6 Crowdin Pull (#6317)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2026-01-05 15:29:42 +00:00
Mick Letofsky
674cde9869 Revert review Code Triggered by labeled event (#6310) 2025-12-31 16:20:51 +00:00
Patrick Honkonen
28c9637655 [deps]: Update Google ProtoBuf dependencies (#6308) 2025-12-30 19:01:39 +00:00
bw-ghapp[bot]
2d228b8496 Update SDK to 2.0.0-4254-6c954013 (#6218)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-12-30 18:12:39 +00:00
bw-ghapp[bot]
3bc538c1f8 Crowdin Pull (#6286)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-12-30 17:58:21 +00:00
Mick Letofsky
99717ab5d5 Review Code Triggered by labeled event (#6307) 2025-12-30 16:56:06 +00:00
Álison Fernandes
d98e459129 [PM-14880] Add pull-request trigger to PR Labeling workflow and address test findings (#6305) 2025-12-30 14:39:00 +00:00
Álison Fernandes
ebed1bd3cd [PM-14880] Label updates to fido2 privileged apps lists (#6304) 2025-12-29 21:00:16 +00:00
Álison Fernandes
f4e23e85d2 [PM-14880] ci: Update labels of automated PRs; set labels for PRs created by the crowdin-pull.yml workflow (#6303) 2025-12-29 20:17:14 +00:00
Álison Fernandes
474acc05a6 [PM-14880] ci: Adds categories for automated release notes (#6302) 2025-12-29 20:16:01 +00:00
aj-rosado
87faba6824 Updated sdk to a version that fixes the password protected export issues (1.0.0-4328-km-fix-cherry-pick) (#6300) 2025-12-29 15:29:22 +00:00
renovate[bot]
89fb9c92d3 [deps]: Lock file maintenance (#6292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 15:17:45 +00:00
renovate[bot]
77a58f344d [deps]: Update actions/upload-artifact action to v6 (#6290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 15:17:08 +00:00
renovate[bot]
dda32075d0 [deps]: Update actions/checkout action to v6 (#6289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 15:16:08 +00:00
renovate[bot]
038931312d [deps]: Update actions/cache action to v5 (#6288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 15:15:11 +00:00
Patrick Honkonen
7cd0e2c176 PM-29843: Record item org migration events (#6275) 2025-12-29 14:18:10 +00:00
Álison Fernandes
0975144342 [PM-29913] ci: Fix release notes fetch failure while creating GitHub Releases (#6282) 2025-12-19 20:59:01 +00:00
Patrick Honkonen
07415844ee [PM-29947] Remove ResetMasterPassword property from token response model (#6285) 2025-12-19 15:34:48 +00:00
David Perez
913d877737 Remove flaky tests (#6278) 2025-12-18 21:47:12 +00:00
Katherine Bertelsen
c16da5090e [PM-29911] Update cron jobs to run at midnight on Sundays (#6280) 2025-12-18 14:50:32 +00:00
David Perez
b79aca7338 Move extensions to common module (#6276) 2025-12-17 16:19:20 +00:00
David Perez
7834d5bf27 PM-29827: Move FlightRecorderManager to common data module (#6274) 2025-12-16 17:37:51 +00:00
Patrick Honkonen
7c929c3713 [PM-29842] Add organization event types for item migration acceptance and rejection (#6273) 2025-12-16 15:38:16 +00:00
Patrick Honkonen
7f032a8732 PM-29824: Add bulk share ciphers network layer implementation (#6271) 2025-12-16 14:12:33 +00:00
David Perez
ef6714fa17 PM-29806: Move FlightRecorderWriter to the data module (#6270) 2025-12-15 21:43:17 +00:00
Patrick Honkonen
d09945d80b [PM-29297] Add MigrateToMyItemsScreen (#6239)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 20:49:43 +00:00
David Perez
30ce512091 PM-29442: Change 2fa field to not be a password field (#6269) 2025-12-15 18:58:44 +00:00
David Perez
bdbcd5bdc2 PM-29795: Move FileManager to data module (#6268) 2025-12-15 18:19:32 +00:00
David Perez
b4414073c7 Update Mockk and Kover (#6260) 2025-12-12 16:40:34 +00:00
David Perez
1594de39c1 Update Androidx Camera to v1.5.2 (#6259) 2025-12-12 16:39:00 +00:00
David Perez
f0c5c8f421 Update to AGP v8.13.2 (#6258) 2025-12-12 16:38:15 +00:00
bw-ghapp[bot]
2a343555bf Crowdin Pull (#6261)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-12-12 15:38:39 +00:00
David Perez
dff6a13cd7 Update OkHttp to v5.3.2 (#6257) 2025-12-11 19:33:29 +00:00
Patrick Honkonen
e415145c53 PM-29491: Implement LeaveOrganizationScreen (#6253)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 16:35:15 +00:00
Patrick Honkonen
54ea921b25 Update STYLE_AND_BEST_PRACTICES.md to clarify KDoc requirements and fix whitespace (#6256) 2025-12-11 16:18:23 +00:00
gitclonebrian
e87ffa3902 [BRE-1333] Added permissions to token generation step to limit token scope (#6171) 2025-12-10 22:36:10 +00:00
David Perez
00cded3a02 PM-1908: Push notifications for non-active accounts prompt for future sync (#6252) 2025-12-10 15:27:09 +00:00
David Perez
1503e3f769 PM-29172: Update Authenticator biometric encryption (#6240) 2025-12-10 14:54:44 +00:00
aj-rosado
6840a6c207 [PM-28836] Add AndroidManifest permission for HEADSET_CAMERA (#6251) 2025-12-10 11:09:08 +00:00
Patrick Honkonen
d32e767c62 [PM-28504] Add testharness build workflow with dynamic versioning (#6181)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 21:32:11 +00:00
aj-rosado
4a874668f2 [PM-28468] Added service methods to migration to MyItems validation (#6248) 2025-12-09 15:58:23 +00:00
David Perez
cd27fe339d Move BiometricsEncryptionManager into the AuthRepository (#6249) 2025-12-09 15:32:25 +00:00
David Perez
2eb8ad4221 PM-28355: Clear pin data on hard-logout or security stamp (#6232) 2025-12-08 16:51:18 +00:00
renovate[bot]
28db795790 [deps]: Update actions/checkout action to v6 (#6247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 14:34:31 +00:00
David Perez
8c6782dcb1 Move MissingPropertyException to common location (#6237) 2025-12-05 19:08:39 +00:00
David Perez
127809b8df Address several small lint warning throughout the app (#6233) 2025-12-05 17:47:52 +00:00
aj-rosado
ca13e615ec [PM-28442] Added feature flag for migrate myvault to myitems (#6235) 2025-12-05 16:50:30 +00:00
bw-ghapp[bot]
5e3e8a04aa Crowdin Pull (#6234)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-12-05 14:53:29 +00:00
Patrick Honkonen
8077895eb8 Update ZXing library version (#6230) 2025-12-04 19:52:24 +00:00
Patrick Honkonen
33e9313c6c Update SonarQube plugin version (#6231) 2025-12-04 19:19:54 +00:00
Patrick Honkonen
593bfbf8cf [PM-28352] Add logging to Credential Manager and Origin Manager flows (#6229) 2025-12-04 18:22:45 +00:00
Patrick Honkonen
4905358adb [PM-28467] Add revisionDate to policy JSON model (#6228) 2025-12-04 18:22:23 +00:00
renovate[bot]
02733f785b [deps]: Lock file maintenance (#6197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 18:01:23 +00:00
Patrick Honkonen
8baa4bf041 [PM-29096] Update Fastlane and Gemfile dependencies (#6216) 2025-12-04 16:20:21 +00:00
David Perez
4d20453d0f PM-25632: Ensure that we use lowercase email addresses when creating a fingerprint (#6227) 2025-12-04 15:34:17 +00:00
David Perez
4b951a1df2 PM-28634: Update Autofill terms to support other languages better (#6226) 2025-12-04 14:55:23 +00:00
André Bispo
9349b235bc [PM-27290] Remove password unlock method (#6176) 2025-12-04 10:53:40 +00:00
Patrick Honkonen
e9ab5f2def [PM-29097] Fix privacy statement alignment in landscape mode (#6225) 2025-12-03 22:10:58 +00:00
David Perez
3bef282426 Update Androidx dependencies to the latest versions (#6224) 2025-12-03 21:25:50 +00:00
Patrick Honkonen
e1bb3a4b5d [PM-27118] Restrict Credential Exchange import based on Personal Ownership policy (#6220) 2025-12-03 20:15:53 +00:00
David Perez
1904c4ffb9 PM-28522: Update the LoginWithDevice ui (#6221) 2025-12-03 19:41:34 +00:00
aj-rosado
26e7178300 [PM-28835] Added validations to prevent duplicate press on buttons (#6209) 2025-12-03 17:46:03 +00:00
David Perez
2c01abda46 [deps]: Update ksp (#6217) 2025-12-02 18:20:51 +00:00
bw-ghapp[bot]
b86cbfcd87 Update SDK to 1.0.0-3958-7f09fd2f (#6213)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-12-02 14:57:18 +00:00
aj-rosado
3f303d3f39 [BWA-179] Added clarification of functionality on Authenticator's ExportScreen (#6190) 2025-12-02 10:01:00 +00:00
David Perez
ca7a65fc95 PM-28522: Update the Login With Device Screen (#6184) 2025-12-01 16:25:30 +00:00
bw-ghapp[bot]
f02b374e98 Update SDK to 1.0.0-3928-2cca3d46 (#6205)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-12-01 14:26:56 +00:00
bw-ghapp[bot]
1a90860080 Crowdin Pull (#6206)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-12-01 14:16:24 +00:00
Patrick Honkonen
adf83cd315 [PM-28157] Revert "Add string extension to prefix URIs with www" (#6192)
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
2025-12-01 14:12:14 +00:00
Patrick Honkonen
489c0ea8d6 Enhance code review skill documentation with TOCs and missing severity categories (#6186)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-27 19:31:25 +00:00
bw-ghapp[bot]
9831358a8b Update SDK to 1.0.0-3908-4b0d1280 (#6201)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-11-26 23:55:56 +00:00
bw-ghapp[bot]
8bdbccd8de Update SDK to 1.0.0-3896-f75a58cd (#6198)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-11-25 00:57:36 +00:00
bw-ghapp[bot]
a75d904317 Update SDK to 1.0.0-3674-c60a5d79 (#6064)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-11-21 22:07:08 +00:00
Patrick Honkonen
a395f28eba [PM-28086] Add testharness for Credential Manager and Autofill testing (#6159)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-21 19:56:24 +00:00
bw-ghapp[bot]
53e358d7b3 Crowdin Pull (#6189)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-11-21 13:16:39 +00:00
David Perez
663eb3641f PM-28545: Remove the compatibility mode toggle from the Autofill screen (#6188) 2025-11-21 13:15:40 +00:00
David Perez
ab305b2631 PM-28525: Update the LoginApprovalScreen ui (#6187) 2025-11-20 22:34:36 +00:00
aj-rosado
946b0784e0 [PM-27816] Not clearing the fingerprint on requests that don't return fingerprint on LoginWithDevice (#6185) 2025-11-20 19:57:04 +00:00
Patrick Honkonen
167a46a073 [PM-21391] Remove debug credential provider configuration (#6182)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 19:15:35 +00:00
Patrick Honkonen
7b491d3c3c [PM-28157] Add string extension to prefix URIs with www (#6183)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 15:32:54 +00:00
David Perez
7918abdccf PM-28492: Replace Authenticator Toasts with Snackbars (#6180) 2025-11-19 22:04:28 +00:00
Nailik
5ec0a1986d [PM-24148] add credential manager provider for create passwords (#5579)
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-11-19 22:00:53 +00:00
renovate[bot]
839e9e8a1a [deps]: Update actions/checkout action to v5 (#6144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 17:38:13 +00:00
David Perez
979237b751 PM-28408: Update CameraPreview composable to address flakey test (#6178) 2025-11-19 15:43:02 +00:00
Dev Sharma
621f97d161 [PM-27869] fix/[PM-26241] : draw out keyboard on talkback click (#6129) 2025-11-18 18:51:15 +00:00
André Bispo
d81b0005ee [PM-27150] React to device changes on device screen unlock method (#6103) 2025-11-18 16:02:35 +00:00
David Perez
794b27a750 Update logic for handling the pin protected user key (#6169) 2025-11-18 15:32:39 +00:00
David Perez
169b21cfdb PM-28053: Ensure any exception thrown during re-auth is an IO exception (#6175) 2025-11-17 20:24:48 +00:00
Álison Fernandes
4623a4f079 [PM-14880] ci: Add automated PR labelling based on file paths and title patterns (#6157) 2025-11-17 20:19:12 +00:00
bw-ghapp[bot]
21afa81322 Crowdin Pull (#6167)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-11-14 15:34:06 +00:00
David Perez
55c7ab4cee Update RTL transitions to go the correct direction (#6166) 2025-11-13 16:19:07 +00:00
Mick Letofsky
7a40bfe522 [PM-27181] - Grant additional permissions for review code (#6165) 2025-11-13 13:55:31 +00:00
Álison Fernandes
0c234ee0aa [PM-28029] ci: add missing permission to fdroid job to fix f-droid build failures (#6163) 2025-11-13 00:28:25 +00:00
David Perez
7b0b93a204 Update Autofill to detect url bar webDomain for certain browsers (#6141) 2025-11-12 22:04:03 +00:00
David Perez
473416c1b4 Update App version name to '2025.11.1' (#6162) 2025-11-12 22:03:36 +00:00
renovate[bot]
f3646790e3 [deps]: Update actions/upload-artifact action to v5 (#6145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 17:26:16 +00:00
David Perez
35b87f4390 Replace blank account name with null (#6158) 2025-11-12 15:11:00 +00:00
David Perez
7120eefc94 PM-28056: Consolidate IntentManager extensions (#6156) 2025-11-11 19:48:58 +00:00
aj-rosado
5eb56cafaa [PM-27119] Prevent import card data when ITEM_RESTRICT_TYPES policy is active (#6123) 2025-11-11 19:24:38 +00:00
renovate[bot]
b2d94fae40 [deps]: Update gradle minor (#6143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 15:21:39 +00:00
Álison Fernandes
ad748eef7f [PM-28029] Address Sonar Cloud and Linter errors (#6151) 2025-11-11 14:16:55 +00:00
David Perez
8010e8d6c3 All tests should assert if a value is displayed (#6153) 2025-11-11 14:16:22 +00:00
Álison Fernandes
7a6a493f24 [PM-28041] Remove SDK Update PR changelog list size limit (#6152) 2025-11-10 21:58:01 +00:00
Álison Fernandes
4032d2bb5c [PM-27901] Add f-droid fastlane metadata (#6134)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-11-10 20:53:40 +00:00
Patrick Honkonen
e2c9aae9c1 Bump JUnit from 6.0.0 to 6.0.1 (#6149) 2025-11-10 16:48:33 +00:00
Patrick Honkonen
56566b958c Update OkHttp to version 5.3.0 (#6148) 2025-11-10 15:42:51 +00:00
Patrick Honkonen
06bf603ec8 Update Firebase BoM to 34.5.0 (#6150) 2025-11-10 15:06:51 +00:00
Patrick Honkonen
79d6da0a61 Exclude Bitwarden Android SDK from Renovate auto-updates (#6147) 2025-11-10 14:23:58 +00:00
renovate[bot]
2d2ea9acee [deps]: Lock file maintenance (#6146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 13:49:13 +00:00
David Perez
05d6fe1ba1 Fix a test that was not running (#6140) 2025-11-07 21:34:18 +00:00
David Perez
1024f77ddf Clarify package id parsing and AutofillView creation (#6138) 2025-11-07 18:07:20 +00:00
bw-ghapp[bot]
50e50eb08c Crowdin Pull (#6135)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-11-07 15:28:24 +00:00
Patrick Honkonen
bd98df6eb9 PM-27902: Logout user after successful master password reset (#6133)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 14:33:07 +00:00
Patrick Honkonen
9baec6e6a5 Enforce strict brevity in reviewing-changes skill output (#6131)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 07:18:07 +00:00
David Perez
efd15b027c PM-27845: Move SnackbarRelayManager to ui module (#6127) 2025-11-06 18:50:06 +00:00
aj-rosado
b715a51188 [PM-27806] Reverted changes to order of StorePolicies after sync (#6130) 2025-11-06 16:54:22 +00:00
Patrick Honkonen
94ed32790f [PM-27752] Add certificate signature verification to AuthenticatorBridge (#6126)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 13:15:26 +00:00
Patrick Honkonen
dca97e0c8e Refine reviewing-changes skill to eliminate verbosity and praise (#6128)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 21:15:40 +00:00
David Perez
aceb96d18f PM-27846: Move DispatcherManager to core module (#6124) 2025-11-05 18:26:44 +00:00
Álison Fernandes
510072b34f [PM-27834] Use Authenticator Bridge as a project reference (#5793) 2025-11-05 14:46:35 +00:00
Patrick Honkonen
7324be04f4 Reduce verbosity in reviewing-changes skill for clean PRs (#6121)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 07:05:21 +00:00
David Perez
bfbe47f48f PM-27597: Update Yubikey illustration to match the rest of the app (#6087) 2025-11-04 22:15:40 +00:00
David Perez
2bb06063c7 PM-27817: Consolidate totp parsing with TotpUriUtils (#6122) 2025-11-04 21:51:01 +00:00
David Perez
ed47ff4d18 PM-27771: Improve TOTP parsing (#6119) 2025-11-04 20:17:03 +00:00
David Perez
448ba97ae2 PM-24277: Add language selector to Authenticator (#6120) 2025-11-04 19:09:20 +00:00
David Perez
14e833247d PM-27770: Update error parsing when creating or updating a cipher (#6118) 2025-11-04 19:08:10 +00:00
David Perez
4b7fcdb6ea PM-27756: Create common ExitManager (#6117) 2025-11-04 18:08:50 +00:00
André Bispo
0959284e6f [PM-26736] Fix push notification logout reason serialization (#6116) 2025-11-03 20:33:56 +00:00
David Perez
1f24ca7de1 PM-27755: Create a common LocalQrCodeAnalyzer for both apps (#6115) 2025-11-03 20:13:39 +00:00
André Bispo
dc79176274 [PM-27657] KDF silent update with MasterPasswordUnlock data (#6113) 2025-11-03 19:51:26 +00:00
Patrick Honkonen
a631d6822a [PM-24971] Sanitize passkey attestation options from AliExpress (#6106) 2025-11-03 18:15:00 +00:00
David Perez
845a5dec22 PM-27703: Update Authenticator navigation (#6109) 2025-11-03 16:28:23 +00:00
renovate[bot]
b1195b5f46 [deps]: Update org.sonarqube to v7 (#6082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 15:07:41 +00:00
Patrick Honkonen
8de3a07715 Optimize reviewing-changes skill (#6099)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-11-01 07:28:18 +00:00
David Perez
9c4bd2ee14 Remove unused xml colors from Authenticator (#6108) 2025-10-31 19:59:10 +00:00
David Perez
6dad8c4def PM-27705: Enable filterTouchesWhenObscured in Authenticator for security (#6105) 2025-10-31 19:16:13 +00:00
David Perez
bc74337eae Add Push chevron to Block autofill button (#6102) 2025-10-31 17:37:04 +00:00
bw-ghapp[bot]
d86443c6dd Crowdin Pull (#6101)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-10-31 14:04:47 +00:00
aj-rosado
d07b119802 [PM-27120] cxp hide user account when remove individual export is enabled (#6089) 2025-10-31 10:08:24 +00:00
David Perez
dbf2e9f68a Update Readme compatibility docs (#6100) 2025-10-30 21:50:44 +00:00
David Perez
9ddfd376a9 Fix topAppBar flicker when text is long (#6098) 2025-10-30 20:32:13 +00:00
Patrick Honkonen
dd1dbd0b97 Update androidx.credentials to 1.6.0-beta03 (#6097) 2025-10-30 17:59:28 +00:00
renovate[bot]
f6be363e98 [deps]: Update com.google.devtools.ksp to v2.3.0 (#6080)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-10-30 14:48:30 +00:00
David Perez
600744538d Fix deprecation within the app (#6096) 2025-10-29 21:02:03 +00:00
David Perez
de33ba021b Update the Google Protobuf library (#6095) 2025-10-29 21:01:35 +00:00
David Perez
290f59441f Update Kotlin, ksp, and kover to the latest versions (#6094) 2025-10-29 19:27:06 +00:00
David Perez
94c51cacf9 Update Androidx dependencies (#6093) 2025-10-29 16:53:48 +00:00
Dev Sharma
6f27642a30 [PM-27589] [PM-27158] fix : Sub folders always show 0 items (#6092) 2025-10-29 15:54:15 +00:00
Dev Sharma
2ad3014da2 [PM-27516] [PM 27157] Custom text field edit multiline fix (#6088) 2025-10-29 15:44:44 +00:00
renovate[bot]
e6dc8e02f8 [deps]: Lock file maintenance (#6083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 21:34:02 +00:00
David Perez
c16d31fb33 PM-27494: Update custom vault timeout UI (#6085) 2025-10-27 21:13:36 +00:00
David Perez
43d7b84d0a PM-27136: Update Snackbar font when there is no header (#6086) 2025-10-27 19:53:31 +00:00
André Bispo
c0f8307361 [PM-26420] FlightRecorder vault unlock method (#6084) 2025-10-27 17:55:51 +00:00
mpbw2
064a98f86b [PM-22157] independent version names in build workflows (#6074) 2025-10-27 17:51:56 +00:00
David Perez
e3b111c383 PM-19302: Add support for a typed vault timeout policy (#6078) 2025-10-27 16:28:01 +00:00
Mick Letofsky
52304a266e Implement reusable Claude code review workflow (#6072)
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-10-27 14:19:37 +00:00
David Perez
51c23ec464 Minor clean up for the Account Security Screen (#6076) 2025-10-24 15:55:50 +00:00
André Bispo
7d7951d4ca [PM-27176] Switch to using SDK's init crypto with MasterPasswordUnlock (#6073) 2025-10-24 13:56:44 +00:00
bw-ghapp[bot]
78b1676745 Crowdin Pull (#6077)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-10-24 13:20:49 +00:00
aj-rosado
be27c76bd3 [PM-27092] Changing screen capture flow from event based to state based on Authenticator (#6062) 2025-10-24 09:26:53 +00:00
David Perez
38bdda0a41 Create reusable supporting content composable (#6075) 2025-10-23 20:07:04 +00:00
David Perez
c61fec176a PM-27271: Update selection button disabled state (#6071) 2025-10-23 16:23:06 +00:00
Patrick Honkonen
bb11b17823 [PM-26810] Clear password input after successful OTP verification (#6070) 2025-10-23 13:09:29 +00:00
David Perez
562b48d689 Fix TopAppBar height for multiline titles (#6069) 2025-10-22 21:09:28 +00:00
Patrick Honkonen
c3496ca60f [PM-26810] Remove loading dialog flicker on vault data updates (#6068) 2025-10-22 20:30:48 +00:00
Nailik
a8f8450ec9 [PM-27088] fix unit test execution (#6048) 2025-10-22 20:27:59 +00:00
David Perez
47628a6da2 Remove night-mode icon variants where possible (#6066) 2025-10-22 20:06:13 +00:00
David Perez
5a540a3460 PM-27263: Add enum for Vault Timeout Policy actions (#6067) 2025-10-22 20:05:48 +00:00
David Perez
92cfce1224 PM-27202: Update ItemListingScreen layout for improved spacing (#6065) 2025-10-22 14:42:35 +00:00
David Perez
4597337500 PM-27210: Add dynamic color support to Authenticator (#6063) 2025-10-22 14:42:18 +00:00
aj-rosado
e610a7541d [PM-27001] Skip account selection only one exists on cxp flow (#6055) 2025-10-22 09:08:35 +00:00
David Perez
ae4b398258 PM-27153: Update copy in Authenticator app (#6061) 2025-10-21 16:06:08 +00:00
David Perez
0482f9eb4d Update drawable names with consistent prefixes (#6060) 2025-10-21 15:54:31 +00:00
André Bispo
9f4bd70c8d [PM-26420] Add flight recorder logs for vault unlock method and PIN migration (#6052) 2025-10-20 22:29:10 +00:00
David Perez
9874aad65a PM-27149: Update empty vault illustration (#6059) 2025-10-20 21:46:31 +00:00
David Perez
97bb93c18e PM-27136: Replace FirstTimeSyncSnackbarHost with BitwardenSnackbarHost (#6058) 2025-10-20 20:42:47 +00:00
Patrick Honkonen
31e7e05eda [PM-27130] Update alert (Snackbar) color to inverseSurface in dynamic color scheme (#6057) 2025-10-20 18:15:03 +00:00
André Bispo
afeeb494da [PM-23290] Migrate PIN unlock keys to PinProtectedUserKeyEnvelope (#6024) 2025-10-20 17:31:12 +00:00
aj-rosado
d5912a5dc3 [PM-26986] Hide select other account button if user has no other account (#6041) 2025-10-20 16:02:43 +00:00
bw-ghapp[bot]
13fa8a1ed0 Update SDK to 1.0.0-3436-2a00b727 (#6042)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-10-20 15:55:31 +00:00
David Perez
5a145ee163 Update OkHttp to latest version (#6054) 2025-10-20 15:35:09 +00:00
celenityy
74b9a12e19 [PM-27076] Add support for IronFox Nightly (#6046)
Signed-off-by: celenity <celenity@celenity.dev>
2025-10-17 13:34:29 +00:00
David Perez
71e830bb09 PM-26912: Update copy for authenticator security (#6045)
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-10-17 13:27:19 +00:00
David Perez
8f3f1fa3ba PM-27071: Add overflow menu to authenticator search (#6044) 2025-10-17 12:58:17 +00:00
bw-ghapp[bot]
9bd35ccca5 Crowdin Pull (#6047)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-10-17 12:57:56 +00:00
Patrick Honkonen
74aa0a78ec [PM-26810] Add OTP support to VerifyPasswordScreen (#6034) 2025-10-16 21:02:52 +00:00
David Perez
ae3470c598 Fix flaky test (#6043) 2025-10-16 19:18:58 +00:00
André Bispo
a70b2172cb [PM-26736] Prevent logout notification on KDF change (#6038) 2025-10-16 18:54:56 +00:00
David Perez
714f7cfadc PM-27046: Add overflow to Authenticator (#6039) 2025-10-16 18:15:41 +00:00
Amy Galles
53d04375b1 Fix workflow name and permissions (#6040) 2025-10-16 18:06:30 +00:00
aj-rosado
3ace095b86 [PM-26909] Implement screen capture toggle authenticator (#6033) 2025-10-16 15:51:54 +00:00
David Perez
8a90d77fd7 Update Item listing and search screens to user immutable lists (#6037) 2025-10-16 15:43:52 +00:00
bw-ghapp[bot]
4b96007a77 Update SDK to 1.0.0-3430-fc75b903 (#6036)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-10-16 10:54:23 +00:00
David Perez
03df341a1e Consolidate the VaultVerificationCodeItems (#6035) 2025-10-15 20:43:30 +00:00
André Bispo
d966423087 [PM-23280] Use masterPasswordUnlock KDF settings on vault unlock (#6026) 2025-10-15 20:01:19 +00:00
Patrick Honkonen
f7cbcd21ec Expand supported credential types for import (#6030) 2025-10-15 13:49:20 +00:00
bw-ghapp[bot]
188ddf98f4 Update SDK to 1.0.0-3404-8b95ae6e (#6021)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-10-15 13:31:32 +00:00
aj-rosado
b8f4129691 [PM-26395] Hide "my items" collection when item is assigned to other collection (#6018) 2025-10-15 10:24:31 +00:00
bw-ghapp[bot]
b8482de96c Crowdin Pull (#6031)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-10-14 21:41:59 +00:00
Amy Galles
9e860008e8 [BRE-1194] temporarily enable hourly checks for github release (#5895)
Co-authored-by: Andy Pixley <3723676+pixman20@users.noreply.github.com>
2025-10-14 21:33:39 +00:00
Patrick Honkonen
af737b3f07 [PM-26803] Show empty state when no items are available for export (#6023) 2025-10-14 20:01:17 +00:00
David Perez
5b5176db40 PM-26910: Minor UI updates for the Authenticator (#6028) 2025-10-14 19:59:08 +00:00
David Perez
e7365b355f Common camera UI (#6027) 2025-10-14 19:47:25 +00:00
Patrick Honkonen
433b3b6fb0 Add optional buttons to BitwardenEmptyContent (#6022) 2025-10-14 15:17:35 +00:00
David Perez
318307c377 Add navigation chevron for Import Items button (#6020) 2025-10-13 20:48:41 +00:00
Patrick Honkonen
912eba14d6 [PM-26802] Update button text for Import items (#6019) 2025-10-13 18:19:49 +00:00
bw-ghapp[bot]
837dd27106 Update SDK to 1.0.0-3390-a0531e84 (#6013)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-10-13 17:55:39 +00:00
renovate[bot]
a75e938070 [deps]: Update gradle/actions action to v5 (#6010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 17:54:56 +00:00
Patrick Honkonen
054afab2cf [PM-26804] Clear password input after verification in VerifyPasswordViewModel (#6017) 2025-10-13 17:53:32 +00:00
bw-ghapp[bot]
c015c8aa43 Crowdin Pull (#6001)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-10-13 17:47:06 +00:00
David Perez
4161020e6c Fix plurals issue for Crowdin (#6016) 2025-10-13 16:02:51 +00:00
David Perez
5543bc6ab5 Add logging to UI module for Auth Tabs (#6015) 2025-10-13 15:52:58 +00:00
Patrick Honkonen
5cdee938bf Correct environment variable names in build workflow (#6008) 2025-10-13 15:18:21 +00:00
David Perez
62f76a4f8b Fix statusbar color when display is turned off (#6006) 2025-10-13 14:43:15 +00:00
renovate[bot]
7ea87505a4 [deps]: Lock file maintenance (#6011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 14:15:44 +00:00
David Perez
c6f132d5f7 PM-26575: Add AuthTab support for WebAuthN, Duo, and SSO (#6002) 2025-10-10 21:38:31 +00:00
David Perez
0604d15d7d PM-26560: Fix cross-origin autofill issues (#5977) 2025-10-10 21:06:59 +00:00
bw-ghapp[bot]
5706ca2ba3 Update SDK to 1.0.0-3367-cc36132b (#5981)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-10-10 19:34:37 +00:00
David Perez
de8c344b46 Update Compose BOM to v2025.10.00 (#6007) 2025-10-10 18:19:04 +00:00
David Perez
957460f403 Update JUnit and Mockk test dependencies (#6005) 2025-10-10 17:59:42 +00:00
Matt Andreko
2b88743bea Remove quotes in fastlane bundle commands (#6003) 2025-10-10 17:08:39 +00:00
David Perez
5243ed27d3 Update Firebase BOM and Google Services (#6004) 2025-10-10 15:35:13 +00:00
aj-rosado
a7bbb81b31 [PM-24258] Building a specific Fido2AttestationResponse to work with Binance (#5986) 2025-10-10 14:07:52 +00:00
Patrick Honkonen
2d2b740ae1 [#5997] Allow underscores in Block Autofill URI patterns (#6000) 2025-10-09 17:58:24 +00:00
Matt Andreko
81fa635430 Implement Zizmor workflow scanner (#5857) 2025-10-09 16:49:10 +00:00
David Perez
a1cb948257 [deps]: Update androidx.camera to v1.5.1 (#5999) 2025-10-09 16:03:58 +00:00
Patrick Honkonen
5bb7abbf5a Remove logging in CredentialExchangeCompletionManager (#5998) 2025-10-09 14:23:49 +00:00
André Bispo
d98ff6478f [PM-23278] Upgrade user KDF settings to minimums (#5955)
Co-authored-by: David Perez <david@livefront.com>
2025-10-09 07:49:22 +00:00
David Perez
44c373a354 Ensure the fab is dismissed when clicking on the lower portion of the content (#5996) 2025-10-08 22:06:02 +00:00
Patrick Honkonen
3d493bb9d0 [PM-26716] Validate credential exchange request (#5994) 2025-10-08 20:56:47 +00:00
Patrick Honkonen
07b8115d7a [PM-26718] Move Credential Exchange intent filter to main manifest (#5995) 2025-10-08 20:10:36 +00:00
David Perez
9ced8647a3 Update Crowdin plurals (#5991) 2025-10-08 20:09:47 +00:00
David Perez
b3c3365b5a [deps]: Update androidx.room to v2.8.2 (#5993) 2025-10-08 20:07:59 +00:00
David Perez
266c16958d [deps]: Update com.google.devtools.ksp to v2.2.20-2.0.4 (#5992) 2025-10-08 20:07:29 +00:00
David Perez
340b4f25f7 Add tests for ShareManager (#5990) 2025-10-08 19:48:04 +00:00
David Perez
572d3357ee Simplify the BitwardenExpandableFloatingActionButton (#5989) 2025-10-08 18:31:49 +00:00
Patrick Honkonen
3a4f1d719f [PM-26315] Register/unregister for CXP export based on feature flag (#5948) 2025-10-08 18:00:50 +00:00
David Perez
bebf94796c PM-20593: sync-org-keys notification should allow token to be refreshed on next request (#5988) 2025-10-08 15:32:39 +00:00
David Perez
10a92dd2a3 PM-26689: Separate share logic from IntentManager (#5987) 2025-10-08 15:15:19 +00:00
David Perez
d306813d1f The QrCodeScanScreen should always be in dark mode (#5983) 2025-10-07 20:31:44 +00:00
David Perez
97c4cd705b PM-25908: Do not use network error message from 401 (#5984) 2025-10-07 20:31:21 +00:00
Patrick Honkonen
9fee973563 Improve CXF message handling (#5982) 2025-10-07 18:28:07 +00:00
David Perez
202dd65229 PM-26579: Remove duplicated BitwardenScaffold from ItemListingScreen (#5978) 2025-10-07 17:04:32 +00:00
David Perez
7849bbbb0a PM-26594: Move the QrCodeAnalyzer to the UI module (#5980) 2025-10-07 17:04:16 +00:00
David Perez
cd9c7f98e7 PM-26358: Integrate the token auth logic with the SDK (#5967) 2025-10-07 16:49:57 +00:00
bw-ghapp[bot]
0c9530472f Update SDK to 1.0.0-3309-9574bd00 (#5979)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-10-07 13:40:19 +00:00
David Perez
2636a4f93a Update Authenticator UI to match Password Manager style (#5969) 2025-10-06 14:59:46 +00:00
bw-ghapp[bot]
ca474b272a Update SDK to 1.0.0-3293-ae9b8b52 (#5975)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-10-06 14:54:29 +00:00
Patrick Honkonen
acc9113f9a [PM-26355] Improve SelectAccountScreen state handling (#5965) 2025-10-02 21:05:08 +00:00
David Perez
2eb829a25b [deps]: Update org.sonarqube to v6.3.1.5724 (#5973) 2025-10-02 20:51:02 +00:00
Álison Fernandes
04a1d4118f Update renovate.json to exclude com.github.bumptech.glide from gradle-minor group (#5974) 2025-10-02 20:39:47 +00:00
David Perez
9f63cede11 Update UI elements for common use in Authenticator (#5971) 2025-10-02 18:37:17 +00:00
David Perez
a93037d63e PM-26445: Common Debug menu components (#5970) 2025-10-02 17:32:22 +00:00
Patrick Honkonen
4e57f306d3 [PM-26330] Correct owner data when individual vault is disabled (#5968) 2025-10-02 15:56:50 +00:00
André Bispo
1638a20bf0 [PM-23280] Save MasterPasswordUnlockData to local state (#5944) 2025-10-02 14:48:28 +00:00
bw-ghapp[bot]
874edfad69 Update SDK to 1.0.0-3194-9947387b (#5938)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Hinton <hinton@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-10-01 17:15:23 +00:00
David Perez
0469731fba Update Kover to v0.9.2 (#5966) 2025-10-01 17:08:54 +00:00
David Perez
0abfa5bb97 Update Androidx Camera to v1.5.0 (#5896) 2025-10-01 17:08:10 +00:00
aj-rosado
13e6728d46 [PM-17870] Always include clientExtensionResults in Fido2AttestationResponse (#5964) 2025-10-01 13:58:57 +00:00
David Perez
116bfd6351 PM-26312: Add browser integration help link (#5963) 2025-09-30 17:47:43 +00:00
David Perez
6ca8a39355 Update Guava to v33.5.0 (#5962) 2025-09-30 17:20:31 +00:00
David Perez
24a54ce214 Update hilt to v2.57.2 (#5961) 2025-09-30 17:20:15 +00:00
David Perez
8d76ef50d3 Firebase BOM update (#5960) 2025-09-30 17:19:59 +00:00
David Perez
22114d588a Update AndroidX libraries (#5959) 2025-09-29 21:39:52 +00:00
Patrick Honkonen
81245cf3e5 [PM-26111] Implement Review Export Screen and Navigation (#5946) 2025-09-29 21:12:09 +00:00
aj-rosado
fec6479f6a [PM-25452] Dont show move to organization when user has no orgs (#5862) 2025-09-29 20:01:32 +00:00
David Perez
a02a84ee08 PM-25642: Force sync or clear last sync time on sync notification (#5958) 2025-09-29 19:45:56 +00:00
Tyler
df63bb4b6c BRE-1158 Dockerfiles shared ownership (#5902) 2025-09-29 19:23:11 +00:00
David Perez
2a134c619d Update the Compose BOM (#5957) 2025-09-29 19:21:36 +00:00
Patrick Honkonen
5c5bd25d16 [PM-26094] Update Credential Manager library and remove stubs (#5947) 2025-09-29 18:41:35 +00:00
David Perez
2363b0d619 PM-26303: Remoe the 'Exit' button from the VaultScreen overflow menu (#5956) 2025-09-29 16:35:25 +00:00
David Perez
f0946e05d5 Fully extract more sync logic into the VaultSyncManager (#5912) 2025-09-29 16:35:00 +00:00
renovate[bot]
24ccebd822 [deps]: Lock file maintenance (#5954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 13:16:29 +00:00
David Perez
fd555e92d3 Commonize minor UI utility functions (#5945) 2025-09-26 20:34:25 +00:00
David Perez
eab2c17614 PM-26187: Add autofill help call-to-action (#5942) 2025-09-26 19:42:51 +00:00
David Perez
617be1fd95 PM-26181: Minor clean up and adjustments for browser autofill integration (#5941) 2025-09-26 15:10:31 +00:00
David Perez
d5d4caea62 PM-23292: Migrate toasts to snackbars (#5940) 2025-09-26 15:09:35 +00:00
Patrick Honkonen
7bf4acbb28 [PM-26110] Add verify password screen for item export (#5935) 2025-09-26 14:57:59 +00:00
André Bispo
2694138aa1 [PM-20977] Handle new sdk exception type. (#5937) 2025-09-26 14:47:21 +00:00
David Perez
d2645863ea PM-26161: Add badging for browser autofill (#5939) 2025-09-25 18:01:14 +00:00
Patrick Honkonen
3edd5bd852 [PM-26095] Add account selection screen for Credential Exchange (#5932) 2025-09-24 19:53:40 +00:00
David Perez
4cd5a1ed56 PM-26025: Add browser autofill screen for onboarding flow (#5931) 2025-09-24 19:50:13 +00:00
David Perez
c122f83fa6 Update onboarding secondary buttons to match designs (#5936) 2025-09-24 19:10:07 +00:00
bw-ghapp[bot]
b558d70703 Update SDK to 1.0.0-3175-c9758478 (#5922)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-24 17:52:04 +00:00
David Perez
89ad7818f9 Minor design tweaks for action cards (#5934) 2025-09-24 17:17:46 +00:00
David Perez
e91ba77105 PM-26151: Disable continue button for Autofill onboarding flow when autofill is disabled (#5933) 2025-09-24 16:55:50 +00:00
Patrick Honkonen
cc685b2307 [PM-26112] Handle Credential Exchange export requests (#5928) 2025-09-23 21:41:58 +00:00
David Perez
d14fba0c01 Remove unnecessary quotes (#5929) 2025-09-23 20:27:38 +00:00
Patrick Honkonen
e965134697 Update Credential Provider Events APIs (#5926) 2025-09-23 18:58:28 +00:00
David Perez
df34db52e4 PM-26106: Update quotes accross all strings (#5924) 2025-09-23 18:20:57 +00:00
David Perez
cf5d208516 Display the CipherKeyEncryption flag in debug menu (#5923) 2025-09-23 16:04:36 +00:00
André Bispo
d74040e7b9 [PM-25933] Replace SDK call updatePassword (#5916) 2025-09-23 15:11:07 +00:00
Patrick Honkonen
8a2bcfade8 [PM-25825] Add ImportItems navigation (#5915)
Co-authored-by: David Perez <david@livefront.com>
2025-09-22 21:33:08 +00:00
bw-ghapp[bot]
bc1dd730ec Update SDK to 1.0.0-3165-92bb5c30 (#5920)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-09-22 20:32:27 +00:00
David Perez
fa5053b5cc Add empty state for debug menu without feature flags (#5918) 2025-09-22 20:30:25 +00:00
bw-ghapp[bot]
ad46d8d7c0 Update SDK to 1.0.0-3157-1ca5a589 (#5917)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-09-22 15:43:16 +00:00
David Perez
98530ed33d PM-26027: Remove the UserManagedPrivilegedApps feature flag (#5914) 2025-09-19 20:09:54 +00:00
David Perez
e57af949fc PM-26026: save layout state through config change (#5913) 2025-09-19 19:03:40 +00:00
bw-ghapp[bot]
6f6aacabfb Update SDK to 1.0.0-3101-0eba924a (#5893)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-19 16:21:39 +00:00
David Perez
b0e0b44671 Pm 25258 browser autofill dialog (#5907) 2025-09-19 16:20:16 +00:00
David Perez
d53f3f313c Refactor Folder logic into FolderManager (#5904) 2025-09-19 15:37:31 +00:00
David Perez
4f244c52fa PM-25908: Process 400 responses from verification code APIs (#5900) 2025-09-19 15:29:28 +00:00
Patrick Honkonen
b4a31764c4 [PM-25824] Add "Import items" screen (#5906) 2025-09-19 13:59:26 +00:00
bw-ghapp[bot]
f4569cef2b Crowdin Pull (#5908)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-09-19 13:54:27 +00:00
Patrick Honkonen
b4926b72d9 Update registerExport to return RegisterExportResponse (#5903) 2025-09-18 14:37:14 +00:00
Patrick Honkonen
0f899df83c [PM-25826] Update folderRelationships type for cipher import (#5885) 2025-09-17 21:49:16 +00:00
Patrick Honkonen
ff03f49f43 [PM-25912] Remove ImportCredentialsRequest (#5901) 2025-09-17 20:42:42 +00:00
David Perez
2756bd9fde Refactor cipher logic into CipherManager (#5898) 2025-09-17 19:51:44 +00:00
Patrick Honkonen
a39f83349f Move NativeLibraryManager to data module (#5899) 2025-09-17 19:21:37 +00:00
Patrick Honkonen
7d3ed2af88 [PM-25822] Add ImportItemsViewModel and related strings (#5882) 2025-09-17 17:58:22 +00:00
David Perez
8de465381e Refactor Send logic into SendManager (#5892) 2025-09-17 14:37:14 +00:00
Patrick Honkonen
f22f4399be [PM-25664] Add CredentialExchangeImportManager for CXF payload import (#5872) 2025-09-16 21:30:24 +00:00
David Perez
766e6b1bb9 Update resources to use LocalResources (#5894) 2025-09-16 21:01:45 +00:00
David Perez
0fb364128e Update Androidx libraries to latest versions (#5890) 2025-09-16 21:01:28 +00:00
bw-ghapp[bot]
0cbce39499 Update SDK to 1.0.0-3005-5a722fd2 (#5860)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-16 17:13:26 +00:00
Patrick Honkonen
f954b0b941 Refactor Vault Sync Logic into VaultSyncManager (#5871) 2025-09-16 16:44:52 +00:00
David Perez
cfd0a5b8a5 Update the Protobuf library (#5891) 2025-09-16 16:40:12 +00:00
renovate[bot]
d61e1cb6f1 [deps]: Update actions/setup-java action to v5 (#5880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:48:08 +00:00
renovate[bot]
b31983da8b [deps]: Update actions/checkout action to v5 (#5879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:47:36 +00:00
David Perez
e22d309423 Update navigation libs to latest version (#5889) 2025-09-15 23:32:36 +00:00
Patrick Honkonen
9b53095b5e [PM-15051] Add CredentialExchangeRegistry (#5869) 2025-09-15 21:48:56 +00:00
David Perez
c6814c8870 Update to Kotlin v2.2.20 (#5888) 2025-09-15 21:12:11 +00:00
David Perez
7710ad8a73 Update to AGP v8.13.0 (#5887) 2025-09-15 19:55:17 +00:00
Patrick Honkonen
80b3a7e675 [PM-25663] Introduce CredentialExchangeImporter (#5868) 2025-09-15 19:44:03 +00:00
David Perez
8235045dad PM-24234: Add missing plurals (#5886) 2025-09-15 19:02:34 +00:00
Patrick Honkonen
481a8c8fbc [PM-25662] Add CredentialExchangeCompletionManager (#5867) 2025-09-15 18:36:48 +00:00
renovate[bot]
1dc6ea2227 [deps]: Lock file maintenance (#5881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 16:17:39 +00:00
renovate[bot]
6554234898 [deps]: Update gh minor (#5877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 16:16:55 +00:00
David Perez
e990397b29 Update Robolectric to v4.16 (#5833) 2025-09-15 15:33:36 +00:00
bw-ghapp[bot]
417835ef3f Crowdin Pull (#5874)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-09-15 13:48:03 +00:00
aj-rosado
39a6dd1c4b [PM-22320] Default to SHA1 on 2fas importer if algorithm is missing (#5875) 2025-09-13 08:57:13 +00:00
Patrick Honkonen
4093e61b09 [PM-25665] Add BitwardenImportCredentialsRequest and helper (#5870) 2025-09-11 17:51:57 +00:00
Patrick Honkonen
c4adf3ad42 [PM-25661] Add placeholder ProviderEvents API for credential import/export (#5866) 2025-09-11 14:06:57 +00:00
André Bispo
417a1494e3 [PM-25640] Dialog flickers when switching accounts (#5865) 2025-09-11 13:41:34 +00:00
André Bispo
ef39ea6d5d [PM-25624] Hide decryption errors from autofill list view (#5855) 2025-09-11 13:41:21 +00:00
Patrick Honkonen
f6c20e08d1 [PM-25637] Add CXF module for Credential Exchange support (#5858) 2025-09-11 12:49:06 +00:00
Álison Fernandes
987e065dd7 Fix sdk-update Test by using Java 21 in setup-android action (#5861) 2025-09-10 18:31:37 +00:00
Patrick Honkonen
ba7ee04281 [PM-15056] Add exportVaultDataToCxf function to VaultRepository (#5847) 2025-09-10 14:40:05 +00:00
Konrad
808d57edc5 Update untranslatable strings (#5854) 2025-09-10 13:43:50 +00:00
David Perez
3356925c7a Update to Java 21 (#5835) 2025-09-10 13:41:41 +00:00
bw-ghapp[bot]
0487d95122 Update SDK to 1.0.0-2944-8447df0c (#5830)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-10 09:03:31 +00:00
bw-ghapp[bot]
0834a7a883 Crowdin Pull (#5853)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-09-08 16:17:54 +00:00
David Perez
2b0e8f9941 Update appVersionName to 2025.9.1 (#5848) 2025-09-05 21:52:04 +00:00
Patrick Honkonen
0702078b04 [PM-25523] Add importCxfPayload to VaultRepository (#5846) 2025-09-05 19:57:43 +00:00
David Perez
46c7e79039 Cleanup minor lint warnings in string resources (#5843) 2025-09-05 19:56:46 +00:00
David Perez
1d6e733c08 Update protobuff library to v4.32.0 (#5845) 2025-09-05 18:56:12 +00:00
Patrick Honkonen
a298b85374 [PM-25522] Add importCxf function to VaultSdkSource (#5841) 2025-09-05 18:56:01 +00:00
David Perez
fe79ea4822 PM-25162: Fix a navigation bug in bottom navigation (#5842) 2025-09-05 16:38:11 +00:00
Patrick Honkonen
4c50f873e2 [PM-15055] Add SDK support for exporting vault data to CXF (#5840) 2025-09-05 16:29:32 +00:00
David Perez
2bd4834b14 PM-25478: Update sends and folders while vault is locked (#5837) 2025-09-05 14:32:49 +00:00
David Perez
393931a5c6 PM-25474: Allow SYNC_CIPHER_DELETE notification to delete Cipher for inactive user (#5836) 2025-09-05 14:32:34 +00:00
bw-ghapp[bot]
fe6346013b Crowdin Pull (#5838)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-09-05 13:38:33 +00:00
Konrad
41e499fdf5 [PM-25133] Plural forms (#5773)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-09-04 18:13:58 +00:00
David Perez
aa39e6c6be PM-25462: Allow SYNC_FOLDER_DELETE notification to delete Folders for inactive user (#5832) 2025-09-04 17:31:00 +00:00
David Perez
eec4233486 PM-25431: Allow SYNC_SEND_DELETE notification to delete sends for inactive user (#5827) 2025-09-04 15:29:35 +00:00
David Perez
58db64da1a Update Kotlin to the latest version v2.2.10 (#5828) 2025-09-03 22:40:07 +00:00
David Perez
a7d0d6844d Update Hilt to v2.57.1 (#5826) 2025-09-03 20:09:57 +00:00
David Perez
249e1d3a5c Update Firebase BOM (#5823) 2025-09-03 18:00:03 +00:00
bw-ghapp[bot]
d8f3e7af92 Update SDK to 1.0.0-2887-7b5d9db2 (#5815)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-09-03 14:08:58 +00:00
Patrick Honkonen
1c4e4dcaf4 [PM-25394] Sort default user collections by Organization name (#5819) 2025-09-03 14:04:45 +00:00
David Perez
9adc25471e Update the Compose BOM and Androidx Lifecycle libraries (#5820) 2025-09-03 13:44:51 +00:00
Patrick Honkonen
ec6562336c [PM-23665] Refactor FIDO2 credential discovery (#5817) 2025-09-03 13:15:32 +00:00
Álison Fernandes
f402391ed8 [PM-25396] Publish store builds when release branches are updated (#5821) 2025-09-03 12:45:05 +00:00
David Perez
9b074f2106 PM-25393: Allow push notifications to update a cipher while vault is locked (#5818) 2025-09-02 20:27:30 +00:00
David Perez
3fa33faa35 Update AGP to v8.12.2 (#5816) 2025-09-02 18:29:25 +00:00
Patrick Honkonen
e1434dfe21 [PM-25327] Display default user collections first (#5810) 2025-09-02 18:25:55 +00:00
Álison Fernandes
659bbc5169 [PM-24930] Fix updating open SDK PRs and set token permissions (#5804) 2025-09-01 13:59:33 +00:00
bw-ghapp[bot]
dfa1f24c30 Crowdin Pull (#5807)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-08-29 16:30:03 +00:00
bw-ghapp[bot]
4f65c3f7d3 Update SDK to 1.0.0-2825-e05ba6eb (#5809)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-08-29 13:34:03 +00:00
Patrick Honkonen
0f74c3dded Fix plurals string for decryption error (#5796) 2025-08-28 15:48:05 +00:00
Patrick Honkonen
f7139b8b91 [PM-25239] Remove unnecessary vault sync from Fido2CredentialStoreImpl (#5794) 2025-08-28 15:47:44 +00:00
David Perez
2b35ac0d3a PM-25143: Retain intent data on recreate (#5787) 2025-08-27 19:45:16 +00:00
David Perez
4a79d7e6c8 PM-25238: Remove debug toast (#5792) 2025-08-27 16:43:03 +00:00
bw-ghapp[bot]
b9a496aa57 Update SDK to 1.0.0-2807-bc66e3d0 (#5785)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-08-27 15:23:01 +00:00
André Bispo
0a398839c4 [PM-18210] Cipher key encryption error handling (#5611)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-08-27 13:00:00 +00:00
Patrick Honkonen
aab8198457 [PM-25057] Refactor card restriction logic in AutofillCipherProvider (#5788) 2025-08-26 18:47:18 +00:00
David Perez
d2d89b5a0f PM-25193: Clear last sync time on push notification for inactive user (#5784) 2025-08-26 18:28:51 +00:00
David Perez
ddadd0135f PM-25194: Fix CollectionTypeJson data type in database (#5786) 2025-08-25 21:40:59 +00:00
David Perez
dc198eaf72 PM-25125: Refactor user state managment into UserStateManager (#5774) 2025-08-25 18:45:43 +00:00
David Perez
ff23dc3ab2 PM-25069: Update VaultAddEditViewModel toasts to snackbars (#5769) 2025-08-25 18:45:12 +00:00
Patrick Honkonen
191ff4c652 Update ARCHITECTURE.md (#5765) 2025-08-25 18:18:38 +00:00
bw-ghapp[bot]
99ab2245f6 Update SDK to 1.0.0-2681-1a956d45 (#5756)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-08-22 18:57:39 +00:00
bw-ghapp[bot]
bc7e682941 Crowdin Pull (#5772)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-08-22 16:50:45 +00:00
David Perez
517829e7b0 Remove the RemoveCardPolicy feature flag (#5770) 2025-08-22 16:33:08 +00:00
Patrick Honkonen
a1c6276092 [PM-25057] Filter Card Autofill Ciphers by Policy (#5768) 2025-08-21 13:57:19 +00:00
Patrick Honkonen
bc67bf3dff Suppress Gradle lint warnings (#5767) 2025-08-20 21:54:37 +00:00
renovate[bot]
bc5788556c [deps]: Update gh minor (#5766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 20:24:25 +00:00
David Perez
45e20d8c9e PM-17755: Fix comparator inconsistency based on Locale (#5762) 2025-08-20 20:20:03 +00:00
David Perez
a972a40a49 Update AGP to v8.12.1 (#5763) 2025-08-20 20:19:44 +00:00
aj-rosado
717d5665e0 [PM-24697] Allow cleartext traffic on OCSP and CRL servers (#5761) 2025-08-20 20:10:03 +00:00
David Perez
bc0a18f250 Standardize ui model packages (#5760) 2025-08-20 16:32:00 +00:00
David Perez
2f72553454 PM-22465: Identity state is not pre-populated on edit screen (#5759) 2025-08-20 16:15:16 +00:00
David Perez
e5a1546291 PM-25028: Migrate coachmarks and tooltips to UI module (#5757) 2025-08-20 16:04:19 +00:00
Patrick Honkonen
d8e319948c [PM-25027] Rename "Ask to add login" to "Ask to add item" (#5758) 2025-08-20 16:03:36 +00:00
David Perez
b3528249e9 PM-24544: Update Segmented Control to handle large font better (#5748) 2025-08-20 14:59:40 +00:00
David Perez
5f42c9bb39 PM-25006: Migrate row components to the UI module (#5753) 2025-08-19 22:07:26 +00:00
Patrick Honkonen
b010c9a29d [PM-24226] Reorder SSH key fields (#5754) 2025-08-19 22:00:41 +00:00
Patrick Honkonen
3e55f561c9 [PM-24940] Add Card Brand to Autofill (#5750) 2025-08-19 21:38:24 +00:00
David Perez
277e4d8d6f PM-20198: Update generator modal 'Save' button to 'Apply' (#5745) 2025-08-19 21:27:17 +00:00
David Perez
32e8fb7d8e PM-25004: Migrate the MultiSelectButton to the UI module (#5752) 2025-08-19 21:03:06 +00:00
David Perez
4a18e57cca PM-25003: Migrate bottom sheet to the UI module (#5751) 2025-08-19 20:58:03 +00:00
David Perez
070ef45087 PM-24993: Move account components to UI module (#5749) 2025-08-19 19:47:59 +00:00
Patrick Honkonen
a658cf890a Refactor AccountKeysJson property names (#5747) 2025-08-19 17:10:16 +00:00
David Perez
d3dea3c9cb PM-24283: Migrate the common dialogs to the UI module (#5746) 2025-08-19 16:33:25 +00:00
Patrick Honkonen
5ab0517bf3 [PM-24577] Provision SDK with AccountKeys (#5682) 2025-08-19 16:00:34 +00:00
Álison Fernandes
e8b01c2d44 [PM-24930] New workflow to update the SDK and test ongoing work (#5742) 2025-08-19 15:19:57 +00:00
Patrick Honkonen
b34d873471 [PM-24411] Migrate IntentManager to ui module (#5634) 2025-08-19 15:13:40 +00:00
David Perez
3c3d8710c9 PM-24944: Migrate scaffold to ui module (#5738) 2025-08-19 13:53:00 +00:00
Igorro
20dea9b5ff Fix autofill overwriting user data with empty field values (#5649) 2025-08-19 13:47:31 +00:00
Patrick Honkonen
44410efe56 [PM-24938] Improve Autofill Card Expiration Month and Year Parsing (#5717) 2025-08-18 21:27:39 +00:00
bitwarden-charlie
a999592fb6 chore/SRE-583 Deprecate usage of Auth-Email Header (#5097)
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
2025-08-18 21:03:34 +00:00
David Perez
25a78f60ab PM-24942: Move Segmented control to UI module (#5727) 2025-08-18 20:51:31 +00:00
renovate[bot]
a8546bb4eb [deps]: Update gh minor (#5722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 20:46:44 +00:00
David Perez
6c6d4f2d91 PM-24950: Migrate the image package to the ui module (#5731) 2025-08-18 20:45:24 +00:00
David Perez
7347d91fdd PM-24949: Move headers package to the ui module (#5730) 2025-08-18 20:40:13 +00:00
David Perez
0a99359978 PM-24943: Move the scrim package to the UI module (#5728) 2025-08-18 20:32:28 +00:00
David Perez
aca4b05b59 PM-24798: Move text components to UI module (#5718) 2025-08-18 18:08:14 +00:00
Patrick Honkonen
b0c7995cb7 Support both camel and pascal case for AccountKeysJson (#5724) 2025-08-18 17:31:59 +00:00
Patrick Honkonen
af322b5d1f [PM-24599] Add cardholderName to AutofillSaveItem.Card (#5716) 2025-08-18 16:29:26 +00:00
Álison Fernandes
9fcfcc9e41 [PM-24930] Add placeholder workflow for sdlc-sdk-update.yml (#5723) 2025-08-18 15:52:19 +00:00
aj-rosado
ff6b7b675d [PM-24347] Tracking UserClientExportedVault event when user exports the vault (#5710) 2025-08-18 15:10:52 +00:00
David Perez
3164c29184 PM-24786: Move radio button to UI module (#5708) 2025-08-15 21:14:14 +00:00
David Perez
c5663431af Update the app dependencies (#5715) 2025-08-15 21:04:04 +00:00
Patrick Honkonen
4fb96cb782 [PM-24598] Map AutofillSaveItem to VaultItemCipherType (#5714) 2025-08-15 20:12:01 +00:00
David Perez
36e06cdac7 PM-24770: Move snackbars to the UI module (#5712) 2025-08-15 18:46:25 +00:00
David Perez
3cf325becf Rename the AutofillTotpCopyActivity (#5713) 2025-08-15 18:24:05 +00:00
Patrick Honkonen
584bdb6277 [PM-24700] Update email validation in LandingViewModel (#5711) 2025-08-15 17:34:42 +00:00
David Perez
b2a9f4b455 Remove context param from IntentManager extensions (#5706) 2025-08-15 17:31:26 +00:00
Patrick Honkonen
b0b4379307 [PM-24411] Extract Authenticator functions from IntentManager (#5702) 2025-08-15 16:09:21 +00:00
Patrick Honkonen
b9cc664efa Refactor Detekt task to use staged files (#5705) 2025-08-15 16:07:56 +00:00
aj-rosado
e30e0ffbb4 [PM-23723] Fix close and cancel text on Match detection dialogs (#5707) 2025-08-15 16:05:37 +00:00
Patrick Honkonen
2ffd71c69a Fix Autofill settings deeplink (#5704) 2025-08-15 15:59:30 +00:00
David Perez
3488ad6217 PM-24771: Move the slider to the UI module (#5698) 2025-08-15 15:26:20 +00:00
Patrick Honkonen
58005d908a [PM-24740] Make VaultAddEditUriItem a multiline URI field (#5700) 2025-08-15 14:06:38 +00:00
David Perez
a320e6ea61 PM-24769: Move the stepper to the UI module (#5699) 2025-08-15 14:04:12 +00:00
bw-ghapp[bot]
5a23ceabc1 Crowdin Pull (#5701)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-08-15 02:06:13 +00:00
David Perez
f4102bcd30 Update Autofill logging (#5697) 2025-08-14 22:09:49 +00:00
David Perez
6d25c12271 PM-24768: Move text fields to the UI module (#5696) 2025-08-14 21:00:21 +00:00
Patrick Honkonen
ef03cdb2db [PM-24652] Remove AEAD enrollment on key rotation feature flag (#5695) 2025-08-14 20:39:08 +00:00
David Perez
474ec4907f PM-24726: Update MDM functionality (#5694) 2025-08-14 18:21:24 +00:00
Patrick Honkonen
a68fd8b44f [PM-24721] Refactor AccountKeys to top-level common model (#5693) 2025-08-14 18:12:03 +00:00
David Perez
3282992221 PM-24727: Update VaultUnlockScreen to use user specific environment (#5690) 2025-08-14 14:06:19 +00:00
Patrick Honkonen
26252ebcdb [PM-24411] Generalize IntentManager activity handling (#5689) 2025-08-13 22:03:09 +00:00
aj-rosado
a688693f43 [PM-23723] URI Matching detection layout updates on advanced options (#5574) 2025-08-13 16:09:29 +00:00
David Perez
3ed63ef5eb PM-24688: Use the realtime elapse time to determine vault lock timeouts (#5684) 2025-08-13 15:04:19 +00:00
David Perez
1e2bc4aa70 PM-24690: Use ToastManager in MainViewModel (#5685) 2025-08-13 15:04:02 +00:00
aj-rosado
694865c213 [PM-24642] Remove captcha connector code (#5677) 2025-08-12 20:56:18 +00:00
David Perez
29243c8f44 Remove unused ClearClipboardWorker from Authenticator (#5683) 2025-08-12 18:02:41 +00:00
Andy Pixley
4e1dfcaeec [BRE-1074] Adding debug info for failing to find release (#5673) 2025-08-12 17:11:13 +00:00
Patrick Honkonen
75f3065085 [PM-24569] Save accountKeys to AuthDiskSource (#5679) 2025-08-12 16:54:53 +00:00
Álison Fernandes
402e399fd4 [PM-24675] Fix renovate update warning (#5680) 2025-08-12 15:09:05 +00:00
Álison Fernandes
810cbc8da5 [PM-24590] Add support to hotfix specific apps in Cut Release Branch workflow (#5671) 2025-08-12 14:37:04 +00:00
Patrick Honkonen
9bfbe0c087 [PM-24568] Add accountKeys to SyncResponseJson.Profile (#5678) 2025-08-11 19:37:30 +00:00
Patrick Honkonen
d06c87beb3 [PM-24411] Use BuildInfoManager for build-related information (#5663) 2025-08-11 18:34:47 +00:00
Matt Andreko
9b120701eb Fix reusable scan in CI build (#5668) 2025-08-08 20:58:04 +00:00
David Perez
e8f1242744 Add header and custom supportContent functionality to BitwardenMultiSelectButton (#5669) 2025-08-08 18:24:43 +00:00
1671 changed files with 110495 additions and 36284 deletions

134
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,134 @@
# Bitwarden Android - Claude Code Configuration
Official Android application for Bitwarden Password Manager and Bitwarden Authenticator, providing secure password management, two-factor authentication, and credential autofill services with zero-knowledge encryption.
## Overview
- Multi-module Android application: `:app` (Password Manager), `:authenticator` (2FA TOTP generator)
- Zero-knowledge architecture: encryption/decryption happens client-side via Bitwarden SDK
- Target users: End-users via Google Play Store and F-Droid
### Key Concepts
- **Zero-Knowledge Architecture**: Server never has access to unencrypted vault data or encryption keys
- **Bitwarden SDK**: Rust-based cryptographic SDK handling all encryption/decryption operations
- **DataState**: Wrapper for streaming data states (Loading, Loaded, Pending, Error, NoNetwork)
- **Result Types**: Custom sealed classes for operation results (never throw exceptions from data layer)
- **UDF (Unidirectional Data Flow)**: State flows down, actions flow up through ViewModels
---
## Architecture
```
User Request (UI Action)
|
Screen (Compose)
|
ViewModel (State/Action/Event)
|
Repository (Business Logic)
|
+----+----+----+
| | | |
Disk Network SDK
| | |
Room Retrofit Bitwarden
DB APIs Rust SDK
```
### Key Principles
1. **No Exceptions from Data Layer**: All suspending functions return `Result<T>` or custom sealed classes
2. **State Hoisting to ViewModel**: All state that affects behavior must live in the ViewModel's state
3. **Interface-Based DI**: All implementations use interface/`...Impl` pairs with Hilt injection
4. **Encryption by Default**: All sensitive data encrypted via SDK before storage
### Core Patterns
- **BaseViewModel**: Enforces UDF with State/Action/Event pattern. See `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt`.
- **Repository Result Pattern**: Type-safe error handling using custom sealed classes for discrete operations and `DataState<T>` wrapper for streaming data.
- **Common Patterns**: Flow collection via `Internal` actions, error handling via `when` branches, `DataState` streaming with `.map { }` and `.stateIn()`.
> For complete architecture patterns, code templates, and module organization, see `docs/ARCHITECTURE.md`.
---
## Development Guide
### Workflow Skills
> **Quick start**: Use `/plan-android-work <task>` to refine requirements and plan,
> then `/work-on-android <task>` for implementation.
**Planning Phase:**
1. `refining-android-requirements` - Gap analysis and structured spec from any input source
2. `planning-android-implementation` - Architecture design and phased task breakdown
**Implementation Phase:**
3. `implementing-android-code` - Patterns, gotchas, and templates for writing code
4. `testing-android-code` - Test patterns and templates for verifying code
5. `build-test-verify` - Build, test, lint, and deploy commands
6. `perform-android-preflight-checklist` - Quality gate before committing
7. `committing-android-changes` - Commit message format and pre-commit workflow
8. `reviewing-changes` - Code review checklists for MVVM/Compose patterns
9. `creating-android-pull-request` - PR creation workflow and templates
---
## Security Rules
**MANDATORY - These rules have no exceptions:**
1. **Zero-Knowledge Architecture**: Never transmit unencrypted vault data or master passwords to the server. All encryption happens client-side via the Bitwarden SDK.
2. **No Plaintext Key Storage**: Encryption keys must be stored using Android Keystore (biometric unlock) or encrypted with PIN/master password.
3. **Sensitive Data Cleanup**: On logout, all sensitive data must be cleared from memory and storage via `UserLogoutManager.logout()`.
4. **Input Validation**: Validate all user inputs before processing, especially URLs and credentials.
5. **SDK Isolation**: Use scoped SDK sources (`ScopedVaultSdkSource`) to prevent cross-user crypto context leakage.
---
## Code Style & Standards
- **Formatter**: Android Studio with `bitwarden-style.xml` | **Line Limit**: 100 chars | **Detekt**: Enabled
- **Naming**: `camelCase` (vars/fns), `PascalCase` (classes), `SCREAMING_SNAKE_CASE` (constants), `...Impl` (implementations)
- **KDoc**: Required for all public APIs
- **String Resources**: Add new strings to `:ui` module (`ui/src/main/res/values/strings.xml`). Use typographic quotes/apostrophes (`"` `"` `'`) not escaped ASCII (`\"` `\'`)
> For complete style rules (imports, formatting, documentation, Compose conventions), see `docs/STYLE_AND_BEST_PRACTICES.md`.
---
## Anti-Patterns
In addition to the Key Principles above, follow these rules:
### DO
- Use `remember(viewModel)` for lambdas passed to composables
- Map async results to internal actions before updating state
- Inject `Clock` for time-dependent operations
- Return early to reduce nesting
### DON'T
- Update state directly inside coroutines (use internal actions)
- Use `any` types or suppress null safety
- Catch generic `Exception` (catch specific types)
- Use `e.printStackTrace()` (use Timber logging)
- Create new patterns when established ones exist
- Skip KDoc for public APIs
---
## Quick Reference
- **Code style**: Full rules: `docs/STYLE_AND_BEST_PRACTICES.md`
- **Before writing code**: Use `implementing-android-code` skill for Bitwarden-specific patterns, gotchas, and templates
- **Before writing tests**: Use `testing-android-code` skill for test patterns and templates
- **Building/testing**: Use `build-test-verify` skill | App tests: `./gradlew app:testStandardDebugUnitTest`
- **Before committing**: Use `perform-android-preflight-checklist` skill, then `committing-android-changes` skill for message format
- **Code review**: Use `reviewing-changes` skill for MVVM/Compose review checklists
- **Creating PRs**: Use `creating-android-pull-request` skill for PR workflow and templates
- **Troubleshooting**: See `docs/TROUBLESHOOTING.md`
- **Architecture**: `docs/ARCHITECTURE.md` | [Bitwarden SDK](https://github.com/bitwarden/sdk) | [Jetpack Compose](https://developer.android.com/jetpack/compose) | [Hilt DI](https://dagger.dev/hilt/)

View File

@@ -0,0 +1,119 @@
---
description: Guided requirements refinement and implementation planning for Bitwarden Android
argument-hint: <Jira ticket (PM-12345), Confluence URL, or free-text description>
---
# Android Planning Workflow
You are guiding the developer through requirements refinement and implementation planning for the Bitwarden Android project. The input to plan from is:
**Input**: $ARGUMENTS
## Prerequisites
- **Jira/Confluence access**: Fetching tickets and wiki pages requires the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin. If the plugin is not installed, Jira ticket IDs and Confluence URLs cannot be fetched automatically.
## Workflow Phases
Work through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable. If starting from a partially completed plan, skip to the appropriate phase.
### Phase 1: Ingest Requirements
Parse the input to detect and fetch all available sources:
**Source Detection Rules:**
- **Jira tickets** (patterns like `PM-\d+`, `BWA-\d+`, `EC-\d+`): Fetch via `get_issue` and `get_issue_comments`. Also fetch linked issue summaries (parent epic, sub-tasks, blockers) for context.
- **Confluence URLs** (containing `atlassian.net/wiki` or confluence page IDs): Extract page ID and fetch via `get_confluence_page`. If the page is a parent page, fetch child pages via `get_child_pages` and ask the user which are relevant.
- **Free text**: Treat as direct requirements — no fetching needed.
- **Multiple inputs**: All are first-class sources. Fetch each independently and consolidate.
- **Tool unavailable**: If `get_issue`, `get_confluence_page`, or other Atlassian tools are not available, inform the user that the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required and prompt them to install and configure it. In the meantime, ask the user to paste the relevant content directly. Treat pasted content as free-text input.
**Present a structured summary:**
1. Sources identified and fetched (with links)
2. Raw requirements extracted from each source
3. Initial scope assessment (small / medium / large)
**Edge cases:**
- Jira ticket with no description → flag as critical gap that Phase 2 must address
- Multiple tickets → fetch all, consolidate, flag any contradictions
- Ticket + free text → both treated as first-class; free text supplements ticket
**Gate**: User confirms the summary is complete and may add additional sources or context before proceeding.
### Phase 2: Refine Requirements
Invoke the `refining-android-requirements` skill and use it to perform gap analysis on the raw requirements from Phase 1.
The skill will:
1. Consolidate all sources into a working document
2. Evaluate requirements against a structured rubric (functional, technical, security, UX, cross-cutting)
3. Present categorized gaps as blocking or non-blocking questions
4. After user answers, produce a structured specification with numbered IDs
**Gate**: User approves the refined specification. They may request changes or provide additional answers.
### Phase 3: Plan Implementation
Invoke the `planning-android-implementation` skill and use it to design the implementation approach based on the refined spec from Phase 2.
The skill will:
1. Classify the change type
2. Explore the codebase for reference implementations and integration points
3. Design the architecture with component relationships
4. Produce a file inventory and phased implementation plan
5. Assess risks and define verification criteria
**Gate**: User reviews the implementation plan and may request changes to architecture, phasing, or scope.
### Phase 4: Finalize & Save
Merge the outputs from Phase 2 (specification) and Phase 3 (implementation plan) into a single design document using this template:
```markdown
# [Feature Name] — Design Document
**Feature**: [concise description]
**Date**: [current date]
**Status**: Ready for Implementation
**Jira**: [ticket ID if available]
**Sources**: [list of all input sources with links]
---
## Requirements Specification
[Full output from Phase 2 — the refined specification with numbered IDs]
---
## Implementation Plan
[Full output from Phase 3 — architecture, file inventory, phases, risks]
---
## Executing This Plan
To implement this plan, run:
/work-on-android [ticket or feature reference]
Reference this design document during implementation for architecture decisions,
file locations, and phase ordering.
```
**Save the document:**
- With ticket: `.claude/outputs/plans/PM-XXXXX-FEATURE-NAME-PLAN.md`
- Without ticket: `.claude/outputs/plans/FEATURE-NAME-PLAN.md`
- Feature name should be uppercase with hyphens (e.g., `BIOMETRIC-TIMEOUT-CONFIG-PLAN.md`)
- Create the output directory if it does not exist
**On completion**: Present the saved file path and remind the user they can execute the plan with `/work-on-android`.
## Guidelines
- Be explicit about which phase you are in at all times.
- If the user wants to skip a phase, acknowledge and move to the next applicable phase.
- When fetching from Jira/Confluence, summarize what was found rather than dumping raw content.
- Questions in Phase 2 should be specific and actionable, not generic.
- The implementation plan in Phase 3 should reference concrete files in the codebase, not abstract descriptions.

View File

@@ -0,0 +1,66 @@
---
description: Guided Android development workflow through all lifecycle phases
argument-hint: <task description, plan, or Jira ticket reference>
---
# Android Development Workflow
You are guiding the developer through a complete Android development lifecycle for the Bitwarden Android project. The task to work on is:
**Task**: $ARGUMENTS
## Workflow Phases
Work through each phase sequentially. **Confirm with the user before advancing to the next phase.** If a phase fails (tests fail, lint errors, etc.), loop on that phase until resolved before advancing. The user may skip phases that are not applicable.
### Phase 1: Implement
Invoke the `implementing-android-code` skill and use it to guide your implementation of the task. Understand what needs to be done, explore the relevant code, and write the implementation.
**Before advancing**: Summarize what was implemented and confirm the user is ready to move to testing.
### Phase 2: Test
Invoke the `testing-android-code` skill and use it to write tests for the changes made in Phase 1. Follow the project's test patterns and conventions.
**Before advancing**: Summarize what tests were written and confirm readiness for build verification.
### Phase 3: Build & Verify
Invoke the `build-test-verify` skill to run tests, lint, and detekt. Ensure everything passes.
**If failures occur**: Fix the issues and re-run verification. Do not advance until all checks pass.
**Before advancing**: Report build/test/lint results and confirm readiness for self-review.
### Phase 4: Self-Review
Invoke the `perform-android-preflight-checklist` skill to perform a quality gate check on all changes. Address any issues found.
**Before advancing**: Share the self-review results and confirm readiness to commit.
### Phase 5: Commit
Invoke the `committing-android-changes` skill to stage and commit the changes with a properly formatted commit message.
**Before advancing**: Confirm the commit was successful and ask if the user wants to proceed to review and PR creation, or stop here.
### Phase 6: Review
**Pre-requisites:**
- `bitwarden-code-review` from the [Bitwarden Plugin Marketplace](https://github.com/bitwarden/ai-plugins) must be installed in order to perform this phase. If it is not installed prompt the user to install it, or skip the review phase.
Launch a subagent with the `/bitwarden-code-review:code-review-local` command to perform a **local** code review of the committed diff. Validate and address any issues found before proceeding.
**Before advancing**: Share review findings and confirm readiness for PR creation.
### Phase 7: Pull Request
Prompt the user to invoke the `creating-android-pull-request` skill to create the pull request with proper description and formatting. **Create as a draft PR by default** unless the user has explicitly requested a ready-for-review PR.
## Guidelines
- Be explicit about which phase you are in at all times.
- Never proceed to another phase without user confirmation.
- If the user wants to skip a phase, acknowledge and move to the next applicable phase.
- If starting from a partially completed task (e.g., code already written), skip to the appropriate phase.

View File

@@ -0,0 +1,3 @@
Use the `reviewing-changes` skill to review this pull request.
The PR branch is already checked out in the current working directory.

14
.claude/settings.json Normal file
View File

@@ -0,0 +1,14 @@
{
"attribution": {
"commit": "",
"pr": ""
},
"extraKnownMarketplaces": {
"bitwarden-marketplace": {
"source": {
"source": "github",
"repo": "bitwarden/ai-plugins"
}
}
}
}

View File

@@ -0,0 +1,136 @@
---
name: build-test-verify
version: 0.1.0
description: Build, test, lint, and deploy commands for the Bitwarden Android project. Use when running tests, building APKs/AABs, running lint/detekt, deploying, using fastlane, or discovering codebase structure. Triggered by "run tests", "build", "gradle", "lint", "detekt", "deploy", "fastlane", "assemble", "verify", "coverage".
---
# Build, Test & Verify
## Environment Setup
| Variable | Required | Description |
|----------|----------|-------------|
| `GITHUB_TOKEN` | Yes (CI) | GitHub Packages auth for SDK (`read:packages` scope) |
| Build flavors | - | `standard` (Play Store), `fdroid` (no Google services) |
| Build types | - | `debug`, `beta`, `release` |
If builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.properties` or environment and check connectivity to `maven.pkg.github.com`.
---
## Building
```bash
# Debug builds
./gradlew app:assembleDebug
./gradlew authenticator:assembleDebug
# Release builds (requires signing keys)
./gradlew app:assembleStandardRelease
./gradlew app:bundleStandardRelease
# F-Droid builds
./gradlew app:assembleFdroidRelease
```
---
## Running Tests
**IMPORTANT**: The app module uses the `standard` flavor. Always use `testStandardDebugUnitTest`, NOT `testDebugUnitTest`.
```bash
# App module tests (correct flavor!)
./gradlew app:testStandardDebugUnitTest
# Run all unit tests across all modules
./gradlew test
# Individual shared modules (no flavor needed)
./gradlew :core:test
./gradlew :data:test
./gradlew :network:test
./gradlew :ui:test
# Authenticator module
./gradlew authenticator:testStandardDebugUnitTest
```
### Test Structure
```
app/src/test/ # App unit tests
app/src/testFixtures/ # App test utilities
core/src/testFixtures/ # Core test utilities (FakeDispatcherManager)
data/src/testFixtures/ # Data test utilities (FakeSharedPreferences)
network/src/testFixtures/ # Network test utilities (BaseServiceTest)
ui/src/testFixtures/ # UI test utilities (BaseViewModelTest, BaseComposeTest)
```
### Test Quick Reference
- **Dispatcher Control**: `FakeDispatcherManager` from `:core:testFixtures`
- **MockK**: `mockk<T> { every { } returns }`, `coEvery { }` for suspend
- **Flow Testing**: Turbine with `stateEventFlow()` helper from `BaseViewModelTest`
- **Time Control**: Inject `Clock` for deterministic time testing
---
## Lint & Static Analysis
```bash
# Detekt (static analysis)
./gradlew detekt
# Android Lint
./gradlew lint
# Full validation suite (detekt + lint + tests + coverage)
./fastlane check
```
---
## Codebase Discovery
```bash
# Find existing Bitwarden UI components
find ui/src/main/kotlin/com/bitwarden/ui/platform/components/ -name "Bitwarden*.kt" | sort
# Find all ViewModels
grep -rl "BaseViewModel<" app/src/main/kotlin/ --include="*.kt"
# Find all Navigation files with @Serializable routes
find app/src/main/kotlin/ -name "*Navigation.kt" | sort
# Find all Hilt modules
find app/src/main/kotlin/ -name "*Module.kt" -path "*/di/*" | sort
# Find all repository interfaces
find app/src/main/kotlin/ -name "*Repository.kt" -not -name "*Impl.kt" -path "*/repository/*" | sort
# Find encrypted disk source examples
grep -rl "EncryptedPreferences" app/src/main/kotlin/ --include="*.kt"
# Find Clock injection usage
grep -rl "private val clock: Clock" app/src/main/kotlin/ --include="*.kt"
# Search existing strings before adding new ones
grep -n "search_term" ui/src/main/res/values/strings.xml
```
---
## Deployment & Versioning
**Version location**: `gradle/libs.versions.toml`
```toml
appVersionCode = "1"
appVersionName = "2025.11.1"
```
Pattern: `YEAR.MONTH.PATCH`
**Publishing channels**:
- **Play Store**: GitHub Actions workflow with signed AAB
- **F-Droid**: Dedicated workflow with F-Droid signing keys
- **Firebase App Distribution**: Beta testing

View File

@@ -0,0 +1,81 @@
---
name: committing-android-changes
version: 0.1.0
description: Git commit conventions and workflow for Bitwarden Android. Use when committing code, writing commit messages, or preparing changes for commit. Triggered by "commit", "git commit", "commit message", "prepare commit", "stage changes".
---
# Git Commit Conventions
## Commit Message Format
```
[PM-XXXXX] <type>: <imperative summary>
<optional body explaining why, not what>
```
### Rules
1. **Ticket prefix**: Always include `[PM-XXXXX]` matching the Jira ticket
2. **Type keyword**: Include a conventional commit type after the ticket prefix (see table below)
3. **Imperative mood**: "Add feature" not "Added feature" or "Adds feature"
4. **Short summary**: Under 72 characters for the first line
5. **Body**: Explain the "why" not the "what" — the diff shows the what
### Type Keywords
Invoke the `labeling-android-changes` skill for the full type keyword table and selection guidance.
### Example
```
[PM-12345] feat: Add biometric unlock timeout configuration
Users reported confusion about when biometric prompts appear.
This adds a configurable timeout setting to the security preferences.
```
### Followup Commits
Only the first commit on a branch needs the full format (ticket prefix, type keyword, body). Subsequent commits — whether addressing review feedback, making intermediate changes, or iterating locally — can use a short, descriptive summary with no prefix or body required.
```
Update error handling in login flow
```
---
## Pre-Commit Checklist
Run the `perform-android-preflight-checklist` skill for the full quality gate. At minimum, before staging and committing:
1. **Run affected module tests** (use `build-test-verify` skill for correct commands)
2. **Check lint**: `./gradlew detekt` on changed modules
3. **Review staged changes**: `git diff --staged` — verify no unintended modifications
4. **Verify no secrets**: No API keys, tokens, passwords, or `.env` files staged
5. **Verify no generated files**: No build outputs, `.idea/` changes, or generated code
---
## What NOT to Commit
- `.env` files or `user.properties` with real tokens
- Credential files or signing keystores
- Build outputs (`build/`, `*.apk`, `*.aab`)
- IDE-specific files (`.idea/` changes, `*.iml`)
- Large binary files
---
## Staging Best Practices
- **Stage specific files** by name rather than `git add -A` or `git add .`
- Put each file path on its own line for readability:
```bash
git add \
path/to/first/File.kt \
path/to/second/File.kt \
path/to/third/File.kt
```
- Review each file being staged to avoid accidentally including sensitive data
- Use `git status` (without `-uall` flag) to see the working tree state

View File

@@ -0,0 +1,64 @@
---
name: creating-android-pull-request
version: 0.1.0
description: Pull request creation workflow for Bitwarden Android. Use when creating PRs, writing PR descriptions, or preparing branches for review. Triggered by "create PR", "pull request", "open PR", "gh pr create", "PR description".
---
# Create Pull Request
## PR Title Format
```
[PM-XXXXX] <type>: <short imperative summary>
```
**Examples:**
- `[PM-12345] feat: Add autofill support for passkeys`
- `[PM-12345] fix: Resolve crash during vault sync`
- `[PM-12345] refactor: Simplify authentication flow`
**Rules:**
- Include Jira ticket prefix
- Keep under 70 characters total
- Use imperative mood in the summary
**Type keywords** (triggers automatic `t:` label via CI):
Invoke the `labeling-android-changes` skill for the full type keyword table and selection guidance.
---
## PR Body Template
**IMPORTANT:** Always follow the repo's PR template at `.github/PULL_REQUEST_TEMPLATE.md`. Delete the Screenshots section entirely if there are no UI changes.
---
## Pre-PR Checklist
1. **All tests pass**: Run `./gradlew app:testStandardDebugUnitTest` (and other affected modules)
2. **Lint clean**: Run `./gradlew detekt`
3. **Self-review done**: Use `perform-android-preflight-checklist` skill
4. **No unintended changes**: Check `git diff origin/main...HEAD` for unexpected files
5. **Branch up to date**: Rebase on `main` if needed
---
## Creating the PR
```bash
# Ensure branch is pushed
git push -u origin <branch-name>
# Create PR as draft by default (body follows .github/PULL_REQUEST_TEMPLATE.md)
gh pr create --draft --title "[PM-XXXXX] feat: Short summary" --body "<fill in from PR template>"
```
**Default to draft PRs.** Only create a non-draft (ready for review) PR if the user explicitly requests it.
---
## Base Branch
- Default target: `main`
- Check with team if targeting a feature branch instead

View File

@@ -0,0 +1,484 @@
---
name: implementing-android-code
version: 0.1.2
description: This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by "How do I implement a ViewModel?", "Create a new screen", "Add navigation", "Write a repository", "BaseViewModel pattern", "State-Action-Event", "type-safe navigation", "@Serializable route", "SavedStateHandle persistence", "process death recovery", "handleAction", "sendAction", "Hilt module", "Repository pattern", "implementing a screen", "adding a data source", "handling navigation", "encrypted storage", "security patterns", "Clock injection", "DataState", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app.
---
# Implementing Android Code - Bitwarden Quick Reference
**This skill provides tactical guidance for Bitwarden-specific patterns.** For comprehensive architecture decisions and complete code style rules, consult `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
---
## Critical Patterns Reference
### A. ViewModel Implementation (State-Action-Event Pattern)
All ViewModels follow the **State-Action-Event (SAE)** pattern via `BaseViewModel<State, Event, Action>`.
**Key Requirements:**
- Annotate with `@HiltViewModel`
- State class MUST be `@Parcelize data class : Parcelable`
- Implement `handleAction(action: A)` - MUST be synchronous
- Post internal actions from coroutines using `sendAction()`
- Save/restore state via `SavedStateHandle[KEY_STATE]`
- Private action handlers: `private fun handle*` naming convention
**Template**: See [ViewModel template](templates.md#viewmodel-template-state-action-event-pattern)
**Pattern Summary:**
```kotlin
@HiltViewModel
class ExampleViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository,
) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(
initialState = savedStateHandle[KEY_STATE] ?: ExampleState(),
) {
init {
stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)
}
override fun handleAction(action: ExampleAction) {
// Synchronous dispatch only
when (action) {
is Action.Click -> handleClick()
is Action.Internal.DataReceived -> handleDataReceived(action)
}
}
private fun handleClick() {
viewModelScope.launch {
val result = repository.fetchData()
sendAction(Action.Internal.DataReceived(result)) // Post internal action
}
}
private fun handleDataReceived(action: Action.Internal.DataReceived) {
mutableStateFlow.update { it.copy(data = action.result) }
}
}
```
**Reference:**
- `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)
- `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt` (see class declaration)
**Critical Gotchas:**
-**NEVER** update `mutableStateFlow` directly inside coroutines
-**ALWAYS** post internal actions from coroutines, update state in `handleAction()`
-**NEVER** forget `@IgnoredOnParcel` for sensitive data (causes security leak)
-**ALWAYS** use `@Parcelize` on state classes for process death recovery
- ✅ State restoration happens automatically if properly saved to `SavedStateHandle`
---
### B. Navigation Implementation (Type-Safe)
All navigation uses **type-safe routes** with kotlinx.serialization.
**Pattern Structure:**
1. `@Serializable` route data class with parameters
2. `...Args` helper class for extracting from `SavedStateHandle`
3. `NavGraphBuilder.{screen}Destination()` extension for adding screen to graph
4. `NavController.navigateTo{Screen}()` extension for navigation calls
**Template**: See [Navigation template](templates.md#navigation-template-type-safe-routes)
**Pattern Summary:**
```kotlin
@Serializable
data class ExampleRoute(val userId: String, val isEditMode: Boolean = false)
data class ExampleArgs(val userId: String, val isEditMode: Boolean)
fun SavedStateHandle.toExampleArgs(): ExampleArgs {
val route = this.toRoute<ExampleRoute>()
return ExampleArgs(userId = route.userId, isEditMode = route.isEditMode)
}
fun NavController.navigateToExample(
userId: String,
isEditMode: Boolean = false,
navOptions: NavOptions? = null,
) {
this.navigate(route = ExampleRoute(userId, isEditMode), navOptions = navOptions)
}
fun NavGraphBuilder.exampleDestination(onNavigateBack: () -> Unit) {
composableWithSlideTransitions<ExampleRoute> {
ExampleScreen(onNavigateBack = onNavigateBack)
}
}
```
**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt` (see `LoginRoute` and extensions)
**Key Benefits:**
- ✅ Type safety: Compile-time errors for missing parameters
- ✅ No string literals in navigation code
- ✅ Automatic serialization/deserialization
- ✅ Clear contract for screen dependencies
---
### C. Screen/Compose Implementation
All screens follow consistent Compose patterns.
**Template**: See [Screen/Compose template](templates.md#screencompose-template)
**Key Patterns:**
```kotlin
@Composable
fun ExampleScreen(
onNavigateBack: () -> Unit,
viewModel: ExampleViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
ExampleEvent.NavigateBack -> onNavigateBack()
}
}
BitwardenScaffold(
topBar = {
BitwardenTopAppBar(
title = stringResource(R.string.title),
navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(ExampleAction.BackClick) }
},
)
},
) {
// UI content
}
}
```
**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt` (see `LoginScreen` composable)
**Essential Requirements:**
- ✅ Use `hiltViewModel()` for dependency injection
- ✅ Use `collectAsStateWithLifecycle()` for state (not `collectAsState()`)
- ✅ Use `EventsEffect(viewModel)` for one-shot events
- ✅ Use `remember(viewModel) { }` for stable callbacks to prevent recomposition
- ✅ Use `Bitwarden*` prefixed components from `:ui` module
**State Hoisting Rules:**
- **ViewModel state**: Data that needs to survive process death or affects business logic
- **UI-only state**: Temporary UI state (scroll position, text field focus) using `remember` or `rememberSaveable`
---
### D. Data Layer Implementation
The data layer follows strict patterns for repositories, managers, and data sources.
**Interface + Implementation Separation (ALWAYS)**
**Template**: See [Data Layer template](templates.md#data-layer-template-repository--hilt-module)
**Pattern Summary:**
```kotlin
// Interface (injected via Hilt)
interface ExampleRepository {
suspend fun fetchData(id: String): ExampleResult
val dataFlow: StateFlow<DataState<ExampleData>>
}
// Implementation (NOT directly injected)
class ExampleRepositoryImpl(
private val exampleDiskSource: ExampleDiskSource,
private val exampleService: ExampleService,
) : ExampleRepository {
override suspend fun fetchData(id: String): ExampleResult {
// NO exceptions thrown - return Result or sealed class
return exampleService.getData(id).fold(
onSuccess = { ExampleResult.Success(it.toModel()) },
onFailure = { ExampleResult.Error(it.message) },
)
}
}
// Sealed result class (domain-specific)
sealed class ExampleResult {
data class Success(val data: ExampleData) : ExampleResult()
data class Error(val message: String?) : ExampleResult()
}
// Hilt Module
@Module
@InstallIn(SingletonComponent::class)
object ExampleRepositoryModule {
@Provides
@Singleton
fun provideExampleRepository(
exampleDiskSource: ExampleDiskSource,
exampleService: ExampleService,
): ExampleRepository = ExampleRepositoryImpl(exampleDiskSource, exampleService)
}
```
**Reference:**
- `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt`
- `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`
**Three-Layer Data Architecture:**
1. **Data Sources** - Raw data access (network, disk, SDK). Return `Result<T>`, never throw.
2. **Managers** - Single responsibility business logic. Wrap OS/external services.
3. **Repositories** - Aggregate sources/managers. Return domain-specific sealed classes.
**Critical Rules:**
-**NEVER** throw exceptions in data layer
-**ALWAYS** use interface + `...Impl` pattern
-**ALWAYS** inject interfaces, never implementations
- ✅ Data sources return `Result<T>`, repositories return domain sealed classes
- ✅ Use `StateFlow` for continuously observed data
---
### E. UI Components
**Use Existing Components First:**
The `:ui` module provides reusable `Bitwarden*` prefixed components. Search before creating new ones.
**Common Components:**
- `BitwardenFilledButton` - Primary action buttons
- `BitwardenOutlinedButton` - Secondary action buttons
- `BitwardenTextField` - Text input fields
- `BitwardenPasswordField` - Password input with show/hide
- `BitwardenSwitch` - Toggle switches
- `BitwardenTopAppBar` - Toolbar/app bar
- `BitwardenScaffold` - Screen container with scaffold
- `BitwardenBasicDialog` - Simple dialogs
- `BitwardenLoadingDialog` - Loading indicators
**Component Discovery:**
Search `ui/src/main/kotlin/com/bitwarden/ui/platform/components/` for existing `Bitwarden*` components. For build, test, and codebase discovery commands, use the **`build-test-verify`** skill.
**When to Create New Reusable Components:**
- Component used in 3+ places
- Component needs consistent theming across app
- Component has semantic meaning (accessibility)
- Component has complex state management
**New Component Requirements:**
- Prefix with `Bitwarden`
- Accept themed colors/styles from `BitwardenTheme`
- Include preview composables for testing
- Support accessibility (content descriptions, semantics)
**String Resources:**
New strings belong in the `:ui` module: `ui/src/main/res/values/strings.xml`
- Use typographic apostrophes and quotes to avoid escape characters: `youll` not `you\'ll`, `“word”` not `\"word\"`
- Reference strings via generated `BitwardenString` resource IDs
- Do not add strings to other modules unless explicitly instructed
---
### F. Security Patterns
**Encrypted vs Unencrypted Storage:**
**Template**: See [Security templates](templates.md#security-templates)
**Pattern Summary:**
```kotlin
class ExampleDiskSourceImpl(
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
) : BaseEncryptedDiskSource(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
),
ExampleDiskSource {
fun storeAuthToken(token: String) {
putEncryptedString(KEY_TOKEN, token) // Sensitive — uses base class method
}
fun storeThemePreference(isDark: Boolean) {
putBoolean(KEY_THEME, isDark) // Non-sensitive — uses base class method
}
}
```
**Android Keystore (Biometric Keys):**
- User-scoped encryption keys: `BiometricsEncryptionManager`
- Keys stored in Android Keystore (hardware-backed when available)
- Integrity validation on biometric state changes
**Input Validation:**
```kotlin
// Validation returns boolean, NEVER throws
interface RequestValidator {
fun validate(request: Request): Boolean
}
// Sanitization removes dangerous content
fun String?.sanitizeTotpUri(issuer: String?, username: String?): String? {
if (this.isNullOrBlank()) return null
// Sanitize and return safe value
}
```
**Security Checklist:**
- ✅ Use `@EncryptedPreferences` for credentials, keys, tokens
- ✅ Use `@UnencryptedPreferences` for UI state, preferences
- ✅ Use `@IgnoredOnParcel` for sensitive ViewModel state
-**NEVER** log sensitive data (passwords, tokens, vault items)
- ✅ Validate all user input before processing
- ✅ Use Timber for non-sensitive logging only
---
### G. Testing Patterns
**ViewModel Testing:**
**Template**: See [Testing templates](templates.md#testing-templates)
**Pattern Summary:**
```kotlin
class ExampleViewModelTest : BaseViewModelTest() {
private val mockRepository: ExampleRepository = mockk()
@Test
fun `ButtonClick should fetch data and update state`() = runTest {
val expectedResult = ExampleResult.Success(data = "test")
coEvery { mockRepository.fetchData(any()) } returns expectedResult
val viewModel = createViewModel()
viewModel.trySendAction(ExampleAction.ButtonClick)
viewModel.stateFlow.test {
assertEquals(EXPECTED_STATE.copy(data = "test"), awaitItem())
}
}
private fun createViewModel(): ExampleViewModel = ExampleViewModel(
savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to EXPECTED_STATE)),
repository = mockRepository,
)
}
```
**Reference:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`
**Key Testing Patterns:**
- ✅ Extend `BaseViewModelTest` for proper dispatcher management
- ✅ Use `runTest` from `kotlinx.coroutines.test`
- ✅ Use Turbine's `.test { awaitItem() }` for Flow assertions
- ✅ Use MockK: `coEvery` for suspend functions, `every` for sync
- ✅ Test both state changes and event emissions
- ✅ Test both success and failure Result paths
**Flow Testing with Turbine:**
```kotlin
// Test state and events simultaneously
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
viewModel.trySendAction(ExampleAction.Submit)
assertEquals(ExpectedState.Loading, stateFlow.awaitItem())
assertEquals(ExampleEvent.ShowSuccess, eventFlow.awaitItem())
}
```
**MockK Quick Reference:**
```kotlin
coEvery { repository.fetchData(any()) } returns Result.success("data") // Suspend
every { diskSource.getData() } returns "cached" // Sync
coVerify { repository.fetchData("123") } // Verify
```
---
### H. Clock/Time Handling
All code needing current time must inject `Clock` for testability.
**Key Requirements:**
- ✅ Inject `Clock` via Hilt in ViewModels
- ✅ Pass `Clock` as parameter in extension functions
- ✅ Use `clock.instant()` to get current time
- ❌ Never call `Instant.now()` or `DateTime.now()` directly
- ❌ Never use `mockkStatic` for datetime classes in tests
**Pattern Summary:**
```kotlin
// ViewModel with Clock
class MyViewModel @Inject constructor(
private val clock: Clock,
) {
val timestamp = clock.instant()
}
// Extension function with Clock parameter
fun State.getTimestamp(clock: Clock): Instant =
existingTime ?: clock.instant()
// Test with fixed clock
val FIXED_CLOCK = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
```
**Reference:**
- `docs/STYLE_AND_BEST_PRACTICES.md` (see Time and Clock Handling section)
- `core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt` (see `provideClock` function)
**Critical Gotchas:**
-`Instant.now()` creates hidden dependency, non-testable
-`mockkStatic(Instant::class)` is fragile, can leak between tests
-`Clock.fixed(...)` provides deterministic test behavior
---
## Bitwarden-Specific Anti-Patterns
**General anti-patterns are documented in CLAUDE.md.** This section covers violations specific to Bitwarden's State-Action-Event, navigation, and data layer patterns:
**NEVER update ViewModel state directly in coroutines**
- Post internal actions, update state synchronously in `handleAction()`
**NEVER inject `...Impl` classes**
- Only inject interfaces via Hilt
**NEVER create navigation without `@Serializable` routes**
- No string-based navigation, always type-safe
**NEVER use raw `Result<T>` in repositories**
- Use domain-specific sealed classes for better error handling
**NEVER make state classes without `@Parcelize`**
- All ViewModel state must survive process death
**NEVER skip `SavedStateHandle` persistence for ViewModels**
- Users lose form progress on process death
**NEVER forget `@IgnoredOnParcel` for passwords/tokens**
- Causes security vulnerability (sensitive data in parcel)
**NEVER use generic `Exception` catching**
- Catch specific exceptions only (`RemoteException`, `IOException`)
**NEVER call `Instant.now()` or `DateTime.now()` directly**
- Inject `Clock` via Hilt, use `clock.instant()` for testability
---
## Quick Reference
For build, test, and codebase discovery commands, use the **`build-test-verify`** skill.
**File Reference Format:**
When pointing to specific code, use: `file_path:line_number`
Example: `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)

View File

@@ -0,0 +1,644 @@
# Code Templates - Bitwarden Android
Copy-pasteable templates derived from actual codebase patterns. Replace `Example` with your feature name.
---
## ViewModel Template (State-Action-Event Pattern)
**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt`
### State Class
```kotlin
@Parcelize
data class ExampleState(
val isLoading: Boolean = false,
val data: String? = null,
@IgnoredOnParcel val sensitiveInput: String = "", // Sensitive data excluded from parcel
val dialogState: DialogState? = null,
) : Parcelable {
/**
* Dialog states for the Example screen.
*/
sealed class DialogState : Parcelable {
@Parcelize
data class Error(
val title: Text? = null,
val message: Text,
val error: Throwable? = null,
) : DialogState()
@Parcelize
data class Loading(val message: Text) : DialogState()
}
}
```
### Event Sealed Class
```kotlin
/**
* One-shot UI events for the Example screen.
*/
sealed class ExampleEvent {
data object NavigateBack : ExampleEvent()
data class ShowToast(val message: Text) : ExampleEvent()
}
```
### Action Sealed Class (with Internal)
```kotlin
/**
* User and system actions for the Example screen.
*/
sealed class ExampleAction {
data object BackClick : ExampleAction()
data object SubmitClick : ExampleAction()
data class InputChanged(val input: String) : ExampleAction()
data object ErrorDialogDismiss : ExampleAction()
/**
* Internal actions dispatched by the ViewModel from coroutines.
*/
sealed class Internal : ExampleAction() {
data class ReceiveDataState(
val dataState: DataState<ExampleData>,
) : Internal()
data class ReceiveDataResult(
val result: ExampleResult,
) : Internal()
}
}
```
### ViewModel
```kotlin
private const val KEY_STATE = "state"
/**
* ViewModel for the Example screen.
*/
@HiltViewModel
class ExampleViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val exampleRepository: ExampleRepository,
) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(
initialState = savedStateHandle[KEY_STATE]
?: run {
val args = savedStateHandle.toExampleArgs()
ExampleState(
data = args.itemId,
)
},
) {
init {
// Persist state for process death recovery
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
// Collect repository flows as internal actions
exampleRepository.dataFlow
.map { ExampleAction.Internal.ReceiveDataState(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: ExampleAction) {
when (action) {
ExampleAction.BackClick -> handleBackClick()
ExampleAction.SubmitClick -> handleSubmitClick()
ExampleAction.ErrorDialogDismiss -> handleErrorDialogDismiss()
is ExampleAction.InputChanged -> handleInputChanged(action)
is ExampleAction.Internal.ReceiveDataState -> {
handleReceiveDataState(action)
}
is ExampleAction.Internal.ReceiveDataResult -> {
handleReceiveDataResult(action)
}
}
}
private fun handleBackClick() {
sendEvent(ExampleEvent.NavigateBack)
}
private fun handleErrorDialogDismiss() {
mutableStateFlow.update { it.copy(dialogState = null) }
}
private fun handleSubmitClick() {
viewModelScope.launch {
val result = exampleRepository.submitData(state.data.orEmpty())
sendAction(ExampleAction.Internal.ReceiveDataResult(result))
}
}
private fun handleInputChanged(action: ExampleAction.InputChanged) {
mutableStateFlow.update { it.copy(sensitiveInput = action.input) }
}
private fun handleReceiveDataState(
action: ExampleAction.Internal.ReceiveDataState,
) {
when (action.dataState) {
is DataState.Loaded -> {
mutableStateFlow.update {
it.copy(
isLoading = false,
data = action.dataState.data.toString(),
)
}
}
is DataState.Loading -> {
mutableStateFlow.update { it.copy(isLoading = true) }
}
is DataState.Error -> {
mutableStateFlow.update {
it.copy(
isLoading = false,
dialogState = ExampleState.DialogState.Error(
message = BitwardenString.generic_error_message.asText(),
error = action.dataState.error,
),
)
}
}
else -> Unit
}
}
private fun handleReceiveDataResult(
action: ExampleAction.Internal.ReceiveDataResult,
) {
when (val result = action.result) {
is ExampleResult.Success -> {
mutableStateFlow.update {
it.copy(
isLoading = false,
data = result.data,
)
}
}
is ExampleResult.Error -> {
mutableStateFlow.update {
it.copy(
isLoading = false,
dialogState = ExampleState.DialogState.Error(
message = result.message?.asText()
?: BitwardenString.generic_error_message.asText(),
),
)
}
}
}
}
}
```
---
## Navigation Template (Type-Safe Routes)
**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt`
```kotlin
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.feature.example
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
/**
* Route for the Example screen.
*/
@Serializable
@OmitFromCoverage
data class ExampleRoute(
val itemId: String,
val isEditMode: Boolean = false,
)
/**
* Args extracted from [SavedStateHandle] for the Example screen.
*/
@OmitFromCoverage
data class ExampleArgs(
val itemId: String,
val isEditMode: Boolean,
)
/**
* Extracts [ExampleArgs] from the [SavedStateHandle].
*/
fun SavedStateHandle.toExampleArgs(): ExampleArgs {
val route = this.toRoute<ExampleRoute>()
return ExampleArgs(
itemId = route.itemId,
isEditMode = route.isEditMode,
)
}
/**
* Navigate to the Example screen.
*/
fun NavController.navigateToExample(
itemId: String,
isEditMode: Boolean = false,
navOptions: NavOptions? = null,
) {
this.navigate(
route = ExampleRoute(
itemId = itemId,
isEditMode = isEditMode,
),
navOptions = navOptions,
)
}
/**
* Add the Example screen destination to the navigation graph.
*/
fun NavGraphBuilder.exampleDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions<ExampleRoute> {
ExampleScreen(
onNavigateBack = onNavigateBack,
)
}
}
```
---
## Screen/Compose Template
**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt`
```kotlin
package com.x8bit.bitwarden.ui.feature.example
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* The Example screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExampleScreen(
onNavigateBack: () -> Unit,
viewModel: ExampleViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
ExampleEvent.NavigateBack -> onNavigateBack()
is ExampleEvent.ShowToast -> {
// Handle toast
}
}
}
// Dialogs
ExampleDialogs(
dialogState = state.dialogState,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) }
},
)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.example),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(ExampleAction.BackClick) }
},
)
},
) {
ExampleScreenContent(
state = state,
onInputChanged = remember(viewModel) {
{ viewModel.trySendAction(ExampleAction.InputChanged(it)) }
},
onSubmitClick = remember(viewModel) {
{ viewModel.trySendAction(ExampleAction.SubmitClick) }
},
modifier = Modifier
.fillMaxSize(),
)
}
}
```
---
## Data Layer Template (Repository + Hilt Module)
**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`
### Interface
```kotlin
/**
* Provides data operations for the Example feature.
*/
interface ExampleRepository {
/**
* Submits data and returns a typed result.
*/
suspend fun submitData(input: String): ExampleResult
/**
* Continuously observed data stream.
*/
val dataFlow: StateFlow<DataState<ExampleData>>
}
```
### Sealed Result Class
```kotlin
/**
* Domain-specific result for Example operations.
*/
sealed class ExampleResult {
data class Success(val data: String) : ExampleResult()
data class Error(val message: String?) : ExampleResult()
}
```
### Implementation
```kotlin
/**
* Default implementation of [ExampleRepository].
*/
class ExampleRepositoryImpl(
private val exampleDiskSource: ExampleDiskSource,
private val exampleService: ExampleService,
private val dispatcherManager: DispatcherManager,
) : ExampleRepository {
override val dataFlow: StateFlow<DataState<ExampleData>>
get() = // ...
override suspend fun submitData(input: String): ExampleResult {
return exampleService
.postData(input)
.fold(
onSuccess = { ExampleResult.Success(it.toModel()) },
onFailure = { ExampleResult.Error(it.message) },
)
}
}
```
### Hilt Module
```kotlin
@Module
@InstallIn(SingletonComponent::class)
object ExampleRepositoryModule {
@Provides
@Singleton
fun provideExampleRepository(
exampleDiskSource: ExampleDiskSource,
exampleService: ExampleService,
dispatcherManager: DispatcherManager,
): ExampleRepository = ExampleRepositoryImpl(
exampleDiskSource = exampleDiskSource,
exampleService = exampleService,
dispatcherManager = dispatcherManager,
)
}
```
---
## Security Templates
**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt` and `AuthDiskSourceImpl.kt`
### Encrypted Disk Source (Module)
```kotlin
@Module
@InstallIn(SingletonComponent::class)
object ExampleDiskModule {
@Provides
@Singleton
fun provideExampleDiskSource(
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): ExampleDiskSource = ExampleDiskSourceImpl(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
json = json,
)
}
```
### Encrypted Disk Source (Implementation)
```kotlin
/**
* Disk source for Example data using encrypted and unencrypted storage.
*/
class ExampleDiskSourceImpl(
encryptedSharedPreferences: SharedPreferences,
sharedPreferences: SharedPreferences,
private val json: Json,
) : BaseEncryptedDiskSource(
encryptedSharedPreferences = encryptedSharedPreferences,
sharedPreferences = sharedPreferences,
),
ExampleDiskSource {
private companion object {
const val ENCRYPTED_TOKEN_KEY = "exampleToken"
const val UNENCRYPTED_PREF_KEY = "examplePreference"
}
override var authToken: String?
get() = getEncryptedString(ENCRYPTED_TOKEN_KEY)
set(value) { putEncryptedString(ENCRYPTED_TOKEN_KEY, value) }
override var uiPreference: Boolean
get() = getBoolean(UNENCRYPTED_PREF_KEY) ?: false
set(value) { putBoolean(UNENCRYPTED_PREF_KEY, value) }
}
```
---
## Testing Templates
**Based on**: `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`
### ViewModel Test
```kotlin
class ExampleViewModelTest : BaseViewModelTest() {
// Mock dependencies
private val mockRepository = mockk<ExampleRepository>()
private val mutableDataFlow = MutableStateFlow<DataState<ExampleData>>(DataState.Loading)
@BeforeEach
fun setup() {
every { mockRepository.dataFlow } returns mutableDataFlow
}
@Test
fun `initial state should be correct when there is no saved state`() {
val viewModel = createViewModel(state = null)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when there is a saved state`() {
val savedState = DEFAULT_STATE.copy(data = "saved")
val viewModel = createViewModel(state = savedState)
assertEquals(savedState, viewModel.stateFlow.value)
}
@Test
fun `SubmitClick should call repository and update state on success`() = runTest {
val expected = ExampleResult.Success(data = "result")
coEvery { mockRepository.submitData(any()) } returns expected
val viewModel = createViewModel()
viewModel.stateFlow.test {
// Initial state
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(ExampleAction.SubmitClick)
// Updated state after result
assertEquals(
DEFAULT_STATE.copy(data = "result", isLoading = false),
awaitItem(),
)
}
}
@Test
fun `SubmitClick should show error dialog on failure`() = runTest {
val expected = ExampleResult.Error(message = "Network error")
coEvery { mockRepository.submitData(any()) } returns expected
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(ExampleAction.SubmitClick)
val errorState = awaitItem()
assertTrue(errorState.dialogState is ExampleState.DialogState.Error)
}
}
@Test
fun `BackClick should emit NavigateBack event`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(ExampleAction.BackClick)
assertEquals(ExampleEvent.NavigateBack, awaitItem())
}
}
// Helper to create ViewModel with optional saved state
private fun createViewModel(
state: ExampleState? = DEFAULT_STATE,
): ExampleViewModel = ExampleViewModel(
savedStateHandle = SavedStateHandle(
mapOf(KEY_STATE to state),
),
exampleRepository = mockRepository,
)
companion object {
private val DEFAULT_STATE = ExampleState(
isLoading = false,
data = null,
)
}
}
```
### Flow Testing with stateEventFlow
```kotlin
@Test
fun `SubmitClick should update state and emit event`() = runTest {
coEvery { mockRepository.submitData(any()) } returns ExampleResult.Success("data")
val viewModel = createViewModel()
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
viewModel.trySendAction(ExampleAction.SubmitClick)
// Assert state change
assertEquals(
DEFAULT_STATE.copy(data = "data"),
stateFlow.awaitItem(),
)
// Assert event emission
assertEquals(
ExampleEvent.ShowToast("Success".asText()),
eventFlow.awaitItem(),
)
}
}
```

View File

@@ -0,0 +1,40 @@
---
name: labeling-android-changes
version: 0.1.0
description: Conventional commit type keywords for PR titles and commit messages. Use when determining the change type for commits or PRs. Triggered by "what type", "label", "change type", "conventional commit", "t: label".
---
# Labeling Changes
PR titles and commit messages must include a conventional commit type keyword. This keyword drives automatic `t:` label assignment via CI (`.github/workflows/sdlc-label-pr.yml`).
## Format
The type keyword appears after the Jira ticket prefix:
```
[PM-XXXXX] <type>: <imperative summary>
```
## Type Keywords
| Type | Label | Use for |
|------|-------|---------|
| `feat` | `t:feature` | New features or functionality |
| `fix` | `t:bug` | Bug fixes |
| `refactor` | `t:tech-debt` | Code restructuring without behavior change |
| `chore` | `t:tech-debt` | Maintenance, cleanup, minor tweaks |
| `test` | `t:tech-debt` | Adding or updating tests |
| `perf` | `t:tech-debt` | Performance improvements |
| `docs` | `t:docs` | Documentation changes |
| `ci` / `build` | `t:ci` | CI/CD and build system changes |
| `deps` | `t:deps` | Dependency updates |
| `llm` | `t:llm` | LLM/Claude configuration changes |
| `breaking` | `t:breaking-change` | Breaking changes requiring migration |
| `misc` | `t:misc` | Changes that do not fit other categories |
## Selecting a Type
Infer the type from the task description and changes made. **If the type cannot be confidently determined, ask the user.**
The CI labeling script matches `<type>:` or `<type>(` in the lowercased PR title, so the keyword must be followed by a colon or parenthesis. CI also accepts additional aliases (e.g., `revert`, `bugfix`, `cleanup`). See `.github/label-pr.json` for the full mapping.

View File

@@ -0,0 +1,37 @@
---
name: perform-android-preflight-checklist
version: 0.1.0
description: Quality gate checklist to run before committing or creating a PR. Use when finishing implementation, checking work quality, or preparing to commit. Triggered by "self review", "check my work", "ready to commit", "done implementing", "review checklist", "quality check".
---
# Self-Review Checklist
Run through this checklist before committing or opening a PR.
## Tests
- [ ] Tests pass with correct flavor: `./gradlew app:testStandardDebugUnitTest`
- [ ] New code has corresponding test coverage
- [ ] Tests for affected modules also pass (`:core:test`, `:data:test`, etc.)
## Code Quality
- [ ] Lint/detekt clean: `./gradlew detekt`
- [ ] No unintended file changes (`git diff` review)
- [ ] KDoc on all new public APIs
- [ ] No TODO comments left behind (or they reference a ticket)
## Security
- [ ] No plaintext keys, tokens, or secrets in code
- [ ] User input validated before processing
- [ ] Sensitive data uses encrypted storage patterns
- [ ] No logging of sensitive data (passwords, keys, tokens)
## Bitwarden Patterns
- [ ] String resources in `:ui` module with typographic quotes
- [ ] Navigation route is `@Serializable` and registered in graph
- [ ] New implementations have Hilt `@Binds` or `@Provides` in a module
- [ ] ViewModel extends `BaseViewModel<S, E, A>` with proper state persistence
- [ ] Async results mapped through internal actions (not direct state updates)
## Files
- [ ] No accidental `.idea/`, build output, or generated files staged
- [ ] No credential files or `.env` files included

View File

@@ -0,0 +1,191 @@
---
name: planning-android-implementation
version: 0.1.0
description: Architecture design and phased implementation planning for Bitwarden Android. Use when planning implementation, designing architecture, creating file inventories, or breaking features into phases. Triggered by "plan implementation", "architecture design", "implementation plan", "break this into phases", "what files do I need", "design the architecture".
---
# Implementation Planning
This skill takes a refined specification (ideally from the `refining-android-requirements` skill) and produces a phased implementation plan with architecture design, file inventory, and risk assessment.
**Prerequisite**: A clear set of requirements. If requirements are vague or incomplete, invoke the `refining-android-requirements` skill first.
---
## Step 1: Classify Change
Determine the change type to guide scope and planning depth:
| Type | Description | Typical Scope |
|------|-------------|---------------|
| **New Feature** | Entirely new functionality, screens, or flows | New files + modifications, multi-phase |
| **Enhancement** | Extending existing feature with new capabilities | Mostly modifications, 1-2 phases |
| **Bug Fix** | Correcting incorrect behavior | Targeted modifications, single phase |
| **Refactoring** | Restructuring without behavior change | Modifications only, migration-aware |
| **Infrastructure** | Build, CI, tooling, or dependency changes | Config files, minimal code changes |
State the classification and rationale before proceeding.
---
## Step 2: Codebase Exploration
Search the codebase to find reference implementations and integration points. Use the discovery commands from the `build-test-verify` skill as needed.
### Find Pattern Anchors
Identify 2-3 existing files that serve as templates for the planned work:
```
**Pattern Anchors:**
1. [file path] — [why this is a good reference]
2. [file path] — [why this is a good reference]
3. [file path] — [why this is a good reference]
```
### Map Integration Points
Identify files that must be modified to integrate the new work:
- **Navigation**: Nav graph registrations, route definitions
- **Dependency Injection**: Hilt modules, `@Provides` / `@Binds` functions
- **Data Layer**: Repository interfaces, data source interfaces, Room DAOs
- **API Layer**: Retrofit service interfaces, request/response models
- **Feature Flags**: Feature flag definitions and checks
- **Managers**: Single-responsibility data layer classes (see `docs/ARCHITECTURE.md` Managers section)
- **Test Fixtures**: Shared test utilities in `src/testFixtures/` directories
- **Product Flavor Source Sets**: Code in `src/standard/` vs `src/main/` for Play Services dependencies
### Document Existing Patterns
Note the specific patterns used by the pattern anchors:
- State class structure (sealed class, data class fields)
- Action/Event naming conventions
- Repository method signatures and return types
- Test structure and assertion patterns
---
## Step 3: Architecture Design
Produce an ASCII diagram showing component relationships for the planned work:
```
┌─────────────────┐
│ Screen │ ← Compose UI
│ (Composable) │
└────────┬────────┘
│ State / Action / Event
┌────────▼────────┐
│ ViewModel │ ← Business logic orchestration
└────────┬────────┘
│ Repository calls
┌────────▼────────┐
│ Repository │ ← Data coordination (sealed class results)
└───┬────┬────┬───┘
│ │ │
┌───▼───┐ │ ┌─▼──────┐
│Manager│ │ │Manager │ ← Single-responsibility (optional)
└───┬───┘ │ └─┬──────┘
│ │ │
┌───▼─────▼───▼────┐
│ Data Sources │ ← Raw data (Result<T>, never throw)
└─┬────┬────┬──────┘
│ │ │
Room Retrofit SDK
```
Adapt the diagram to show the actual components planned. _Consult `docs/ARCHITECTURE.md` for full data layer patterns and conventions._
### Design Decisions
Document key architectural decisions in a table:
| Decision | Resolution | Rationale |
|----------|-----------|-----------|
| [What needed deciding] | [What was chosen] | [Why] |
---
## Step 4: File Inventory
### Files to Create
| File Path | Type | Pattern Reference |
|-----------|------|-------------------|
| [full path] | [ViewModel / Screen / Repository / etc.] | [pattern anchor file] |
**Include in file inventory:**
- `...Navigation.kt` files for new screens
- `...Module.kt` Hilt module files for new DI bindings
- Paired test files (`...Test.kt`) for each new class
### Files to Modify
| File Path | Change Description | Risk Level |
|-----------|-------------------|------------|
| [full path] | [what changes] | Low / Medium / High |
**Risk levels:**
- **Low**: Additive changes (new entries in nav graph, new bindings in Hilt module)
- **Medium**: Modifying existing logic (adding parameters, new branches)
- **High**: Changing interfaces, data models, or shared utilities
---
## Step 5: Implementation Phases
Break the work into sequential phases. Each phase should be independently testable and committable.
**Phase ordering principle**: Foundation → SDK/Data → Network → UI (tests accompany each phase)
For each phase:
```markdown
### Phase N: [Name]
**Goal**: [What this phase accomplishes]
**Files**:
- Create: [list]
- Modify: [list]
**Tasks**:
1. [Specific implementation task]
2. [Specific implementation task]
3. ...
**Verification**:
- [Test command or manual verification step]
**Skills**: [Which workflow skills apply — e.g., `implementing-android-code`, `testing-android-code`]
```
### Phase Guidelines
- Each phase should be small enough to be independently testable and committable
- Tests are written within the same phase as the code they verify (not deferred to a "testing phase")
- UI phases come after their data dependencies are in place
- If a phase has more than 5 tasks, consider splitting it
---
## Step 6: Risk & Verification
### Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| [What could go wrong] | Low/Med/High | Low/Med/High | [How to prevent or handle] |
### Verification Plan
**Automated Verification:**
- Unit test commands (from `build-test-verify` skill)
- Lint/detekt commands
- Build verification
**Manual Verification:**
- [Specific manual test scenarios]
- [Edge cases to manually verify]
- Verify ViewModel state survives process death (test via `SavedStateHandle` persistence and `Don't keep activities` developer option)

View File

@@ -0,0 +1,181 @@
---
name: refining-android-requirements
version: 0.1.0
description: Requirements gap analysis and structured specification for Bitwarden Android. Use when refining requirements, analyzing specs, identifying gaps, or producing structured specifications from tickets or descriptions. Triggered by "refine requirements", "gap analysis", "spec review", "requirements analysis", "what's missing from this spec", "analyze this ticket".
---
# Requirements Refinement
This skill takes raw requirements (from Jira tickets, Confluence pages, or free-text descriptions) and produces a structured, implementation-ready specification through systematic gap analysis.
**Key principle**: This skill identifies gaps and produces specifications. It does NOT propose solutions or architecture — that is the responsibility of the `planning-android-implementation` skill.
---
## Step 1: Source Consolidation
Combine all input sources into a single working document. For each requirement, note its source:
```
- [Source: PM-12345] User must be able to configure timeout
- [Source: Confluence] Timeout range is 1-60 minutes
- [Source: User] Default timeout should be 15 minutes
```
Flag any contradictions between sources for immediate resolution.
---
## Step 2: Gap Analysis
Evaluate the consolidated requirements against the following 5-category rubric. For each category, check every item and note whether it is **covered**, **partially covered**, or **missing**.
### A. Functional Requirements
| Check | Question to Ask If Missing |
|-------|---------------------------|
| User actions defined? | What specific user actions trigger this feature? |
| All states covered? (empty, loading, error, success) | What should the user see in [empty/loading/error] state? |
| Edge cases identified? | What happens when [boundary condition]? |
| Cancellation/back navigation flows? | Can the user cancel mid-flow? What happens to partial data? |
| Input validation rules? | What are the valid ranges/formats for [input]? |
| Success/failure criteria? | How does the user know the operation succeeded or failed? |
| Offline behavior? | What happens if this is attempted offline? |
### B. Technical Requirements
| Check | Question to Ask If Missing |
|-------|---------------------------|
| Module scope identified? (`:app`, `:authenticator`, shared) | Which module(s) does this feature belong to? |
| SDK dependencies? | Does this require Bitwarden SDK operations? Which ones? |
| Data storage approach? (Room, DataStore, in-memory) | Where is the data for this feature persisted? |
| Network API endpoints? | Which API endpoints are involved? Are they existing or new? |
| Process death handling? | What state needs to survive process death? |
| Migration requirements? | Does existing data need migration? |
| Feature flag needed? | Should this be behind a feature flag for staged rollout? |
| Product flavors (standard vs fdroid)? | Does this feature depend on Google Play Services? Available on F-Droid? |
| Data layer tier? | Does this need a new Manager (single-responsibility) or only Repository/DataSource? Consult `docs/ARCHITECTURE.md` Data Layer section. |
| Streaming vs discrete data? | Is data continuously observed (`DataState<T>` + `StateFlow`) or a one-shot operation (custom sealed class)? See `docs/ARCHITECTURE.md` Repositories section. |
### C. Security Requirements
| Check | Question to Ask If Missing |
|-------|---------------------------|
| Data sensitivity classified? | What sensitivity level does this data have? (vault-level, account-level, non-sensitive) |
| Storage encryption required? | Must this data be encrypted at rest? Via SDK or Android Keystore? |
| Logout cleanup behavior? | What must be cleared when the user logs out? |
| Auth-gating? | Does accessing this feature require active authentication? |
| Input sanitization? | Are there URL or credential inputs that need validation? |
| Sensitive data in ViewModel state? | Will passwords, tokens, or keys appear in state? Must use `@IgnoredOnParcel`. See `implementing-android-code` skill Section F. |
| SDK crypto context isolation? | Does this use vault encryption? Must use `ScopedVaultSdkSource` for multi-account safety. See CLAUDE.md Security Rules. |
### D. UX/UI Requirements
| Check | Question to Ask If Missing |
|-------|---------------------------|
| UI copy/strings defined? | What text should appear for [label/button/message]? |
| Error messages specified? | What should the error message say when [failure case]? |
| Loading states designed? | Should loading show a spinner, skeleton, or shimmer? |
| Navigation flow clear? | Where does the user go after [action]? Back stack behavior? |
| Accessibility considerations? | Are there content descriptions or focus order requirements? |
| Toast/snackbar/dialog for feedback? | What feedback mechanism for [action result]? |
### E. Cross-Cutting Concerns
| Check | Question to Ask If Missing |
|-------|---------------------------|
| Multi-account behavior? | How does this behave with multiple accounts? Per-account or global? |
| Backwards compatibility? | Does this affect existing users? Migration path? |
| Feature flag strategy? | Is this behind a server-side or local feature flag? |
| Analytics/logging? | Are there analytics events to track? |
| Bitwarden Authenticator impact? | Does this affect the `:authenticator` module? |
| F-Droid compatibility? | Does this degrade gracefully without Google Play Services (no push notifications, no Play Integrity)? |
---
## Step 3: Present Gaps
Organize all identified gaps into two categories:
### Blocking Questions
Questions that **must** be answered before implementation can begin because they change the architecture, data model, or core flow.
Format each question as:
```
**G[N]** ([Category]) — [Question text]
Context: [Why this matters / what depends on the answer]
```
### Non-Blocking Questions
Questions that have **reasonable defaults** and can be resolved during implementation. Note the assumed default.
Format each question as:
```
**G[N]** ([Category]) — [Question text]
Default assumption: [What we'll assume if not answered]
Context: [Why this matters]
```
---
## Step 4: Produce Specification
After the user answers blocking questions (and optionally non-blocking ones), produce a structured specification:
```markdown
## Overview
[1-2 paragraph summary of the feature, its purpose, and scope]
## Functional Requirements
| ID | Requirement | Source | Notes |
|----|------------|--------|-------|
| FR1 | [requirement] | [source] | [any notes] |
| FR2 | ... | ... | ... |
## Technical Requirements
| ID | Requirement | Source | Notes |
|----|------------|--------|-------|
| TR1 | [requirement] | [source] | [any notes] |
| TR2 | ... | ... | ... |
## Security Requirements
| ID | Requirement | Source | Notes |
|----|------------|--------|-------|
| SR1 | [requirement] | [source] | [any notes] |
## UX Requirements
| ID | Requirement | Source | Notes |
|----|------------|--------|-------|
| UX1 | [requirement] | [source] | [any notes] |
## Open Items
Non-blocking items with assumed defaults that may be revisited:
| ID | Question | Assumed Default | Category |
|----|----------|----------------|----------|
| G[N] | [question] | [default] | [category] |
## Source Documentation
| Source | Type | Link |
|--------|------|------|
| [name] | Jira / Confluence / User-provided | [link if available] |
```
### Output Guidelines
- Requirements use numbered IDs (FR1, TR1, SR1, UX1) for traceability through implementation
- Each requirement cites its source (ticket, page, or user-provided)
- Technical requirements use table format for structured key/value data
- Interface signatures are included as fenced code blocks when applicable
- Open items preserve the gap ID (G[N]) for cross-referencing

View File

@@ -0,0 +1,98 @@
---
name: reviewing-changes
version: 3.0.0
description: Guides Android code reviews with type-specific checklists and MVVM/Compose pattern validation. Use when reviewing Android PRs, pull requests, diffs, or local changes involving Kotlin, ViewModel, Composable, Repository, or Gradle files. Triggered by "review PR", "review changes", "check this code", "Android review", or code review requests mentioning bitwarden/android. Loads specialized checklists for feature additions, bug fixes, UI refinements, refactoring, dependency updates, and infrastructure changes.
---
# Reviewing Changes - Android Additions
This skill provides Android-specific workflow additions that complement the base `bitwarden-code-reviewer` agent standards.
## Instructions
**IMPORTANT**: Use structured thinking throughout your review process. Plan your analysis in `<thinking>` tags before providing final feedback.
### Step 1: Retrieve Additional Details
<thinking>
Determine if more context is available for the changes:
1. Are there JIRA tickets or GitHub Issues mentioned in the PR title or body?
2. Are there other GitHub pull requests mentioned in the PR title or body?
</thinking>
Retrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).
If pull request title and message do not provide enough context, request additional details from the reviewer:
- Link a JIRA ticket
- Associate a GitHub issue
- Link to another pull request
- Add more detail to the PR title or body
### Step 2: Detect Change Type with Android Refinements
<thinking>
Analyze the changeset systematically:
1. What files were modified? (code vs config vs docs)
2. What is the PR/commit title indicating?
3. Is there new functionality or just modifications?
4. What's the risk level of these changes?
</thinking>
Use the base change type detection from the agent, with Android-specific refinements:
**Android-specific patterns:**
- **Feature Addition**: New `ViewModel`, new `Repository`, new `@Composable` functions, new `*Screen.kt` files
- **UI Refinement**: Changes only in `*Screen.kt`, `*Composable.kt`, `ui/` package files
- **Infrastructure**: Changes to `.github/workflows/`, `gradle/`, `build.gradle.kts`, `libs.versions.toml`
- **Dependency Update**: Changes only to `libs.versions.toml` or `build.gradle.kts` with version bumps
### Step 3: Load Appropriate Checklist
Based on detected type, read the relevant checklist file:
- **Dependency Update** → `checklists/dependency-update.md` (expedited review)
- **Bug Fix** → `checklists/bug-fix.md` (focused review)
- **Feature Addition** → `checklists/feature-addition.md` (comprehensive review)
- **UI Refinement** → `checklists/ui-refinement.md` (design-focused review)
- **Refactoring** → `checklists/refactoring.md` (pattern-focused review)
- **Infrastructure** → `checklists/infrastructure.md` (tooling-focused review)
The checklist provides:
- Multi-pass review strategy
- Type-specific focus areas
- What to check and what to skip
- Structured thinking guidance
### Step 4: Execute Review Following Checklist
<thinking>
Before diving into details:
1. What are the highest-risk areas of this change?
2. Which architectural patterns need verification?
3. What security implications exist?
4. How should I prioritize my findings?
5. What tone is appropriate for this feedback?
</thinking>
Follow the checklist's multi-pass strategy, thinking through each pass systematically.
### Step 5: Consult Android Reference Materials As Needed
Load reference files only when needed for specific questions:
- **Issue prioritization** → `reference/priority-framework.md` (Critical vs Suggested vs Optional)
- **Phrasing feedback** → `reference/review-psychology.md` (questions vs commands, I-statements)
- **Architecture questions** → `reference/architectural-patterns.md` (MVVM, Hilt DI, module org, error handling)
- **Security questions (quick reference)** → `reference/security-patterns.md` (common patterns and anti-patterns)
- **Security questions (comprehensive)** → `docs/ARCHITECTURE.md#security` (full zero-knowledge architecture)
- **Testing questions** → `reference/testing-patterns.md` (unit tests, mocking, null safety)
- **UI questions** → `reference/ui-patterns.md` (Compose patterns, theming)
- **Style questions** → `docs/STYLE_AND_BEST_PRACTICES.md`
## Core Principles
- **Appropriate depth**: Match review rigor to change complexity and risk
- **Specific references**: Always use `file:line_number` format for precise location
- **Actionable feedback**: Say what to do and why, not just what's wrong
- **Efficient reviews**: Use multi-pass strategy, skip what's not relevant
- **Android patterns**: Validate MVVM, Hilt DI, Compose conventions, Kotlin idioms

View File

@@ -0,0 +1,164 @@
# Bug Fix Review Checklist
## Multi-Pass Strategy
### First Pass: Understand the Bug
<thinking>
Before evaluating the fix:
1. What was the original bug/broken behavior?
2. What is the expected correct behavior?
3. What was the root cause?
4. How was the bug discovered? (user report, test, production)
5. What's the severity? (crash, data loss, UI glitch, minor annoyance)
</thinking>
**1. Understand root cause:**
- What was the broken behavior?
- What caused it?
- How does this fix address the root cause?
**2. Assess scope:**
- How many files changed?
- Is this a targeted fix or broader refactoring?
- Does this affect multiple features?
**3. Check for side effects:**
- Could this break other features?
- Are there edge cases not considered?
### Second Pass: Verify the Fix
<thinking>
Evaluate the fix systematically:
1. Does this fix address the root cause or just symptoms?
2. Are there edge cases not covered?
3. Could this break other functionality?
4. Is the fix localized or does it ripple through the codebase?
5. How do we prevent this bug from returning?
</thinking>
**4. Code changes:**
- Does the fix make sense?
- Is it the simplest solution?
- Any unnecessary changes included?
**5. Testing:**
- Is there a regression test?
- Does test verify the bug is fixed?
- Are edge cases covered?
**6. Related code:**
- Same pattern in other places that might have same bug?
- Should other similar code be fixed too?
## What to CHECK
**Root Cause Analysis**
- Does the fix address the root cause or just symptoms?
- Is the explanation in PR/commit clear?
**Regression Testing**
- Is there a new test that would fail without this fix?
- Does test cover the reported bug scenario?
- Are related edge cases tested?
**Side Effects**
- Could this break existing functionality?
- Are there similar code paths that need checking?
- Does this change behavior in unexpected ways?
**Fix Scope**
- Is the fix appropriately scoped (not too broad, not too narrow)?
- Are all instances of the bug fixed?
- Any related bugs discovered during investigation?
## What to SKIP
**Full Architecture Review** - Unless fix reveals architectural problems
**Comprehensive Testing Review** - Focus on regression tests, not entire test suite
**Major Refactoring Suggestions** - Unless directly related to preventing similar bugs
## Red Flags
🚩 **No test for the bug** - How will we prevent regression?
🚩 **Fix doesn't match root cause** - Is this fixing symptoms?
🚩 **Broad changes beyond the bug** - Should this be split into separate PRs?
🚩 **Similar patterns elsewhere** - Should those be fixed too?
## Key Questions to Ask
Use `reference/review-psychology.md` for phrasing:
- "Can we add a test that would fail without this fix?"
- "I see this pattern in [other file] - does it have the same issue?"
- "Is this fixing the root cause or masking the symptom?"
- "Could this change affect [related feature]?"
## Prioritizing Findings
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
## Output Format
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
```markdown
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [One-line summary of each critical blocking issue with file:line reference]
See inline comments for all issue details.
```
## Example Review
```markdown
**Overall Assessment:** APPROVE
See inline comments for suggested improvements.
```
**Inline comment examples:**
```
**data/auth/BiometricRepository.kt:120** - SUGGESTED: Extract null handling
<details>
<summary>Details</summary>
Root cause analysis: BiometricPrompt result was nullable but code assumed non-null, causing crash on cancellation (PM-12345).
Consider extracting null handling pattern:
\```kotlin
private fun handleBiometricResult(result: BiometricPrompt.AuthenticationResult?): AuthResult {
return result?.let { AuthResult.Success(it) } ?: AuthResult.Cancelled
}
\```
This pattern could be reused if we add other biometric auth points.
</details>
```
```
**app/auth/BiometricViewModel.kt:89** - SUGGESTED: Add regression test
<details>
<summary>Details</summary>
Add test for cancellation scenario to prevent regression:
\```kotlin
@Test
fun `when biometric cancelled then returns cancelled state`() = runTest {
coEvery { repository.authenticate() } returns Result.failure(CancelledException())
viewModel.onBiometricAuth()
assertEquals(AuthState.Cancelled, viewModel.state.value)
}
\```
This prevents regression of the bug just fixed.
</details>
```

View File

@@ -0,0 +1,166 @@
# Dependency Update Review Checklist
## Multi-Pass Strategy
### First Pass: Identify and Assess
<thinking>
Before diving into details:
1. Which dependencies were updated?
2. What are the version changes? (patch, minor, major)
3. Are any security-sensitive libraries involved? (crypto, auth, networking)
4. Any pre-release versions (alpha, beta, RC)?
5. What's the blast radius if something breaks?
</thinking>
**1. Identify the change:**
- Which library? Old version → New version?
- Major (X.0.0), Minor (0.X.0), or Patch (0.0.X) version change?
- Single dependency or multiple?
**2. Check compilation safety:**
- Any imports in codebase that might break?
- Any deprecated APIs we're currently using?
- Check if this is a breaking change version
### Second Pass: Deep Analysis
<thinking>
For each dependency update:
1. What changes are in this release?
2. Are there breaking changes?
3. Are there security fixes?
4. Do we use the affected APIs?
5. How does this affect our codebase?
</thinking>
**3. Review release notes** (if available):
- Breaking changes mentioned?
- Security fixes included?
- New features we should know about?
- Deprecations that affect our usage?
**4. Verify consistency:**
- If updating androidx library, are related libraries updated consistently?
- BOM (Bill of Materials) consistency if applicable?
- Test dependencies updated alongside main dependencies?
## What to CHECK
**Compilation Safety**
- Look for API deprecations in our codebase
- Check if import statements still valid
- Major version bumps require extra scrutiny
- Beta/alpha versions need stability assessment
**Security Implications** (if applicable)
- Security-related libraries (crypto, auth, networking)?
- Check for CVEs addressed in release notes
- Review security advisories for this library
**Testing Implications**
- Does this affect test utilities?
- Are there breaking changes in test APIs?
- Do existing tests still cover the same scenarios?
**Changelog Review**
- Read release notes for breaking changes
- Note any behavioral changes
- Check migration guides if major version
## What to SKIP
**Full Architecture Review** - No code changed, patterns unchanged
**Code Style Review** - No code to review
**New Test Requirements** - Unless API changed significantly
**Security Deep-Dive** - Unless crypto/auth/networking library
**Performance Analysis** - Unless release notes mention performance changes
## Red Flags (Escalate to Full Review)
🚩 **Major version bump** (e.g., 1.x → 2.0) - Read `checklists/feature-addition.md`
🚩 **Security/crypto library** - Read `reference/architectural-patterns.md` and `docs/ARCHITECTURE.md#security`
🚩 **Breaking changes in release notes** - Read relevant code sections carefully
🚩 **Multiple dependency updates at once** - Check for interaction risks
🚩 **Beta/Alpha versions** - Assess stability concerns and rollback plan
If any red flags present, escalate to more comprehensive review using appropriate checklist.
## Prioritizing Findings
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
## Output Format
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
```markdown
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [One-line summary of each critical blocking issue with file:line reference]
See inline comments for all issue details.
```
## Example Reviews
### Example 1: Simple Patch Version (No Critical Issues)
```markdown
**Overall Assessment:** APPROVE
See inline comments for all issue details.
```
**Inline comment example:**
```
**libs.versions.toml:45** - SUGGESTED: Beta version in production
<details>
<summary>Details</summary>
androidx.credentials updated from 1.5.0 to 1.6.0-beta03
Monitor for stability issues - beta releases may have unexpected behavior in production.
Changelog: Adds support for additional credential types, internal bug fixes.
</details>
```
### Example 2: Major Version with Breaking Changes (With Critical Issues)
```markdown
**Overall Assessment:** REQUEST CHANGES
**Critical Issues:**
- Breaking API changes in Retrofit 3.0.0 (network/api/BitwardenApiService.kt)
- Breaking API changes in Retrofit 3.0.0 (network/api/VaultApiService.kt)
See inline comments for migration details.
```
**Inline comment example:**
```
**network/api/BitwardenApiService.kt:15** - CRITICAL: Breaking API changes
<details>
<summary>Details and fix</summary>
Retrofit 3.0.0 removes `Call<T>` return type. Migration required:
\```kotlin
// Before
fun getUser(): Call<UserResponse>
// After
suspend fun getUser(): Response<UserResponse>
\```
Update all API service interfaces to use suspend functions, update call sites to use coroutines instead of enqueue/execute, and update tests accordingly.
Consider creating a separate PR for this migration due to scope.
Reference: https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-300
</details>
```

View File

@@ -0,0 +1,380 @@
# Feature Addition Review Checklist
## Multi-Pass Strategy
### First Pass: High-Level Assessment
<thinking>
Before diving into details:
1. What is this feature supposed to do?
2. How does it fit into the existing architecture?
3. What are the security implications?
4. What's the scope? (files touched, modules affected)
5. What are the highest-risk areas?
</thinking>
**1. Understand the feature:**
- Read PR description - what problem does this solve?
- Identify user-facing changes vs internal changes
- Note any security implications (auth, encryption, data handling)
**2. Scan file structure:**
- Which modules affected? (app, data, network, ui, core?)
- Are files organized correctly per module structure?
- Any new public APIs introduced?
**3. Initial risk assessment:**
- Does this touch sensitive data or security-critical paths?
- Does this affect existing features or only add new ones?
- Are there obvious compilation or null safety issues?
### Second Pass: Architecture Deep-Dive
<thinking>
Verify architectural integrity:
1. Does this follow MVVM + UDF pattern?
2. Is Hilt DI used correctly?
3. Is state management proper (StateFlow, immutability)?
4. Are modules organized correctly?
5. Is error handling robust (Result types)?
</thinking>
**4. MVVM + UDF Pattern Compliance:**
- ViewModels properly structured?
- State management using StateFlow?
- Business logic in correct layer?
**5. Dependency Injection:**
- Hilt DI used correctly?
- Dependencies injected, not manually instantiated?
- Proper scoping applied?
**6. Module Organization:**
- Code placed in correct modules?
- No circular dependencies introduced?
- Proper separation of concerns?
**7. Error Handling:**
- Using Result types, not exception-based handling?
- Errors propagated correctly through layers?
### Third Pass: Details and Quality
<thinking>
Check quality and completeness:
1. Is code quality high? (null safety, documentation, naming)
2. Are tests comprehensive? (unit + integration)
3. Are there edge cases not covered?
4. Is documentation clear?
5. Are there any code smells or anti-patterns?
</thinking>
**8. Testing:**
- Unit tests for ViewModels and repositories?
- Test coverage for edge cases and error scenarios?
- Tests verify behavior, not implementation?
**9. Code Quality:**
- Null safety handled properly?
- Public APIs have KDoc documentation?
- Naming follows project conventions?
**10. Security:**
- Sensitive data encrypted properly?
- Authentication/authorization handled correctly?
- Zero-knowledge architecture preserved?
## Architecture Review
### MVVM Pattern Compliance
Read `reference/architectural-patterns.md` for detailed patterns.
**ViewModels must:**
- Use `@HiltViewModel` annotation
- Use `@Inject constructor`
- Expose `StateFlow<T>`, NOT `MutableStateFlow<T>` publicly
- Delegate business logic to Repository/Manager
- Avoid direct Android framework dependencies (except ViewModel, SavedStateHandle)
**Common Violations:**
```kotlin
// ❌ BAD - Exposes mutable state
class FeatureViewModel @Inject constructor() : ViewModel() {
val state: MutableStateFlow<State> = MutableStateFlow(State.Initial)
}
// ✅ GOOD - Exposes immutable state
class FeatureViewModel @Inject constructor() : ViewModel() {
private val _state = MutableStateFlow<State>(State.Initial)
val state: StateFlow<State> = _state.asStateFlow()
}
// ❌ BAD - Business logic in ViewModel
fun onSubmit() {
val encrypted = encryptionManager.encrypt(password) // Should be in Repository
_state.value = State.Success
}
// ✅ GOOD - Business logic in Repository, state updated via internal event
fun onSubmit() {
viewModelScope.launch {
// The result of the async operation is captured
val result = repository.submitData(password)
// A single event is sent with the result, not updating state directly
sendAction(FeatureAction.Internal.SubmissionComplete(result))
}
}
// The ViewModel has a handler that processes the internal event
private fun handleInternalAction(action: FeatureAction.Internal) {
when (action) {
is FeatureAction.Internal.SubmissionComplete -> {
// The event handler evaluates the result and updates state
action.result.fold(
onSuccess = { _state.value = State.Success },
onFailure = { _state.value = State.Error(it) }
)
}
}
}
```
**UI Layer must:**
- Only observe state, never modify
- Pass user actions as events to ViewModel
- Contain no business logic
- Use existing UI components from `:ui` module where possible
### Hilt Dependency Injection
Reference: `docs/ARCHITECTURE.md#dependency-injection`
**Required Patterns:**
- ViewModels: `@HiltViewModel` + `@Inject constructor`
- Repositories: `@Inject constructor` on implementation
- Inject interfaces, not concrete implementations
- Modules must provide proper scoping (`@Singleton`, `@ViewModelScoped`)
**Common Violations:**
```kotlin
// ❌ BAD - Manual instantiation
class FeatureViewModel : ViewModel() {
private val repository = FeatureRepositoryImpl()
}
// ✅ GOOD - Injected interface
@HiltViewModel
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository // Interface, not implementation
) : ViewModel()
// ❌ BAD - Injecting implementation
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepositoryImpl // Should inject interface
)
// ✅ GOOD - Interface injection
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository // Interface
)
```
### Module Organization
Reference: `docs/ARCHITECTURE.md#module-structure`
**Correct Placement:**
- `:core` - Shared utilities (cryptography, analytics, logging)
- `:data` - Repositories, database, domain models
- `:network` - API clients, network utilities
- `:ui` - Reusable Compose components, theme
- `:app` - Feature screens, ViewModels, navigation
- `:authenticator` - Authenticator app (separate from password manager)
**Check:**
- UI code in `:ui` or `:app` modules
- Data models in `:data`
- Network clients in `:network`
- No circular dependencies between modules
### Error Handling
Reference: `docs/ARCHITECTURE.md#error-handling`
**Required Pattern - Use Result types:**
```kotlin
// ✅ GOOD - Result type
suspend fun fetchData(): Result<Data> = runCatching {
apiService.getData()
}
// ViewModel handles Result
repository.fetchData().fold(
onSuccess = { data -> _state.value = State.Success(data) },
onFailure = { error -> _state.value = State.Error(error) }
)
// ❌ BAD - Exception-based in business logic
suspend fun fetchData(): Data {
try {
return apiService.getData()
} catch (e: Exception) {
throw FeatureException(e) // Don't throw in business logic
}
}
```
## Security Review
Reference: `docs/ARCHITECTURE.md#security`
**Critical Security Checks:**
- **Sensitive data encrypted**: Passwords, keys, tokens use Android Keystore or EncryptedSharedPreferences
- **No plaintext secrets**: No passwords/keys in logs, memory dumps, or SharedPreferences
- **Input validation**: All user-provided data validated and sanitized
- **Authentication tokens**: Securely stored and transmitted
- **Zero-knowledge architecture**: Encryption happens client-side, server never sees plaintext
**Red Flags:**
```kotlin
// ❌ CRITICAL - Plaintext storage
sharedPreferences.edit {
putString("pin", userPin) // Must use EncryptedSharedPreferences
}
// ❌ CRITICAL - Logging sensitive data
Log.d("Auth", "Password: $password") // Never log sensitive data
// ❌ CRITICAL - Weak encryption
val cipher = Cipher.getInstance("DES") // Use AES-256-GCM
// ✅ GOOD - Keystore encryption
val encryptedData = keystoreManager.encrypt(sensitiveData)
secureStorage.store(encryptedData)
```
**If security concerns found, classify as CRITICAL using `reference/priority-framework.md`**
## Testing Review
Reference: `reference/testing-patterns.md`
**Required Test Coverage:**
- **ViewModels**: Unit tests for state transitions, actions, error scenarios
- **Repositories**: Unit tests for data transformations, error handling
- **Business logic**: Unit tests for complex algorithms, calculations
- **Edge cases**: Null inputs, empty states, network failures, concurrent operations
**Test Quality:**
```kotlin
// ✅ GOOD - Tests behavior
@Test
fun `when login succeeds then state updates to success`() = runTest {
val viewModel = LoginViewModel(mockRepository)
coEvery { mockRepository.login(any(), any()) } returns Result.success(User())
viewModel.onLoginClicked("user", "pass")
viewModel.state.test {
assertEquals(LoginState.Success, awaitItem())
}
}
// ❌ BAD - Tests implementation
@Test
fun `repository is called with correct parameters`() {
// This is testing internal implementation, not behavior
}
```
**Testing Frameworks:**
- JUnit 5 for test structure
- MockK for mocking
- Turbine for Flow testing
- Kotlinx-coroutines-test for coroutine testing
## Code Quality
### Null Safety
- No `!!` (non-null assertion) without clear safety guarantee
- Platform types (from Java) handled with explicit nullability
- Nullable types have proper null checks or use safe operators (`?.`, `?:`)
```kotlin
// ❌ BAD - Unsafe assertion
val result = apiService.getData()!! // Could crash
// ✅ GOOD - Safe handling
val result = apiService.getData() ?: return State.Error("No data")
// ❌ BAD - Platform type unchecked
val intent: Intent = getIntent() // Could be null from Java
intent.getStringExtra("key") // Potential NPE
// ✅ GOOD - Explicit nullability
val intent: Intent? = getIntent()
intent?.getStringExtra("key")
```
### Documentation
- **Public APIs**: Have KDoc comments explaining purpose, parameters, return values
- **Complex algorithms**: Explained in comments
- **Non-obvious behavior**: Documented with rationale
```kotlin
// ✅ GOOD - Documented public API
/**
* Encrypts the given data using AES-256-GCM with a key from Android Keystore.
*
* @param plaintext The data to encrypt
* @return Result containing encrypted data or encryption error
*/
suspend fun encrypt(plaintext: ByteArray): Result<EncryptedData>
```
### Style Compliance
Reference: `docs/STYLE_AND_BEST_PRACTICES.md`
Only flag style issues if:
- Not caught by linters (Detekt, ktlint)
- Have architectural implications
- Significantly impact readability
Skip minor formatting (spaces, line breaks, etc.) - linters handle this.
## Prioritizing Findings
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
## Providing Feedback
Use `reference/review-psychology.md` for phrasing guidance.
**Key principles:**
- **Ask questions** for design decisions: "Can we use the existing BitwardenTextField component here?"
- **Be prescriptive** for clear violations: "Change MutableStateFlow to StateFlow (MVVM pattern requirement)"
- **Explain rationale**: "This exposes mutable state, violating unidirectional data flow"
- **Use I-statements**: "It's hard for me to understand this logic without comments"
- **Avoid condescension**: Don't use "just", "simply", "obviously"
## Output Format
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
See `examples/review-outputs.md` for comprehensive feature review example.
```markdown
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [One-line summary of each critical blocking issue with file:line reference]
See inline comments for all issue details.
```

View File

@@ -0,0 +1,250 @@
# Infrastructure Review Checklist
## Multi-Pass Strategy
### First Pass: Understand the Change
<thinking>
Assess infrastructure change:
1. What problem does this solve?
2. Does this affect production builds, CI/CD, or dev workflow?
3. What's the risk if this breaks?
4. Can this be tested before merge?
5. What's the rollback plan?
</thinking>
**1. Identify the goal:**
- What problem does this solve?
- Is this optimization, fix, or new capability?
- What's the expected impact?
**2. Assess risk:**
- Does this affect production builds?
- Could this break CI/CD pipelines?
- Impact on developer workflow?
**3. Performance implications:**
- Will builds be faster or slower?
- CI time impact?
- Resource usage changes?
### Second Pass: Verify Implementation
<thinking>
Verify configuration and impact:
1. Is the configuration syntax valid?
2. Are secrets/credentials handled securely?
3. What's the impact on build times and CI performance?
4. How will this affect the team's workflow?
5. Is there adequate testing/validation?
</thinking>
**4. Configuration correctness:**
- Syntax valid?
- References correct?
- Secrets/credentials handled securely?
**5. Impact analysis:**
- What workflows/builds are affected?
- Rollback plan if this breaks?
- Documentation for team?
**6. Testing strategy:**
- How can this be tested before merge?
- Canary/gradual rollout possible?
- Monitoring for issues post-merge?
## What to CHECK
**Configuration Correctness**
- YAML/Groovy syntax valid
- File references correct
- Version numbers/tags valid
- Conditional logic sound
**Security**
- No hardcoded secrets or credentials
- GitHub secrets used properly
- Permissions appropriately scoped
- No sensitive data in logs
**Performance Impact**
- Build time implications understood
- CI queue time impact assessed
- Resource usage reasonable
**Rollback Plan**
- Can this be reverted easily?
- Dependencies on other changes?
- Gradual rollout possible?
**Documentation**
- Changes documented for team?
- README or CONTRIBUTING updated?
- Breaking changes clearly noted?
## What to SKIP
**Bikeshedding Configuration** - Unless clear performance/maintenance benefit
**Over-Optimization** - Unless current system has proven problems
**Suggesting Major Rewrites** - Unless current approach is fundamentally broken
## Red Flags
🚩 **Hardcoded secrets** - Use GitHub secrets or secure storage
🚩 **No rollback plan** - Critical infrastructure should be revertible
🚩 **Untested changes** - CI changes should be validated
🚩 **Breaking changes without notice** - Team needs advance warning
🚩 **Performance regression** - Builds shouldn't get significantly slower
## Key Questions to Ask
Use `reference/review-psychology.md` for phrasing:
- "What's the rollback plan if this breaks CI?"
- "Can we test this on a feature branch before main?"
- "Will this impact build times? By how much?"
- "Should this be documented in CONTRIBUTING.md?"
## Common Infrastructure Patterns
### GitHub Actions
```yaml
# ✅ GOOD - Secure, clear, tested
name: Build and Test
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30 # Prevent runaway builds
steps:
- uses: actions/checkout@v4
- name: Run tests
env:
API_KEY: ${{ secrets.API_KEY }} # Secure secret usage
run: ./gradlew test
# ❌ BAD - Insecure, unclear
name: Build
on: push # Too broad, runs on all branches
jobs:
build:
runs-on: ubuntu-latest
# No timeout - could run forever
steps:
- run: |
export API_KEY="hardcoded_key_here" # Hardcoded secret!
./gradlew test
```
### Gradle Configuration
```kotlin
// ✅ GOOD - Clear, maintainable
dependencies {
implementation(libs.androidx.core.ktx) // Version catalog
implementation(libs.hilt.android)
testImplementation(libs.junit5)
testImplementation(libs.mockk)
}
// ❌ BAD - Hardcoded versions
dependencies {
implementation("androidx.core:core-ktx:1.12.0") // Hardcoded version
implementation("com.google.dagger:hilt-android:2.48")
}
```
### Build Optimization
```kotlin
// ✅ GOOD - Parallel, cached
tasks.register("checkAll") {
dependsOn("detekt", "ktlintCheck", "testStandardDebug")
group = "verification"
description = "Run all checks in parallel"
// Enable caching for faster builds
outputs.upToDateWhen { false }
}
// ❌ BAD - Sequential, no caching
tasks.register("checkAll") {
doLast {
exec { commandLine("./gradlew", "detekt") }
exec { commandLine("./gradlew", "ktlintCheck") } // Sequential
exec { commandLine("./gradlew", "test") }
}
}
```
## Prioritizing Findings
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
## Output Format
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
```markdown
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [One-line summary of each critical blocking issue with file:line reference]
See inline comments for all issue details.
```
## Example Review
```markdown
## Summary
Optimizes CI build by parallelizing test execution and caching dependencies
Impact: Estimated 40% reduction in CI time (12 min → 7 min per build)
## Critical Issues
None
## Suggested Improvements
**.github/workflows/build.yml:23** - Add timeout for safety
```yaml
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30 # Prevent builds from hanging
steps:
# ...
```
This prevents runaway builds if something goes wrong.
**.github/workflows/build.yml:45** - Consider matrix strategy for module tests
Can we run module tests in parallel using a matrix strategy?
```yaml
strategy:
matrix:
module: [app, data, network, ui]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: ./gradlew :${{ matrix.module }}:test
```
This could further reduce CI time.
**build.gradle.kts:12** - Document caching strategy
Can we add a comment explaining the caching configuration?
Future maintainers will appreciate understanding why these specific cache keys are used.
## Rollback Plan
If CI breaks:
- Revert commit: `git revert [commit-hash]`
- Previous workflow available at: `.github/workflows/build.yml@main^`
- Monitor CI times at: https://github.com/[org]/[repo]/actions
```

View File

@@ -0,0 +1,294 @@
# Refactoring Review Checklist
## Multi-Pass Strategy
### First Pass: Understand the Refactoring
<thinking>
Analyze the refactoring scope:
1. What pattern is being improved?
2. Why is this refactoring needed?
3. Does this change behavior or just structure?
4. What's the scope? (files affected, migration completeness)
5. What are the risks if something breaks?
</thinking>
**1. Understand the goal:**
- What pattern is being improved?
- Why is this refactoring needed?
- What's the scope of changes?
**2. Assess completeness:**
- Are all instances refactored or just some?
- Are there related areas that should also change?
- Is the migration complete or partial?
**3. Risk assessment:**
- Does this change behavior?
- How many files affected?
- Are tests updated to reflect changes?
### Second Pass: Verify Consistency
<thinking>
Verify refactoring quality:
1. Is the new pattern applied consistently throughout?
2. Are there missed instances of the old pattern?
3. Do tests still pass with same behavior?
4. Is the migration complete or partial?
5. Does this introduce any new issues?
</thinking>
**4. Pattern consistency:**
- Is the new pattern applied consistently throughout?
- Are there missed instances of the old pattern?
- Does this match established project patterns?
**5. Migration completeness:**
- Old pattern fully removed or deprecated?
- All usages updated?
- Documentation updated?
**6. Test coverage:**
- Do tests still pass?
- Are tests refactored to match?
- Does behavior remain unchanged?
## What to CHECK
**Pattern Consistency**
- New pattern applied consistently across all touched code
- Follows established project patterns (MVVM, DI, error handling)
- No mix of old and new patterns
**Migration Completeness**
- All instances of old pattern updated?
- Deprecated methods removed or marked @Deprecated?
- Related code also updated (tests, docs)?
**Behavior Preservation**
- Refactoring doesn't change behavior
- Tests still pass
- Edge cases still handled
**Deprecation Strategy** (if applicable)
- Old APIs marked @Deprecated with migration guidance
- Replacement clearly documented
- Timeline for removal specified
## What to SKIP
**Suggesting Additional Refactorings** - Unless directly related to current changes
**Scope Creep** - Don't request refactoring of untouched code
**Perfection** - Better code is better than perfect code
## Red Flags
🚩 **Incomplete migration** - Mix of old and new patterns
🚩 **Behavior changes** - Refactoring shouldn't change behavior
🚩 **Broken tests** - Tests should be updated to match refactoring
🚩 **Undocumented pattern** - New pattern should be clear to team
## Key Questions to Ask
Use `reference/review-psychology.md` for phrasing:
- "I see the old pattern still used in [file:line] - should that be updated too?"
- "Can we add @Deprecated to the old method with migration guidance?"
- "How do we ensure this behavior remains the same?"
- "Should this pattern be documented in ARCHITECTURE.md?"
## Common Refactoring Patterns
### Extract Interface/Repository
```kotlin
// ✅ GOOD - Complete migration
interface FeatureRepository {
suspend fun getData(): Result<Data>
}
class FeatureRepositoryImpl @Inject constructor(
private val apiService: FeatureApiService
) : FeatureRepository {
override suspend fun getData(): Result<Data> = runCatching {
apiService.fetchData()
}
}
// All usages updated to inject interface
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository // Interface
) : ViewModel()
// ❌ BAD - Incomplete migration
// Some files still inject FeatureRepositoryImpl directly
```
### Modernize Error Handling
```kotlin
// ✅ GOOD - Complete migration
// Old exception-based removed
suspend fun fetchData(): Result<Data> = runCatching {
apiService.getData()
}
// All call sites updated
repository.fetchData().fold(
onSuccess = { /* handle */ },
onFailure = { /* handle */ }
)
// ❌ BAD - Mixed patterns
// Some functions use Result, others still throw exceptions
```
### Extract Reusable Component
```kotlin
// ✅ GOOD - Complete extraction
// Component moved to :ui module
@Composable
fun BitwardenButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
)
// All usages updated to use new component
// Old inline button implementations removed
// ❌ BAD - Incomplete extraction
// Some screens use new component, others still have inline implementation
```
## Prioritizing Findings
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
## Output Format
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
```markdown
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [One-line summary of each critical blocking issue with file:line reference]
See inline comments for all issue details.
```
## Example Reviews
### Example 1: Refactoring with Incomplete Migration
**Context**: Refactoring authentication to Repository pattern, but one ViewModel still uses old pattern
**Summary Comment:**
```markdown
**Overall Assessment:** REQUEST CHANGES
**Critical Issues:**
- Incomplete migration (app/vault/VaultViewModel.kt:89)
See inline comments for details.
```
**Inline Comment 1** (on `app/vault/VaultViewModel.kt:89`):
```markdown
**IMPORTANT**: Incomplete migration
<details>
<summary>Details and fix</summary>
This ViewModel still injects AuthManager directly. Should it use AuthRepository like the other 11 ViewModels?
\```kotlin
// Current (old pattern)
class VaultViewModel @Inject constructor(
private val authManager: AuthManager
)
// Should be (new pattern)
class VaultViewModel @Inject constructor(
private val authRepository: AuthRepository
)
\```
This is the only ViewModel still using the old pattern.
</details>
```
**Inline Comment 2** (on `data/auth/AuthManager.kt:1`):
```markdown
**SUGGESTED**: Add deprecation notice
<details>
<summary>Details</summary>
Can we add @Deprecated to AuthManager to guide future development?
\```kotlin
@Deprecated(
message = "Use AuthRepository interface instead",
replaceWith = ReplaceWith("AuthRepository"),
level = DeprecationLevel.WARNING
)
class AuthManager @Inject constructor(...)
\```
This helps prevent new code from using the old pattern.
</details>
```
---
### Example 2: Clean Refactoring (No Issues)
**Context**: Refactoring with complete migration, all patterns followed correctly, tests passing
**Review Comment:**
```markdown
**Overall Assessment:** APPROVE
Clean refactoring moving ExitManager to :ui module. Follows established patterns, eliminates duplication, tests updated correctly.
```
**Token count:** ~30 tokens (vs ~800 for verbose format)
**Why this works:**
- 3 lines total
- Clear approval decision
- Briefly notes what was done
- No elaborate sections, checkmarks, or excessive praise
- Author gets immediate green light to merge
**What NOT to do for clean refactorings:**
```markdown
❌ DO NOT create these sections:
## Summary
This PR successfully refactors ExitManager into shared code...
## Key Strengths
- ✅ Follows established module organization patterns
- ✅ Removes code duplication between apps
- ✅ Improves test coverage
- ✅ Maintains consistent behavior
[...20 more checkmarks...]
## Code Quality & Architecture
**Architectural Compliance:** ✅
- Correctly places manager in :ui module
- Follows established pattern for UI-layer managers
[...detailed analysis...]
## Changes
- ✅ Moved ExitManager interface from app → ui module
- ✅ Moved ExitManagerImpl from app → ui module
[...listing every file...]
```
This is excessive. **For clean PRs: 2-3 lines maximum.**

View File

@@ -0,0 +1,233 @@
# UI Refinement Review Checklist
## Multi-Pass Strategy
### First Pass: Visual Changes
<thinking>
Analyze the UI changes:
1. What visual/UX problem is being solved?
2. Are there designs or screenshots to reference?
3. Is this affecting existing screens or new ones?
4. What's the scope of visual changes?
5. Are design tokens (colors, spacing, typography) being used correctly?
</thinking>
**1. Understand the changes:**
- What visual/UX problem is being solved?
- Are there designs or screenshots to reference?
- Is this a bug fix or enhancement?
**2. Component usage:**
- Using existing components from `:ui` module?
- Any new custom components created?
- Could existing components be reused?
### Second Pass: Implementation Review
<thinking>
Check implementation quality:
1. Are Compose best practices followed?
2. Is state hoisting applied correctly?
3. Are existing components reused where possible?
4. Is accessibility properly handled?
5. Does this follow design system patterns?
</thinking>
**3. Compose best practices:**
- Composables properly structured?
- State hoisted correctly?
- Preview composables included?
**4. Accessibility:**
- Content descriptions for images/icons?
- Semantic properties for screen readers?
- Touch targets meet minimum size (48dp)?
**5. Design consistency:**
- Using theme colors, spacing, typography?
- Consistent with other screens?
- Responsive to different screen sizes?
## What to CHECK
**Compose Best Practices**
- Composables are stateless where possible
- State hoisting follows patterns
- Side effects (LaunchedEffect, DisposableEffect) used correctly
- Preview composables provided for development
**Component Reuse**
- Using existing BitwardenButton, BitwardenTextField, etc.?
- Could custom UI be replaced with existing components?
- New reusable components placed in `:ui` module?
**Accessibility**
- `contentDescription` for icons and images
- `semantics` for custom interactions
- Sufficient contrast ratios
- Touch targets ≥ 48dp minimum
**Design Consistency**
- Using `BitwardenTheme` colors (not hardcoded)
- Using `BitwardenTheme` spacing (16.dp, 8.dp, etc.)
- Using `BitwardenTheme` typography styles
- Consistent with existing screen patterns
**Responsive Design**
- Handles different screen sizes?
- Scrollable content where appropriate?
- Landscape orientation considered?
## What to SKIP
**Deep Architecture Review** - Unless ViewModel changes are substantial
**Business Logic Review** - Focus is on presentation, not logic
**Security Review** - Unless UI exposes sensitive data improperly
## Red Flags
🚩 **Duplicating existing components** - Should reuse from `:ui` module
🚩 **Hardcoded colors/dimensions** - Should use theme
🚩 **Missing accessibility properties** - Critical for screen readers
🚩 **State management in UI** - Should be hoisted to ViewModel
## Key Questions to Ask
Use `reference/review-psychology.md` for phrasing:
- "Can we use BitwardenButton here instead of this custom button?"
- "Should this color come from BitwardenTheme instead of being hardcoded?"
- "How will this look on a small screen?"
- "Is there a contentDescription for this icon?"
## Common Patterns
### Composable Structure
```kotlin
// ✅ GOOD - Stateless, hoisted state
@Composable
fun FeatureScreen(
state: FeatureState,
onActionClick: () -> Unit,
modifier: Modifier = Modifier
) {
// UI rendering only
}
// ❌ BAD - Business state in composable
@Composable
fun FeatureScreen() {
var userData by remember { mutableStateOf<User?>(null) } // Business state should be in ViewModel
var isLoading by remember { mutableStateOf(false) } // App state should be in ViewModel
// ...
}
// ✅ OK - UI-local state in composable
@Composable
fun LoginForm(onSubmit: (String, String) -> Unit) {
var username by remember { mutableStateOf("") } // UI-local input state is fine
var password by remember { mutableStateOf("") }
// Hoist only as high as needed
}
```
### Theme Usage
```kotlin
// ✅ GOOD - Using theme
Text(
text = "Title",
style = BitwardenTheme.typography.titleLarge,
color = BitwardenTheme.colorScheme.primary
)
// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)
Spacer(modifier = Modifier.height(16.dp))
// ❌ BAD - Hardcoded
Text(
text = "Title",
style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), // Should use theme
color = Color(0xFF0000FF) // Should use theme color
)
Spacer(modifier = Modifier.height(17.dp)) // Non-standard spacing
```
### Accessibility
```kotlin
// ✅ GOOD - Interactive element with description
Icon(
painter = painterResource(R.drawable.ic_password),
contentDescription = "Password visibility toggle",
modifier = Modifier.clickable { onToggle() }
)
// ✅ GOOD - Decorative icon with explicit null
Icon(
painter = painterResource(R.drawable.ic_check),
contentDescription = null, // Decorative icon next to descriptive text
tint = BitwardenTheme.colorScheme.success
)
// ❌ BAD - Interactive element missing description
Icon(
painter = painterResource(R.drawable.ic_delete),
contentDescription = null, // Interactive elements need descriptions
modifier = Modifier.clickable { onDelete() }
)
```
## Prioritizing Findings
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
## Output Format
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
```markdown
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [One-line summary of each critical blocking issue with file:line reference]
See inline comments for all issue details.
```
## Example Review
```markdown
## Summary
Updates login screen layout for improved visual hierarchy and touch targets
## Critical Issues
None
## Suggested Improvements
**app/auth/LoginScreen.kt:67** - Can we use BitwardenTextField?
This custom text field looks very similar to `ui/components/BitwardenTextField.kt:89`.
Would using the existing component maintain consistency?
**app/auth/LoginScreen.kt:123** - Add contentDescription
```kotlin
Icon(
painter = painterResource(R.drawable.ic_visibility),
contentDescription = "Show password", // Add for accessibility
modifier = Modifier.clickable { onToggleVisibility() }
)
```
**app/auth/LoginScreen.kt:145** - Use design system spacing
```kotlin
// Current
Spacer(modifier = Modifier.height(17.dp))
// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)
Spacer(modifier = Modifier.height(16.dp))
```
```

View File

@@ -0,0 +1,446 @@
# Review Output Examples
Well-structured code reviews demonstrating appropriate depth, tone, and formatting for different change types.
## Table of Contents
**Format Reference:**
- [Quick Format Reference](#quick-format-reference)
- [Inline Comment Format](#inline-comment-format-required)
- [Summary Comment Format](#summary-comment-format)
**Examples:**
- [Example 1: Clean PR (No Issues)](#example-1-clean-pr-no-issues)
- [Example 2: Dependency Update with Breaking Changes](#example-2-dependency-update-with-breaking-changes)
- [Example 3: Feature Addition with Critical Issues](#example-3-feature-addition-with-critical-issues)
**Anti-Patterns:**
- [❌ Anti-Patterns to Avoid](#-anti-patterns-to-avoid)
- [Problem: Verbose Summary with Multiple Sections](#problem-verbose-summary-with-multiple-sections)
- [Problem: Praise-Only Inline Comments](#problem-praise-only-inline-comments)
- [Problem: Missing `<details>` Tags](#problem-missing-details-tags)
**Summary:**
- [Summary](#summary)
---
## Quick Format Reference
### Inline Comment Format (REQUIRED)
**MUST use `<details>` tags.** Only severity + description visible; all other content collapsed.
```
[emoji] **[SEVERITY]**: [One-line issue description]
<details>
<summary>Details and fix</summary>
[Code example or specific fix]
[Rationale explaining why]
Reference: [docs link if applicable]
</details>
```
**Severity Levels:**
-**CRITICAL** - Blocking, must fix (security, crashes, architecture violations)
- ⚠️ **IMPORTANT** - Should fix (missing tests, quality issues)
- ♻️ **DEBT** - Technical debt (duplication, convention violations, future rework needed)
- 🎨 **SUGGESTED** - Nice to have (refactoring, improvements)
- 💭 **QUESTION** - Seeking clarification (requirements, design decisions)
### Summary Comment Format
**Required format for ALL PRs:**
```
**Overall Assessment:** APPROVE / REQUEST CHANGES
**Critical Issues** (if any):
- [issue with file:line]
See inline comments for details.
```
All PRs use the same minimal format - no exceptions for size or complexity. Summary must be 5-10 lines maximum.
---
## Example 1: Clean PR (No Issues)
**Context**: Moving shared code to common module, complete migration, all patterns followed
**Review Comment:**
```markdown
**Overall Assessment:** APPROVE
Clean refactoring that moves ExitManager to :ui module, eliminating duplication between apps.
```
**Why this works:**
- Immediate approval visible (2-3 lines)
- One sentence acknowledging the work
- No unnecessary sections or elaborate praise
- Author gets quick feedback and can proceed
---
## Example 2: Dependency Update with Breaking Changes
**Context**: Major version update requiring code migration
**Summary Comment:**
```markdown
**Overall Assessment:** REQUEST CHANGES
**Critical Issues:**
- API migration required for Retrofit 3.0 breaking changes (network/api/BitwardenApiService.kt:34)
See inline comments for migration details.
```
**Inline Comment 1** (on `network/api/BitwardenApiService.kt:34`):
```markdown
**CRITICAL**: API migration required for Retrofit 3.0
<details>
<summary>Details and fix</summary>
Retrofit 3.0 removes the `Call<T>` return type. All 12 API methods in this file need migration:
```kotlin
// Current (deprecated in Retrofit 3.0)
@GET("api/accounts/profile")
fun getProfile(): Call<ProfileResponse>
// Must migrate to
@GET("api/accounts/profile")
suspend fun getProfile(): Response<ProfileResponse>
```
Breaking API change affects:
- 12 methods in BitwardenApiService
- 8 methods in VaultApiService
- All call sites using enqueue/execute
- Test utilities
Consider creating separate PR for this migration given the scope.
Reference: [Retrofit 3.0 migration guide](https://square.github.io/retrofit/changelogs/changelog-3.x/)
</details>
```
**Key Features:**
- Minimal summary (2-3 lines)
- Full details in collapsed inline comment
- Specific file:line references
- Code examples in <details>
- Migration guidance and scope assessment
---
## Example 3: Feature Addition with Critical Issues
**Context**: Implements PIN unlock for vault access
**Summary Comment:**
```markdown
**Overall Assessment:** REQUEST CHANGES
**Critical Issues:**
- Exposes mutable state violating MVVM (UnlockViewModel.kt:78)
- PIN stored without encryption - SECURITY ISSUE (UnlockRepository.kt:145)
See inline comments for all issues and suggestions.
```
**Inline Comment 1** (on `app/vault/unlock/UnlockViewModel.kt:78`):
```markdown
**CRITICAL**: Exposes mutable state
<details>
<summary>Details and fix</summary>
Change `MutableStateFlow<State>` to `StateFlow<State>`:
```kotlin
// Current (problematic)
val unlockState: MutableStateFlow<UnlockState>
// Should be
private val _unlockState = MutableStateFlow<UnlockState>()
val unlockState: StateFlow<UnlockState> = _unlockState.asStateFlow()
```
Exposing MutableStateFlow allows external mutation, violating MVVM unidirectional data flow.
Reference: docs/ARCHITECTURE.md#mvvm-pattern
</details>
```
**Inline Comment 2** (on `data/vault/UnlockRepository.kt:145`):
```markdown
❌ **CRITICAL**: PIN stored without encryption - SECURITY ISSUE
<details>
<summary>Details and fix</summary>
Storing PIN in plaintext SharedPreferences exposes it to backup systems and rooted devices.
```kotlin
// Current (CRITICAL SECURITY ISSUE)
sharedPreferences.edit {
putString(KEY_PIN, pin)
}
// Must use Android Keystore encryption
suspend fun storePin(pin: String): Result<Unit> = runCatching {
val encrypted = keystoreManager.encrypt(pin.toByteArray())
encryptedPrefs.putBytes(KEY_PIN, encrypted)
}
```
Use Android Keystore encryption or EncryptedSharedPreferences per security architecture.
Reference: docs/ARCHITECTURE.md#security
</details>
```
**Inline Comment 3** (on `app/vault/unlock/UnlockViewModel.kt:92`):
```markdown
⚠️ **IMPORTANT**: Missing error handling test
<details>
<summary>Details and fix</summary>
Add test to prevent regression if error handling changes:
```kotlin
@Test
fun `when incorrect PIN entered then returns error state`() = runTest {
val viewModel = UnlockViewModel(mockRepository)
coEvery { mockRepository.validatePin("1234") }
returns Result.failure(InvalidPinException())
viewModel.onPinEntered("1234")
assertEquals(UnlockState.Error("Invalid PIN"), viewModel.state.value)
}
```
Ensures error flow remains robust across refactorings.
</details>
```
**Inline Comment 4** (on `app/vault/unlock/UnlockViewModel.kt:105`):
```markdown
🎨 **SUGGESTED**: Consider rate limiting for PIN attempts
<details>
<summary>Details and fix</summary>
Currently allows unlimited attempts, which could enable brute force attacks.
```kotlin
private var attemptCount = 0
private var lockoutUntil: Instant? = null
fun onPinEntered(pin: String) {
if (isLockedOut()) {
_state.value = UnlockState.LockedOut(lockoutUntil!!)
return
}
// ... validate PIN ...
if (invalid) {
attemptCount++
if (attemptCount >= MAX_ATTEMPTS) {
lockoutUntil = clock.millis() + 15.minutes
}
}
}
```
Would add security layer against brute force. Consider discussing threat model with security team.
</details>
```
**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):
```markdown
💭 **QUESTION**: Can we use BitwardenTextField?
<details>
<summary>Details</summary>
This custom PIN input field looks similar to `ui/components/BitwardenTextField.kt:67`.
Would using the existing component maintain consistency and reduce custom UI code?
</details>
```
**Key Features:**
- Minimal summary (3-4 lines) with critical issues only
- Each issue gets separate inline comment with `<details>` tag
- Multiple severity levels demonstrated (CRITICAL, IMPORTANT, SUGGESTED, QUESTION)
- Mix of prescriptive fixes and collaborative questions
- Code examples collapsed in <details>
- No "Good Practices" or "Action Items" sections
---
## ❌ Anti-Patterns to Avoid
### Problem: Verbose Summary with Multiple Sections
**What NOT to do:**
```markdown
### Review Complete ✅
## Summary
[Lengthy description of what the PR does]
### Strengths 👍
1. **Excellent documentation** - KDoc comments are comprehensive
2. **Proper fail-closed design** - Security defaults to rejection
3. **Defense in depth** - Multiple validation layers
[7 total items with elaboration]
### Critical Issues ⚠️
- Missing test coverage for security-critical code (with full details)
- [More issues with full explanations]
### Recommendations 🎨
- [Multiple recommendations]
### Test Coverage Status 📊
- [Analysis]
### Architecture Compliance ✅
- [Analysis]
## Recommendation
**Conditional approval** with follow-up...
```
**Why this is wrong:**
- 800+ tokens for a summary comment
- Multiple sections (Strengths, Recommendations, Test Coverage, Architecture)
- Elaborates on positive aspects ("Excellent documentation...")
- Duplicates critical issues (summary has details + inline comments have same details)
- Creates visual clutter in PR conversation
**Correct approach:**
```markdown
**Overall Assessment:** REQUEST CHANGES
**Critical Issues:**
- Missing test coverage for security-critical code (PasswordManagerSignatureVerifierImpl.kt:47)
See inline comments for details.
```
**Key differences:**
- 3-5 lines vs 800+ tokens
- Verdict + critical issues only
- All details belong in inline comments
- No positive commentary sections
- Scales with PR complexity, not analysis thoroughness
### Problem: Praise-Only Inline Comments
**What NOT to do:**
Creating inline comment on `AuthenticatorBridgeManagerImpl.kt:73`:
```markdown
👍 **Excellent integration of signature verification**
The signature verification is properly integrated into the connection flow:
- Checked during initialization (line 73)
- Checked before binding (line 134)
- Ensures only validated apps can connect
This is exactly the right approach for fail-safe security.
```
**Why this is wrong:**
- Entire comment is positive feedback with no actionable issue
- Takes up space in PR conversation
- Distracts from actual issues
- Violates "focus on actionable feedback" principle
**Correct approach:**
- Do not create this comment at all
- Reserve inline comments exclusively for issues requiring attention
### Problem: Missing `<details>` Tags
**What NOT to do:**
```markdown
**CRITICAL**: Missing test coverage for security-critical code
The `@OmitFromCoverage` annotation excludes this entire class from test coverage.
**Problems:**
1. No validation that certificate hashes match actual Bitwarden certificates
2. No verification of fail-closed behavior on edge cases
3. No tests for multiple signer rejection logic
4. Certificate hash typos would go undetected until production
**Recommendation:**
Replace `@OmitFromCoverage` with proper unit tests.
Example test structure:
[long code block]
Security-critical code should have the highest test coverage, not be omitted.
```
**Why this is wrong:**
- All content visible immediately (code examples, problems list, rationale)
- Creates visual clutter in PR conversation
- Makes it hard to scan multiple issues quickly
**Correct approach:**
```markdown
**CRITICAL**: Missing test coverage for security-critical code
<details>
<summary>Details and fix</summary>
The `@OmitFromCoverage` annotation excludes this entire class from test coverage.
**Problems:**
1. No validation that certificate hashes match actual Bitwarden certificates
2. No verification of fail-closed behavior on edge cases
3. No tests for multiple signer rejection logic
4. Certificate hash typos would go undetected until production
**Recommendation:**
Replace `@OmitFromCoverage` with proper unit tests.
Example test structure:
[code block]
Security-critical code should have the highest test coverage, not be omitted.
</details>
```
**Key difference:** Only severity + one-line description visible. All details collapsed.
---
## Summary
**Always use:**
- Minimal summary (verdict + critical issues)
- Separate inline comments with `<details>` tags
- Hybrid emoji + text severity prefixes
- Focus exclusively on actionable feedback
**Never use:**
- Multiple summary sections (Strengths, Recommendations, etc.)
- Praise-only inline comments
- Duplication between summary and inline comments
- Verbose analysis in summary (belongs in inline comments)

View File

@@ -0,0 +1,351 @@
# Architectural Patterns Quick Reference
Quick reference for Bitwarden Android architectural patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
## Table of Contents
**Core Patterns:**
- [MVVM + UDF Pattern](#mvvm--udf-pattern)
- [ViewModel Structure](#viewmodel-structure)
- [UI Layer (Compose)](#ui-layer-compose)
- [Hilt Dependency Injection](#hilt-dependency-injection)
- [ViewModels](#viewmodels)
- [Repositories and Managers](#repositories-and-managers)
- [Clock/Time Handling](#clocktime-handling)
- [Module Organization](#module-organization)
- [Error Handling](#error-handling)
- [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)
- [Quick Checklist](#quick-checklist)
---
## MVVM + UDF Pattern
### ViewModel Structure
**✅ GOOD - Proper state encapsulation**:
```kotlin
@HiltViewModel
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository
) : ViewModel() {
// Private mutable state
private val _state = MutableStateFlow<FeatureState>(FeatureState.Initial)
// Public immutable state
val state: StateFlow<FeatureState> = _state.asStateFlow()
// Actions as functions, state updated via internal action
fun onActionClicked() {
viewModelScope.launch {
val result = repository.performAction()
sendAction(FeatureAction.Internal.ActionComplete(result))
}
}
// The ViewModel has a handler that processes the internal action
private fun handleInternalAction(action: FeatureAction.Internal) {
when (action) {
is FeatureAction.Internal.ActionComplete -> {
// The action handler evaluates the result and updates state
action.result.fold(
onSuccess = { _state.value = State.Success },
onFailure = { _state.value = State.Error(it) }
)
}
}
}
}
```
**❌ BAD - Common violations**:
```kotlin
class FeatureViewModel : ViewModel() {
// ❌ Exposes mutable state
val state: MutableStateFlow<FeatureState>
// ❌ Business logic in ViewModel
fun onSubmit() {
val encrypted = encryptionManager.encrypt(data) // Should be in Repository
_state.value = FeatureState.Success
}
// ❌ Direct Android framework dependency
fun onCreate(context: Context) { // ViewModels shouldn't depend on Context
// ...
}
}
```
**Key Rules**:
- Expose `StateFlow<T>`, never `MutableStateFlow<T>`
- Delegate business logic to Repository/Manager
- No direct Android framework dependencies (except ViewModel, SavedStateHandle)
- Use `viewModelScope` for coroutines
Reference: `docs/ARCHITECTURE.md#mvvm-pattern`
---
### UI Layer (Compose)
**✅ GOOD - Stateless, observes only**:
```kotlin
@Composable
fun FeatureScreen(
state: FeatureState,
onActionClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
when (state) {
is FeatureState.Loading -> LoadingIndicator()
is FeatureState.Success -> SuccessContent(state.data)
is FeatureState.Error -> ErrorMessage(state.error)
}
BitwardenButton(
text = "Action",
onClick = onActionClick // Sends event to ViewModel
)
}
}
```
**❌ BAD - Stateful, modifies state**:
```kotlin
@Composable
fun FeatureScreen(viewModel: FeatureViewModel) {
var localState by remember { mutableStateOf(...) } // ❌ State in UI
Button(onClick = {
viewModel._state.value = FeatureState.Loading // ❌ Directly modifying ViewModel state
})
}
```
**Key Rules**:
- Compose screens observe state, never modify
- User actions passed as events/callbacks to ViewModel
- No business logic in UI layer
- Use existing components from `:ui` module
---
## Hilt Dependency Injection
### ViewModels
**✅ GOOD - Interface injection**:
```kotlin
@HiltViewModel
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository, // Interface, not implementation
private val authManager: AuthManager,
savedStateHandle: SavedStateHandle
) : ViewModel()
```
**❌ BAD - Common violations**:
```kotlin
// ❌ No @HiltViewModel annotation
class FeatureViewModel @Inject constructor(...)
// ❌ Injecting implementation instead of interface
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepositoryImpl // Should inject interface
)
// ❌ Manual instantiation
class FeatureViewModel : ViewModel() {
private val repository = FeatureRepositoryImpl() // Should use @Inject
}
```
**Key Rules**:
- Annotate with `@HiltViewModel`
- Use `@Inject constructor`
- Inject interfaces, not implementations
- Use `SavedStateHandle` for process death survival
Reference: `docs/ARCHITECTURE.md#dependency-injection`
---
### Repositories and Managers
**✅ GOOD - Implementation with @Inject**:
```kotlin
interface FeatureRepository {
suspend fun fetchData(): Result<Data>
}
class FeatureRepositoryImpl @Inject constructor(
private val apiService: FeatureApiService,
private val database: FeatureDao
) : FeatureRepository {
override suspend fun fetchData(): Result<Data> = runCatching {
apiService.getData()
}
}
```
**Module provides interface**:
```kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
@Singleton
abstract fun bindFeatureRepository(
impl: FeatureRepositoryImpl
): FeatureRepository
}
```
**Key Rules**:
- Define interface for abstraction
- Implementation uses `@Inject constructor`
- Module binds implementation to interface
- Appropriate scoping (`@Singleton`, `@ViewModelScoped`)
---
### Clock/Time Handling
Time-dependent code must use injected `Clock` rather than direct `Instant.now()` or `DateTime.now()` calls. This follows the same DI principle as other dependencies.
**✅ GOOD - Injected Clock**:
```kotlin
// ViewModel with Clock injection
class MyViewModel @Inject constructor(
private val clock: Clock,
) {
fun save() {
val timestamp = clock.instant()
}
}
// Extension function with Clock parameter
fun State.getTimestamp(clock: Clock): Instant =
existingTime ?: clock.instant()
```
**❌ BAD - Static/direct calls**:
```kotlin
// Hidden dependency, non-testable
val timestamp = Instant.now()
val dateTime = DateTime.now()
```
**Key Rules**:
- Inject `Clock` via Hilt constructor (like other dependencies)
- Pass `Clock` as parameter to extension functions
- `Clock` is provided via `CoreModule` as singleton
- Enables deterministic testing with `Clock.fixed(...)`
Reference: `docs/STYLE_AND_BEST_PRACTICES.md#best-practices--time-and-clock-handling`
---
## Module Organization
```
android/
├── core/ # Shared utilities (cryptography, analytics, logging)
├── data/ # Repositories, database, domain models
├── network/ # API clients, network utilities
├── ui/ # Reusable Compose components, theme
├── app/ # Application, feature screens, ViewModels
└── authenticator/ # Authenticator app (separate from password manager)
```
**Correct Placement**:
- UI screens and ViewModels → `:app`
- Reusable Compose components → `:ui`
- Data models and Repositories → `:data`
- API services → `:network`
- Cryptography, logging → `:core`
**Check for**:
- No circular dependencies
- Correct module placement
- Proper visibility (internal vs public)
Reference: `docs/ARCHITECTURE.md#module-structure`
---
## Error Handling
### Use Result Types, Not Exceptions
**✅ GOOD - Result-based**:
```kotlin
// Repository
suspend fun fetchData(): Result<Data> = runCatching {
apiService.getData()
}
// ViewModel
fun onFetch() {
viewModelScope.launch {
val result = repository.fetchData()
sendAction(FeatureAction.Internal.FetchComplete(result))
}
}
```
**❌ BAD - Exception-based in business logic**:
```kotlin
// ❌ Don't throw in business logic
suspend fun fetchData(): Data {
try {
return apiService.getData()
} catch (e: Exception) {
throw FeatureException(e) // Don't throw in repositories
}
}
// ❌ Try-catch in ViewModel
fun onFetch() {
viewModelScope.launch {
try {
val data = repository.fetchData()
sendAction(FeatureAction.Internal.FetchComplete(data))
} catch (e: Exception) {
sendAction(FeatureAction.Internal.FetchComplete(e))
}
}
}
```
**Key Rules**:
- Use `Result<T>` return types in repositories
- Use `runCatching { }` to wrap API calls
- Handle results with `.fold()` in ViewModels
- Don't throw exceptions in business logic
Reference: `docs/ARCHITECTURE.md#error-handling`
---
## Quick Checklist
### Architecture
- [ ] ViewModels expose StateFlow, not MutableStateFlow?
- [ ] Business logic in Repository, not ViewModel?
- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?
- [ ] Injecting interfaces, not implementations?
- [ ] Time-dependent code uses injected `Clock` (not `Instant.now()`)?
- [ ] Correct module placement?
### Error Handling
- [ ] Using Result types, not exceptions in business logic?
- [ ] Errors handled with .fold() in ViewModels?
---
For comprehensive details, always refer to:
- `docs/ARCHITECTURE.md` - Full architecture patterns
- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide

View File

@@ -0,0 +1,431 @@
# Finding Priority Framework
Use this framework to classify findings during code review. Clear prioritization helps authors triage and address issues effectively.
## Table of Contents
**Severity Categories:**
- [❌ CRITICAL (Blocker - Must Fix Before Merge)](#critical-blocker---must-fix-before-merge)
- [⚠️ IMPORTANT (Should Fix)](#important-should-fix)
- [♻️ DEBT (Technical Debt)](#debt-technical-debt)
- [🎨 SUGGESTED (Nice to Have)](#suggested-nice-to-have)
- [💭 QUESTION (Seeking Clarification)](#question-seeking-clarification)
- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)
**Guidelines:**
- [Classification Guidelines](#classification-guidelines)
- [When Something is Between Categories](#when-something-is-between-categories)
- [Context Matters](#context-matters)
- [Examples by Change Type](#examples-by-change-type)
- [Special Cases](#special-cases)
- [Summary](#summary)
---
## ❌ **CRITICAL** (Blocker - Must Fix Before Merge)
These issues **must** be addressed before the PR can be merged. They pose immediate risks to security, stability, or architecture integrity.
### Security
- Data leaks or plaintext sensitive data (passwords, keys, tokens)
- Weak encryption or insecure key storage
- Missing authentication or authorization checks
- Input injection vulnerabilities (SQL, XSS, command injection)
- Sensitive data in logs or error messages
**Example**:
```
**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption
PIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.
Reference: docs/ARCHITECTURE.md#security
```
### Stability
- Compilation errors or warnings
- Null pointer exceptions in production paths
- Resource leaks (file handles, network connections, memory)
- Crashes or unhandled exceptions in critical paths
- Thread safety violations
**Example**:
```
**app/auth/BiometricRepository.kt:120** - CRITICAL: Missing null safety check
biometricPrompt result can be null. Add explicit null check to prevent crash.
```
### Architecture
- Mutable state exposure in ViewModels (violates MVVM)
- Exception-based error handling in business logic (should use Result)
- Circular dependencies between modules
- Violation of zero-knowledge principles
- Direct dependency instantiation (should use DI)
**Example**:
```
**app/login/LoginViewModel.kt:45** - CRITICAL: Exposes mutable state
Change MutableStateFlow to StateFlow in public API to prevent external state mutation.
This violates MVVM encapsulation pattern.
```
---
## ⚠️ **IMPORTANT** (Should Fix)
These issues should be addressed but don't block merge if there's a compelling reason. They improve code quality, maintainability, or robustness.
### Testing
- Missing tests for critical paths (authentication, encryption, data sync)
- Missing tests for new public APIs
- Tests that don't verify actual behavior (test implementation, not behavior)
- Missing test coverage for error scenarios
**Example**:
```
**data/auth/BiometricRepository.kt** - IMPORTANT: Missing test for cancellation
Add test for user cancellation scenario to prevent regression.
```
### Architecture
- Inconsistent patterns within PR (mixing error handling approaches)
- Poor separation of concerns
- Tight coupling between components
- Not following established project patterns
**Example**:
```
**app/vault/VaultViewModel.kt:89** - IMPORTANT: Business logic in ViewModel
Encryption logic should be in Repository, not ViewModel.
Reference: docs/ARCHITECTURE.md#mvvm-pattern
```
### Documentation
- Undocumented public APIs (missing KDoc)
- Missing documentation for complex algorithms
- Unclear naming or confusing interfaces
**Example**:
```
**core/crypto/EncryptionManager.kt:34** - IMPORTANT: Missing KDoc
Public encryption method should document parameters, return value, and exceptions.
```
### Performance
- Inefficient algorithms in hot paths (with evidence from profiling)
- Blocking main thread with I/O operations
- Memory inefficient data structures (with evidence)
**Example**:
```
**app/vault/VaultListViewModel.kt:78** - IMPORTANT: N+1 query pattern
Fetching items one-by-one in loop. Consider batch fetch to reduce database queries.
```
---
## ♻️ **DEBT** (Technical Debt)
Code that duplicates existing patterns, violates established conventions, or will require rework within 6 months. Introduces technical debt that should be tracked for future cleanup.
### Duplication
- Copy-pasted code blocks across files
- Repeated validation or business logic
- Multiple implementations of same pattern
- Data transformation duplicated in multiple places
**Example**:
```
**app/vault/VaultListViewModel.kt:156** - DEBT: Duplicates encryption logic
Same encryption pattern exists in VaultRepository.kt:234 and SyncManager.kt:89.
Extract to shared EncryptionUtil to reduce maintenance burden.
```
### Convention Violations
- Inconsistent error handling approaches within same module
- Mixing architectural patterns (MVVM + MVC)
- Not following established DI patterns
- Deviating from project code style significantly
**Example**:
```
**data/auth/AuthRepository.kt:78** - DEBT: Exception-based error handling
Project standard is Result<T> for error handling. This uses try-catch with throws.
Creates inconsistency and makes testing harder.
Reference: docs/ARCHITECTURE.md#error-handling
```
### Future Rework Required
- Hardcoded values that should be configurable
- Temporary workarounds without TODO/FIXME
- Code that will need changes when planned features arrive
- Tight coupling that prevents future extensibility
**Example**:
```
**app/settings/SettingsViewModel.kt:45** - DEBT: Hardcoded feature flags
Feature flags should come from remote config for A/B testing.
Will require rework when experimentation framework launches.
```
---
## 🎨 **SUGGESTED** (Nice to Have)
These are improvement opportunities but not required. Consider the effort vs. benefit before requesting changes.
### Code Quality
- Minor style inconsistencies (if not caught by linter)
- Opportunities for DRY improvements
- Better variable naming for clarity
- Simplification opportunities
**Example**:
```
**app/vault/VaultScreen.kt:145** - SUGGESTED: Consider extracting helper function
This 20-line block appears in 3 places. Consider extracting to reduce duplication.
```
### Testing
- Additional test coverage for edge cases (beyond critical paths)
- More comprehensive integration tests
- Performance tests for non-critical paths
**Example**:
```
**app/login/LoginViewModelTest.kt** - SUGGESTED: Add test for concurrent login attempts
Not critical, but would increase confidence in edge case handling.
```
### Refactoring
- Extracting reusable patterns
- Modernizing old patterns (if touching related code)
- Improving testability
**Example**:
```
**data/vault/VaultRepository.kt:200** - SUGGESTED: Consider extracting validation logic
Could be extracted to separate validator class for reusability and testing.
```
---
## 💭 **QUESTION** (Seeking Clarification)
Questions about requirements, unclear intent, or potential conflicts that require human knowledge to answer. Open inquiries that cannot be resolved through code inspection alone.
### Requirements Clarification
- Ambiguous acceptance criteria
- Multiple valid implementation approaches
- Unclear business rules or edge case handling
- Conflicting requirements between specs and implementation
**Example**:
```
**app/vault/ItemListViewModel.kt:67** - QUESTION: Expected sort behavior for equal timestamps?
When items have identical timestamps, should secondary sort be by:
- Name (alphabetical)
- Creation order
- Item type priority
Spec doesn't specify tie-breaking logic.
```
### Design Decisions
- Architecture choices that could go multiple ways
- Trade-offs between approaches without clear winner
- Feature flag strategy or rollout approach
- API design with multiple valid patterns
**Example**:
```
**data/sync/SyncManager.kt:134** - QUESTION: Should sync failures retry automatically?
Current implementation fails immediately. Options:
- Exponential backoff (3 retries)
- User-triggered retry only
- Background retry on network restore
What's the expected UX?
```
### System Integration
- Unclear contracts with external systems
- Potential conflicts with other features/modules
- Assumptions about third-party API behavior
- Cross-team coordination needs
**Example**:
```
**app/auth/BiometricPrompt.kt:89** - QUESTION: Compatibility with pending device credentials PR?
PR #1234 is refactoring device credentials. Should this:
- Merge first and adapt later
- Wait for #1234 to land
- Coordinate with that author
Timing unclear.
```
### Testing Strategy
- Uncertainty about test scope or approach
- Questions about mocking external dependencies
- Edge cases that need product input
- Performance testing requirements
**Example**:
```
**data/vault/EncryptionTest.kt:45** - QUESTION: Should we test against real Keystore?
Currently using mocked Keystore. Real Keystore testing would:
+ Catch hardware-specific issues
- Slow down CI significantly
- Require API 23+ emulators
What's the priority?
```
---
## Optional (Acknowledge But Don't Require)
Note good practices to reinforce positive patterns. Keep these **brief** - list only, no elaboration.
### Good Practices
**Format**: Simple bullet list, no explanation
```markdown
## Good Practices
- Proper Hilt DI usage throughout
- Comprehensive unit test coverage
- Clear separation of concerns
- Well-documented public APIs
```
**Don't do this** (too verbose):
```markdown
## Good Practices
- Proper Hilt DI usage throughout: Great job using @Inject constructor and injecting interfaces! This follows our established patterns perfectly and makes the code very testable. Really excellent work here! 👍
```
---
## Classification Guidelines
### When Something is Between Categories
**If unsure between Critical and Important**:
- Ask: "Could this cause production incidents, data loss, or security breaches?"
- If yes → Critical
- If no → Important
**If unsure between Important and Debt**:
- Ask: "Is this a bug/defect or just duplication/inconsistency?"
- If bug/defect → Important
- If duplication/inconsistency → Debt
**If unsure between Important and Suggested**:
- Ask: "Would I block merge over this?"
- If yes → Important
- If no → Suggested
**If unsure between Debt and Suggested**:
- Ask: "Will this require rework within 6 months?"
- If yes → Debt
- If no → Suggested
**If unsure between Suggested and Question**:
- Ask: "Am I requesting a change or asking for clarification?"
- If requesting change → Suggested
- If seeking clarification → Question
**If unsure between Suggested and Optional**:
- Ask: "Is this actionable feedback or just acknowledgment?"
- If actionable → Suggested
- If acknowledgment → Optional
### Context Matters
**Same issue, different contexts**:
```
// Critical for production code
Missing null safety check in auth flow → CRITICAL
// Suggested for internal test utility
Missing null safety check in test helper → SUGGESTED
```
**Same pattern, different risk levels**:
```
// Critical for new feature
Missing tests for new auth method → CRITICAL
// Important for bug fix
Missing regression test → IMPORTANT
// Suggested for refactoring
Missing tests for refactored helper → SUGGESTED
```
---
## Examples by Change Type
### Dependency Update
- **Critical**: Known CVEs in old version not addressed
- **Important**: Breaking changes that need migration
- **Suggested**: Beta/alpha version stability concerns
### Bug Fix
- **Critical**: Fix doesn't address root cause
- **Important**: Missing regression test
- **Suggested**: Similar bugs in related code
### Feature Addition
- **Critical**: Security vulnerabilities, architecture violations
- **Important**: Missing tests for critical paths
- **Suggested**: Additional test coverage, minor refactoring
### UI Refinement
- **Critical**: Missing accessibility for key actions
- **Important**: Not using theme (hardcoded colors)
- **Suggested**: Minor spacing/alignment improvements
### Refactoring
- **Critical**: Changes behavior (should be behavior-preserving)
- **Important**: Incomplete migration (mix of old/new patterns)
- **Suggested**: Additional instances that could be refactored
### Infrastructure
- **Critical**: Hardcoded secrets, no rollback plan
- **Important**: Performance regression in build times
- **Suggested**: Further optimization opportunities
---
## Special Cases
### Technical Debt
- Acknowledge existing tech debt but don't require fixing in unrelated PR
- Exception: If change makes tech debt worse, it's Important to address
### Scope Creep
- Don't request changes outside PR scope
- Can note as "Future consideration" but not required for this PR
### Linter-Catchable Issues
- Don't flag issues that automated tools handle
- Exception: If linter is misconfigured and missing real issues
### Personal Preferences
- Don't flag unless grounded in project standards or architectural principles
- Use "I-statements" if suggesting alternative approaches
---
## Summary
**Critical**: Block merge, must fix (security, stability, architecture)
**Important**: Should fix before merge (testing, quality, performance)
**Debt**: Technical debt introduced, track for future cleanup
**Suggested**: Nice to have, consider effort vs benefit
**Question**: Seeking clarification on requirements or design
**Optional**: Acknowledge good practices, keep brief

View File

@@ -0,0 +1,175 @@
# Review Psychology: Constructive Feedback Phrasing
Effective code review feedback is clear, actionable, and constructive. This guide provides phrasing patterns for inline comments.
## Table of Contents
**Guidelines:**
- [Core Directives](#core-directives)
- [Phrasing Templates](#phrasing-templates)
- [Critical Issues (Prescriptive)](#critical-issues-prescriptive)
- [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)
- [Questions (Collaborative)](#questions-collaborative)
- [Test Suggestions](#test-suggestions)
- [When to Be Prescriptive vs Ask Questions](#when-to-be-prescriptive-vs-ask-questions)
- [Special Cases](#special-cases)
---
## Core Directives
- **Keep positive feedback minimal**: For clean PRs with no issues, use 2-3 line approval only. When acknowledging good practices in PRs with issues, use single bullet list with no elaboration. Never create elaborate sections praising correct implementations.
- Ask questions for design decisions, be prescriptive for clear violations
- Focus on code, not people ("This code..." not "You...")
- Use I-statements for subjective feedback ("Hard for me to understand...")
- Explain rationale with every recommendation
- Avoid: "just", "simply", "obviously", "easy"
---
## Phrasing Templates
### Critical Issues (Prescriptive)
**Pattern**: State problem + Provide solution + Explain why
```
**[file:line]** - CRITICAL: [Issue description]
[Specific fix with code example if applicable]
[Rationale explaining why this is critical]
Reference: [docs link if applicable]
```
**Example**:
```
**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption
PIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.
Plaintext storage exposes the PIN to backup systems and rooted devices.
Reference: docs/ARCHITECTURE.md#security
```
---
### Suggested Improvements (Exploratory)
**Pattern**: Observe + Suggest + Explain benefit
```
**[file:line]** - Consider [alternative approach]
[Current observation]
Can we [specific suggestion]?
[Benefit or rationale]
```
**Example**:
```
**app/login/LoginScreen.kt:89** - Consider using existing BitwardenButton
This custom button implementation looks similar to `ui/components/BitwardenButton.kt:45`.
Can we use the existing component to maintain consistency across the app?
```
---
### Questions (Collaborative)
**Pattern**: Ask + Provide context (optional)
```
**[file:line]** - [Question about intent or approach]?
[Optional context or observation]
```
**Example**:
```
**data/sync/SyncManager.kt:234** - How does this handle concurrent sync attempts?
It looks like multiple coroutines could call `startSync()` simultaneously.
Is there a mechanism to prevent race conditions, or is that handled elsewhere?
```
---
### Test Suggestions
**Pattern**: Observe gap + Suggest specific test + Provide skeleton
```
**[file:line]** - Consider adding test for [scenario]
[Rationale]
```kotlin
@Test
fun `test description`() = runTest {
// Test skeleton
}
```
```
**Example**:
```
**data/auth/BiometricRepository.kt** - Consider adding test for cancellation scenario
This would prevent regression of the bug you just fixed:
```kotlin
@Test
fun `when biometric cancelled then returns cancelled state`() = runTest {
coEvery { biometricPrompt.authenticate() } returns null
val result = repository.authenticate()
assertEquals(AuthResult.Cancelled, result)
}
```
```
---
## When to Be Prescriptive vs Ask Questions
**Be Prescriptive** (Tell them what to do):
- Security issues
- Architecture pattern violations
- Null safety problems
- Compilation errors
- Documented project standards
**Ask Questions** (Seek explanation):
- Design decisions with multiple valid approaches
- Performance trade-offs without data
- Unclear intent or reasoning
- Scope decisions (this PR vs future work)
- Patterns not documented in project guidelines
---
## Special Cases
**Nitpicks** - For truly minor suggestions, use "Nit:" prefix:
```
**Nit**: Extra blank line at line 145
```
**Uncertainty** - If unsure, acknowledge it:
```
I'm not certain, but this might be called frequently.
Has this been profiled?
```
**Positive Feedback** - Brief list only, no elaboration:
```
## Good Practices
- Proper Hilt DI usage throughout
- Comprehensive unit test coverage
- Clear separation of concerns
```

View File

@@ -0,0 +1,90 @@
# Security Patterns Quick Reference
Quick reference for Bitwarden Android security patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md#security`.
## Encryption and Key Storage
**✅ GOOD - Android Keystore**:
```kotlin
// Sensitive data encrypted with Keystore
class SecureStorage @Inject constructor(
private val keystoreManager: KeystoreManager
) {
suspend fun storePin(pin: String): Result<Unit> = runCatching {
val encrypted = keystoreManager.encrypt(pin.toByteArray())
securePreferences.putBytes(KEY_PIN, encrypted)
}
}
// Or use EncryptedSharedPreferences
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
```
**❌ BAD - Plaintext or weak encryption**:
```kotlin
// ❌ CRITICAL - Plaintext storage
sharedPreferences.edit {
putString("pin", userPin) // Never store sensitive data in plaintext
}
// ❌ CRITICAL - Weak encryption
val cipher = Cipher.getInstance("DES") // Use AES-256-GCM
// ❌ CRITICAL - Hardcoded keys
val key = "my_secret_key_123" // Use Android Keystore
```
**Key Rules**:
- Use Android Keystore for encryption keys
- Use EncryptedSharedPreferences for simple key-value storage
- Use AES-256-GCM for encryption
- Never store sensitive data in plaintext
- Never hardcode encryption keys
Reference: `docs/ARCHITECTURE.md#security`
---
## Logging Sensitive Data
**✅ GOOD - No sensitive data**:
```kotlin
Log.d(TAG, "Authentication attempt for user")
Log.d(TAG, "Vault sync completed with ${items.size} items")
```
**❌ BAD - Logs sensitive data**:
```kotlin
// ❌ CRITICAL
Log.d(TAG, "Password: $password")
Log.d(TAG, "Auth token: $token")
Log.d(TAG, "PIN: $pin")
Log.d(TAG, "Encryption key: ${key.encoded}")
```
**Key Rules**:
- Never log passwords, PINs, tokens, keys
- Never log encryption keys or sensitive data
- Be careful with error messages (don't include sensitive context)
---
## Quick Checklist
### Security
- [ ] Sensitive data encrypted with Keystore?
- [ ] No plaintext passwords/keys?
- [ ] No sensitive data in logs?
- [ ] Using AES-256-GCM for encryption?
- [ ] No hardcoded encryption keys?
---
For comprehensive security details, always refer to:
- `docs/ARCHITECTURE.md#security` - Complete security architecture and zero-knowledge principles

View File

@@ -0,0 +1,127 @@
# Testing Patterns Quick Reference
Quick reference for Bitwarden Android testing patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
## ViewModel Tests
**✅ GOOD - Tests behavior**:
```kotlin
@Test
fun `when login succeeds then state updates to success`() = runTest {
// Arrange
val viewModel = LoginViewModel(mockRepository)
coEvery { mockRepository.login(any(), any()) } returns Result.success(User())
// Act
viewModel.onLoginClicked("user@example.com", "password")
// Assert
viewModel.state.test {
assertEquals(LoginState.Loading, awaitItem())
assertEquals(LoginState.Success, awaitItem())
}
}
```
**❌ BAD - Tests implementation**:
```kotlin
@Test
fun `repository is called with correct parameters`() {
// ❌ This tests implementation details, not behavior
viewModel.onLoginClicked("user", "pass")
coVerify { mockRepository.login("user", "pass") }
}
```
**Key Rules**:
- Test behavior, not implementation
- Use `runTest` for coroutine tests
- Use Turbine for Flow testing
- Use MockK for mocking
---
## Repository Tests
**✅ GOOD - Tests data transformations**:
```kotlin
@Test
fun `fetchItems maps API response to domain model`() = runTest {
// Arrange
val apiResponse = listOf(ApiItem(id = "1", name = "Test"))
coEvery { apiService.getItems() } returns apiResponse
// Act
val result = repository.fetchItems()
// Assert
assertTrue(result.isSuccess)
assertEquals(
listOf(DomainItem(id = "1", name = "Test")),
result.getOrThrow()
)
}
```
**Key Rules**:
- Test data transformations
- Test error handling (network failures, API errors)
- Test caching behavior if applicable
- Mock API services and databases
Reference: Project uses JUnit 5, MockK, Turbine, kotlinx-coroutines-test
---
## Null Safety
**✅ GOOD - Safe handling**:
```kotlin
// Safe call with elvis operator
val result = apiService.getData() ?: return State.Error("No data")
// Let with safe call
intent?.getStringExtra("key")?.let { value ->
processValue(value)
}
// Require with message
val data = requireNotNull(response.data) { "Response data must not be null" }
```
**❌ BAD - Unsafe assertions**:
```kotlin
// ❌ Unsafe - can crash
val result = apiService.getData()!!
// ❌ Platform type unchecked
val intent: Intent = getIntent() // Could be null from Java
val value = intent.getStringExtra("key") // Potential NPE
```
**Key Rules**:
- Avoid `!!` unless safety is guaranteed (rare)
- Handle platform types with explicit nullability
- Use safe calls (`?.`), elvis operator (`?:`), or explicit checks
- Use `requireNotNull` with descriptive message if crash is acceptable
---
## Quick Checklist
### Testing
- [ ] ViewModels have unit tests?
- [ ] Tests verify behavior, not implementation?
- [ ] Edge cases covered?
- [ ] Error scenarios tested?
### Code Quality
- [ ] Null safety handled properly (no `!!` without guarantee)?
- [ ] Public APIs have KDoc?
- [ ] Following naming conventions?
---
For comprehensive details, always refer to:
- `docs/ARCHITECTURE.md` - Full architecture patterns
- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide

View File

@@ -0,0 +1,85 @@
# Compose UI Patterns Quick Reference
Quick reference for Bitwarden Android Compose UI patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
## Component Reuse
**✅ GOOD - Uses existing components**:
```kotlin
BitwardenButton(
text = "Submit",
onClick = onSubmit
)
BitwardenTextField(
value = text,
onValueChange = onTextChange,
label = "Email"
)
```
**❌ BAD - Duplicates existing components**:
```kotlin
// ❌ Recreating BitwardenButton
Button(
onClick = onSubmit,
colors = ButtonDefaults.buttonColors(
containerColor = BitwardenTheme.colorScheme.primary
)
) {
Text("Submit")
}
```
**Key Rules**:
- Check `:ui` module for existing components before creating custom ones
- Use BitwardenButton, BitwardenTextField, etc. for consistency
- Place new reusable components in `:ui` module
---
## Theme Usage
**✅ GOOD - Uses theme**:
```kotlin
Text(
text = "Title",
style = BitwardenTheme.typography.titleLarge,
color = BitwardenTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp)) // Standard spacing
```
**❌ BAD - Hardcoded values**:
```kotlin
Text(
text = "Title",
style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), // Use theme
color = Color(0xFF0066FF) // Use theme color
)
Spacer(modifier = Modifier.height(17.dp)) // Non-standard spacing
```
**Key Rules**:
- Use `BitwardenTheme.colorScheme` for colors
- Use `BitwardenTheme.typography` for text styles
- Use standard spacing (4.dp, 8.dp, 16.dp, 24.dp)
---
## Quick Checklist
### UI Patterns
- [ ] Using existing Bitwarden components from `:ui` module?
- [ ] Using BitwardenTheme for colors and typography?
- [ ] Using standard spacing values (4, 8, 16, 24 dp)?
- [ ] No hardcoded colors or text styles?
- [ ] UI is stateless (observes state, doesn't modify)?
---
For comprehensive details, always refer to:
- `docs/ARCHITECTURE.md` - Full architecture patterns
- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide

View File

@@ -0,0 +1,319 @@
---
name: testing-android-code
description: This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by "BaseViewModelTest", "BitwardenComposeTest", "BaseServiceTest", "stateEventFlow", "bufferedMutableSharedFlow", "FakeDispatcherManager", "expectNoEvents", "assertCoroutineThrows", "createMockCipher", "createMockSend", "asSuccess", "Why is my Bitwarden test failing?", or testing questions about ViewModels, repositories, Compose screens, or data sources in Bitwarden.
version: 1.0.0
---
# Testing Android Code - Bitwarden Testing Patterns
**This skill provides tactical testing guidance for Bitwarden-specific patterns.** For comprehensive architecture and testing philosophy, consult `docs/ARCHITECTURE.md`.
## Test Framework Configuration
**Required Dependencies:**
- **JUnit 5** (jupiter), **MockK**, **Turbine** (app.cash.turbine)
- **kotlinx.coroutines.test**, **Robolectric**, **Compose Test**
**Critical Note:** Tests run with en-US locale for consistency. Don't assume other locales.
---
## A. ViewModel Testing Patterns
### Base Class: BaseViewModelTest
**Always extend `BaseViewModelTest` for ViewModel tests.**
**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`
**Benefits:**
- Automatically registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`
- Provides `stateEventFlow()` helper for simultaneous StateFlow/EventFlow testing
**Pattern:**
```kotlin
class ExampleViewModelTest : BaseViewModelTest() {
private val mockRepository: ExampleRepository = mockk()
private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))
@Test
fun `ButtonClick should fetch data and update state`() = runTest {
coEvery { mockRepository.fetchData(any()) } returns Result.success("data")
val viewModel = ExampleViewModel(savedStateHandle, mockRepository)
viewModel.stateFlow.test {
assertEquals(INITIAL_STATE, awaitItem())
viewModel.trySendAction(ExampleAction.ButtonClick)
assertEquals(INITIAL_STATE.copy(data = "data"), awaitItem())
}
coVerify { mockRepository.fetchData(any()) }
}
}
```
**For complete examples:** See `references/test-base-classes.md`
### StateFlow vs EventFlow (Critical Distinction)
| Flow Type | Replay | First Action | Pattern |
|-----------|--------|--------------|---------|
| StateFlow | Yes (1) | `awaitItem()` gets current state | Expect initial → trigger → expect new |
| EventFlow | No | `expectNoEvents()` first | expectNoEvents → trigger → expect event |
**For detailed patterns:** See `references/flow-testing-patterns.md`
---
## B. Compose UI Testing Patterns
### Base Class: BitwardenComposeTest
**Always extend `BitwardenComposeTest` for Compose screen tests.**
**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`
**Benefits:**
- Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)
- Wraps content in `BitwardenTheme` and `LocalManagerProvider`
- Provides fixed Clock for deterministic time-based tests
**Pattern:**
```kotlin
class ExampleScreenTest : BitwardenComposeTest() {
private var haveCalledNavigateBack = false
private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<ExampleViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
setContent {
ExampleScreen(
onNavigateBack = { haveCalledNavigateBack = true },
viewModel = viewModel,
)
}
}
@Test
fun `on back click should send BackClick action`() {
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(ExampleAction.BackClick) }
}
}
```
**Note:** Use `bufferedMutableSharedFlow` for event testing in Compose tests. Default replay is 0; pass `replay = 1` if needed.
**For complete base class details:** See `references/test-base-classes.md`
---
## C. Repository and Service Testing
### Service Testing with MockWebServer
**Base Class:** `BaseServiceTest` (`network/src/testFixtures/`)
```kotlin
class ExampleServiceTest : BaseServiceTest() {
private val api: ExampleApi = retrofit.create()
private val service = ExampleServiceImpl(api)
@Test
fun `getConfig should return success when API succeeds`() = runTest {
server.enqueue(MockResponse().setBody(EXPECTED_JSON))
val result = service.getConfig()
assertEquals(EXPECTED_RESPONSE.asSuccess(), result)
}
}
```
### Repository Testing Pattern
```kotlin
class ExampleRepositoryTest {
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val dispatcherManager = FakeDispatcherManager()
private val mockDiskSource: ExampleDiskSource = mockk()
private val mockService: ExampleService = mockk()
private val repository = ExampleRepositoryImpl(
clock = fixedClock,
exampleDiskSource = mockDiskSource,
exampleService = mockService,
dispatcherManager = dispatcherManager,
)
@Test
fun `fetchData should return success when service succeeds`() = runTest {
coEvery { mockService.getData(any()) } returns expectedData.asSuccess()
val result = repository.fetchData(userId)
assertTrue(result.isSuccess)
}
}
```
**Key patterns:** Use `FakeDispatcherManager`, fixed Clock, and `.asSuccess()` helpers.
---
## D. Test Data Builders
### Builder Pattern with Number Parameter
**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/model/`
```kotlin
fun createMockCipher(
number: Int,
id: String = "mockId-$number",
name: String? = "mockName-$number",
// ... more parameters with defaults
): SyncResponseJson.Cipher
// Usage:
val cipher1 = createMockCipher(number = 1) // mockId-1, mockName-1
val cipher2 = createMockCipher(number = 2) // mockId-2, mockName-2
val custom = createMockCipher(number = 3, name = "Custom")
```
**Available Builders (35+):**
- **Cipher:** `createMockCipher()`, `createMockLogin()`, `createMockCard()`, `createMockIdentity()`, `createMockSecureNote()`, `createMockSshKey()`, `createMockField()`, `createMockUri()`, `createMockFido2Credential()`, `createMockPasswordHistory()`, `createMockCipherPermissions()`
- **Sync:** `createMockSyncResponse()`, `createMockFolder()`, `createMockCollection()`, `createMockPolicy()`, `createMockDomains()`
- **Send:** `createMockSend()`, `createMockFile()`, `createMockText()`, `createMockSendJsonRequest()`
- **Profile:** `createMockProfile()`, `createMockOrganization()`, `createMockProvider()`, `createMockPermissions()`
- **Attachments:** `createMockAttachment()`, `createMockAttachmentJsonRequest()`, `createMockAttachmentResponse()`
See `network/src/testFixtures/kotlin/com/bitwarden/network/model/` for full list.
---
## E. Result Type Testing
**Locations:**
- `.asSuccess()`, `.asFailure()`: `core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt`
- `assertCoroutineThrows`: `core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt`
```kotlin
// Create results
"data".asSuccess() // Result.success("data")
throwable.asFailure() // Result.failure<T>(throwable)
// Assertions
assertTrue(result.isSuccess)
assertEquals(expectedValue, result.getOrNull())
```
---
## F. Test Utilities and Helpers
### Fake Implementations
| Fake | Location | Purpose |
|------|----------|---------|
| `FakeDispatcherManager` | `core/src/testFixtures/` | Deterministic coroutine execution |
| `FakeConfigDiskSource` | `data/src/testFixtures/` | In-memory config storage |
| `FakeSharedPreferences` | `data/src/testFixtures/` | Memory-backed SharedPreferences |
### Exception Testing (CRITICAL)
```kotlin
// CORRECT - Call directly, NOT inside runTest
@Test
fun `test exception`() {
assertCoroutineThrows<IllegalStateException> {
repository.throwingFunction()
}
}
```
**Why:** `runTest` catches exceptions and rethrows them, breaking the assertion pattern.
---
## G. Critical Gotchas
Common testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`
**Core Patterns:**
- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly
- **Static mock cleanup** - Always `unmockkStatic()` in `@After`
- **StateFlow vs EventFlow** - StateFlow: `awaitItem()` first; EventFlow: `expectNoEvents()` first
- **FakeDispatcherManager** - Always use instead of real `DispatcherManagerImpl`
- **Coroutine test wrapper** - Use `runTest { }` for all Flow/coroutine tests
**Assertion Patterns:**
- **Complete state assertions** - Assert entire state objects, not individual fields
- **JUnit over Kotlin** - Use `assertTrue()`, not Kotlin's `assert()`
- **Use Result extensions** - Use `asSuccess()` and `asFailure()` for Result type assertions
**Test Design:**
- **Fake vs Mock strategy** - Use Fakes for happy paths, Mocks for error paths
- **DI over static mocking** - Extract interfaces (like UuidManager) instead of mockkStatic
- **Null stream testing** - Test null returns from ContentResolver operations
- **bufferedMutableSharedFlow** - Use with `.onSubscription { emit(state) }` in Fakes
- **Test factory methods** - Accept domain state types, not SavedStateHandle
---
## H. Test File Organization
### Directory Structure
```
module/src/test/kotlin/com/bitwarden/.../
├── ui/*ScreenTest.kt, *ViewModelTest.kt
├── data/repository/*RepositoryTest.kt
└── network/service/*ServiceTest.kt
module/src/testFixtures/kotlin/com/bitwarden/.../
├── util/TestHelpers.kt
├── base/Base*Test.kt
└── model/*Util.kt
```
### Test Naming
- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`
- Functions: `` `given state when action should result` ``
---
## Summary
Key Bitwarden-specific testing patterns:
1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper
2. **BitwardenComposeTest** - Pre-configured with all managers and theme
3. **BaseServiceTest** - MockWebServer setup for network testing
4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)
5. **Test Data Builders** - Consistent `number: Int` parameter pattern
6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource
7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`
**Always consult:** `docs/ARCHITECTURE.md` and existing test files for reference implementations.
---
## Reference Documentation
For detailed information, see:
- `references/test-base-classes.md` - Detailed base class documentation and usage patterns
- `references/flow-testing-patterns.md` - Complete Turbine patterns for StateFlow/EventFlow
- `references/critical-gotchas.md` - Full anti-pattern reference and debugging tips
**Complete Examples:**
- `examples/viewmodel-test-example.md` - Full ViewModel test with StateFlow/EventFlow
- `examples/compose-screen-test-example.md` - Full Compose screen test
- `examples/repository-test-example.md` - Full repository test with mocks and fakes

View File

@@ -0,0 +1,337 @@
/**
* Complete Compose Screen Test Example
*
* Key patterns demonstrated:
* - Extending BitwardenComposeTest
* - Mocking ViewModel with flows
* - Testing UI interactions
* - Testing navigation callbacks
* - Using bufferedMutableSharedFlow for events
* - Testing dialogs with isDialog() and hasAnyAncestor()
*/
package com.bitwarden.example.feature
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.util.assertNoDialogExists
import com.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import junit.framework.TestCase.assertTrue
import org.junit.Before
import org.junit.Test
class ExampleScreenTest : BitwardenComposeTest() {
// Track navigation callbacks
private var haveCalledNavigateBack = false
private var haveCalledNavigateToNext = false
// Use bufferedMutableSharedFlow for events (default replay = 0)
private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
// Mock ViewModel with relaxed = true
private val viewModel = mockk<ExampleViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
haveCalledNavigateBack = false
haveCalledNavigateToNext = false
setContent {
ExampleScreen(
onNavigateBack = { haveCalledNavigateBack = true },
onNavigateToNext = { haveCalledNavigateToNext = true },
viewModel = viewModel,
)
}
}
/**
* Test: Back button sends action to ViewModel
*/
@Test
fun `on back click should send BackClick action`() {
composeTestRule
.onNodeWithContentDescription("Back")
.performClick()
verify { viewModel.trySendAction(ExampleAction.BackClick) }
}
/**
* Test: Submit button sends action to ViewModel
*/
@Test
fun `on submit click should send SubmitClick action`() {
composeTestRule
.onNodeWithText("Submit")
.performClick()
verify { viewModel.trySendAction(ExampleAction.SubmitClick) }
}
/**
* Test: Loading state shows progress indicator
*/
@Test
fun `loading state should display progress indicator`() {
mutableStateFlow.update { it.copy(isLoading = true) }
composeTestRule
.onNode(isProgressBar)
.assertIsDisplayed()
}
/**
* Test: Data state shows content
*/
@Test
fun `data state should display content`() {
mutableStateFlow.update { it.copy(data = "Test Data") }
composeTestRule
.onNodeWithText("Test Data")
.assertIsDisplayed()
}
/**
* Test: Error state shows error message
*/
@Test
fun `error state should display error message`() {
mutableStateFlow.update { it.copy(errorMessage = "Something went wrong") }
composeTestRule
.onNodeWithText("Something went wrong")
.assertIsDisplayed()
}
/**
* Test: NavigateBack event triggers navigation callback
*/
@Test
fun `NavigateBack event should call onNavigateBack`() {
mutableEventFlow.tryEmit(ExampleEvent.NavigateBack)
assertTrue(haveCalledNavigateBack)
}
/**
* Test: NavigateToNext event triggers navigation callback
*/
@Test
fun `NavigateToNext event should call onNavigateToNext`() {
mutableEventFlow.tryEmit(ExampleEvent.NavigateToNext)
assertTrue(haveCalledNavigateToNext)
}
/**
* Test: Item in list can be clicked
*/
@Test
fun `on item click should send ItemClick action`() {
val itemId = "item-123"
mutableStateFlow.update {
it.copy(items = listOf(ExampleItem(id = itemId, name = "Test Item")))
}
composeTestRule
.onNodeWithText("Test Item")
.performClick()
verify { viewModel.trySendAction(ExampleAction.ItemClick(itemId)) }
}
// ==================== DIALOG TESTS ====================
/**
* Test: No dialog exists when dialogState is null
*/
@Test
fun `no dialog should exist when dialogState is null`() {
mutableStateFlow.update { it.copy(dialogState = null) }
composeTestRule.assertNoDialogExists()
}
/**
* Test: Loading dialog displays when state updates
* PATTERN: Use isDialog() to check dialog exists
*/
@Test
fun `loading dialog should display when dialogState is Loading`() {
mutableStateFlow.update {
it.copy(dialogState = ExampleState.DialogState.Loading("Please wait..."))
}
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
// Verify loading text within dialog using hasAnyAncestor(isDialog())
composeTestRule
.onAllNodesWithText("Please wait...")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
/**
* Test: Error dialog displays title and message
* PATTERN: Use filterToOne(hasAnyAncestor(isDialog())) to find text within dialogs
*/
@Test
fun `error dialog should display title and message`() {
mutableStateFlow.update {
it.copy(
dialogState = ExampleState.DialogState.Error(
title = "An error has occurred",
message = "Something went wrong. Please try again.",
),
)
}
// Verify dialog exists
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
// Verify title within dialog
composeTestRule
.onAllNodesWithText("An error has occurred")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
// Verify message within dialog
composeTestRule
.onAllNodesWithText("Something went wrong. Please try again.")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
/**
* Test: Dialog button click sends action
* PATTERN: Find button with hasAnyAncestor(isDialog()) then performClick()
*/
@Test
fun `error dialog dismiss button should send DismissDialog action`() {
mutableStateFlow.update {
it.copy(
dialogState = ExampleState.DialogState.Error(
title = "Error",
message = "An error occurred",
),
)
}
// Click dismiss button within dialog
composeTestRule
.onAllNodesWithText("Ok")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(ExampleAction.DismissDialog) }
}
/**
* Test: Confirmation dialog with multiple buttons
* PATTERN: Test both confirm and cancel actions
*/
@Test
fun `confirmation dialog confirm button should send ConfirmAction`() {
mutableStateFlow.update {
it.copy(
dialogState = ExampleState.DialogState.Confirmation(
title = "Confirm Action",
message = "Are you sure you want to proceed?",
),
)
}
// Click confirm button
composeTestRule
.onAllNodesWithText("Confirm")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(ExampleAction.ConfirmAction) }
}
@Test
fun `confirmation dialog cancel button should send DismissDialog action`() {
mutableStateFlow.update {
it.copy(
dialogState = ExampleState.DialogState.Confirmation(
title = "Confirm Action",
message = "Are you sure?",
),
)
}
// Click cancel button
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(ExampleAction.DismissDialog) }
}
}
private val DEFAULT_STATE = ExampleState(
isLoading = false,
data = null,
errorMessage = null,
items = emptyList(),
dialogState = null,
)
// Example types (normally in separate files)
data class ExampleState(
val isLoading: Boolean = false,
val data: String? = null,
val errorMessage: String? = null,
val items: List<ExampleItem> = emptyList(),
val dialogState: DialogState? = null,
) {
/**
* PATTERN: Nested sealed class for dialog states.
* Common dialog types: Loading, Error, Confirmation
*/
sealed class DialogState {
data class Loading(val message: String) : DialogState()
data class Error(val title: String, val message: String) : DialogState()
data class Confirmation(val title: String, val message: String) : DialogState()
}
}
data class ExampleItem(val id: String, val name: String)
sealed class ExampleAction {
data object BackClick : ExampleAction()
data object SubmitClick : ExampleAction()
data class ItemClick(val itemId: String) : ExampleAction()
data object DismissDialog : ExampleAction()
data object ConfirmAction : ExampleAction()
}
sealed class ExampleEvent {
data object NavigateBack : ExampleEvent()
data object NavigateToNext : ExampleEvent()
}

View File

@@ -0,0 +1,255 @@
/**
* Complete Repository Test Example
*
* Key patterns demonstrated:
* - Fake for disk sources, Mock for network services
* - Using FakeDispatcherManager for deterministic coroutines
* - Using fixed Clock for deterministic time
* - Testing Result types with .asSuccess() / .asFailure()
* - Asserting actual objects (not isSuccess/isFailure) for better diagnostics
* - Testing Flow emissions with Turbine
*/
package com.bitwarden.example.data.repository
import app.cash.turbine.test
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class ExampleRepositoryTest {
// Fixed clock for deterministic time-based tests
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
// Use FakeDispatcherManager for deterministic coroutine execution
private val dispatcherManager = FakeDispatcherManager()
// Mock service (network layer is always mocked)
private val mockService: ExampleService = mockk()
/**
* PATTERN: Use Fake for disk source in happy path tests.
* This is the Bitwarden convention for repository testing.
*/
private val fakeDiskSource = FakeExampleDiskSource()
private lateinit var repository: ExampleRepositoryImpl
@BeforeEach
fun setup() {
repository = ExampleRepositoryImpl(
clock = fixedClock,
service = mockService,
diskSource = fakeDiskSource,
dispatcherManager = dispatcherManager,
)
}
// ==================== HAPPY PATH TESTS (use Fake) ====================
/**
* Test: Successful fetch returns data and saves to disk
*/
@Test
fun `fetchData should return success and save to disk when service succeeds`() = runTest {
val expectedData = ExampleData(id = "1", name = "Test", updatedAt = fixedClock.instant())
coEvery { mockService.getData() } returns expectedData.asSuccess()
val result = repository.fetchData()
assertEquals(expectedData, result.getOrThrow())
// Fake automatically stores the data - verify it's there
assertEquals(expectedData, fakeDiskSource.storedData)
}
/**
* Test: Service failure returns failure without saving
*/
@Test
fun `fetchData should return failure when service fails`() = runTest {
val exception = Exception("Network error")
coEvery { mockService.getData() } returns exception.asFailure()
val result = repository.fetchData()
assertEquals(exception, result.exceptionOrNull())
// Fake was not updated
assertNull(fakeDiskSource.storedData)
}
/**
* Test: Repository flow emits when disk source updates
*/
@Test
fun `dataFlow should emit when disk source updates`() = runTest {
val data1 = ExampleData(id = "1", name = "First", updatedAt = fixedClock.instant())
val data2 = ExampleData(id = "2", name = "Second", updatedAt = fixedClock.instant())
repository.dataFlow.test {
// Initial null value from Fake
assertNull(awaitItem())
// Update via Fake property setter (triggers emission)
fakeDiskSource.storedData = data1
assertEquals(data1, awaitItem())
// Another update
fakeDiskSource.storedData = data2
assertEquals(data2, awaitItem())
}
}
/**
* Test: Refresh fetches and saves new data
*/
@Test
fun `refresh should fetch new data and update disk source`() = runTest {
val newData = ExampleData(id = "new", name = "Fresh", updatedAt = fixedClock.instant())
coEvery { mockService.getData() } returns newData.asSuccess()
val result = repository.refresh()
assertEquals(Unit, result.getOrThrow())
coVerify { mockService.getData() }
assertEquals(newData, fakeDiskSource.storedData)
}
/**
* Test: Delete clears data from disk
*/
@Test
fun `deleteData should clear disk source`() = runTest {
// Pre-populate the fake
fakeDiskSource.storedData = ExampleData(id = "1", name = "Test", updatedAt = fixedClock.instant())
repository.deleteData()
assertNull(fakeDiskSource.storedData)
}
/**
* Test: Cached data returns from disk when available
*/
@Test
fun `getCachedData should return disk data without network call`() = runTest {
val cachedData = ExampleData(
id = "cached",
name = "Cached",
updatedAt = fixedClock.instant(),
)
fakeDiskSource.storedData = cachedData
val result = repository.getCachedData()
assertEquals(cachedData, result)
coVerify(exactly = 0) { mockService.getData() }
}
// ==================== ERROR PATH TESTS ====================
/**
* PATTERN: For error paths, reconfigure the class-level mock per-test.
* Use coEvery to change mock behavior for each specific test case.
*/
@Test
fun `fetchData should return failure when service returns error`() = runTest {
val exception = Exception("Server unavailable")
coEvery { mockService.getData() } returns exception.asFailure()
val result = repository.fetchData()
assertEquals(exception, result.exceptionOrNull())
// Fake state unchanged on failure
assertNull(fakeDiskSource.storedData)
}
@Test
fun `refresh should return failure and preserve cached data when service fails`() = runTest {
// Pre-populate cache via Fake
val cachedData = ExampleData(id = "cached", name = "Old", updatedAt = fixedClock.instant())
fakeDiskSource.storedData = cachedData
// Reconfigure mock to return failure
coEvery { mockService.getData() } returns Exception("Network error").asFailure()
val result = repository.refresh()
assertTrue(result.isFailure)
// Cached data preserved on failure
assertEquals(cachedData, fakeDiskSource.storedData)
}
}
// Example types (normally in separate files)
data class ExampleData(
val id: String,
val name: String,
val updatedAt: Instant,
)
interface ExampleService {
suspend fun getData(): Result<ExampleData>
}
interface ExampleDiskSource {
val dataFlow: kotlinx.coroutines.flow.Flow<ExampleData?>
fun getData(): ExampleData?
fun saveData(data: ExampleData)
fun clearData()
}
/**
* PATTERN: Fake implementation for happy path testing.
*
* Key characteristics:
* - Uses bufferedMutableSharedFlow(replay = 1) for proper replay behavior
* - Uses .onSubscription { emit(state) } for immediate state emission
* - Private storage with override property setter that emits to flow
* - Test assertions done via the override property getter
*/
class FakeExampleDiskSource : ExampleDiskSource {
private var storedDataValue: ExampleData? = null
private val mutableDataFlow = bufferedMutableSharedFlow<ExampleData?>(replay = 1)
/**
* Override property with getter/setter. Setter emits to flow automatically.
* Tests can read this property for assertions and write to trigger emissions.
*/
var storedData: ExampleData?
get() = storedDataValue
set(value) {
storedDataValue = value
mutableDataFlow.tryEmit(value)
}
override val dataFlow: Flow<ExampleData?>
get() = mutableDataFlow.onSubscription { emit(storedData) }
override fun getData(): ExampleData? = storedData
override fun saveData(data: ExampleData) {
storedData = data
}
override fun clearData() {
storedData = null
}
}

View File

@@ -0,0 +1,161 @@
/**
* Complete ViewModel Test Example
*
* Key patterns demonstrated:
* - Extending BaseViewModelTest
* - Testing StateFlow with Turbine
* - Testing EventFlow with Turbine
* - Using stateEventFlow() for simultaneous testing
* - MockK mocking patterns
* - Test factory method design (accepts domain state, not SavedStateHandle)
* - Complete state assertions (assert entire state objects)
*/
package com.bitwarden.example.feature
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ExampleViewModelTest : BaseViewModelTest() {
// Mock dependencies
private val mockRepository: ExampleRepository = mockk()
private val mockAuthDiskSource: AuthDiskSource = mockk {
every { userStateFlow } returns MutableStateFlow(null)
}
/**
* StateFlow has replay=1, so first awaitItem() returns current state
*/
@Test
fun `initial state should be default state`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
}
}
/**
* Test state transitions: initial -> loading -> success
*/
@Test
fun `LoadData action should update state from idle to loading to success`() = runTest {
val expectedData = "loaded data"
coEvery { mockRepository.fetchData(any()) } returns Result.success(expectedData)
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(ExampleAction.LoadData)
assertEquals(DEFAULT_STATE.copy(isLoading = true), awaitItem())
assertEquals(DEFAULT_STATE.copy(isLoading = false, data = expectedData), awaitItem())
}
coVerify { mockRepository.fetchData(any()) }
}
/**
* EventFlow has no replay - MUST call expectNoEvents() first
*/
@Test
fun `SubmitClick action should emit NavigateToNext event`() = runTest {
coEvery { mockRepository.submitData(any()) } returns Result.success(Unit)
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents() // CRITICAL for EventFlow
viewModel.trySendAction(ExampleAction.SubmitClick)
assertEquals(ExampleEvent.NavigateToNext, awaitItem())
}
}
/**
* Use stateEventFlow() helper for simultaneous testing
*/
@Test
fun `complex action should update state and emit event`() = runTest {
coEvery { mockRepository.complexOperation(any()) } returns Result.success("result")
val viewModel = createViewModel()
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
eventFlow.expectNoEvents()
viewModel.trySendAction(ExampleAction.ComplexAction)
assertEquals(DEFAULT_STATE.copy(isLoading = true), stateFlow.awaitItem())
assertEquals(DEFAULT_STATE.copy(data = "result"), stateFlow.awaitItem())
assertEquals(ExampleEvent.ShowToast("Success!"), eventFlow.awaitItem())
}
}
/**
* Test state restoration from saved state.
* Note: Use initialState parameter, NOT SavedStateHandle directly.
*/
@Test
fun `initial state from saved state should be preserved`() = runTest {
// Build complete expected state - always assert full objects
val savedState = ExampleState(
isLoading = false,
data = "restored data",
errorMessage = null,
)
val viewModel = createViewModel(initialState = savedState)
viewModel.stateFlow.test {
assertEquals(savedState, awaitItem())
}
}
/**
* Factory method accepts domain state, NOT SavedStateHandle.
* This hides Android framework details from test logic.
*/
private fun createViewModel(
initialState: ExampleState? = null,
): ExampleViewModel = ExampleViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", initialState) },
repository = mockRepository,
authDiskSource = mockAuthDiskSource,
)
}
private val DEFAULT_STATE = ExampleState(
isLoading = false,
data = null,
errorMessage = null,
)
// Example types (normally in separate files)
data class ExampleState(
val isLoading: Boolean = false,
val data: String? = null,
val errorMessage: String? = null,
)
sealed class ExampleAction {
data object LoadData : ExampleAction()
data object SubmitClick : ExampleAction()
data object ComplexAction : ExampleAction()
}
sealed class ExampleEvent {
data object NavigateToNext : ExampleEvent()
data class ShowToast(val message: String) : ExampleEvent()
}

View File

@@ -0,0 +1,698 @@
# Critical Gotchas and Anti-Patterns
Common mistakes and pitfalls when writing tests in the Bitwarden Android codebase.
## ❌ NEVER wrap assertCoroutineThrows in runTest
### The Problem
`runTest` catches exceptions and rethrows them, which breaks the `assertCoroutineThrows` assertion pattern.
### Wrong
```kotlin
@Test
fun `test exception`() = runTest {
assertCoroutineThrows<Exception> {
repository.throwingFunction()
} // Won't work - exception is caught by runTest!
}
```
### Correct
```kotlin
@Test
fun `test exception`() {
assertCoroutineThrows<Exception> {
repository.throwingFunction()
} // Works correctly
}
```
### Why This Happens
`runTest` provides a coroutine scope and catches exceptions to provide better error messages. However, `assertCoroutineThrows` needs to catch the exception itself to verify it was thrown. When wrapped in `runTest`, the exception is caught twice, breaking the assertion.
## ❌ ALWAYS unmock static functions
### The Problem
MockK's static mocking persists across tests. Forgetting to clean up causes mysterious failures in subsequent tests.
### Wrong
```kotlin
@Before
fun setup() {
mockkStatic(::isBuildVersionAtLeast)
every { isBuildVersionAtLeast(any()) } returns true
}
// Forgot @After - subsequent tests will fail mysteriously!
```
### Correct
```kotlin
@Before
fun setup() {
mockkStatic(::isBuildVersionAtLeast)
every { isBuildVersionAtLeast(any()) } returns true
}
@After
fun tearDown() {
unmockkStatic(::isBuildVersionAtLeast) // CRITICAL
}
```
### Common Static Functions to Watch
```kotlin
// Platform version checks
mockkStatic(::isBuildVersionAtLeast)
unmockkStatic(::isBuildVersionAtLeast)
// URI parsing
mockkStatic(Uri::class)
unmockkStatic(Uri::class)
// Static utility functions
mockkStatic(MyUtilClass::class)
unmockkStatic(MyUtilClass::class)
```
### Debugging Tip
If tests pass individually but fail when run together, suspect static mocking cleanup issues.
## ❌ Don't confuse StateFlow and EventFlow testing
### StateFlow (replay = 1)
```kotlin
// CORRECT - StateFlow always has current value
viewModel.stateFlow.test {
val initial = awaitItem() // Gets current state immediately
viewModel.trySendAction(action)
val updated = awaitItem() // Gets new state
}
```
### EventFlow (no replay)
```kotlin
// CORRECT - EventFlow has no initial value
viewModel.eventFlow.test {
expectNoEvents() // MUST do this first
viewModel.trySendAction(action)
val event = awaitItem() // Gets emitted event
}
```
### Common Mistake
```kotlin
// WRONG - Forgetting expectNoEvents() on EventFlow
viewModel.eventFlow.test {
viewModel.trySendAction(action) // May cause flaky tests
assertEquals(event, awaitItem())
}
```
## ❌ Don't mix real and test dispatchers
### Wrong
```kotlin
private val repository = ExampleRepositoryImpl(
dispatcherManager = DispatcherManagerImpl(), // Real dispatcher!
)
@Test
fun `test repository`() = runTest {
// Test will have timing issues - real dispatcher != test dispatcher
}
```
### Correct
```kotlin
private val repository = ExampleRepositoryImpl(
dispatcherManager = FakeDispatcherManager(), // Test dispatcher
)
@Test
fun `test repository`() = runTest {
// Test runs deterministically
}
```
### Why This Matters
Real dispatchers use actual thread pools and delays. Test dispatchers (UnconfinedTestDispatcher) execute immediately and deterministically. Mixing them causes:
- Non-deterministic test failures
- Real delays in tests (slow test suite)
- Race conditions
### Always Use
- `FakeDispatcherManager()` for repositories
- `UnconfinedTestDispatcher()` when manually creating dispatchers
- `runTest` for coroutine tests (provides TestDispatcher automatically)
## ❌ Don't forget to use runTest for coroutine tests
### Wrong
```kotlin
@Test
fun `test coroutine`() {
viewModel.stateFlow.test { /* ... */ } // Missing runTest!
}
```
This causes:
- Test completes before coroutines finish
- False positives (test passes but assertions never run)
- Mysterious failures
### Correct
```kotlin
@Test
fun `test coroutine`() = runTest {
viewModel.stateFlow.test { /* ... */ }
}
```
### When runTest is Required
- Testing ViewModels (they use `viewModelScope`)
- Testing Flows with Turbine `.test {}`
- Testing repositories with suspend functions
- Any test calling suspend functions
### Exception: assertCoroutineThrows
As noted above, `assertCoroutineThrows` should NOT be wrapped in `runTest`.
## ❌ Don't forget relaxed = true for complex mocks
### Without relaxed
```kotlin
private val viewModel = mockk<ExampleViewModel>() // Must mock every method!
// Error: "no answer found for: stateFlow"
```
### With relaxed
```kotlin
private val viewModel = mockk<ExampleViewModel>(relaxed = true) {
// Only mock what you care about
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
}
```
### When to Use relaxed
- Mocking ViewModels in Compose tests
- Mocking complex objects with many methods
- When you only care about specific method calls
### When NOT to Use relaxed
- Mocking repository interfaces (be explicit about behavior)
- When you want to verify NO unexpected calls
- Testing error paths (want test to fail if unexpected method called)
## ❌ Don't assert individual fields when complete state is available
### The Problem
Asserting individual state fields can miss unintended side effects on other fields.
### Wrong
```kotlin
@Test
fun `action should update state`() = runTest {
viewModel.trySendAction(SomeAction.DoThing)
val state = viewModel.stateFlow.value
assertEquals(null, state.dialog) // Only checks one field!
}
```
### Correct
```kotlin
@Test
fun `action should update state`() = runTest {
viewModel.trySendAction(SomeAction.DoThing)
val expected = SomeState(
isLoading = false,
data = "result",
dialog = null,
)
assertEquals(expected, viewModel.stateFlow.value) // Checks all fields
}
```
### Why This Matters
- Catches unintended mutations to other state fields
- Makes expected state explicit and readable
- Prevents silent regressions when state structure changes
---
## ❌ Don't use Kotlin assert() for boolean checks
### The Problem
Kotlin's `assert()` doesn't follow JUnit conventions and provides poor failure messages.
### Wrong
```kotlin
@Test
fun `event should trigger callback`() {
mutableEventFlow.tryEmit(SomeEvent.Navigate)
assert(onNavigateCalled) // Kotlin assert - bad failure messages
}
```
### Correct
```kotlin
@Test
fun `event should trigger callback`() {
mutableEventFlow.tryEmit(SomeEvent.Navigate)
assertTrue(onNavigateCalled) // JUnit assertTrue - proper assertion
}
```
### Always Use JUnit Assertions
- `assertTrue()` / `assertFalse()` for booleans
- `assertEquals()` for value comparisons
- `assertNotNull()` / `assertNull()` for nullability
- `assertThrows<T>()` for exceptions
---
## ❌ Don't pass SavedStateHandle to test factory methods
### The Problem
Exposing `SavedStateHandle` in test factory methods leaks Android framework details into test logic.
### Wrong
```kotlin
private fun createViewModel(
savedStateHandle: SavedStateHandle = SavedStateHandle(), // Framework type exposed
): MyViewModel = MyViewModel(
savedStateHandle = savedStateHandle,
repository = mockRepository,
)
@Test
fun `initial state from saved state`() = runTest {
val savedState = MyState(isLoading = true)
val savedStateHandle = SavedStateHandle(mapOf("state" to savedState))
val viewModel = createViewModel(savedStateHandle = savedStateHandle)
// ...
}
```
### Correct
```kotlin
private fun createViewModel(
initialState: MyState? = null, // Domain type only
): MyViewModel = MyViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", initialState) },
repository = mockRepository,
)
@Test
fun `initial state from saved state`() = runTest {
val savedState = MyState(isLoading = true)
val viewModel = createViewModel(initialState = savedState)
// ...
}
```
### Why This Matters
- Cleaner, more intuitive test code
- Hides SavedStateHandle implementation details
- Follows Bitwarden conventions
---
## ❌ Don't test SavedStateHandle persistence in unit tests
### The Problem
Testing whether state persists to SavedStateHandle is testing Android framework behavior, not your business logic.
### Wrong
```kotlin
@Test
fun `state should persist to SavedStateHandle`() = runTest {
val savedStateHandle = SavedStateHandle()
val viewModel = createViewModel(savedStateHandle = savedStateHandle)
viewModel.trySendAction(SomeAction)
val savedState = savedStateHandle.get<MyState>("state")
assertEquals(expectedState, savedState) // Testing framework, not logic!
}
```
### Correct
Focus on testing business logic and state transformations:
```kotlin
@Test
fun `action should update state correctly`() = runTest {
val viewModel = createViewModel()
viewModel.trySendAction(SomeAction)
assertEquals(expectedState, viewModel.stateFlow.value) // Test observable state
}
```
---
## ❌ Don't use static mocking when DI pattern is available
### The Problem
Static mocking (`mockkStatic`) is harder to maintain and less testable than dependency injection.
### Wrong
```kotlin
class ParserTest {
@BeforeEach
fun setup() {
mockkStatic(UUID::class)
every { UUID.randomUUID() } returns mockk {
every { toString() } returns "fixed-uuid"
}
}
@AfterEach
fun tearDown() {
unmockkStatic(UUID::class)
}
}
```
### Correct
Extract an interface and inject it:
```kotlin
// Production code
interface UuidManager {
fun generateUuid(): String
}
class UuidManagerImpl : UuidManager {
override fun generateUuid(): String = UUID.randomUUID().toString()
}
class Parser(private val uuidManager: UuidManager) { ... }
// Test code
class ParserTest {
private val mockUuidManager = mockk<UuidManager>()
@BeforeEach
fun setup() {
every { mockUuidManager.generateUuid() } returns "fixed-uuid"
}
// No tearDown needed - no static mocking!
}
```
### When to Use This Pattern
- UUID generation
- Timestamp/Clock operations
- System property access
- Any static function that needs deterministic testing
---
## ❌ Don't forget to test null stream returns from Android APIs
### The Problem
Android's `ContentResolver.openOutputStream()` and `openInputStream()` can return null, not just throw exceptions.
### Wrong
```kotlin
class FileManagerTest {
@Test
fun `stringToUri with exception should return false`() = runTest {
every { mockContentResolver.openOutputStream(any()) } throws IOException()
val result = fileManager.stringToUri(mockUri, "data")
assertFalse(result)
}
// Missing: test for null return!
}
```
### Correct
```kotlin
class FileManagerTest {
@Test
fun `stringToUri with exception should return false`() = runTest {
every { mockContentResolver.openOutputStream(any()) } throws IOException()
val result = fileManager.stringToUri(mockUri, "data")
assertFalse(result)
}
@Test
fun `stringToUri with null stream should return false`() = runTest {
every { mockContentResolver.openOutputStream(any()) } returns null
val result = fileManager.stringToUri(mockUri, "data")
assertFalse(result) // CRITICAL: must handle null!
}
}
```
### Common Android APIs That Return Null
- `ContentResolver.openOutputStream()` / `openInputStream()`
- `Context.getExternalFilesDir()`
- `PackageManager.getApplicationInfo()` (can throw)
---
## Bitwarden Mocking Guidelines
**Mock at architectural boundaries:**
- Repository → ViewModel (mock repository)
- Service → Repository (mock service)
- API → Service (use MockWebServer, not mocks)
- DiskSource → Repository (mock disk source)
**Fake vs Mock Strategy (IMPORTANT):**
- **Happy paths**: Use Fake implementations (`FakeAuthenticatorDiskSource`, `FakeVaultDiskSource`)
- **Error paths**: Use MockK with isolated repository instances
```kotlin
// Happy path - use Fake
private val fakeDiskSource = FakeAuthenticatorDiskSource()
@Test
fun `createItem should return Success`() = runTest {
val result = repository.createItem(mockItem)
assertEquals(CreateItemResult.Success, result)
}
// Error path - use isolated Mock
@Test
fun `createItem with exception should return Error`() = runTest {
val mockDiskSource = mockk<AuthenticatorDiskSource> {
coEvery { saveItem(any()) } throws RuntimeException()
}
val repository = RepositoryImpl(diskSource = mockDiskSource)
val result = repository.createItem(mockItem)
assertEquals(CreateItemResult.Error, result)
}
```
**Use Fakes for:**
- `FakeDispatcherManager` - deterministic coroutines
- `FakeConfigDiskSource` - in-memory config storage
- `FakeSharedPreferences` - memory-backed preferences
- `FakeAuthenticatorDiskSource` - in-memory authenticator storage
**Create real instances for:**
- Data classes, value objects (User, Config, CipherView)
- Test data builders (`createMockCipher(number = 1)`)
## ❌ Don't forget bufferedMutableSharedFlow with onSubscription for Fakes
### The Problem
Fake data sources using `MutableSharedFlow` won't emit cached state to new subscribers without explicit handling.
### Wrong
```kotlin
class FakeDataSource : DataSource {
private val mutableFlow = MutableSharedFlow<List<Item>>()
private val storedItems = mutableListOf<Item>()
override fun getItems(): Flow<List<Item>> = mutableFlow
override suspend fun saveItem(item: Item) {
storedItems.add(item)
mutableFlow.emit(storedItems)
}
}
// Test: Initial collection gets nothing!
repository.dataFlow.test {
// Hangs or fails - no initial emission
}
```
### Correct
```kotlin
class FakeDataSource : DataSource {
private val mutableFlow = bufferedMutableSharedFlow<List<Item>>()
private val storedItems = mutableListOf<Item>()
override fun getItems(): Flow<List<Item>> = mutableFlow
.onSubscription { emit(storedItems.toList()) }
override suspend fun saveItem(item: Item) {
storedItems.add(item)
mutableFlow.emit(storedItems.toList())
}
}
// Test: Initial collection receives current state
repository.dataFlow.test {
assertEquals(emptyList(), awaitItem()) // Works!
}
```
### Key Points
- Use `bufferedMutableSharedFlow()` from `core/data/repository/util/`
- Add `.onSubscription { emit(currentState) }` for immediate state emission
- This ensures new collectors receive the current cached state
---
## ✅ Use Result extension functions for assertions
### The Pattern
Use `asSuccess()` and `asFailure()` extensions from `com.bitwarden.core.data.util` for cleaner Result assertions.
### Success Path
```kotlin
@Test
fun `getData should return success`() = runTest {
val result = repository.getData()
val expected = expectedData.asSuccess()
assertEquals(expected.getOrNull(), result.getOrNull())
}
```
### Failure Path
```kotlin
@Test
fun `getData with error should return failure`() = runTest {
val exception = IOException("Network error")
coEvery { mockService.getData() } returns exception.asFailure()
val result = repository.getData()
assertTrue(result.isFailure)
assertEquals(exception, result.exceptionOrNull())
}
```
### Avoid Redundant Assertions
```kotlin
// WRONG - redundant success checks
assertTrue(result.isSuccess)
assertTrue(expected.isSuccess)
assertArrayEquals(expected.getOrNull(), result.getOrNull())
// CORRECT - final assertion is sufficient
assertArrayEquals(expected.getOrNull(), result.getOrNull())
```
---
## Summary Checklist
Before submitting tests, verify:
**Core Patterns:**
- [ ] No `assertCoroutineThrows` inside `runTest`
- [ ] All static mocks have `unmockk` in `@After`
- [ ] EventFlow tests start with `expectNoEvents()`
- [ ] Using FakeDispatcherManager, not real dispatchers
- [ ] All coroutine tests use `runTest`
**Assertion Patterns:**
- [ ] Assert complete state objects, not individual fields
- [ ] Use JUnit `assertTrue()`, not Kotlin `assert()`
- [ ] Use `asSuccess()` for Result type assertions
- [ ] Avoid redundant assertion patterns
**Test Design:**
- [ ] Test factory methods accept domain types, not SavedStateHandle
- [ ] Use Fakes for happy paths, Mocks for error paths
- [ ] Prefer DI patterns over static mocking
- [ ] Test null returns from Android APIs (streams, files)
- [ ] Fakes use `bufferedMutableSharedFlow()` with `.onSubscription`
**General:**
- [ ] Tests don't depend on execution order
- [ ] Complex mocks use `relaxed = true`
- [ ] Test data is created fresh for each test
- [ ] Mocking behavior, not value objects
- [ ] Testing observable behavior, not implementation
When tests fail mysteriously, check these gotchas first.

View File

@@ -0,0 +1,274 @@
# Flow Testing with Turbine
Bitwarden Android uses Turbine for testing Kotlin Flows, including the critical distinction between StateFlow and EventFlow patterns.
## StateFlow vs EventFlow
### StateFlow (Replayed)
**Characteristics:**
- `replay = 1` - Always emits current value to new collectors
- First `awaitItem()` returns the current/initial state
- Survives configuration changes
- Used for UI state that needs to be immediately available
**Test Pattern:**
```kotlin
@Test
fun `action should update state`() = runTest {
val viewModel = MyViewModel(savedStateHandle, mockRepository)
viewModel.stateFlow.test {
// First awaitItem() gets CURRENT state
assertEquals(INITIAL_STATE, awaitItem())
// Trigger action
viewModel.trySendAction(MyAction.LoadData)
// Next awaitItem() gets UPDATED state
assertEquals(LOADING_STATE, awaitItem())
assertEquals(SUCCESS_STATE, awaitItem())
}
}
```
### EventFlow (No Replay)
**Characteristics:**
- `replay = 0` - Only emits new events after subscription
- No initial value emission
- One-time events (navigation, toasts, dialogs)
- Does not survive configuration changes
**Test Pattern:**
```kotlin
@Test
fun `action should emit event`() = runTest {
val viewModel = MyViewModel(savedStateHandle, mockRepository)
viewModel.eventFlow.test {
// MUST call expectNoEvents() first - nothing emitted yet
expectNoEvents()
// Trigger action
viewModel.trySendAction(MyAction.Submit)
// Now expect the event
assertEquals(MyEvent.NavigateToNext, awaitItem())
}
}
```
**Critical:** Always call `expectNoEvents()` before triggering actions on EventFlow. Forgetting this causes flaky tests.
## Testing State and Events Simultaneously
Use the `stateEventFlow()` helper from `BaseViewModelTest`:
```kotlin
@Test
fun `complex action should update state and emit event`() = runTest {
val viewModel = MyViewModel(savedStateHandle, mockRepository)
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
// Initial state
assertEquals(INITIAL_STATE, stateFlow.awaitItem())
// No events yet
eventFlow.expectNoEvents()
// Trigger action
viewModel.trySendAction(MyAction.ComplexAction)
// Verify state progression
assertEquals(LOADING_STATE, stateFlow.awaitItem())
assertEquals(SUCCESS_STATE, stateFlow.awaitItem())
// Verify event emission
assertEquals(MyEvent.ShowToast, eventFlow.awaitItem())
}
}
```
## Repository Flow Testing
### Testing Database Flows
```kotlin
@Test
fun `dataFlow should emit when database updates`() = runTest {
val dataFlow = MutableStateFlow(initialData)
every { mockDiskSource.dataFlow } returns dataFlow
repository.dataFlow.test {
// Initial value
assertEquals(initialData, awaitItem())
// Update disk source
dataFlow.value = updatedData
// Verify emission
assertEquals(updatedData, awaitItem())
}
}
```
### Testing Transformed Flows
```kotlin
@Test
fun `flow transformation should map correctly`() = runTest {
val sourceFlow = MutableStateFlow(UserEntity(id = "1", name = "John"))
every { mockDao.observeUser() } returns sourceFlow
// Repository transforms entity to domain model
repository.userFlow.test {
val expectedUser = User(id = "1", name = "John")
assertEquals(expectedUser, awaitItem())
}
}
```
## Common Patterns
### Pattern 1: Testing Initial State + Action
```kotlin
@Test
fun `load data should update from idle to loading to success`() = runTest {
coEvery { repository.getData() } returns "data".asSuccess()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.loadData()
assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())
assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Success), awaitItem())
}
}
```
### Pattern 2: Testing Error States
```kotlin
@Test
fun `load data with error should emit failure state`() = runTest {
val error = Exception("Network error")
coEvery { repository.getData() } returns error.asFailure()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.loadData()
assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())
assertEquals(
DEFAULT_STATE.copy(loadingState = LoadingState.Error("Network error")),
awaitItem(),
)
}
}
```
### Pattern 3: Testing Event Sequences
```kotlin
@Test
fun `submit should emit validation then navigation events`() = runTest {
viewModel.eventFlow.test {
expectNoEvents()
viewModel.trySendAction(MyAction.Submit)
assertEquals(MyEvent.ShowValidation, awaitItem())
assertEquals(MyEvent.NavigateToNext, awaitItem())
}
}
```
### Pattern 4: Testing Cancellation
```kotlin
@Test
fun `cancelling collection should stop emissions`() = runTest {
val flow = flow {
repeat(100) {
emit(it)
delay(100)
}
}
flow.test {
assertEquals(0, awaitItem())
assertEquals(1, awaitItem())
// Cancel after 2 items
cancel()
// No more items received
}
}
```
## Anti-Patterns
### ❌ Forgetting expectNoEvents() on EventFlow
```kotlin
// WRONG
viewModel.eventFlow.test {
viewModel.trySendAction(action) // May fail - no initial expectNoEvents
assertEquals(event, awaitItem())
}
// CORRECT
viewModel.eventFlow.test {
expectNoEvents() // ALWAYS do this first
viewModel.trySendAction(action)
assertEquals(event, awaitItem())
}
```
### ❌ Not Using runTest
```kotlin
// WRONG - Missing runTest
@Test
fun `test flow`() {
flow.test { /* ... */ }
}
// CORRECT
@Test
fun `test flow`() = runTest {
flow.test { /* ... */ }
}
```
### ❌ Mixing StateFlow and EventFlow Patterns
```kotlin
// WRONG - Treating StateFlow like EventFlow
stateFlow.test {
expectNoEvents() // Unnecessary - StateFlow always has value
/* ... */
}
// WRONG - Treating EventFlow like StateFlow
eventFlow.test {
val item = awaitItem() // Will hang - no initial value!
/* ... */
}
```
## Reference Implementations
**ViewModel with StateFlow and EventFlow:**
`app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`
**Repository Flow Testing:**
`data/src/test/kotlin/com/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt`
**Complex Flow Transformations:**
`data/src/test/kotlin/com/bitwarden/data/vault/repository/VaultRepositoryTest.kt`

View File

@@ -0,0 +1,259 @@
# Test Base Classes Reference
Bitwarden Android provides specialized base classes that configure test environments and provide helper utilities.
## BaseViewModelTest
**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`
### Purpose
Provides essential setup for testing ViewModels with proper coroutine dispatcher configuration and Flow testing helpers.
### Automatic Configuration
- Registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`
- Ensures deterministic coroutine execution in tests
- All coroutines complete immediately without real delays
### Key Feature: stateEventFlow() Helper
**Use Case:** When you need to test both StateFlow and EventFlow simultaneously.
```kotlin
@Test
fun `complex action should update state and emit event`() = runTest {
val viewModel = ExampleViewModel(savedStateHandle, mockRepository)
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
// Verify initial state
assertEquals(INITIAL_STATE, stateFlow.awaitItem())
// No events yet
eventFlow.expectNoEvents()
// Trigger action
viewModel.trySendAction(ExampleAction.ComplexAction)
// Verify state updated
assertEquals(LOADING_STATE, stateFlow.awaitItem())
// Verify event emitted
assertEquals(ExampleEvent.ShowToast, eventFlow.awaitItem())
}
}
```
### Usage Pattern
```kotlin
class MyViewModelTest : BaseViewModelTest() {
private val mockRepository: MyRepository = mockk()
private val savedStateHandle = SavedStateHandle(
mapOf(KEY_STATE to INITIAL_STATE)
)
@Test
fun `test action`() = runTest {
val viewModel = MyViewModel(
savedStateHandle = savedStateHandle,
repository = mockRepository
)
// Test with automatic dispatcher setup
viewModel.stateFlow.test {
assertEquals(INITIAL_STATE, awaitItem())
}
}
}
```
## BitwardenComposeTest
**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`
### Purpose
Pre-configured test class for Compose UI tests with all Bitwarden managers and theme setup.
### Automatic Configuration
- All Bitwarden managers pre-configured (FeatureFlags, AuthTab, Biometrics, etc.)
- Wraps content in `BitwardenTheme` and `LocalManagerProvider`
- Provides fixed `Clock` for deterministic time-based tests
- Extends `BaseComposeTest` for Robolectric and dispatcher setup
### Key Features
**Pre-configured Managers:**
- `FeatureFlagManager` - Controls feature flag behavior
- `AuthTabManager` - Manages auth tab state
- `BiometricsManager` - Handles biometric authentication
- `ClipboardManager` - Clipboard operations
- `NotificationManager` - Notification display
**Fixed Clock:**
All tests use a fixed clock for deterministic time-based testing:
```kotlin
// Tests use consistent time: 2023-10-27T12:00:00Z
val fixedClock: Clock
```
### Usage Pattern
```kotlin
class MyScreenTest : BitwardenComposeTest() {
private var haveCalledNavigateBack = false
private val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<MyViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
setContent {
MyScreen(
onNavigateBack = { haveCalledNavigateBack = true },
viewModel = viewModel
)
}
}
@Test
fun `on back click should send action`() {
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(MyAction.BackClick) }
}
@Test
fun `loading state should show progress`() {
mutableStateFlow.value = DEFAULT_STATE.copy(isLoading = true)
composeTestRule.onNode(isProgressBar).assertIsDisplayed()
}
}
```
### Important: bufferedMutableSharedFlow for Events
In Compose tests, use `bufferedMutableSharedFlow` instead of regular `MutableSharedFlow` (default replay is 0):
```kotlin
// Correct for Compose tests
private val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()
// This allows triggering events and having the UI react
mutableEventFlow.tryEmit(MyEvent.NavigateBack)
```
## BaseServiceTest
**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt`
### Purpose
Provides MockWebServer setup for testing API service implementations.
### Automatic Configuration
- `server: MockWebServer` - Auto-started before each test, stopped after
- `retrofit: Retrofit` - Pre-configured with:
- JSON converter (kotlinx.serialization)
- NetworkResultCallAdapter for Result<T> responses
- Base URL pointing to MockWebServer
- `json: Json` - kotlinx.serialization JSON instance
### Usage Pattern
```kotlin
class MyServiceTest : BaseServiceTest() {
private val api: MyApi = retrofit.create()
private val service = MyServiceImpl(api)
@Test
fun `getConfig should return success when API succeeds`() = runTest {
// Enqueue mock response
server.enqueue(MockResponse().setBody(EXPECTED_JSON))
// Call service
val result = service.getConfig()
// Verify result
assertEquals(EXPECTED_RESPONSE.asSuccess(), result)
}
@Test
fun `getConfig should return failure when API fails`() = runTest {
// Enqueue error response
server.enqueue(MockResponse().setResponseCode(500))
// Call service
val result = service.getConfig()
// Verify failure
assertTrue(result.isFailure)
}
}
```
### MockWebServer Patterns
**Enqueue successful response:**
```kotlin
server.enqueue(MockResponse().setBody("""{"key": "value"}"""))
```
**Enqueue error response:**
```kotlin
server.enqueue(MockResponse().setResponseCode(404))
server.enqueue(MockResponse().setResponseCode(500))
```
**Enqueue delayed response:**
```kotlin
server.enqueue(
MockResponse()
.setBody("""{"key": "value"}""")
.setBodyDelay(1000, TimeUnit.MILLISECONDS)
)
```
**Verify request details:**
```kotlin
val request = server.takeRequest()
assertEquals("/api/config", request.path)
assertEquals("GET", request.method)
assertEquals("Bearer token", request.getHeader("Authorization"))
```
## BaseComposeTest
**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseComposeTest.kt`
### Purpose
Base class for Compose tests that extends `BaseRobolectricTest` and provides `setTestContent()` helper.
### Features
- Robolectric configuration for Compose
- Proper dispatcher setup
- `composeTestRule` for UI testing
- `setTestContent()` helper wraps content in theme
### Usage
Typically you'll extend `BitwardenComposeTest` which extends this class. Use `BaseComposeTest` directly only for tests that don't need Bitwarden-specific manager configuration.
## When to Use Each Base Class
| Test Type | Base Class | Use When |
|-----------|------------|----------|
| ViewModel tests | `BaseViewModelTest` | Testing ViewModel state and events |
| Compose screen tests | `BitwardenComposeTest` | Testing Compose UI with Bitwarden components |
| API service tests | `BaseServiceTest` | Testing network layer with MockWebServer |
| Repository tests | None (manual setup) | Testing repository logic with mocked dependencies |
| Utility/helper tests | None (manual setup) | Testing pure functions or utilities |
## Complete Examples
**ViewModel Test:**
`../examples/viewmodel-test-example.md`
**Compose Screen Test:**
`../examples/compose-screen-test-example.md`
**Repository Test:**
`../examples/repository-test-example.md`

11
.github/CODEOWNERS vendored
View File

@@ -10,6 +10,11 @@
# Actions and workflow changes.
.github/ @bitwarden/dept-development-mobile
# Claude related files
.claude/ @bitwarden/team-ai-sme
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme
# Auth
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev
# app/src/main/java/com/x8bit/bitwarden/ui/auth @bitwarden/team-auth-dev
@@ -48,3 +53,9 @@
# app/src/main/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
# app/src/test/java/com/x8bit/bitwarden/data/vault @bitwarden/team-vault-dev
# app/src/test/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
# Docker-related files
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre

View File

@@ -9,27 +9,3 @@
## 📸 Screenshots
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
## ⏰ Reminders before review
- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
## 🦮 Reviewer guidelines
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes

View File

@@ -11,10 +11,14 @@ runs:
steps:
- name: Log inputs to job summary
shell: bash
env:
INPUTS: ${{ inputs.inputs }}
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ inputs.inputs }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
{
echo "<details><summary>Job Inputs</summary>"
echo ""
echo '```json'
echo "$INPUTS"
echo '```'
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -0,0 +1,29 @@
name: 'Setup Android Build'
description: 'Setup Android build environment with Gradle, Ruby, and Fastlane'
inputs:
java-version:
description: 'Java version to use'
required: false
default: '21'
runs:
using: 'composite'
steps:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ inputs.java-version }}
- name: Install Fastlane
shell: bash
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3

58
.github/label-pr.json vendored Normal file
View File

@@ -0,0 +1,58 @@
{
"title_patterns": {
"t:feature": ["feat", "feature", "tool"],
"t:bug": ["fix", "bug", "bugfix"],
"t:tech-debt": ["refactor", "chore", "cleanup", "revert", "debt", "test", "perf"],
"t:docs": ["docs"],
"t:ci": ["ci", "build", "chore(ci)"],
"t:deps": ["deps"],
"t:breaking-change": ["breaking", "breaking-change"],
"t:misc": ["misc"],
"t:llm": ["llm"]
},
"path_patterns": {
"app:shared": [
"annotation/",
"core/",
"data/",
"network/",
"ui/",
"authenticatorbridge/",
"gradle/"
],
"app:password-manager": [
"app/",
"cxf/",
"testharness/"
],
"app:authenticator": [
"authenticator/"
],
"t:feature": [
"app/src/main/assets/fido2_privileged_community.json",
"app/src/main/assets/fido2_privileged_google.json",
"testharness/"
],
"t:tech-debt": [
"gradle.properties",
"keystore/"
],
"t:ci": [
".checkmarx/",
".github/",
"scripts/",
"fastlane/",
".gradle/",
"detekt-config.yml"
],
"t:docs": [
"docs/"
],
"t:deps": [
"gradle/"
],
"t:llm": [
".claude/"
]
}
}

34
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
changelog:
exclude:
labels:
- ignore-for-release
categories:
- title: '✨ Community Highlight'
labels:
- community-pr
- title: ':shipit: Feature Development'
labels:
- t:feature
- t:feature-app
- t:feature-tool
- t:new-feature
- t:enhancement
- title: '❗ Breaking Changes'
labels:
- t:breaking-change
- title: '🐛 Bug fixes'
labels:
- t:bug
- title: '⚙️ Maintenance'
labels:
- t:tech-debt
- t:ci
- t:docs
- t:misc
- title: '📦 Dependency Updates'
labels:
- dependencies
- t:deps
- title: '🎨 Other'
labels:
- '*'

11
.github/renovate.json vendored
View File

@@ -3,6 +3,7 @@
"extends": [
"github>bitwarden/renovate-config"
],
"ignoreDeps": ["com.bitwarden:sdk-android"],
"enabledManagers": [
"github-actions",
"gradle",
@@ -19,16 +20,6 @@
"patch"
]
},
{
"groupName": "gradle minor",
"matchUpdateTypes": [
"minor",
"patch"
],
"matchManagers": [
"gradle"
]
},
{
"groupName": "kotlin",
"description": "Kotlin and Compose dependencies that must be updated together to maintain compatibility.",

View File

@@ -40,7 +40,7 @@ Single line of release notes text
```json
...
"customfield_10335": {
"customfield_9999": {
"type": "doc",
"version": 1,
"content": [
@@ -62,7 +62,7 @@ Single line of release notes text
```json
...
"customfield_10335": {
"customfield_9999": {
"type": "doc",
"version": 1,
"content": [

View File

@@ -5,6 +5,8 @@ import base64
import json
import requests
SCRIPT_NAME = "jira_release_notes.py"
def extract_text_from_content(content):
if isinstance(content, list):
texts = [extract_text_from_content(item) for item in content]
@@ -23,19 +25,42 @@ def extract_text_from_content(content):
return ''
def parse_release_notes(response_json):
try:
fields = response_json.get('fields', {})
release_notes_field = fields.get('customfield_10335', {})
def log_customfields_with_content(fields):
"""Log all customfield_* fields that have a 'content' key to help troubleshoot structure changes."""
print(f"[{SCRIPT_NAME}] Available customfield_* fields with 'content':", file=sys.stderr)
found = False
for key, value in fields.items():
if key.startswith('customfield_') and isinstance(value, dict) and 'content' in value:
found = True
print(f"[{SCRIPT_NAME}] {key}: {json.dumps(value, indent=2)}", file=sys.stderr)
if not found:
print(f"[{SCRIPT_NAME}] None found", file=sys.stderr)
if not release_notes_field or not release_notes_field.get('content'):
def parse_release_notes(response_json):
release_notes_field_name = 'customfield_10309'
try:
fields = response_json.get('fields')
if not fields:
print(f"[{SCRIPT_NAME}] 'fields' is empty or missing in response", file=sys.stderr)
return ''
release_notes = extract_text_from_content(release_notes_field.get('content', []))
release_notes_field = fields.get(release_notes_field_name)
if not release_notes_field:
print(f"[{SCRIPT_NAME}] Release notes field is empty or missing. Field name: {release_notes_field_name}", file=sys.stderr)
log_customfields_with_content(fields)
return ''
content = release_notes_field.get('content', [])
if not content:
print(f"[{SCRIPT_NAME}] Release notes field was found but 'content' is empty or missing in {release_notes_field_name}", file=sys.stderr)
log_customfields_with_content(fields)
return ''
release_notes = extract_text_from_content(content)
return release_notes
except Exception as e:
print(f"Error parsing release notes: {str(e)}", file=sys.stderr)
print(f"[{SCRIPT_NAME}] Error parsing release notes: {str(e)}", file=sys.stderr)
return ''
def main():
@@ -60,7 +85,7 @@ def main():
)
if response.status_code != 200:
print(f"Error fetching Jira issue: {response.status_code}", file=sys.stderr)
print(f"[{SCRIPT_NAME}] Error fetching Jira issue ({jira_issue_id}). Status code: {response.status_code}. Msg: {response.text}", file=sys.stderr)
sys.exit(1)
release_notes = parse_release_notes(response.json())

263
.github/scripts/label-pr.py vendored Normal file
View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# Requires Python 3.9+
"""
Label pull requests based on changed file paths and PR title patterns (conventional commit format).
Usage:
python label-pr.py <pr-number> <pr-labels> [-a|--add|-r|--replace] [-d|--dry-run] [-c|--config CONFIG]
Arguments:
pr-number: The pull request number
pr-labels: Current PR labels as JSON array string
-a, --add: Add labels without removing existing ones (default)
-r, --replace: Replace all existing labels
-d, --dry-run: Run without actually applying labels
-c, --config: Path to JSON config file (default: .github/label-pr.json)
Examples:
python label-pr.py 1234 '[]'
python label-pr.py 1234 '[{"name":"label1"}]' -a
python label-pr.py 1234 '[{"name":"label1"}]' --replace
python label-pr.py 1234 '[{"name":"label1"}]' -r -d
python label-pr.py 1234 '[]' --config custom-config.json
"""
import argparse
import json
import os
import subprocess
import sys
DEFAULT_MODE = "add"
DEFAULT_CONFIG_PATH = ".github/label-pr.json"
def load_config_json(config_file: str) -> dict:
"""Load configuration from JSON file."""
if not os.path.exists(config_file):
print(f"❌ Config file not found: {config_file}")
sys.exit(1)
try:
with open(config_file, 'r') as f:
config = json.load(f)
print(f"✅ Loaded config from: {config_file}")
valid_config = True
if not config.get("title_patterns"):
print("❌ Missing 'title_patterns' in config file")
valid_config = False
if not config.get("path_patterns"):
print("❌ Missing 'path_patterns' in config file")
valid_config = False
if not valid_config:
print("::error::Invalid label-pr.json config file, exiting...")
sys.exit(1)
return config
except json.JSONDecodeError as e:
print(f"❌ JSON deserialization error in label-pr.json config: {e}")
sys.exit(1)
except Exception as e:
print(f"❌ Unexpected error loading label-pr.json config: {e}")
sys.exit(1)
def gh_get_changed_files(pr_number: str) -> list[str]:
"""Get list of changed files in a pull request."""
try:
result = subprocess.run(
["gh", "pr", "diff", pr_number, "--name-only"],
capture_output=True,
text=True,
check=True
)
changed_files = result.stdout.strip().split("\n")
return list(filter(None, changed_files))
except subprocess.CalledProcessError as e:
print(f"::error::Error getting changed files: {e}")
return []
def gh_get_pr_title(pr_number: str) -> str:
"""Get the title of a pull request."""
try:
result = subprocess.run(
["gh", "pr", "view", pr_number, "--json", "title", "--jq", ".title"],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"::error::Error getting PR title: {e}")
return ""
def gh_add_labels(pr_number: str, labels: list[str]) -> None:
"""Add labels to a pull request (doesn't remove existing labels)."""
gh_labels = ','.join(labels)
subprocess.run(
["gh", "pr", "edit", pr_number, "--add-label", gh_labels],
check=True
)
def gh_replace_labels(pr_number: str, labels: list[str]) -> None:
"""Replace all labels on a pull request with the specified labels."""
payload = json.dumps({"labels": labels})
subprocess.run(
["gh", "api", "repos/{owner}/{repo}/issues/" + pr_number, "-X", "PATCH", "--silent", "--input", "-"],
input=payload,
text=True,
check=True
)
def label_filepaths(changed_files: list[str], path_patterns: dict) -> list[str]:
"""Check changed files against path patterns and return labels to apply."""
if not changed_files:
return []
labels_to_apply = set() # Use set to avoid duplicates
for label, patterns in path_patterns.items():
for file in changed_files:
if any(file.startswith(pattern) for pattern in patterns):
print(f"👀 File '{file}' matches pattern for label '{label}'")
labels_to_apply.add(label)
break
if "app:shared" in labels_to_apply:
labels_to_apply.add("app:password-manager")
labels_to_apply.add("app:authenticator")
labels_to_apply.remove("app:shared")
if not labels_to_apply:
print("::notice::No matching file paths found.")
return list(labels_to_apply)
def label_title(pr_title: str, title_patterns: dict) -> list[str]:
"""Check PR title against patterns and return labels to apply."""
if not pr_title:
return []
labels_to_apply = set()
title_lower = pr_title.lower()
for label, patterns in title_patterns.items():
for pattern in patterns:
# Check for pattern with : or ( suffix (conventional commits format)
if f"{pattern}:" in title_lower or f"{pattern}(" in title_lower:
print(f"📝 Title matches pattern '{pattern}' for label '{label}'")
labels_to_apply.add(label)
break
if not labels_to_apply:
print("::notice::No matching title patterns found.")
return list(labels_to_apply)
def parse_pr_labels(pr_labels_str: str) -> list[str]:
"""Parse PR labels from JSON array string."""
try:
labels = json.loads(pr_labels_str)
if not isinstance(labels, list):
print("::warning::Failed to parse PR labels: not a list")
return []
return [item.get("name") for item in labels if item.get("name")]
except (json.JSONDecodeError, TypeError) as e:
print(f"::error::Error parsing PR labels: {e}")
return []
def get_preserved_labels(pr_labels_str: str) -> list[str]:
"""Get existing PR labels that should be preserved (exclude app: and t: labels)."""
existing_labels = parse_pr_labels(pr_labels_str)
print(f"🔍 Parsed PR labels: {existing_labels}")
preserved_labels = [label for label in existing_labels if not (label.startswith("app:") or label.startswith("t:"))]
if preserved_labels:
print(f"🔍 Preserving existing labels: {', '.join(preserved_labels)}")
return preserved_labels
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Label pull requests based on changed file paths and PR title patterns."
)
parser.add_argument(
"pr_number",
help="The pull request number"
)
parser.add_argument(
"pr_labels",
help="Current PR labels (JSON array)"
)
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument(
"-a", "--add",
action="store_true",
help="Add labels without removing existing ones (default)"
)
mode_group.add_argument(
"-r", "--replace",
action="store_true",
help="Replace all existing labels"
)
parser.add_argument(
"-d", "--dry-run",
action="store_true",
help="Run without actually applying labels"
)
parser.add_argument(
"-c", "--config",
default=DEFAULT_CONFIG_PATH,
help=f"Path to JSON config file (default: {DEFAULT_CONFIG_PATH})"
)
args, unknown = parser.parse_known_args() # required to handle --dry-run passed as an empty string ("") by the workflow
return args
def main():
args = parse_args()
config = load_config_json(args.config)
LABEL_TITLE_PATTERNS = config["title_patterns"]
LABEL_PATH_PATTERNS = config["path_patterns"]
pr_number = args.pr_number
mode = "replace" if args.replace else "add"
if args.dry_run:
print("🔍 DRY RUN MODE - Labels will not be applied")
print(f"📌 Labeling mode: {mode}")
print(f"🔍 Checking PR #{pr_number}...")
pr_title = gh_get_pr_title(pr_number)
print(f"📋 PR Title: {pr_title}\n")
changed_files = gh_get_changed_files(pr_number)
print("👀 Changed files:\n" + "\n".join(changed_files) + "\n")
filepath_labels = label_filepaths(changed_files, LABEL_PATH_PATTERNS)
title_labels = label_title(pr_title, LABEL_TITLE_PATTERNS)
all_labels = set(filepath_labels + title_labels)
if all_labels:
print("--------------------------------")
labels_str = ', '.join(sorted(all_labels))
if mode == "add":
print(f"::notice::🏷️ Adding labels: {labels_str}")
if not args.dry_run:
gh_add_labels(pr_number, list(all_labels))
else:
preserved_labels = get_preserved_labels(args.pr_labels)
if preserved_labels:
all_labels.update(preserved_labels)
labels_str = ', '.join(sorted(all_labels))
print(f"::notice::🏷️ Replacing labels with: {labels_str}")
if not args.dry_run:
gh_replace_labels(pr_number, list(all_labels))
else:
print("::warning::No matching patterns found, no labels applied.")
print("✅ Done")
if __name__ == "__main__":
main()

23
.github/scripts/set-build-version.sh vendored Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
# Runs fastlane setBuildVersionInfo and appends Version Name/Number to GITHUB_STEP_SUMMARY.
# Usage: set-build-version.sh <version_code> [version_name] [toml_path]
VERSION_CODE="${1:?Usage: $0 <version_code> [version_name] [toml_path]}"
VERSION_NAME="${2:-}"
TOML_FILE="${3:-gradle/libs.versions.toml}"
bundle exec fastlane setBuildVersionInfo \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
VERSION_NAME=""
regex='appVersionName = "([^"]+)"'
if [[ "$(cat "$TOML_FILE")" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> "$GITHUB_STEP_SUMMARY"
echo "Version Number: $VERSION_CODE" >> "$GITHUB_STEP_SUMMARY"
fi

173
.github/workflows/_version.yml vendored Normal file
View File

@@ -0,0 +1,173 @@
name: Calculate Version Name and Number
on:
workflow_dispatch:
inputs:
app_codename:
description: "App Name - e.g. 'bwpm' or 'bwa'"
base_version_number:
description: "Base Version Number - Will be added to the calculated version number"
type: number
default: 0
version_name:
description: "Version Name Override - e.g. '2024.8.1'"
version_number:
description: "Version Number Override - e.g. '1021'"
patch_version:
description: "Patch Version Override - e.g. '999'"
distinct_id:
description: "Unique ID for this dispatch, used by dispatch-and-download.yml"
skip_checkout:
description: "Skip checking out the repository"
type: boolean
workflow_call:
inputs:
app_codename:
description: "App Name - e.g. 'bwpm' or 'bwa'"
type: string
base_version_number:
description: "Base Version Number - Will be added to the calculated version number"
type: number
default: 0
version_name:
description: "Version Name Override - e.g. '2024.8.1'"
type: string
version_number:
description: "Version Number Override - e.g. '1021'"
type: string
patch_version:
description: "Patch Version Override - e.g. '999'"
type: string
distinct_id:
description: "Unique ID for this dispatch, used by dispatch-and-download.yml"
type: string
skip_checkout:
description: "Skip checking out the repository"
type: boolean
outputs:
version_name:
description: "Version Name"
value: ${{ jobs.calculate-version.outputs.version_name }}
version_number:
description: "Version Number"
value: ${{ jobs.calculate-version.outputs.version_number }}
env:
APP_CODENAME: ${{ inputs.app_codename }}
BASE_VERSION_NUMBER: ${{ inputs.base_version_number || 0 }}
jobs:
calculate-version:
name: Calculate Version Name and Number
runs-on: ubuntu-22.04
permissions:
contents: read
outputs:
version_name: ${{ steps.calc-version-name.outputs.version_name }}
version_number: ${{ steps.calc-version-number.outputs.version_number }}
steps:
- name: Log inputs to job summary
uses: bitwarden/android/.github/actions/log-inputs@main
with:
inputs: "${{ toJson(inputs) }}"
- name: Echo distinct ID ${{ github.event.inputs.distinct_id }}
env:
_DISTINCT_ID: ${{ inputs.distinct_id }}
run: echo "${_DISTINCT_ID}"
- name: Check out repository
if: ${{ !inputs.skip_checkout || false }}
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Calculate version name
id: calc-version-name
env:
_VERSION_NAME: ${{ inputs.version_name }}
_PATCH_VERSION: ${{ inputs.patch_version }}
run: |
output() {
local version_name=$1
echo "version_name=$version_name" >> "$GITHUB_OUTPUT"
}
# override version name if provided
if [[ ! -z "${_VERSION_NAME}" ]]; then
version_name=${_VERSION_NAME}
echo "::warning::Override applied: $version_name"
output "$version_name"
exit 0
fi
current_year=$(date +%Y)
current_month=$(date +%-m)
latest_tag_version=$(git tag -l --sort=-creatordate | grep "$APP_CODENAME" | head -n 1)
if [[ -z "$latest_tag_version" ]]; then
version_name="${current_year}.${current_month}.${_PATCH_VERSION:-0}"
echo "::warning::No tags found, did you checkout? Calculating version from current date: $version_name"
output "$version_name"
exit 0
fi
# Git tag was found, calculate version from latest tag
latest_version=${latest_tag_version:1} # remove 'v' from tag version
latest_major_version=$(echo "$latest_version" | cut -d "." -f 1)
latest_minor_version=$(echo "$latest_version" | cut -d "." -f 2)
patch_version=0
if [[ ! -z "${_PATCH_VERSION}" ]]; then
patch_version=${_PATCH_VERSION}
echo "::warning::Patch Version Override applied: $patch_version"
elif [[ "$current_year" == "$latest_major_version" && "$current_month" == "$latest_minor_version" ]]; then
latest_patch_version=$(echo "$latest_version" | cut -d "." -f 3)
patch_version=$(($latest_patch_version + 1))
fi
version_name="${current_year}.${current_month}.${patch_version}"
output "$version_name"
- name: Calculate version number
id: calc-version-number
env:
_VERSION_NUMBER: ${{ inputs.version_number }}
run: |
# override version number if provided
if [[ ! -z "${_VERSION_NUMBER}" ]]; then
version_number=${_VERSION_NUMBER}
echo "::warning::Override applied: $version_number"
echo "version_number=$version_number" >> "$GITHUB_OUTPUT"
exit 0
fi
version_number=$(($GITHUB_RUN_NUMBER + ${BASE_VERSION_NUMBER}))
echo "version_number=$version_number" >> "$GITHUB_OUTPUT"
- name: Create version info JSON
env:
_VERSION_NUMBER: ${{ steps.calc-version-number.outputs.version_number }}
_VERSION_NAME: ${{ steps.calc-version-name.outputs.version_name }}
run: |
json=$(cat <<EOF
{
"version_number": "${_VERSION_NUMBER}",
"version_name": "${_VERSION_NAME}"
}
EOF
)
echo "$json" > version_info.json
echo "## version-info.json" >> "$GITHUB_STEP_SUMMARY"
echo '```json' >> "$GITHUB_STEP_SUMMARY"
echo "$json" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
- name: Upload version info artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: version-info
path: version_info.json

View File

@@ -15,82 +15,57 @@ on:
description: "Optional. Build number to use. Overrides default of GitHub run number."
required: false
type: number
patch_version:
description: "Order 999 - Overrides Patch version"
type: boolean
distribute-to-firebase:
description: "Optional. Distribute artifacts to Firebase."
required: false
default: false
default: true
type: boolean
publish-to-play-store:
description: "Optional. Deploy bundle artifact to Google Play Store"
required: false
default: false
default: true
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
DISTRIBUTE_TO_FIREBASE: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
PUBLISH_TO_PLAY_STORE: ${{ inputs.publish-to-play-store || github.event_name == 'push' }}
permissions:
contents: read
packages: read
id-token: write
jobs:
version:
name: Calculate Version Name and Number
uses: bitwarden/android/.github/workflows/_version.yml@main
with:
app_codename: "bwa"
base_version_number: 0
version_name: ${{ inputs.version-name }}
version_number: ${{ inputs.version-code }}
patch_version: ${{ inputs.patch_version && '999' || '' }}
build:
name: Build Authenticator
runs-on: ubuntu-24.04
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
uses: bitwarden/android/.github/actions/log-inputs@main
with:
inputs: "${{ toJson(inputs) }}"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
persist-credentials: false
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Check Authenticator
run: bundle exec fastlane check
@@ -101,8 +76,11 @@ jobs:
publish_playstore:
name: Publish Authenticator Play Store artifacts
needs:
- version
- build
runs-on: ubuntu-24.04
permissions:
id-token: write
strategy:
fail-fast: false
matrix:
@@ -110,18 +88,9 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
persist-credentials: false
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -145,121 +114,102 @@ jobs:
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/keystores
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name authenticator_apk-keystore.jks --file ${{ github.workspace }}/keystores/authenticator_apk-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name authenticator_aab-keystore.jks --file ${{ github.workspace }}/keystores/authenticator_aab-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name com.bitwarden.authenticator-google-services.json --file ${{ github.workspace }}/authenticator/src/google-services.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name com.bitwarden.authenticator.dev-google-services.json --file ${{ github.workspace }}/authenticator/src/debug/google-services.json --output none
- name: Download Firebase credentials
if : ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ env.DISTRIBUTE_TO_FIREBASE }}
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name authenticator_play_firebase-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json --output none
- name: Download Play Store credentials
if: ${{ inputs.publish-to-play-store }}
if: ${{ env.PUBLISH_TO_PLAY_STORE }}
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: AZ Logout
uses: bitwarden/gh-actions/azure-logout@main
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Verify Play Store credentials
if: ${{ inputs.publish-to-play-store }}
if: ${{ env.PUBLISH_TO_PLAY_STORE }}
run: |
bundle exec fastlane run validate_play_store_json_key \
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
json_key:"${{ github.workspace }}/secrets/authenticator_play_store-creds.json"
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
"$GITHUB_REPOSITORY" \
"$GITHUB_REF_NAME" \
"$GITHUB_SHA" \
"$GITHUB_RUN_ID" \
"$GITHUB_RUN_ATTEMPT"
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }}
regex='appVersionName = "([^"]+)"'
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
env:
VERSION_CODE: ${{ needs.version.outputs.version_number || github.run_number }}
VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: ./.github/scripts/set-build-version.sh "$VERSION_CODE" "$VERSION_NAME"
- name: Generate release Play Store bundle
if: ${{ matrix.variant == 'aab' }}
env:
STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}
KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}
run: |
bundle exec fastlane bundleAuthenticatorRelease \
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
keyAlias:authenticatorupload \
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
storeFile:"${{ github.workspace }}/keystores/authenticator_aab-keystore.jks" \
storePassword:"$STORE_PASSWORD" \
keyAlias:"authenticatorupload" \
keyPassword:"$KEY_PASSWORD"
- name: Generate release Play Store APK
if: ${{ matrix.variant == 'apk' }}
env:
STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}
KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}
run: |
bundle exec fastlane buildAuthenticatorRelease \
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
keyAlias:bitwardenauthenticator \
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
storeFile:"${{ github.workspace }}/keystores/authenticator_apk-keystore.jks" \
storePassword:"$STORE_PASSWORD" \
keyAlias:"bitwardenauthenticator" \
keyPassword:"$KEY_PASSWORD"
- name: Upload release Play Store .aab artifact
- name: Upload to GitHub Artifacts - prod.aab
if: ${{ matrix.variant == 'aab' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.bitwarden.authenticator.aab
path: authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab
if-no-files-found: error
- name: Upload release .apk artifact
- name: Upload to GitHub Artifacts - prod.apk
if: ${{ matrix.variant == 'apk' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.bitwarden.authenticator.apk
path: authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk
@@ -277,40 +227,38 @@ jobs:
sha256sum "authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk" \
> ./authenticator-android-apk-sha256.txt
- name: Upload .apk SHA file for release
- name: Upload to GitHub Artifacts - prod.apk-sha256.txt
if: ${{ matrix.variant == 'apk' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: authenticator-android-apk-sha256.txt
path: ./authenticator-android-apk-sha256.txt
if-no-files-found: error
- name: Upload .aab SHA file for release
- name: Upload to GitHub Artifacts - prod.aab-sha256.txt
if: ${{ matrix.variant == 'aab' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: authenticator-android-aab-sha256.txt
path: ./authenticator-android-aab-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ matrix.variant == 'aab' && env.DISTRIBUTE_TO_FIREBASE }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release bundle to Firebase
if: ${{ matrix.variant == 'aab' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
- name: Distribute to Firebase - prod.aab
if: ${{ matrix.variant == 'aab' && env.DISTRIBUTE_TO_FIREBASE }}
env:
FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json
run: |
bundle exec fastlane distributeAuthenticatorReleaseBundleToFirebase \
serviceCredentialsFile:${{ env.FIREBASE_CREDS_PATH }}
serviceCredentialsFile:"$FIREBASE_CREDS_PATH"
# Only publish bundles to Play Store when `publish-to-play-store` is true while building
# bundles
- name: Publish release bundle to Google Play Store
if: ${{ inputs.publish-to-play-store && matrix.variant == 'aab' }}
- name: Publish to Play Store - prod.aab
if: ${{ matrix.variant == 'aab' && env.PUBLISH_TO_PLAY_STORE }}
env:
PLAY_STORE_CREDS_FILE: ${{ github.workspace }}/secrets/authenticator_play_store-creds.json
run: |
bundle exec fastlane publishAuthenticatorReleaseToGooglePlayStore \
serviceCredentialsFile:${{ env.PLAY_STORE_CREDS_FILE }} \
serviceCredentialsFile:"$PLAY_STORE_CREDS_FILE" \

84
.github/workflows/build-testharness.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: Build Test Harness
on:
push:
paths:
- testharness/**
workflow_dispatch:
inputs:
version-name:
description: "Optional. Version string to use, in X.Y.Z format. Overrides default in the project."
required: false
type: string
version-code:
description: "Optional. Build number to use. Overrides default of GitHub run number."
required: false
type: number
patch_version:
description: "Order 999 - Overrides Patch version"
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
jobs:
version:
name: Calculate Version Name and Number
uses: bitwarden/android/.github/workflows/_version.yml@main
with:
app_codename: "bwpm"
base_version_number: 0
version_name: ${{ inputs.version-name }}
version_number: ${{ inputs.version-code }}
patch_version: ${{ inputs.patch_version && '999' || '' }}
build:
name: Build Test Harness
runs-on: ubuntu-24.04
needs: version
steps:
- name: Log inputs to job summary
uses: bitwarden/android/.github/actions/log-inputs@main
with:
inputs: "${{ toJson(inputs) }}"
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Increment version
env:
VERSION_CODE: ${{ needs.version.outputs.version_number || github.run_number }}
VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: ./.github/scripts/set-build-version.sh "$VERSION_CODE" "$VERSION_NAME"
- name: Build Test Harness Debug APK
run: ./gradlew :testharness:assembleDebug
- name: Upload Test Harness APK
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.bitwarden.testharness.dev-debug.apk
path: testharness/build/outputs/apk/debug/com.bitwarden.testharness.dev.apk
if-no-files-found: error
- name: Create checksum for Test Harness APK
run: |
sha256sum "testharness/build/outputs/apk/debug/com.bitwarden.testharness.dev.apk" \
> ./com.bitwarden.testharness.dev.apk-sha256.txt
- name: Upload Test Harness SHA file
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.bitwarden.testharness.dev.apk-sha256.txt
path: ./com.bitwarden.testharness.dev.apk-sha256.txt
if-no-files-found: error

View File

@@ -15,83 +15,59 @@ on:
description: "Optional. Build number to use. Overrides default of GitHub run number."
required: false
type: number
patch_version:
description: "Order 999 - Overrides Patch version"
type: boolean
distribute-to-firebase:
description: "Optional. Distribute artifacts to Firebase."
required: false
default: false
default: true
type: boolean
publish-to-play-store:
description: "Optional. Deploy bundle artifact to Google Play Store"
required: false
default: false
default: true
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
DISTRIBUTE_TO_FIREBASE: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
PUBLISH_TO_PLAY_STORE: ${{ inputs.publish-to-play-store || github.event_name == 'push' }}
permissions:
contents: read
packages: read
id-token: write
jobs:
version:
name: Calculate Version Name and Number
uses: bitwarden/android/.github/workflows/_version.yml@main
with:
app_codename: "bwpm"
# Start from 11000 to prevent collisions with mobile build version codes
base_version_number: 11000
version_name: ${{ inputs.version-name }}
version_number: ${{ inputs.version-code }}
patch_version: ${{ inputs.patch_version && '999' || '' }}
build:
name: Build
runs-on: ubuntu-24.04
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
uses: bitwarden/android/.github/actions/log-inputs@main
with:
inputs: "${{ toJson(inputs) }}"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
persist-credentials: false
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Check
run: bundle exec fastlane check
@@ -100,7 +76,7 @@ jobs:
run: bundle exec fastlane assembleDebugApks
- name: Upload test reports on failure
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: failure()
with:
name: test-reports
@@ -109,8 +85,11 @@ jobs:
publish_playstore:
name: Publish Play Store artifacts
needs:
- version
- build
runs-on: ubuntu-24.04
permissions:
id-token: write
strategy:
fail-fast: false
matrix:
@@ -118,18 +97,9 @@ jobs:
artifact: ["apk", "aab"]
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
persist-credentials: false
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -154,166 +124,140 @@ jobs:
mkdir -p ${{ github.workspace }}/app/src/standardBeta
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_play-keystore.jks --file ${{ github.workspace }}/keystores/app_play-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_upload-keystore.jks --file ${{ github.workspace }}/keystores/app_upload-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_beta_play-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_play-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_beta_upload-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_upload-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name google-services.json --file ${{ github.workspace }}/app/src/standardBeta/google-services.json --output none
- name: Download Firebase credentials
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && env.DISTRIBUTE_TO_FIREBASE }}
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
"$GITHUB_REPOSITORY" \
"$GITHUB_REF_NAME" \
"$GITHUB_SHA" \
"$GITHUB_RUN_ID" \
"$GITHUB_RUN_ATTEMPT"
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
bundle exec fastlane setBuildVersionInfo \
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
versionName:${{ inputs.version-name }}
env:
VERSION_CODE: ${{ needs.version.outputs.version_number || github.run_number }}
VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: ./.github/scripts/set-build-version.sh "$VERSION_CODE" "$VERSION_NAME"
- name: Generate release Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
env:
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
UPLOAD_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane bundlePlayStoreRelease \
storeFile:app_upload-keystore.jks \
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
storePassword:$UPLOAD_KEYSTORE_PASSWORD \
keyAlias:upload \
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
keyPassword:$UPLOAD_KEYSTORE_PASSWORD
- name: Generate beta Play Store bundle
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
env:
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
UPLOAD_BETA_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane bundlePlayStoreBeta \
storeFile:app_beta_upload-keystore.jks \
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
storePassword:$UPLOAD_BETA_KEYSTORE_PASSWORD \
keyAlias:bitwarden-beta-upload \
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
keyPassword:$UPLOAD_BETA_KEY_PASSWORD
- name: Generate release Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane assemblePlayStoreReleaseApk \
storeFile:app_play-keystore.jks \
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
storePassword:$PLAY_KEYSTORE_PASSWORD \
keyAlias:bitwarden \
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
keyPassword:$PLAY_KEYSTORE_PASSWORD
- name: Generate beta Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
PLAY_BETA_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_BETA_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane assemblePlayStoreBetaApk \
storeFile:app_beta_play-keystore.jks \
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
storePassword:$PLAY_BETA_KEYSTORE_PASSWORD \
keyAlias:bitwarden-beta \
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
keyPassword:$PLAY_BETA_KEY_PASSWORD
- name: Generate debug Play Store APKs
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
run: |
bundle exec fastlane assembleDebugApks
- name: Upload release Play Store .aab artifact
- name: Upload to GitHub Artifacts - prod.aab
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
if-no-files-found: error
- name: Upload beta Play Store .aab artifact
- name: Upload to GitHub Artifacts - beta.aab
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
if-no-files-found: error
- name: Upload release .apk artifact
- name: Upload to GitHub Artifacts - prod.apk
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
if-no-files-found: error
- name: Upload beta .apk artifact
- name: Upload to GitHub Artifacts - beta.apk
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
if-no-files-found: error
# When building variants other than 'prod'
- name: Upload debug .apk artifact
- name: Upload to GitHub Artifacts - dev.apk
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
@@ -349,75 +293,75 @@ jobs:
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk" \
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
- name: Upload .apk SHA file for release
- name: Upload to GitHub Artifacts - prod.apk-sha256.txt
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.apk-sha256.txt
path: ./com.x8bit.bitwarden.apk-sha256.txt
if-no-files-found: error
- name: Upload .apk SHA file for beta
- name: Upload to GitHub Artifacts - beta.apk-sha256.txt
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.beta.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
if-no-files-found: error
- name: Upload .aab SHA file for release
- name: Upload to GitHub Artifacts - prod.aab-sha256.txt
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.aab-sha256.txt
path: ./com.x8bit.bitwarden.aab-sha256.txt
if-no-files-found: error
- name: Upload .aab SHA file for beta
- name: Upload to GitHub Artifacts - beta.aab-sha256.txt
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.beta.aab-sha256.txt
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
if-no-files-found: error
- name: Upload .apk SHA file for debug
- name: Upload to GitHub Artifacts - debug.apk-sha256.txt
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && env.DISTRIBUTE_TO_FIREBASE }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release artifacts to Firebase
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
- name: Distribute to Firebase - prod.apk
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && env.DISTRIBUTE_TO_FIREBASE }}
env:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
bundle exec fastlane distributeReleasePlayStoreToFirebase \
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
actionUrl:$GITHUB_ACTION_RUN_URL \
service_credentials_file:$APP_PLAY_FIREBASE_CREDS_PATH
- name: Publish beta artifacts to Firebase
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && (inputs.distribute-to-firebase || github.event_name == 'push') }}
- name: Distribute to Firebase - beta.apk
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && env.DISTRIBUTE_TO_FIREBASE }}
env:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
bundle exec fastlane distributeBetaPlayStoreToFirebase \
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
actionUrl:$GITHUB_ACTION_RUN_URL \
service_credentials_file:$APP_PLAY_FIREBASE_CREDS_PATH
- name: Verify Play Store credentials
if: ${{ matrix.variant == 'prod' && inputs.publish-to-play-store }}
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && env.PUBLISH_TO_PLAY_STORE }}
run: |
bundle exec fastlane run validate_play_store_json_key
- name: Publish Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
- name: Publish to Play Store - prod.aab
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && env.PUBLISH_TO_PLAY_STORE }}
run: |
bundle exec fastlane publishProdToPlayStore
bundle exec fastlane publishBetaToPlayStore
@@ -425,22 +369,16 @@ jobs:
publish_fdroid:
name: Publish F-Droid artifacts
needs:
- version
- build
runs-on: ubuntu-24.04
permissions:
id-token: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
persist-credentials: false
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -461,100 +399,65 @@ jobs:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_fdroid-keystore.jks --file ${{ github.workspace }}/keystores/app_fdroid-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_beta_fdroid-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_fdroid-keystore.jks --output none
- name: Download Firebase credentials
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ env.DISTRIBUTE_TO_FIREBASE }}
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
"$GITHUB_REPOSITORY" \
"$GITHUB_REF_NAME" \
"$GITHUB_SHA" \
"$GITHUB_RUN_ID" \
"$GITHUB_RUN_ATTEMPT"
# Start from 11000 to prevent collisions with mobile build version codes
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }}
regex='appVersionName = "([^"]+)"'
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
env:
VERSION_CODE: ${{ needs.version.outputs.version_number || github.run_number }}
VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: ./.github/scripts/set-build-version.sh "$VERSION_CODE" "$VERSION_NAME"
- name: Generate F-Droid artifacts
env:
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane assembleFDroidReleaseApk \
storeFile:app_fdroid-keystore.jks \
storePassword:"${{ env.FDROID_STORE_PASSWORD }}" \
storePassword:$FDROID_STORE_PASSWORD \
keyAlias:bitwarden \
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
keyPassword:$FDROID_STORE_PASSWORD
- name: Generate F-Droid Beta Artifacts
env:
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
FDROID_BETA_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
FDROID_BETA_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane assembleFDroidBetaApk \
storeFile:app_beta_fdroid-keystore.jks \
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
storePassword:$FDROID_BETA_KEYSTORE_PASSWORD \
keyAlias:bitwarden-beta \
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
keyPassword:$FDROID_BETA_KEY_PASSWORD
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Upload to GitHub Artifacts - fdroid.apk
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
@@ -565,15 +468,15 @@ jobs:
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk" \
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
- name: Upload F-Droid SHA file
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Upload to GitHub Artifacts - fdroid.apk-sha256.txt
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Upload F-Droid Beta .apk artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Upload to GitHub Artifacts - beta.fdroid.apk
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.beta-fdroid.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
@@ -584,22 +487,22 @@ jobs:
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk" \
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
- name: Upload F-Droid Beta SHA file
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Upload to GitHub Artifacts - beta.fdroid.apk-sha256.txt
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ env.DISTRIBUTE_TO_FIREBASE }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release F-Droid artifacts to Firebase
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
- name: Distribute to Firebase - fdroid.apk
if: ${{ env.DISTRIBUTE_TO_FIREBASE }}
env:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: |
bundle exec fastlane distributeReleaseFDroidToFirebase \
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
service_credentials_file:${{ env.APP_FDROID_FIREBASE_CREDS_PATH }}
actionUrl:$GITHUB_ACTION_RUN_URL \
service_credentials_file:$APP_FDROID_FIREBASE_CREDS_PATH

View File

@@ -2,8 +2,8 @@ name: Cron / Sync Google Privileged Browsers List
on:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: '0 0 * * 1'
# Run weekly on Sunday at 00:00 UTC
- cron: '0 0 * * 0'
workflow_dispatch:
env:
@@ -21,25 +21,26 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: true
- name: Download Google Privileged Browsers List
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
run: curl -s "$SOURCE_URL" -o "$GOOGLE_FILE"
- name: Check for changes
id: check-changes
run: |
if git diff --quiet -- $GOOGLE_FILE; then
if git diff --quiet -- "$GOOGLE_FILE"; then
echo "👀 No changes detected, skipping..."
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "👀 Changes detected, validating fido2_privileged_google.json..."
python .github/scripts/validate-json/validate_json.py validate $GOOGLE_FILE
if [ $? -ne 0 ]; then
if ! python .github/scripts/validate-json/validate_json.py validate "$GOOGLE_FILE"; then
echo "::error::JSON validation failed for $GOOGLE_FILE"
exit 1
fi
@@ -47,14 +48,14 @@ jobs:
echo "👀 fido2_privileged_google.json is valid, checking for duplicates..."
# Check for duplicates between Google and Community files
python .github/scripts/validate-json/validate_json.py duplicates $GOOGLE_FILE $COMMUNITY_FILE duplicates.txt
python .github/scripts/validate-json/validate_json.py duplicates "$GOOGLE_FILE" "$COMMUNITY_FILE" duplicates.txt
if [ -f duplicates.txt ]; then
echo "::warning::Duplicate package names found between Google and Community files."
echo "duplicates_found=true" >> $GITHUB_OUTPUT
echo "duplicates_found=true" >> "$GITHUB_OUTPUT"
else
echo "✅ No duplicate package names found between Google and Community files"
echo "duplicates_found=false" >> $GITHUB_OUTPUT
echo "duplicates_found=false" >> "$GITHUB_OUTPUT"
fi
- name: Create branch and commit
@@ -65,11 +66,11 @@ jobs:
BRANCH_NAME="cron-sync-privileged-browsers/$GITHUB_RUN_NUMBER-sync"
git config user.name "GitHub Actions Bot"
git config user.email "actions@github.com"
git checkout -b $BRANCH_NAME
git add $GOOGLE_FILE
git checkout -b "$BRANCH_NAME"
git add "$GOOGLE_FILE"
git commit -m "Update Google privileged browsers list"
git push origin $BRANCH_NAME
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
git push origin "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV"
echo "🌱 Branch created: $BRANCH_NAME"
- name: Create Pull Request
@@ -89,10 +90,10 @@ jobs:
fi
# Use echo -e to interpret escape sequences and pipe to gh pr create
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
echo -e "$PR_BODY" | gh pr create \
--title "Update Google privileged browsers list" \
--body-file - \
--base main \
--head $BRANCH_NAME \
--head "$BRANCH_NAME" \
--label "automated-pr" \
--label "t:ci")
--label "t:deps"

View File

@@ -4,19 +4,23 @@ run-name: Crowdin Pull - ${{ github.event_name == 'workflow_dispatch' && 'Manual
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 5'
# Run weekly on Sunday at 00:00 UTC
- cron: '0 0 * * 0'
permissions: {}
jobs:
crowdin-sync:
name: Crowdin Pull - ${{ github.event_name }}
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
contents: read
id-token: write
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -43,14 +47,16 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-contents: write # for creating and pushing a new branch
permission-pull-requests: write # for creating pull request
- name: Download translations
uses: crowdin/github-action@9fd07c1c5b36b15f082d1d860dc399f16f849bd7 # v2.9.0
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
@@ -67,5 +73,6 @@ jobs:
create_pull_request: true
pull_request_title: "Crowdin Pull"
pull_request_body: ":inbox_tray: New translations received!"
pull_request_labels: "automated-pr, t:misc"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@@ -16,7 +16,9 @@ jobs:
id-token: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -33,7 +35,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@9fd07c1c5b36b15f082d1d860dc399f16f849bd7 # v2.9.0
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -4,16 +4,16 @@ on:
workflow_dispatch:
inputs:
artifact-run-id:
description: 'GitHub Action Run ID containing artifacts'
description: "GitHub Action Run ID containing artifacts"
required: true
type: string
release-ticket-id:
description: 'Release Ticket ID - e.g. RELEASE-1762'
description: "Release Ticket ID - e.g. RELEASE-1762"
required: true
type: string
env:
ARTIFACTS_PATH: artifacts
ARTIFACTS_PATH: artifacts
jobs:
create-release:
@@ -25,9 +25,10 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
persist-credentials: true
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
@@ -40,7 +41,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
run: |
workflow_data=$(gh run view $ARTIFACT_RUN_ID --json headBranch,workflowName)
workflow_data=$(gh run view "$ARTIFACT_RUN_ID" --json headBranch,workflowName)
release_branch=$(echo "$workflow_data" | jq -r .headBranch)
workflow_name=$(echo "$workflow_data" | jq -r .workflowName)
@@ -52,8 +53,8 @@ jobs:
echo "🔖 Release branch: $release_branch"
echo "🔖 Workflow name: $workflow_name"
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
echo "release_branch=$release_branch" >> "$GITHUB_OUTPUT"
echo "workflow_name=$workflow_name" >> "$GITHUB_OUTPUT"
case "$workflow_name" in
*"Password Manager"* | "Build")
@@ -71,8 +72,8 @@ jobs:
esac
echo "🔖 App name: $app_name"
echo "🔖 App name suffix: $app_name_suffix"
echo "app_name=$app_name" >> $GITHUB_OUTPUT
echo "app_name_suffix=$app_name_suffix" >> $GITHUB_OUTPUT
echo "app_name=$app_name" >> "$GITHUB_OUTPUT"
echo "app_name_suffix=$app_name_suffix" >> "$GITHUB_OUTPUT"
- name: Get version info from run logs and set release tag name
id: get_release_info
@@ -81,7 +82,7 @@ jobs:
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_APP_NAME_SUFFIX: ${{ steps.get_release_branch.outputs.app_name_suffix }}
run: |
workflow_log=$(gh run view $ARTIFACT_RUN_ID --log)
workflow_log=$(gh run view "$ARTIFACT_RUN_ID" --log)
version_number_with_trailing_dot=$(grep -m 1 "Setting version code to" <<< "$workflow_log" | sed 's/.*Setting version code to //')
version_number=${version_number_with_trailing_dot%.} # remove trailing dot
@@ -103,28 +104,28 @@ jobs:
echo "✅ Found version number: $version_number"
fi
echo "version_number=$version_number" >> $GITHUB_OUTPUT
echo "version_name=$version_name" >> $GITHUB_OUTPUT
echo "version_number=$version_number" >> "$GITHUB_OUTPUT"
echo "version_name=$version_name" >> "$GITHUB_OUTPUT"
tag_name="v$version_name-$_APP_NAME_SUFFIX" # e.g. v2025.6.0-bwpm
echo "🔖 New tag name: $tag_name"
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
echo "tag_name=$tag_name" >> "$GITHUB_OUTPUT"
last_release_tag=$(git tag -l --sort=-authordate | grep "$_APP_NAME_SUFFIX" | head -n 1)
echo "🔖 Last release tag: $last_release_tag"
echo "last_release_tag=$last_release_tag" >> $GITHUB_OUTPUT
echo "last_release_tag=$last_release_tag" >> "$GITHUB_OUTPUT"
- name: Download artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
run: |
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
gh run download "$ARTIFACT_RUN_ID" -D "$ARTIFACTS_PATH"
file_count=$(find "$ARTIFACTS_PATH" -type f | wc -l)
echo "Downloaded $file_count file(s)."
if [ "$file_count" -gt 0 ]; then
echo "Downloaded files:"
find $ARTIFACTS_PATH -type f
find "$ARTIFACTS_PATH" -type f
fi
# Files that won't be included in any release
@@ -148,12 +149,12 @@ jobs:
)
for file in "${files_to_remove[@]}"; do
find $ARTIFACTS_PATH -name "$file" -type f -delete
find "$ARTIFACTS_PATH" -name "$file" -type f -delete
done
echo "🔖 Removed internal artifacts."
echo ""
echo "🔖 Files to be included in the release:"
find $ARTIFACTS_PATH -type f
find "$ARTIFACTS_PATH" -type f
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -182,11 +183,15 @@ jobs:
_JIRA_API_EMAIL: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
run: |
echo "Getting product release notes"
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py $_RELEASE_TICKET_ID $_JIRA_API_EMAIL $_JIRA_API_TOKEN)
echo "Getting product release notes..."
# capture output and exit code so this step continues even if we can't retrieve release notes.
script_exit_code=0
product_release_notes=$(python .github/scripts/jira-get-release-notes/jira_release_notes.py "$_RELEASE_TICKET_ID" "$_JIRA_API_EMAIL" "$_JIRA_API_TOKEN") || script_exit_code=$?
echo "--------------------------------"
if [[ -z "$product_release_notes" || $product_release_notes == "Error checking"* ]]; then
echo "::warning::Failed to fetch release notes from Jira. Output: $product_release_notes"
if [[ $script_exit_code -ne 0 || -z "$product_release_notes" ]]; then
echo "Script Output: $product_release_notes"
echo "::warning::Failed to fetch release notes from Jira. Check script logs for more details."
product_release_notes="<insert product release notes here>"
else
echo "✅ Product release notes:"
@@ -219,12 +224,12 @@ jobs:
--notes-start-tag "$_LAST_RELEASE_TAG" \
--latest=$is_latest_release \
--draft \
$ARTIFACTS_PATH/*/*)
"$ARTIFACTS_PATH/*/*")
# Extract release tag from URL
release_id_from_url=$(echo "$release_url" | sed 's/.*\/tag\///')
echo "release_id_from_url=$release_id_from_url" >> $GITHUB_OUTPUT
echo "url=$release_url" >> $GITHUB_OUTPUT
echo "release_id_from_url=$release_id_from_url" >> "$GITHUB_OUTPUT"
echo "url=$release_url" >> "$GITHUB_OUTPUT"
echo "✅ Release created: $release_url"
echo "🔖 Release ID from URL: $release_id_from_url"
@@ -252,7 +257,7 @@ jobs:
new_release_url=$(gh release edit "$_RELEASE_ID" --notes "$updated_body")
# draft release links change after editing
echo "release_url=$new_release_url" >> $GITHUB_OUTPUT
echo "release_url=$new_release_url" >> "$GITHUB_OUTPUT"
- name: Add Release Summary
env:
@@ -263,20 +268,26 @@ jobs:
_RELEASE_BRANCH: ${{ steps.get_release_branch.outputs.release_branch }}
_RELEASE_URL: ${{ steps.update_release_description.outputs.release_url }}
run: |
echo "# :fish_cake: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$_RELEASE_URL" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
{
echo "# :fish_cake: Release ready at:"
echo "$_RELEASE_URL"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$_VERSION_NAME" == "0.0.0" || "$_VERSION_NUMBER" == "0" ]]; then
echo "> [!CAUTION]" >> $GITHUB_STEP_SUMMARY
echo "> Version name or number wasn't previously found and a default value was used. You'll need to manually update the release Title, Tag and Description, specifically, the "Full Changelog" link." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
{
echo "> [!CAUTION]"
echo "> Version name or number wasn't previously found and a default value was used. You'll need to manually update the release Title, Tag and Description, specifically, the \"Full Changelog\" link."
echo ""
} >> "$GITHUB_STEP_SUMMARY"
fi
echo ":clipboard: Confirm that the defined GitHub Release options are correct:" >> $GITHUB_STEP_SUMMARY
echo " * :bookmark: New tag name: \`$_RELEASE_TAG\`" >> $GITHUB_STEP_SUMMARY
echo " * :palm_tree: Target branch: \`$_RELEASE_BRANCH\`" >> $GITHUB_STEP_SUMMARY
echo " * :ocean: Previous tag set in the description \"Full Changelog\" link: \`$_LAST_RELEASE_TAG\`" >> $GITHUB_STEP_SUMMARY
echo " * :white_check_mark: Description has automated release notes and they match the commits in the release branch" >> $GITHUB_STEP_SUMMARY
echo "> [!NOTE]" >> $GITHUB_STEP_SUMMARY
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes." >> $GITHUB_STEP_SUMMARY
{
echo ":clipboard: Confirm that the defined GitHub Release options are correct:"
echo " * :bookmark: New tag name: \`$_RELEASE_TAG\`"
echo " * :palm_tree: Target branch: \`$_RELEASE_BRANCH\`"
echo " * :ocean: Previous tag set in the description \"Full Changelog\" link: \`$_LAST_RELEASE_TAG\`"
echo " * :white_check_mark: Description has automated release notes and they match the commits in the release branch"
echo "> [!NOTE]"
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes."
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -0,0 +1,24 @@
name: Publish Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
permissions:
contents: write
id-token: write
actions: write
jobs:
publish-release-authenticator:
name: Publish Authenticator Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Authenticator"
workflow_name: "publish-github-release-bwa.yml"
credentials_filename: "authenticator_play_store-creds.json"
project_type: android
make_latest: false
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
secrets: inherit

View File

@@ -0,0 +1,25 @@
name: Publish Password Manager GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
permissions:
contents: write
id-token: write
actions: write
jobs:
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Password Manager"
workflow_name: "publish-github-release-bwpm.yml"
credentials_filename: "play_creds.json"
project_type: android
make_latest: true
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit

View File

@@ -1,36 +0,0 @@
name: Publish Password Manager and Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 3 * * 1-5'
permissions:
contents: write
id-token: write
actions: read
jobs:
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Password Manager"
workflow_name: "publish-github-release.yml"
credentials_filename: "play_creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit
publish-release-authenticator:
name: Publish Authenticator Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Authenticator"
workflow_name: "publish-github-release.yml"
credentials_filename: "authenticator_play_store-creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
secrets: inherit

View File

@@ -1,5 +1,6 @@
name: Publish to Google Play
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
run-name: >
${{ inputs.dry-run && ' (Dry Run)' || '' }}Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}
on:
workflow_dispatch:
inputs:
@@ -17,15 +18,15 @@ on:
required: true
type: string
rollout-percentage:
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
release-notes:
description: "Change notes to be included with this release."
type: string
@@ -46,6 +47,10 @@ on:
- production
- Fastlane Automation Target
required: true
dry-run:
description: "Dry-Run, Run the workflow without publishing to the store"
type: boolean
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
@@ -54,106 +59,123 @@ permissions:
contents: read
packages: read
id-token: write
actions: write
jobs:
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
steps:
- name: Log inputs to job summary
uses: bitwarden/android/.github/actions/log-inputs@main
with:
inputs: "${{ toJson(inputs) }}"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check out repo
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Format Release Notes
env:
RELEASE_NOTES: ${{ inputs.release-notes }}
run: |
FORMATTED_MESSAGE="$(echo "$RELEASE_NOTES" | sed 's/ /\n/g')"
{
echo "RELEASE_NOTES<<EOF"
printf '%s\n' "$FORMATTED_MESSAGE"
echo "EOF"
} >> "$GITHUB_ENV"
- name: Format Release Notes
run: |
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE"
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE" \
packageName:"$PACKAGE_NAME"
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"
- name: Enable Publish Github Release Workflow
env:
PRODUCT: ${{ inputs.product }}
run: |
if ${{ inputs.dry-run }} ; then
gh workflow view publish-github-release-bwpm.yml
exit 0
fi
if [ "$PRODUCT" = "Password Manager" ]; then
gh workflow enable publish-github-release-bwpm.yml
elif [ "$PRODUCT" = "Authenticator" ]; then
gh workflow enable publish-github-release-bwa.yml
fi

View File

@@ -4,12 +4,13 @@ on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Type'
description: "Release Type"
required: true
type: choice
options:
- RC
- Hotfix
- Hotfix Password Manager
- Hotfix Authenticator
- Test
jobs:
@@ -21,9 +22,10 @@ jobs:
actions: write
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
persist-credentials: true
- name: Create RC or Test Branch
id: rc_branch
@@ -41,30 +43,38 @@ jobs:
branch_name="release/${branch_name}"
git switch main
git switch -c $branch_name
git push origin $branch_name
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
git switch -c "$branch_name"
git push origin "$branch_name"
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> "$GITHUB_STEP_SUMMARY"
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
- name: Create Hotfix Branch
id: hotfix_branch
if: inputs.release_type == 'Hotfix'
if: startsWith(inputs.release_type, 'Hotfix')
env:
_RELEASE_TYPE: ${{ inputs.release_type }}
run: |
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
app_codename="bwpm"
if [ "$_RELEASE_TYPE" == "Hotfix Authenticator" ]; then
app_codename="bwa"
fi
echo "🌿 app codename: $app_codename"
latest_tag=$(git tag -l --sort=-creatordate | grep "$app_codename" | head -n 1)
if [ -z "$latest_tag" ]; then
echo "::error::No tags found in the repository"
exit 1
fi
branch_name="release/hotfix-${latest_tag}"
echo "🌿 branch name: $branch_name"
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git switch -c $branch_name $latest_tag
git push origin $branch_name
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
git switch -c "$branch_name" "$latest_tag"
git push origin "$branch_name"
echo "# :fire: Hotfix branch: ${branch_name}" >> "$GITHUB_STEP_SUMMARY"
- name: Trigger CI Workflows
env:
@@ -72,5 +82,5 @@ jobs:
_BRANCH_NAME: ${{ steps.rc_branch.outputs.branch_name || steps.hotfix_branch.outputs.branch_name }}
run: |
echo "🌿 branch name: $_BRANCH_NAME"
gh workflow run build.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build-authenticator.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build.yml --ref "$_BRANCH_NAME" -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build-authenticator.yml --ref "$_BRANCH_NAME" -f distribute-to-firebase=true -f publish-to-play-store=true

28
.github/workflows/respond.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Respond
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions: {}
jobs:
respond:
name: Respond
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: write
id-token: write
issues: write
pull-requests: write

21
.github/workflows/review-code.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
review:
name: Review
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: read
id-token: write
pull-requests: write

View File

@@ -9,16 +9,9 @@ on:
permissions: {}
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
permissions:
contents: read
sast:
name: Checkmarx
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
needs: check-run
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -32,7 +25,6 @@ jobs:
quality:
name: Sonar
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
needs: check-run
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}

View File

@@ -6,7 +6,7 @@ on:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target:
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, synchronize, reopened]
branches:
- main

View File

@@ -0,0 +1,64 @@
name: SDLC / Enforce PR labels
run-name: Enforce labels for PR ${{ github.event.pull_request.number }}
on:
pull_request:
types: [labeled, unlabeled, opened, reopened, edited, synchronize]
permissions: {}
jobs:
enforce-label:
name: Enforce Label
runs-on: ubuntu-24.04
permissions:
pull-requests: read
steps:
- name: Enforce banned labels (e.g. hold, needs-qa)
env:
_HOLD_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'hold') }}
_NEEDS_QA_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'needs-qa') }}
run: |
if [ "$_HOLD_LABEL" = "true" ]; then
echo "::error::PR has banned label: hold"
exit 1
fi
if [ "$_NEEDS_QA_LABEL" = "true" ]; then
echo "::error::PR has banned label: needs-qa"
exit 1
fi
echo "✅ No banned labels found."
- name: Enforce exactly one Change Type (t:*) label
env:
_PR_ACTION: ${{ github.event.action }}
_PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }}
_REPO: ${{ github.repository }}
_PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
if [ "$_PR_ACTION" = "opened" ] || [ "$_PR_ACTION" = "reopened" ]; then
echo "⏳ Waiting 15s for labeler to run..."
sleep 15
_PR_LABELS=$(gh api "repos/$_REPO/pulls/$_PR_NUMBER" --jq '.labels')
echo "Labels fetched from PR: $_PR_LABELS"
fi
_IGNORE_FOR_RELEASE_LABEL=$(echo "$_PR_LABELS" | jq 'any(.[]; .name == "ignore-for-release")')
if [ "$_IGNORE_FOR_RELEASE_LABEL" = "true" ]; then
echo "⏭️ Skipping type label check - 'ignore-for-release' label present"
exit 0
fi
_T_LABEL_COUNT=$(echo "$_PR_LABELS" | jq '[.[] | select(.name | startswith("t:"))] | length')
case "$_T_LABEL_COUNT" in
1)
echo "✅ PR has exactly one Change Type (t:*) label"
;;
0)
echo "::error::PR is missing a Change Type (t:*) label. PRs must have exactly one Change Type (t:*) label"
exit 1
;;
*)
echo "::error::PR has $_T_LABEL_COUNT Change Type (t:*) labels. PRs must have exactly one Change Type (t:*) label"
exit 1
;;
esac

90
.github/workflows/sdlc-label-pr.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: SDLC / Label PR
run-name: Label PR ${{ github.event.pull_request.number || inputs.pr-number }}${{ github.event_name == 'workflow_dispatch' && format(' / mode "{0}" dry-run "{1}"', inputs.mode, inputs.dry-run) || '' }}
on:
pull_request:
types: [opened, synchronize]
workflow_dispatch:
inputs:
pr-number:
description: "Pull Request Number"
required: true
type: number
mode:
description: "Labeling Mode"
type: choice
options:
- add
- replace
default: add
dry-run:
description: "Dry Run - Don't apply labels"
type: boolean
default: false
env:
_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr-number }}
jobs:
label-pr:
name: Label PR by Changed Files
runs-on: ubuntu-24.04
permissions:
pull-requests: write # required to update labels
contents: read
steps:
- name: Check out repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Determine label mode for Pull Request
id: label-mode
env:
GH_TOKEN: ${{ github.token }}
_PR_USER: ${{ github.event.pull_request.user.login }}
_IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
run: |
# Support workflow_dispatch testing by retrieving PR data
if [ -z "$_PR_USER" ]; then
echo "👀 PR User is empty, retrieving PR data for PR #$_PR_NUMBER..."
PR_DATA=$(gh pr view "$_PR_NUMBER" --json author,isCrossRepository)
_PR_USER=$(echo "$PR_DATA" | jq -r '.author.login')
_IS_FORK=$(echo "$PR_DATA" | jq -r '.isCrossRepository')
fi
echo "📋 PR User: $_PR_USER"
echo "📋 Is Fork: $_IS_FORK"
# Handle PRs with labels set by other automations by adding instead of replacing
if [ "$_IS_FORK" = "true" ]; then
echo "➡️ Fork PR ($_PR_USER). Label mode: --add"
echo "label_mode=--add" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$_PR_USER" == app/* || "$_PR_USER" == *\[bot\] ]]; then
echo "➡️ Bot PR ($_PR_USER). Label mode: --add"
echo "label_mode=--add" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "➡️ Normal PR. Label mode: --replace"
echo "label_mode=--replace" >> "$GITHUB_OUTPUT"
- name: Label PR based on changed files
env:
GH_TOKEN: ${{ github.token }}
_LABEL_MODE: ${{ inputs.mode && format('--{0}', inputs.mode) || steps.label-mode.outputs.label_mode }}
_DRY_RUN: ${{ inputs.dry-run == true && '--dry-run' || '' }}
_PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }}
run: |
if [ -z "$_PR_LABELS" ] || [ "$_PR_LABELS" = "null" ] || [ "$_PR_LABELS" = "[]" ]; then
echo "🔍 No current PR labels found, retrieving PR data for PR #$_PR_NUMBER..."
_PR_LABELS=$(gh pr view "$_PR_NUMBER" --json labels --jq '.labels')
fi
echo "🔍 Labeling PR #$_PR_NUMBER with mode: \"$_LABEL_MODE\" and dry-run: \"$_DRY_RUN\" and current PR labels: \"$_PR_LABELS\"..."
echo "🐍 Running label-pr.py script..."
echo ""
python3 .github/scripts/label-pr.py "$_PR_NUMBER" "$_PR_LABELS" "$_LABEL_MODE" "$_DRY_RUN"

230
.github/workflows/sdlc-sdk-update.yml vendored Normal file
View File

@@ -0,0 +1,230 @@
name: SDLC / SDK Update
run-name: "SDK ${{inputs.run-mode == 'Update' && format('Update - {0}', inputs.sdk-version) || format('Test #{0} - {1}', inputs.pr-id, inputs.sdk-version)}}"
on:
workflow_dispatch:
inputs:
run-mode:
description: "Run Mode"
type: choice
options:
- Test # used for testing sdk-internal repo PRs
- Update # opens a PR in this repo updating the SDK
default: Test
sdk-package:
description: "SDK Package ID"
required: true
default: "com.bitwarden:sdk-android.dev"
sdk-version:
description: "SDK Version"
required: true
default: "1.0.0-2686-km-update-kdf-sdk"
pr-id:
description: "Pull Request ID"
env:
_BOT_NAME: "bw-ghapp[bot]"
_BOT_EMAIL: "178206702+bw-ghapp[bot]@users.noreply.github.com"
jobs:
update:
name: Update and PR
if: ${{ inputs.run-mode == 'Update' }}
runs-on: ubuntu-24.04
permissions:
id-token: write
steps:
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-pull-requests: write
permission-actions: read
permission-contents: write
- name: Check out repo
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
persist-credentials: true
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
with:
inputs: ${{ toJson(inputs) }}
- name: Switch to branch
id: switch-branch
run: |
BRANCH_NAME="sdlc/sdk-update"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
if git switch "$BRANCH_NAME"; then
echo "✅ Switched to existing branch: $BRANCH_NAME"
echo "updating_existing_branch=true" >> "$GITHUB_OUTPUT"
else
echo "📝 Creating new branch: $BRANCH_NAME"
git switch -c "$BRANCH_NAME"
echo "updating_existing_branch=false" >> "$GITHUB_OUTPUT"
fi
- name: Prevent updating the branch when the last committer isn't the bot
if: ${{ steps.switch-branch.outputs.updating_existing_branch == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
run: |
LATEST_COMMIT_AUTHOR=$(git log -1 --format='%ae' "$_BRANCH_NAME")
echo "Latest commit author in branch ($_BRANCH_NAME): $LATEST_COMMIT_AUTHOR"
echo "Expected bot email: $_BOT_EMAIL"
if [ "$LATEST_COMMIT_AUTHOR" != "$_BOT_EMAIL" ]; then
echo "::error::Branch $_BRANCH_NAME has a commit not made by the bot." \
"This indicates manual changes have been made to the branch," \
"PR has to be merged or closed before running this workflow again."
echo "👀 Fetching existing PR..."
gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty'
EXISTING_PR=$(gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty')
if [ -z "$EXISTING_PR" ]; then
echo "::error::Couldn't find an existing PR for branch $_BRANCH_NAME."
exit 1
fi
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
echo "## ❌ Merge or close: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "✅ Branch tip commit was made by the bot. Safe to proceed."
# Using main to retrieve the changelog on consecutive updates of the same PR.
- name: Get current SDK version from main branch
id: get-current-sdk
run: |
git show origin/main:gradle/libs.versions.toml
SDK_VERSION=$(git show origin/main:gradle/libs.versions.toml | grep "bitwardenSdk =" | cut -d'"' -f2)
if [ -z "$SDK_VERSION" ]; then
echo "::error::Failed to get current SDK version from main branch."
exit 1
fi
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
echo "Current SDK version (from main): $SDK_VERSION"
echo "Current SDK git ref: $GIT_REF"
echo "version=$SDK_VERSION" >> "$GITHUB_OUTPUT"
echo "git_ref=$GIT_REF" >> "$GITHUB_OUTPUT"
- name: Update SDK Version
env:
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
run: |
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
- name: Create branch and commit
env:
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
run: |
echo "👀 Committing SDK version update..."
git config user.name "$_BOT_NAME"
git config user.email "$_BOT_EMAIL"
git add gradle/libs.versions.toml
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
git push origin "$_BRANCH_NAME"
- name: Create or Update Pull Request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
_OLD_SDK_VERSION: ${{ steps.get-current-sdk.outputs.version }}
_OLD_SDK_GIT_REF: ${{ steps.get-current-sdk.outputs.git_ref }}
run: |
NEW_SDK_GIT_REF=$(echo "$_SDK_VERSION" | cut -d'-' -f3-)
CHANGELOG=$(./scripts/get-repo-changelog.sh "bitwarden/sdk-internal" "$_OLD_SDK_GIT_REF" "$NEW_SDK_GIT_REF")
PR_BODY="Updates the SDK version from \`$_OLD_SDK_VERSION\` to \`$_SDK_PACKAGE $_SDK_VERSION\`
## What's Changed
$CHANGELOG"
EXISTING_PR=$(gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "🔄 Updating existing PR #$EXISTING_PR..."
echo -e "$PR_BODY" | gh pr edit "$EXISTING_PR" \
--title "Update SDK to $_SDK_VERSION" \
--body-file -
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
echo "## ✅ Updated PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
else
echo "📝 Creating new PR..."
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update SDK to $_SDK_VERSION" \
--body-file - \
--base main \
--head "$_BRANCH_NAME" \
--label "automated-pr" \
--label "t:deps")
echo "## 🚀 Created PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
fi
test:
name: Test Update
if: ${{ inputs.run-mode == 'Test' }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Check out repo
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
with:
inputs: ${{ toJson(inputs) }}
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Update SDK Version
env:
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
run: |
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
run: |
./gradlew assembleDebug --warn

View File

@@ -3,9 +3,8 @@ name: Test
on:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
- main
- release/**/*
pull_request:
types: [opened, synchronize]
merge_group:
@@ -13,104 +12,147 @@ on:
workflow_dispatch:
env:
_JAVA_VERSION: 17
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
jobs:
test:
name: Test
test-sharded:
name: "Test ${{ matrix.group }}"
runs-on: ubuntu-24.04
permissions:
packages: read
pull-requests: write
strategy:
fail-fast: false
matrix:
include:
- group: static-analysis
fastlane_method: checkLint
fastlane_options: ""
# App shards
- group: app-data
fastlane_method: testAppShard
fastlane_options: "--tests com.x8bit.bitwarden.data.*"
- group: app-ui-auth-tools
fastlane_method: testAppShard
fastlane_options: "--tests com.x8bit.bitwarden.ui.auth.* --tests com.x8bit.bitwarden.ui.tools.* --tests com.x8bit.bitwarden.ui.autofill.* --tests com.x8bit.bitwarden.ui.credentials.*"
- group: app-ui-platform
fastlane_method: testAppShard
fastlane_options: "--tests com.x8bit.bitwarden.ui.platform.*"
- group: app-ui-vault
fastlane_method: testAppShard
fastlane_options: "--tests com.x8bit.bitwarden.ui.vault.*"
# Authenticator
- group: authenticator
fastlane_method: testLibraries
fastlane_options: ":authenticator"
# Library shards
- group: lib-core-network-bridge
fastlane_method: testLibraries
fastlane_options: ":core :network :cxf :authenticatorbridge :testharness"
- group: lib-data-ui
fastlane_method: testLibraries
fastlane_options: ":data :ui"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
persist-credentials: false
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Configure Ruby
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env._JAVA_VERSION }}
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Build and test
- name: Run tests
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
_GROUP: ${{ matrix.group }}
_FASTLANE_METHOD: ${{ matrix.fastlane_method }}
_FASTLANE_OPTIONS: ${{ matrix.fastlane_options }}
run: |
bundle exec fastlane check
if [ "$_GROUP" = "app-ui-auth-tools" ]; then
_TOP_LEVEL_TESTS=$(basename -a -s .kt app/src/test/kotlin/com/x8bit/bitwarden/*Test.kt \
| xargs -I{} printf ' --tests com.x8bit.bitwarden.{}')
_FASTLANE_OPTIONS="${_FASTLANE_OPTIONS} ${_TOP_LEVEL_TESTS}"
fi
- name: Upload test reports
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: test-reports
path: |
build/reports/kover/reportMergedCoverage.xml
app/build/reports/tests/
authenticator/build/reports/tests/
authenticatorbridge/build/reports/tests/
core/build/reports/tests/
data/build/reports/tests/
network/build/reports/tests/
ui/build/reports/tests/
if [ "$_GROUP" = "static-analysis" ]; then
bundle exec fastlane "$_FASTLANE_METHOD"
else
bundle exec fastlane "$_FASTLANE_METHOD" target:"$_FASTLANE_OPTIONS"
fi
- name: Generate coverage report
if: always() && matrix.group != 'static-analysis' && (github.event_name == 'push' || github.event_name == 'pull_request')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bundle exec fastlane generateCoverageReport
- name: Upload to codecov.io
id: upload-to-codecov
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
if: github.event_name == 'push' || github.event_name == 'pull_request'
if: always() && matrix.group != 'static-analysis' && (github.event_name == 'push' || github.event_name == 'pull_request')
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
continue-on-error: true
with:
os: linux
files: build/reports/kover/reportMergedCoverage.xml
flags: ${{ matrix.group }}
fail_ci_if_error: true
disable_search: true
- name: Comment PR if tests failed
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')
- name: Upload test reports
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: test-reports-${{ matrix.group }}
path: |
**/build/reports/tests/
app/build/reports/lint-results-*.html
app/build/reports/detekt/
if-no-files-found: warn
coverage-notify:
name: Coverage Notification
runs-on: ubuntu-24.04
needs: test-sharded
if: always() && !cancelled() && (github.event_name == 'push' || github.event_name == 'pull_request')
permissions:
pull-requests: write
steps:
- name: Notify Codecov that all uploads are complete
id: codecov-notify
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
continue-on-error: true
with:
run_command: send-notifications
- name: Comment PR if coverage notification failed
if: steps.codecov-notify.outcome == 'failure'
env:
PR_NUMBER: ${{ github.event.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ACTOR: ${{ github.triggering_actor }}
PR_NUMBER: ${{ github.event.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ACTOR: ${{ github.triggering_actor }}
run: |
echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY
echo "> Uploading code coverage report failed. Please check the \"Upload to codecov.io\" step of \"Process Test Reports\" job for more details." >> $GITHUB_STEP_SUMMARY
echo "> [!WARNING]" >> "$GITHUB_STEP_SUMMARY"
echo "> Uploading code coverage report failed. Please check the \"Notify Codecov\" step for more details." >> "$GITHUB_STEP_SUMMARY"
if [ -n "$PR_NUMBER" ]; then
message=$'> [!WARNING]\n> @'$RUN_ACTOR' Uploading code coverage report failed. Please check the "Upload to codecov.io" step of [Process Test Reports job]('$_GITHUB_ACTION_RUN_URL') for more details.'
gh pr comment --repo $GITHUB_REPOSITORY $PR_NUMBER --body "$message"
message=$'> [!WARNING]\n> @'$RUN_ACTOR' Uploading code coverage report failed. Please check the "Coverage Notification" step of [Test]('$_GITHUB_ACTION_RUN_URL') for more details.'
gh pr comment --repo "$GITHUB_REPOSITORY" "$PR_NUMBER" --body "$message"
fi
test:
name: Test
runs-on: ubuntu-24.04
permissions: {}
needs: test-sharded
if: always()
steps:
- name: Ensure sharded tests passed
env:
TESTS_RESULT: ${{ needs.test-sharded.result }}
run: |
if [ "$TESTS_RESULT" != "success" ]; then
echo "❌ Tests failed"
exit 1
fi
echo "✅ All tests passed!"

5
.github/zizmor.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
rules:
unpinned-uses:
config:
policies:
bitwarden/gh-actions/*: ref-pin

7
.gitignore vendored
View File

@@ -3,6 +3,13 @@
fastlane/report.xml
fastlane/README.md
# Ruby / Bundler
.bundle/
vendor/
# Backup files
*.bak
# General
.DS_Store
Thumbs.db

View File

@@ -1 +0,0 @@
npx lint-staged

View File

@@ -14,5 +14,8 @@ gem 'logger'
gem 'mutex_m'
gem 'csv'
# Since ruby 3.4.1 these are not included in the standard library
gem 'nkf'
# Starting with Ruby 3.5.0, these are not included in the standard library
gem 'ostruct'

View File

@@ -1,18 +1,15 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1139.0)
aws-sdk-core (3.228.0)
aws-partitions (1.1213.0)
aws-sdk-core (3.242.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -20,25 +17,25 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.109.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (1.121.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.195.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-s3 (1.213.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (3.2.2)
bigdecimal (4.0.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
date (3.4.1)
date (3.5.1)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
@@ -46,7 +43,7 @@ GEM
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
faraday (1.10.5)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -58,14 +55,14 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@@ -75,8 +72,9 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.228.0)
fastlane (2.229.0)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
@@ -84,6 +82,7 @@ GEM
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -103,6 +102,7 @@ GEM
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
@@ -169,38 +169,38 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.13.2)
json (2.18.1)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.17.0)
multi_json (1.19.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
public_suffix (7.0.2)
rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.20.0)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
@@ -209,7 +209,7 @@ GEM
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
time (0.4.1)
time (0.4.2)
date
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
@@ -241,6 +241,7 @@ DEPENDENCIES
fastlane-plugin-firebase_app_distribution
logger
mutex_m
nkf
ostruct
time
@@ -248,4 +249,4 @@ RUBY VERSION
ruby 3.4.2p28
BUNDLED WITH
2.6.9
2.6.2

View File

@@ -4,13 +4,12 @@
- [Compatibility](#compatibility)
- [Setup](#setup)
- [Theme](#theme)
- [Dependencies](#dependencies)
## Compatibility
- **Minimum SDK**: 29
- **Target SDK**: 35
- **Minimum SDK**: 29 (Android 10)
- **Target SDK**: 36 (Android 16)
- **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape
@@ -52,34 +51,46 @@
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
4. Setup JDK `Version` `17`:
4. Setup JDK `Version` `21`:
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
- Hit the selected Gradle JDK next to `Gradle JDK:`.
- Select a `17.x` version or hit `Download JDK...` if not present.
- Select `Version` `17`.
- Select a `21.x` version or hit `Download JDK...` if not present.
- Select `Version` `21`.
- Select your preferred `Vendor`.
- Hit `Download`.
- Hit `Apply`.
## Theme
5. Setup `detekt` pre-commit hook (optional):
### Icons & Illustrations
Run the following script from the root of the repository to install the hook. This will overwrite any existing pre-commit hook if present.
The app supports light mode, dark mode and dynamic colors. Most icons in the app will display correctly using tinting but multi-tonal icons and illustrations require extra processing in order to be displayed properly with dynamic colors.
```shell
echo "Writing detekt pre-commit hook..."
cat << 'EOL' > .git/hooks/pre-commit
#!/usr/bin/env bash
All illustrations and multi-tonal icons require the svg paths to be tagged with the `name` attribute in order for each individual path to be tinted the appropriate color. Any untagged path will not be tinted and the resulting image will be incorrect.
echo "Running detekt check..."
OUTPUT="/tmp/detekt-$(date +%s)"
./gradlew -Pprecommit=true detekt > $OUTPUT
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
cat $OUTPUT
rm $OUTPUT
echo "***********************************************"
echo " detekt failed "
echo " Please fix the above issues before committing "
echo "***********************************************"
exit $EXIT_CODE
fi
rm $OUTPUT
EOL
echo "detekt pre-commit hook written to .git/hooks/pre-commit"
echo "Making the hook executable"
chmod +x .git/hooks/pre-commit
The supported tags are as follows:
* outline
* primary
* secondary
* tertiary
* accent
* logo
* navigation
* navigationActiveAccent
echo "detekt pre-commit hook installed successfully to .git/hooks/pre-commit"
```
## Dependencies

View File

@@ -1,17 +1,20 @@
import com.android.build.api.dsl.LibraryExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
configure<LibraryExtension> {
namespace = "com.bitwarden.annotation"
compileSdk = libs.versions.compileSdk.get().toInt()
compileSdk {
version = release(libs.versions.compileSdk.get().toInt())
}
defaultConfig {
minSdk = libs.versions.minSdkBwa.get().toInt()
minSdk {
version = release(libs.versions.minSdkBwa.get().toInt())
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
@@ -37,6 +40,6 @@ android {
kotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
}
}

View File

21
annotation/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,9 +1,10 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.impl.VariantOutputImpl
import com.android.utils.cxx.io.removeExtensionIfPresent
import com.google.firebase.crashlytics.buildtools.gradle.tasks.InjectMappingFileIdTask
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
import com.google.gms.googleservices.GoogleServicesTask
import dagger.hilt.android.plugin.util.capitalize
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.FileInputStream
import java.util.Properties
@@ -15,7 +16,6 @@ plugins {
// standardDebug builds in the merged manifest.
alias(libs.plugins.crashlytics)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
@@ -43,27 +43,35 @@ val ciProperties = Properties().apply {
}
}
android {
namespace = "com.x8bit.bitwarden"
compileSdk = libs.versions.compileSdk.get().toInt()
base {
// Set the base archive name for publishing purposes. This is used to derive the
// APK and AAB artifact names when uploading to Firebase and Play Store.
archivesName.set("com.x8bit.bitwarden")
}
room {
schemaDirectory("$projectDir/schemas")
room {
schemaDirectory("$projectDir/schemas")
}
configure<ApplicationExtension> {
namespace = "com.x8bit.bitwarden"
compileSdk {
version = release(libs.versions.compileSdk.get().toInt())
}
defaultConfig {
applicationId = "com.x8bit.bitwarden"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
minSdk {
version = release(libs.versions.minSdk.get().toInt())
}
targetSdk {
version = release(libs.versions.targetSdk.get().toInt())
}
versionCode = libs.versions.appVersionCode.get().toInt()
versionName = libs.versions.appVersionName.get()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// Set the base archive name for publishing purposes. This is used to derive the APK and AAB
// artifact names when uploading to Firebase and Play Store.
base.archivesName = "com.x8bit.bitwarden"
buildConfigField(
type = "String",
name = "CI_INFO",
@@ -141,39 +149,6 @@ android {
}
}
applicationVariants.all {
val bundlesDir = "${layout.buildDirectory.get()}/outputs/bundle"
outputs
.mapNotNull { it as? BaseVariantOutputImpl }
.forEach { output ->
val fileNameWithoutExtension = when (flavorName) {
"fdroid" -> "$applicationId-$flavorName"
"standard" -> "$applicationId"
else -> output.outputFileName.removeExtensionIfPresent(".apk")
}
// Set the APK output filename.
output.outputFileName = "$fileNameWithoutExtension.apk"
val variantName = name
val renameTaskName = "rename${variantName.capitalize()}AabFiles"
tasks.register(renameTaskName) {
group = "build"
description = "Renames the bundle files for $variantName variant"
doLast {
renameFile(
"$bundlesDir/$variantName/$namespace-$flavorName-${buildType.name}.aab",
"$fileNameWithoutExtension.aab",
)
}
}
// Force renaming task to execute after the variant is built.
tasks
.getByName("bundle${variantName.capitalize()}")
.finalizedBy(renameTaskName)
}
}
compileOptions {
sourceCompatibility(libs.versions.jvmTarget.get())
targetCompatibility(libs.versions.jvmTarget.get())
@@ -200,9 +175,50 @@ android {
}
}
androidComponents {
onVariants { appVariant ->
val bundlesDir = "${layout.buildDirectory.get()}/outputs/bundle"
val applicationId = appVariant.applicationId.get()
val flavorName = appVariant.flavorName
val variantName = appVariant.name
val buildType = appVariant.buildType
appVariant
.outputs
.mapNotNull { it as? VariantOutputImpl }
.forEach { output ->
val fileNameWithoutExtension = when (flavorName) {
"fdroid" -> "$applicationId-$flavorName"
"standard" -> applicationId
else -> output.outputFileName.get().removeExtensionIfPresent(".apk")
}
// Set the APK output filename.
output.outputFileName.set("$fileNameWithoutExtension.apk")
val renameTaskName = "rename${variantName.uppercaseFirstChar()}AabFiles"
tasks.register(renameTaskName) {
group = "build"
description = "Renames the bundle files for $variantName variant"
doLast {
val namespace = appVariant.namespace.get()
renameFile(
"$bundlesDir/$variantName/$namespace-$flavorName-$buildType.aab",
"$fileNameWithoutExtension.aab",
)
}
}
// Force renaming task to execute after the variant is built.
val bundleTaskName = "bundle${variantName.uppercaseFirstChar()}"
tasks
.named { it == bundleTaskName }
.configureEach { finalizedBy(renameTaskName) }
}
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
}
}
@@ -220,10 +236,11 @@ dependencies {
add("standardImplementation", dependencyNotation)
}
implementation(files("libs/authenticatorbridge-1.0.1-release.aar"))
implementation(project(":authenticatorbridge"))
implementation(project(":annotation"))
implementation(project(":core"))
implementation(project(":cxf"))
implementation(project(":data"))
implementation(project(":network"))
implementation(project(":ui"))
@@ -234,8 +251,7 @@ dependencies {
implementation(libs.androidx.browser)
implementation(libs.androidx.biometrics)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.material3)
@@ -245,6 +261,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.providerevents)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.runtime.compose)
@@ -258,7 +276,8 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.bitwarden.sdk)
implementation(libs.bumptech.glide)
implementation(libs.androidx.credentials)
implementation(libs.bumptech.glide.okhttp)
ksp(libs.bumptech.glide.compiler)
implementation(libs.google.hilt.android)
ksp(libs.google.hilt.compiler)
implementation(libs.kotlinx.collections.immutable)
@@ -269,7 +288,6 @@ dependencies {
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.timber)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only
debugImplementation(libs.androidx.compose.ui.test.manifest)
@@ -282,6 +300,7 @@ dependencies {
standardImplementation(libs.google.play.review)
// Pull in test fixtures from other modules
testImplementation(testFixtures(project(":core")))
testImplementation(testFixtures(project(":data")))
testImplementation(testFixtures(project(":network")))
testImplementation(testFixtures(project(":ui")))
@@ -290,7 +309,7 @@ dependencies {
testImplementation(libs.google.hilt.android.testing)
testImplementation(platform(libs.junit.bom))
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.junit.junit5)
testImplementation(libs.junit.jupiter)
testImplementation(libs.junit.vintage)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk.mockk)
@@ -298,15 +317,6 @@ dependencies {
testImplementation(libs.square.turbine)
}
tasks {
withType<Test> {
useJUnitPlatform()
maxHeapSize = "2g"
maxParallelForks = Runtime.getRuntime().availableProcessors()
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
}
}
afterEvaluate {
// Disable Fdroid-specific tasks that we want to exclude
val fdroidTasksToDisable = tasks.withType<GoogleServicesTask>() +
@@ -328,6 +338,7 @@ private fun renameFile(path: String, newName: String) {
if (originalFile.renameTo(newFile)) {
println("Renamed $originalFile to $newFile")
} else {
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("Failed to rename $originalFile to $newFile")
}
}

View File

@@ -0,0 +1,70 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "2835802f9de260f6f5109c81081e9b46",
"entities": [
{
"tableName": "organization_events",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `organization_event_type` TEXT NOT NULL, `cipher_id` TEXT, `date` INTEGER NOT NULL, `organization_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationEventType",
"columnName": "organization_event_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherId",
"columnName": "cipher_id",
"affinity": "TEXT"
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_organization_events_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_organization_events_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2835802f9de260f6f5109c81081e9b46')"
]
}
}

View File

@@ -0,0 +1,264 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "11387825dab701f9d2dd2e940ffbd794",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `has_totp` INTEGER NOT NULL DEFAULT 1, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasTotp",
"columnName": "has_totp",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, `default_user_collection_email` TEXT, `type` TEXT NOT NULL DEFAULT '0', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT"
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canManage",
"columnName": "manage",
"affinity": "INTEGER"
},
{
"fieldPath": "defaultUserCollectionEmail",
"columnName": "default_user_collection_email",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'0'"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
}
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11387825dab701f9d2dd2e940ffbd794')"
]
}
}

View File

@@ -0,0 +1,279 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "61353072161e3101ade140e2c4b65495",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `has_totp` INTEGER NOT NULL DEFAULT 1, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, `organization_id` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasTotp",
"columnName": "has_totp",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
},
{
"name": "index_ciphers_user_id_organization_id",
"unique": false,
"columnNames": [
"user_id",
"organization_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id_organization_id` ON `${TABLE_NAME}` (`user_id`, `organization_id`)"
}
]
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, `default_user_collection_email` TEXT, `type` TEXT NOT NULL DEFAULT '0', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT"
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canManage",
"columnName": "manage",
"affinity": "INTEGER"
},
{
"fieldPath": "defaultUserCollectionEmail",
"columnName": "default_user_collection_email",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'0'"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
}
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '61353072161e3101ade140e2c4b65495')"
]
}
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<credential-provider>
<capabilities>
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
</capabilities>
</credential-provider>

View File

@@ -0,0 +1,44 @@
Recognized as best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
SECURE YOUR DIGITAL LIFE
Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access.
ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords and passkeys across unlimited devices without restrictions.
USE PASSKEYS WHEREVER YOU LOG IN
Create, store, and sync passkeys across the Bitwarden mobile app and browser extensions for a secure, passwordless experience no matter what device you're on.
EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE
Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
EMPOWER YOUR TEAMS WITH BITWARDEN
Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more.
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
More reasons to choose Bitwarden:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private.
3rd-party Audits
Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications.
Advanced 2FA
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
Bitwarden Send
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
Built-in Generator
Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy.
Global Translations
Bitwarden translations exist for more than 50 languages.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Accessibility Services Disclosure: Bitwarden offers the ability to use the Accessibility Service to augment Autofill on older devices or in cases where autofill is not working correctly. When enabled, the Accessibility Service is used to search for login fields in apps and websites. This establishes the appropriate field IDs when a match for the app or site is found and inserts credentials. When the Accessibility Service is active Bitwarden does not store information or control any on-screen elements beyond inserting credentials.

View File

@@ -0,0 +1 @@
Bitwarden is a login and password manager that helps keep you safe while online.

View File

@@ -0,0 +1 @@
Bitwarden Password Manager

View File

@@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="horizons.permission.HEADSET_CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
@@ -83,14 +84,6 @@
<data android:host="*.bitwarden.eu" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -105,6 +98,46 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="bitwarden" />
</intent-filter>
<!-- Handle Credential Exchange transfer requests -->
<intent-filter
android:autoVerify="true"
tools:ignore="AppLinkUrlError">
<action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:scheme="content"
tools:ignore="AppLinkUriRelativeFilterGroupError" />
</intent-filter>
</activity>
<!-- Credential Provider Activity for handling passkey and password credential operations.
This activity is NOT exported to protect against external apps attempting to extract
vault credentials by sending malicious intents. Only our own PendingIntents can
launch this activity.
This is a transparent trampoline activity that launches MainActivity for credential
operations and forwards results back to the Credential Manager framework.
Uses Theme.Translucent.NoTitleBar for invisibility while allowing normal lifecycle
(Theme.NoDisplay requires finish() before onResume(), incompatible with ActivityResult).
Note: Unlike AuthCallbackActivity, this does NOT use noHistory="true" because it
must remain in the back stack to receive the ActivityResult callback from
MainActivity. -->
<activity
android:name=".CredentialProviderActivity"
android:exported="false"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
@@ -115,11 +148,11 @@
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".AutofillTotpCopyActivity"
android:name=".AutofillCallbackActivity"
android:exported="true"
android:launchMode="singleTop"
android:noHistory="true"
android:theme="@style/AutofillTotpCopyTheme" />
android:theme="@style/AutofillCallbackTheme" />
<activity
android:name=".AuthCallbackActivity"
@@ -127,15 +160,18 @@
android:launchMode="singleTop"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="captcha-callback"
android:scheme="bitwarden" />
<data android:scheme="https" />
<data android:host="bitwarden.com" />
<data android:host="bitwarden.eu" />
<data android:pathPattern="/duo-callback" />
<data android:pathPattern="/sso-callback" />
<data android:pathPattern="/webauthn-callback" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -167,6 +203,16 @@
android:host="webauthn-callback"
android:scheme="bitwarden" />
</intent-filter>
<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:host="sso-cookie-vendor"
android:scheme="bitwarden" />
</intent-filter>
</activity>
<provider
@@ -259,7 +305,7 @@
android:name="com.x8bit.bitwarden.AutofillTileService"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/autofill"
android:label="@string/autofill_verb"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:ignore="MissingClass">
<intent-filter>

View File

@@ -1,5 +1,33 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "com.iode.firefox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C9:96:DA:AB:86:A8:CD:32:53:77:49:A5:EE:1D:C2:F9:84:F2:9D:43:F3:06:7D:2C:0A:54:BF:8B:BF:AB:62:C0"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "eu.weblibre.gecko",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BB:2A:97:F5:61:53:35:C9:E5:7C:86:6F:1C:30:ED:4F:D7:D7:BD:DC:BC:BC:06:68:FE:93:A5:79:17:3D:3D:2D"
},
{
"build": "release",
"cert_fingerprint_sha256": "8F:52:6E:1E:53:D6:BD:4D:FB:F4:F4:B9:3C:2A:91:EC:B5:CB:8D:A5:E1:4A:D9:4C:25:70:E1:E3:C7:13:52:7F"
}
]
}
},
{
"type": "android",
"info": {
@@ -12,18 +40,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
}
]
}
},
{
"type": "android",
"info": {
@@ -48,6 +64,18 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox.nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
}
]
}
},
{
"type": "android",
"info": {

View File

@@ -815,6 +815,38 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.zoho.primeum.stable",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A9:D6:D0:A2:AF:DB:15:84:9B:8C:D3:1D:51:FE:73:B8:E1:B1:70:BA:A5:70:C2:F8:F2:A3:F8:65:28:29:CB:BD"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.amazon.cloud9",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "2F:19:AD:EB:28:4E:B3:6F:7F:07:78:61:52:B9:A1:D1:4B:21:65:32:03:AD:0B:04:EB:BF:9C:73:AB:6D:76:25"
},
{
"build": "release",
"cert_fingerprint_sha256": "70:D5:68:EC:6A:E6:F3:38:BC:1A:63:99:A6:53:7E:E0:69:08:CA:1D:72:FB:8F:F0:48:74:AB:95:43:3B:25:0E"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "7C:AC:39:19:37:98:1B:61:34:BD:CE:1F:D9:83:4C:25:31:81:F5:AB:F9:1D:ED:60:78:21:0D:0F:91:AC:E3:60"
}
]
}
}
]
}

View File

@@ -3,7 +3,7 @@ package com.x8bit.bitwarden
import android.content.Intent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getCookieCallbackResultOrNull
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
@@ -27,20 +27,14 @@ class AuthCallbackViewModel @Inject constructor(
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
val webAuthResult = action.intent.getWebAuthResultOrNull()
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
val ssoCallbackResult = action.intent.getSsoCallbackResult()
val cookieCallbackResult = action.intent.getCookieCallbackResultOrNull()
when {
yubiKeyResult != null -> {
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
}
captchaCallbackTokenResult != null -> {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = captchaCallbackTokenResult,
)
}
duoCallbackTokenResult != null -> {
authRepository.setDuoCallbackTokenResult(
tokenResult = duoCallbackTokenResult,
@@ -53,6 +47,12 @@ class AuthCallbackViewModel @Inject constructor(
)
}
cookieCallbackResult != null -> {
authRepository.setCookieCallbackResult(
result = cookieCallbackResult,
)
}
webAuthResult != null -> {
authRepository.setWebAuthResult(webAuthResult = webAuthResult)
}

View File

@@ -15,18 +15,18 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
/**
* An activity for copying a TOTP code to the clipboard. This is done when an autofill item is
* selected and it requires TOTP authentication. Due to the constraints of the autofill framework,
* we also have to re-fulfill the autofill for the views that are being filled.
* An activity that is launched to complete Autofill. This is done when an autofill item is selected
* and is associated with a valid cipher. Due to the constraints of the autofill framework, we also
* have to re-fulfill the autofill for the views that are being filled.
*/
@OmitFromCoverage
@AndroidEntryPoint
class AutofillTotpCopyActivity : AppCompatActivity() {
class AutofillCallbackActivity : AppCompatActivity() {
@Inject
lateinit var autofillCompletionManager: AutofillCompletionManager
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
private val viewModel: AutofillCallbackViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
@@ -34,11 +34,7 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
observeViewModelEvents()
autofillTotpCopyViewModel.trySendAction(
AutofillTotpCopyAction.IntentReceived(
intent = intent,
),
)
viewModel.trySendAction(AutofillCallbackAction.IntentReceived(intent = intent))
}
override fun onNewIntent(intent: Intent) {
@@ -50,17 +46,12 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
}
private fun observeViewModelEvents() {
autofillTotpCopyViewModel
viewModel
.eventFlow
.onEach { event ->
when (event) {
is AutofillTotpCopyEvent.CompleteAutofill -> {
handleCompleteAutofill(event)
}
is AutofillTotpCopyEvent.FinishActivity -> {
finishActivity()
}
is AutofillCallbackEvent.CompleteAutofill -> handleCompleteAutofill(event)
is AutofillCallbackEvent.FinishActivity -> finishActivity()
}
}
.launchIn(lifecycleScope)
@@ -69,7 +60,7 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
/**
* Complete autofill with the provided data.
*/
private fun handleCompleteAutofill(event: AutofillTotpCopyEvent.CompleteAutofill) {
private fun handleCompleteAutofill(event: AutofillCallbackEvent.CompleteAutofill) {
autofillCompletionManager.completeAutofill(
activity = this,
cipherView = event.cipherView,

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillCallbackIntentOrNull
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.first
import timber.log.Timber
import javax.inject.Inject
/**
@@ -21,45 +22,63 @@ import javax.inject.Inject
private const val CIPHER_WAIT_TIMEOUT_MILLIS: Long = 500
/**
* A view model that handles logic for the [AutofillTotpCopyActivity].
* A view model that handles logic for the [AutofillCallbackActivity].
*/
@HiltViewModel
class AutofillTotpCopyViewModel @Inject constructor(
class AutofillCallbackViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
) : BaseViewModel<Unit, AutofillTotpCopyEvent, AutofillTotpCopyAction>(Unit) {
) : BaseViewModel<Unit, AutofillCallbackEvent, AutofillCallbackAction>(Unit) {
private val activeUserId: String? get() = authRepository.activeUserId
override fun handleAction(action: AutofillTotpCopyAction): Unit = when (action) {
is AutofillTotpCopyAction.IntentReceived -> handleIntentReceived(action)
override fun handleAction(action: AutofillCallbackAction): Unit = when (action) {
is AutofillCallbackAction.IntentReceived -> handleIntentReceived(action)
}
/**
* Process the received intent and alert the activity of what to do next.
*/
private fun handleIntentReceived(action: AutofillTotpCopyAction.IntentReceived) {
private fun handleIntentReceived(action: AutofillCallbackAction.IntentReceived) {
viewModelScope
.launchWithTimeout(
timeoutBlock = { finishActivity() },
timeoutBlock = {
Timber.w("Autofill -- Timeout")
finishActivity()
},
timeoutDuration = CIPHER_WAIT_TIMEOUT_MILLIS,
) {
// Extract TOTP copy data from the intent.
val cipherId = action
.intent
.getTotpCopyIntentOrNull()
.getAutofillCallbackIntentOrNull()
?.cipherId
if (cipherId == null || isVaultLocked()) {
if (cipherId == null) {
Timber.w("Autofill -- Cipher was not provided")
finishActivity()
return@launchWithTimeout
}
if (isVaultLocked()) {
Timber.w("Autofill -- Vault is locked")
finishActivity()
return@launchWithTimeout
}
// Try and find the matching cipher.
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
GetCipherResult.CipherNotFound -> finishActivity()
is GetCipherResult.Failure -> finishActivity()
GetCipherResult.CipherNotFound -> {
Timber.w("Autofill -- Cipher not found")
finishActivity()
}
is GetCipherResult.Failure -> {
Timber.w(result.error, "Autofill -- Get cipher failure")
finishActivity()
}
is GetCipherResult.Success -> {
sendEvent(AutofillTotpCopyEvent.CompleteAutofill(result.cipherView))
Timber.d("Autofill -- Cipher found")
sendEvent(AutofillCallbackEvent.CompleteAutofill(result.cipherView))
}
}
}
@@ -69,7 +88,7 @@ class AutofillTotpCopyViewModel @Inject constructor(
* Send an event to the activity that signals it to finish.
*/
private fun finishActivity() {
sendEvent(AutofillTotpCopyEvent.FinishActivity)
sendEvent(AutofillCallbackEvent.FinishActivity)
}
private suspend fun isVaultLocked(): Boolean {
@@ -86,30 +105,30 @@ class AutofillTotpCopyViewModel @Inject constructor(
}
/**
* Represents actions that can be sent to the [AutofillTotpCopyViewModel].
* Represents actions that can be sent to the [AutofillCallbackViewModel].
*/
sealed class AutofillTotpCopyAction {
sealed class AutofillCallbackAction {
/**
* An [intent] has been received and is ready to be processed.
*/
data class IntentReceived(
val intent: Intent,
) : AutofillTotpCopyAction()
) : AutofillCallbackAction()
}
/**
* Represents events emitted by the [AutofillTotpCopyViewModel].
* Represents events emitted by the [AutofillCallbackViewModel].
*/
sealed class AutofillTotpCopyEvent {
sealed class AutofillCallbackEvent {
/**
* Complete autofill with the provided [cipherView].
*/
data class CompleteAutofill(
val cipherView: CipherView,
) : AutofillTotpCopyEvent()
) : AutofillCallbackEvent()
/**
* Finish the activity.
*/
data object FinishActivity : AutofillTotpCopyEvent()
data object FinishActivity : AutofillCallbackEvent()
}

View File

@@ -6,8 +6,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
@@ -23,9 +23,6 @@ class BitwardenApplication : Application() {
@Inject
lateinit var logsManager: LogsManager
@Inject
lateinit var networkConnectionManager: NetworkConnectionManager
@Inject
lateinit var networkConfigManager: NetworkConfigManager
@@ -38,6 +35,17 @@ class BitwardenApplication : Application() {
@Inject
lateinit var restrictionManager: RestrictionManager
@Inject
lateinit var environmentRepository: EnvironmentRepository
override fun onCreate() {
super.onCreate()
// These must be initialized in order to ensure that the restrictionManager does not
// override the environmentRepository values.
restrictionManager.initialize()
environmentRepository.initialize()
}
override fun onLowMemory() {
super.onLowMemory()
Timber.w("onLowMemory")

View File

@@ -0,0 +1,89 @@
package com.x8bit.bitwarden
import android.app.ComponentCaller
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.util.validate
import com.x8bit.bitwarden.data.credentials.BitwardenCredentialProviderService
import dagger.hilt.android.AndroidEntryPoint
/**
* Transparent trampoline activity for handling credential provider operations.
*
* This activity is declared as `exported="false"` in the manifest to ensure only
* our own PendingIntents can launch it. This protects against external apps attempting
* to extract vault credentials by sending malicious intents via CredentialManager.
*
* All credential flows (FIDO2 passkeys, password credentials) are routed through this
* activity when triggered by the Android CredentialManager framework via our
* [BitwardenCredentialProviderService].
*
* ## Architecture
*
* This activity does not host any UI itself. It acts as a trampoline that:
* 1. Receives the credential intent from the CredentialManager framework
* 2. Sets the pending credential request via [CredentialProviderViewModel], which stores
* it in `CredentialProviderRequestManager` for secure relay to [MainViewModel]
* 3. Launches [MainActivity] to handle the actual credential UI
* 4. Forwards the result back to the CredentialManager framework
*
* This preserves the single-Activity architecture where all UI is hosted by MainActivity,
* while still allowing the CredentialManager framework to receive results properly.
*/
@OmitFromCoverage
@AndroidEntryPoint
class CredentialProviderActivity : ComponentActivity() {
private val viewModel: CredentialProviderViewModel by viewModels()
/**
* Launcher for MainActivity that forwards the result back to Credential Manager.
*/
private val mainActivityLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result ->
// Forward result back to Credential Manager framework
setResult(result.resultCode, result.data)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
// Process credential intent (sets pending request on CredentialProviderRequestManager)
viewModel.trySendAction(CredentialProviderAction.ReceiveFirstIntent(intent))
launchMainActivityForResult()
}
// On restoration (process death), result comes via mainActivityLauncher callback
}
private fun launchMainActivityForResult() {
val mainIntent = Intent(this, MainActivity::class.java).apply {
// Pending credential request is retrieved by MainViewModel from
// CredentialProviderRequestManager, triggering appropriate navigation.
// CredentialProviderCompletionManager handles setResult/finish.
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
mainActivityLauncher.launch(mainIntent)
}
override fun onNewIntent(intent: Intent) {
val newIntent = intent.validate()
super.onNewIntent(newIntent)
viewModel.trySendAction(CredentialProviderAction.ReceiveNewIntent(newIntent))
launchMainActivityForResult()
}
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
val newIntent = intent.validate()
super.onNewIntent(newIntent, caller)
viewModel.trySendAction(CredentialProviderAction.ReceiveNewIntent(newIntent))
launchMainActivityForResult()
}
}

View File

@@ -0,0 +1,110 @@
package com.x8bit.bitwarden
import android.content.Intent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.manager.CredentialProviderRequestManager
import com.x8bit.bitwarden.data.credentials.manager.model.CredentialProviderRequest
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
import com.x8bit.bitwarden.data.credentials.util.getCreateCredentialRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getGetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getProviderGetPasswordRequestOrNull
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* A view model that handles credential provider operations for [CredentialProviderActivity].
*
* This ViewModel processes credential-related intents and sets the pending credential request
* on [CredentialProviderRequestManager] for relay to [MainViewModel]. This ensures credential
* data is never passed through intent extras to exported activities, providing security
* hardening against malicious intent attacks.
*
* Since [CredentialProviderActivity] is a transparent trampoline with no UI, this ViewModel only
* handles intent processing. All UI state management (theme, feature flags, auth flows) is
* handled by [MainActivity].
*
* @see RootNavViewModel for navigation based on SpecialCircumstance.
*/
@HiltViewModel
class CredentialProviderViewModel @Inject constructor(
private val credentialProviderRequestManager: CredentialProviderRequestManager,
private val authRepository: AuthRepository,
private val bitwardenCredentialManager: BitwardenCredentialManager,
) : BaseViewModel<Unit, Unit, CredentialProviderAction>(initialState = Unit) {
override fun handleAction(action: CredentialProviderAction) {
when (action) {
is CredentialProviderAction.ReceiveFirstIntent -> handleIntent(action.intent)
is CredentialProviderAction.ReceiveNewIntent -> handleIntent(action.intent)
}
}
private fun handleIntent(intent: Intent) {
intent.getCreateCredentialRequestOrNull()?.let { handleCreateCredential(it) }
?: intent.getFido2AssertionRequestOrNull()?.let { handleFido2Assertion(it) }
?: intent.getProviderGetPasswordRequestOrNull()?.let { handlePasswordGet(it) }
?: intent.getGetCredentialsRequestOrNull()?.let { handleGetCredentials(it) }
}
private fun handleCreateCredential(request: CreateCredentialRequest) {
bitwardenCredentialManager.isUserVerified = request.isUserPreVerified
// Switch accounts if the selected user is not the active user
if (authRepository.activeUserId != null &&
authRepository.activeUserId != request.userId
) {
authRepository.switchAccount(request.userId)
}
credentialProviderRequestManager.setPendingCredentialRequest(
CredentialProviderRequest.CreateCredential(request),
)
}
private fun handleFido2Assertion(request: Fido2CredentialAssertionRequest) {
// Set the user's verification status when a new FIDO 2 request is received
bitwardenCredentialManager.isUserVerified = request.isUserPreVerified
credentialProviderRequestManager.setPendingCredentialRequest(
CredentialProviderRequest.Fido2Assertion(request),
)
}
private fun handlePasswordGet(request: ProviderGetPasswordCredentialRequest) {
// Set the user's verification status when a new GetPassword request is received
bitwardenCredentialManager.isUserVerified = request.isUserPreVerified
credentialProviderRequestManager.setPendingCredentialRequest(
CredentialProviderRequest.GetPassword(request),
)
}
private fun handleGetCredentials(request: GetCredentialsRequest) {
credentialProviderRequestManager.setPendingCredentialRequest(
CredentialProviderRequest.GetCredentials(request),
)
}
}
/**
* Models actions for the [CredentialProviderViewModel].
*/
sealed class CredentialProviderAction {
/**
* Receive the first intent when the activity is created.
*/
data class ReceiveFirstIntent(val intent: Intent) : CredentialProviderAction()
/**
* Receive a new intent when the activity receives onNewIntent.
*/
data class ReceiveNewIntent(val intent: Intent) : CredentialProviderAction()
}

View File

@@ -7,14 +7,16 @@ import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.auth.AuthTabIntent
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -33,19 +35,20 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers
import com.x8bit.bitwarden.ui.platform.util.appLanguage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private const val ANDROID_15_BUG_MAX_REVISION: Int = 241007
/**
* Primary entry point for the application.
*/
@@ -71,6 +74,20 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager
private val duoLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.DuoResult(it))
}
private val ssoLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.SsoResult(it))
}
private val webAuthnLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.WebAuthnResult(it))
}
private val cookieLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.CookieAcquisitionResult(it))
}
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
var shouldShowSplashScreen = true
@@ -91,7 +108,15 @@ class MainActivity : AppCompatActivity() {
SetupEventsEffect(navController = navController)
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
LocalManagerProvider(
featureFlagsState = state.featureFlagsState,
authTabLaunchers = AuthTabLaunchers(
duo = duoLauncher,
sso = ssoLauncher,
webAuthn = webAuthnLauncher,
cookie = cookieLauncher,
),
) {
ObserveScreenDataEffect(
onDataUpdate = remember(mainViewModel) {
{ mainViewModel.trySendAction(MainAction.ResumeScreenDataReceived(it)) }
@@ -103,16 +128,23 @@ class MainActivity : AppCompatActivity() {
) {
NavHost(
navController = navController,
startDestination = ROOT_ROUTE,
startDestination = RootNavigationRoute,
modifier = Modifier
.background(color = BitwardenTheme.colorScheme.background.primary),
) {
// Nothing else should end up at this top level, we just want the ability
// to have the debug menu appear on top of the rest of the app without
// interacting with the state-based navigation used by the RootNavScreen.
// Root navigation, debug menu, and cookie acquisition exist at
// this top level. They can appear on top of the rest of the app
// without interacting with the state-based navigation used by
// RootNavScreen.
rootNavDestination { shouldShowSplashScreen = false }
debugMenuDestination(
onNavigateBack = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
cookieAcquisitionDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
}
}
}
@@ -186,11 +218,7 @@ class MainActivity : AppCompatActivity() {
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
is MainEvent.ShowToast -> {
Toast
.makeText(baseContext, event.message.invoke(resources), Toast.LENGTH_SHORT)
.show()
}
MainEvent.NavigateToCookieAcquisition -> navController.navigateToCookieAcquisition()
is MainEvent.UpdateAppLocale -> {
AppCompatDelegate.setApplicationLocales(
@@ -224,35 +252,7 @@ class MainActivity : AppCompatActivity() {
}
private fun handleRecreate() {
val isOldAndroidBuildRevision = {
// This fetches the date portion of the ID in order to determine the revision of
// Android 15 being used and whether we want to use the `recreate` API or not.
// If we fail to parse a date, we assume it is not an old revision.
"\\.([^.]+)\\."
.toRegex()
.find(Build.ID)
?.groups
?.get(1)
?.value
?.toIntOrNull()
?.let { it <= ANDROID_15_BUG_MAX_REVISION } == true
}
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM &&
isOldAndroidBuildRevision()
) {
// This is done to avoid a bug in specific older revisions of Android 15. The bug has
// been fixed but certain phones that are no longer supported will never get the fix.
// The OS bug is tracked here: https://issuetracker.google.com/issues/370180732
startActivity(
Intent
.makeMainActivity(componentName)
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION),
)
finish()
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
} else {
ActivityCompat.recreate(this)
}
ActivityCompat.recreate(this)
}
private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) {

View File

@@ -2,29 +2,35 @@ package com.x8bit.bitwarden
import android.content.Intent
import android.os.Parcelable
import androidx.browser.auth.AuthTabIntent
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.share.ShareManager
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getCookieCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResult
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.util.getCreateCredentialRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getGetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getProviderGetPasswordRequestOrNull
import com.x8bit.bitwarden.data.credentials.manager.CredentialProviderRequestManager
import com.x8bit.bitwarden.data.credentials.manager.model.CredentialProviderRequest
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
@@ -36,12 +42,10 @@ import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticato
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.FlowPreview
@@ -49,6 +53,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -72,11 +77,12 @@ private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val bitwardenCredentialManager: BitwardenCredentialManager,
private val intentManager: IntentManager,
private val credentialProviderRequestManager: CredentialProviderRequestManager,
private val shareManager: ShareManager,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
@@ -84,6 +90,7 @@ class MainViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val appResumeManager: AppResumeManager,
private val clock: Clock,
private val toastManager: ToastManager,
) : BaseViewModel<MainState, MainEvent, MainAction>(
initialState = MainState(
theme = settingsRepository.appTheme,
@@ -158,6 +165,13 @@ class MainViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
cookieAcquisitionRequestManager
.cookieAcquisitionRequestFlow
.filterNotNull()
.map { MainAction.Internal.CookieAcquisitionReady }
.onEach(::sendAction)
.launchIn(viewModelScope)
// On app launch, mark all active users as having previously logged in.
// This covers any users who are active prior to this value being recorded.
viewModelScope.launch {
@@ -179,6 +193,10 @@ class MainViewModel @Inject constructor(
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
is MainAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
is MainAction.DuoResult -> handleDuoResult(action)
is MainAction.SsoResult -> handleSsoResult(action)
is MainAction.WebAuthnResult -> handleWebAuthnResult(action)
is MainAction.CookieAcquisitionResult -> handleCookieAcquisitionResult(action)
is MainAction.Internal -> handleInternalAction(action)
}
}
@@ -200,6 +218,7 @@ class MainViewModel @Inject constructor(
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
is MainAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
}
}
@@ -207,6 +226,26 @@ class MainViewModel @Inject constructor(
settingsRepository.appLanguage = action.appLanguage
}
private fun handleDuoResult(action: MainAction.DuoResult) {
authRepository.setDuoCallbackTokenResult(
tokenResult = action.authResult.getDuoCallbackTokenResult(),
)
}
private fun handleSsoResult(action: MainAction.SsoResult) {
authRepository.setSsoCallbackResult(result = action.authResult.getSsoCallbackResult())
}
private fun handleWebAuthnResult(action: MainAction.WebAuthnResult) {
authRepository.setWebAuthResult(webAuthResult = action.authResult.getWebAuthResult())
}
private fun handleCookieAcquisitionResult(action: MainAction.CookieAcquisitionResult) {
authRepository.setCookieCallbackResult(
result = action.cookieCallbackResult.getCookieCallbackResult(),
)
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
@@ -250,6 +289,10 @@ class MainViewModel @Inject constructor(
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
}
private fun handleCookieAcquisitionReady() {
sendEvent(MainEvent.NavigateToCookieAcquisition)
}
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
handleIntent(
intent = action.intent,
@@ -272,7 +315,7 @@ class MainViewModel @Inject constructor(
val passwordlessRequestData = intent.getPasswordlessRequestDataIntentOrNull()
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
val shareData = shareManager.getShareDataOrNull(intent = intent)
val totpData: TotpData? =
// First grab TOTP URI directly from the intent data:
intent.getTotpDataOrNull()
@@ -291,10 +334,9 @@ class MainViewModel @Inject constructor(
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val createCredentialRequest = intent.getCreateCredentialRequestOrNull()
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
val importCredentialsRequest = intent.getProviderImportCredentialsRequest()
val credentialProviderRequest =
credentialProviderRequestManager.getPendingCredentialRequest()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
@@ -352,59 +394,6 @@ class MainViewModel @Inject constructor(
)
}
createCredentialRequest != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
bitwardenCredentialManager.isUserVerified =
createCredentialRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
createCredentialRequest = createCredentialRequest,
)
// Switch accounts if the selected user is not the active user.
if (authRepository.activeUserId != null &&
authRepository.activeUserId != createCredentialRequest.userId
) {
authRepository.switchAccount(createCredentialRequest.userId)
}
}
fido2AssertCredentialRequest != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
bitwardenCredentialManager.isUserVerified =
fido2AssertCredentialRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = fido2AssertCredentialRequest,
)
}
providerGetPasswordRequest != null -> {
// Set the user's verification status when a new GetPassword request is
// received to force explicit verification if the user's vault is
// unlocked when the request is received.
bitwardenCredentialManager.isUserVerified =
providerGetPasswordRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderGetPasswordRequest(
passwordGetRequest = providerGetPasswordRequest,
)
}
getCredentialsRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderGetCredentials(
getCredentialsRequest = getCredentialsRequest,
)
}
hasGeneratorShortcut -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.GeneratorShortcut
@@ -418,6 +407,58 @@ class MainViewModel @Inject constructor(
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
importCredentialsRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.CredentialExchangeExport(
data = ImportCredentialsRequestData(
uri = importCredentialsRequest.uri,
credentialTypes = importCredentialsRequest.request.credentialTypes,
knownExtensions = importCredentialsRequest.request.knownExtensions,
),
)
}
credentialProviderRequest != null -> {
handleCredentialRequest(credentialProviderRequest)
}
}
}
/**
* Handles a credential request relayed from [CredentialProviderActivity] via
* [CredentialProviderRequestManager].
*
* This method converts the [CredentialProviderRequest] into the appropriate
* [SpecialCircumstance] for routing by [RootNavViewModel]. The credential data is trusted
* because it was set by our own [CredentialProviderActivity] through the internal manager,
* not parsed from intent extras.
*/
private fun handleCredentialRequest(request: CredentialProviderRequest) {
specialCircumstanceManager.specialCircumstance = when (request) {
is CredentialProviderRequest.CreateCredential -> {
SpecialCircumstance.ProviderCreateCredential(
createCredentialRequest = request.request,
)
}
is CredentialProviderRequest.Fido2Assertion -> {
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = request.request,
)
}
is CredentialProviderRequest.GetPassword -> {
SpecialCircumstance.ProviderGetPasswordRequest(
passwordGetRequest = request.request,
)
}
is CredentialProviderRequest.GetCredentials -> {
SpecialCircumstance.ProviderGetCredentials(
getCredentialsRequest = request.request,
)
}
}
}
@@ -433,16 +474,15 @@ class MainViewModel @Inject constructor(
)
when (emailTokenResult) {
is EmailTokenResult.Error -> {
sendEvent(
MainEvent.ShowToast(
message = emailTokenResult
.message
?.asText()
?: BitwardenString
.there_was_an_issue_validating_the_registration_token
.asText(),
),
)
emailTokenResult
.message
?.let { toastManager.show(message = it) }
?: run {
toastManager.show(
messageId = BitwardenString
.there_was_an_issue_validating_the_registration_token,
)
}
}
EmailTokenResult.Expired -> {
@@ -486,6 +526,28 @@ data class MainState(
* Models actions for the [MainActivity].
*/
sealed class MainAction {
/**
* Receive the result from the Duo login flow.
*/
data class DuoResult(val authResult: AuthTabIntent.AuthResult) : MainAction()
/**
* Receive the result from the SSO login flow.
*/
data class SsoResult(val authResult: AuthTabIntent.AuthResult) : MainAction()
/**
* Receive the result from the WebAuthn login flow.
*/
data class WebAuthnResult(val authResult: AuthTabIntent.AuthResult) : MainAction()
/**
* Receive the result from the cookie acquisition flow.
*/
data class CookieAcquisitionResult(
val cookieCallbackResult: AuthTabIntent.AuthResult,
) : MainAction()
/**
* Receive first Intent by the application.
*/
@@ -556,6 +618,12 @@ sealed class MainAction {
data class DynamicColorsUpdate(
val isDynamicColorsEnabled: Boolean,
) : Internal()
/**
* Indicates that the cookie acquisition conditions are met and navigation
* should proceed.
*/
data object CookieAcquisitionReady : Internal()
}
}
@@ -586,9 +654,9 @@ sealed class MainEvent {
data object NavigateToDebugMenu : MainEvent()
/**
* Show a toast with the given [message].
* Navigate to the cookie acquisition screen.
*/
data class ShowToast(val message: Text) : MainEvent()
data object NavigateToCookieAcquisition : MainEvent()
/**
* Indicates that the app language has been updated.

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.provider.AppIdProvider
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
@@ -126,13 +127,34 @@ interface AuthDiskSource : AppIdProvider {
/**
* Retrieves a private key using a [userId].
*/
@Deprecated(
message = "Use getAccountKeys instead.",
replaceWith = ReplaceWith("getAccountKeys"),
)
fun getPrivateKey(userId: String): String?
/**
* Stores a private key using a [userId].
*/
@Deprecated(
message = "Use storeAccountKeys instead.",
replaceWith = ReplaceWith("storeAccountKeys"),
)
fun storePrivateKey(userId: String, privateKey: String?)
/**
* Returns the profile account keys for the given [userId].
*/
fun getAccountKeys(userId: String): AccountKeysJson?
/**
* Stores the profile account keys for the given [userId].
*/
fun storeAccountKeys(
userId: String,
accountKeys: AccountKeysJson?,
)
/**
* Retrieves a user auto-unlock key for the given [userId].
*/
@@ -189,30 +211,64 @@ interface AuthDiskSource : AppIdProvider {
/**
* Gets the flow for the biometrics key for the given [userId].
*/
fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?>
fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?>
/**
* Retrieves a pin-protected user key for the given [userId].
*/
@Deprecated(
message = "Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
fun getPinProtectedUserKey(userId: String): String?
/**
* Retrieves a pin-protected user key envelope for the given [userId].
*/
fun getPinProtectedUserKeyEnvelope(userId: String): String?
/**
* Stores a pin-protected user key for the given [userId].
*
* When [inMemoryOnly] is `true`, the value will only be available via a call to
* [getPinProtectedUserKey] during the current app session.
*/
@Deprecated(
message = "Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
inMemoryOnly: Boolean = false,
)
/**
* Stores a pin-protected user key envelope for the given [userId].
*
* When [inMemoryOnly] is `true`, the value will only be available via a call to
* [getPinProtectedUserKeyEnvelope] during the current app session.
*/
fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean = false,
)
/**
* Retrieves a flow for the pin-protected user key for the given [userId].
*/
@Deprecated(
message = "Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
fun getPinProtectedUserKeyFlow(userId: String): Flow<String?>
/**
* Retrieves a flow for the pin-protected user key envelope for the given [userId].
*/
fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?>
/**
* Gets a two-factor auth token using a user's [email].
*/

View File

@@ -4,6 +4,7 @@ import android.content.SharedPreferences
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
@@ -36,6 +37,7 @@ private const val INVALID_UNLOCK_ATTEMPTS_KEY = "invalidUnlockAttempts"
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey"
private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "encPrivateKey"
private const val PIN_PROTECTED_USER_KEY_KEY = "pinKeyEncryptedUserKey"
private const val PIN_PROTECTED_USER_KEY_KEY_ENVELOPE = "pinKeyEncryptedUserKeyEnvelope"
private const val ENCRYPTED_PIN_KEY = "protectedPin"
private const val ORGANIZATIONS_KEY = "organizations"
private const val ORGANIZATION_KEYS_KEY = "encOrgKeys"
@@ -48,6 +50,7 @@ private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
/**
* Primary implementation of [AuthDiskSource].
@@ -65,6 +68,7 @@ class AuthDiskSourceImpl(
AuthDiskSource {
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
private val inMemoryPinProtectedUserKeyEnvelopes = mutableMapOf<String, String?>()
private val mutableShouldUseKeyConnectorFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableOrganizationsFlowMap =
@@ -80,6 +84,8 @@ class AuthDiskSourceImpl(
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyEnvelopeFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@@ -139,9 +145,8 @@ class AuthDiskSourceImpl(
storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null)
storeUserKey(userId = userId, userKey = null)
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
storeEncryptedPin(userId = userId, encryptedPin = null)
storePrivateKey(userId = userId, privateKey = null)
storeAccountKeys(userId = userId, accountKeys = null)
storeOrganizationKeys(userId = userId, organizationKeys = null)
storeOrganizations(userId = userId, organizations = null)
storeUserBiometricInitVector(userId = userId, iv = null)
@@ -154,10 +159,14 @@ class AuthDiskSourceImpl(
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null)
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
storeEncryptedPin(userId = userId, encryptedPin = null)
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
// indefinitely unless the TDE flow explicitly removes them.
// Do not remove OnboardingStatus we want to keep track of this even after logout.
// Certain values are never removed as required by the feature requirements:
// * DeviceKey
// * PendingAuthRequest
// * OnboardingStatus
}
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
@@ -228,9 +237,11 @@ class AuthDiskSourceImpl(
)
}
@Deprecated("Use getAccountKeys instead.", replaceWith = ReplaceWith("getAccountKeys"))
override fun getPrivateKey(userId: String): String? =
getString(key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId))
@Deprecated("Use storeAccountKeys instead.", replaceWith = ReplaceWith("storeAccountKeys"))
override fun storePrivateKey(userId: String, privateKey: String?) {
putString(
key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId),
@@ -238,6 +249,20 @@ class AuthDiskSourceImpl(
)
}
override fun getAccountKeys(userId: String): AccountKeysJson? =
getEncryptedString(key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId))
?.let { json.decodeFromStringOrNull(it) }
override fun storeAccountKeys(
userId: String,
accountKeys: AccountKeysJson?,
) {
putEncryptedString(
key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId),
value = accountKeys?.let { json.encodeToString(it) },
)
}
override fun getUserAutoUnlockKey(userId: String): String? =
getEncryptedString(
key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId),
@@ -306,14 +331,28 @@ class AuthDiskSourceImpl(
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
}
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
override fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?> =
getMutableBiometricUnlockKeyFlow(userId)
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
@Deprecated(
"Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
override fun getPinProtectedUserKey(userId: String): String? =
inMemoryPinProtectedUserKeys[userId]
?: getString(key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId))
override fun getPinProtectedUserKeyEnvelope(userId: String): String? =
inMemoryPinProtectedUserKeyEnvelopes[userId]
?: getString(
key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
)
@Deprecated(
"Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
override fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
@@ -328,10 +367,35 @@ class AuthDiskSourceImpl(
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
}
override fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean,
) {
inMemoryPinProtectedUserKeyEnvelopes[userId] = pinProtectedUserKeyEnvelope
if (inMemoryOnly) {
getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
return
}
putString(
key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
value = pinProtectedUserKeyEnvelope,
)
getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
}
@Deprecated(
"Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyFlow(userId)
.onSubscription { emit(getPinProtectedUserKey(userId = userId)) }
override fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyEnvelopeFlow(userId)
.onSubscription { emit(getPinProtectedUserKeyEnvelope(userId = userId)) }
override fun getTwoFactorToken(email: String): String? =
getString(key = TWO_FACTOR_TOKEN_KEY.appendIdentifier(email))
@@ -560,6 +624,12 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePinProtectedUserKeyEnvelopeFlow(
userId: String,
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyEnvelopeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@@ -8,7 +8,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.ZonedDateTime
import java.time.Instant
/**
* Represents the current account information for a given user.
@@ -103,7 +103,7 @@ data class AccountJson(
@SerialName("creationDate")
@Contextual
val creationDate: ZonedDateTime?,
val creationDate: Instant?,
)
/**

View File

@@ -27,6 +27,12 @@ enum class OnboardingStatus {
@SerialName("autofillSetup")
AUTOFILL_SETUP,
/**
* The user is completing the browser autofill service setup.
*/
@SerialName("browserAutofillSetup")
BROWSER_AUTOFILL_SETUP,
/**
* The user is completing the final step of the onboarding process.
*/

Some files were not shown because too many files have changed in this diff Show More