Compare commits

...

328 Commits

Author SHA1 Message Date
Andre Rosado
88e23a4130 Fixed ServerConfigRepo tests 2025-07-10 16:27:55 +01:00
Andre Rosado
3a75ba5110 added missing trailing comma 2025-07-10 10:24:35 +01:00
Andre Rosado
29882c9df8 Added logs and debugmenu in order to debug if feature flags are missing 2025-07-10 10:15:05 +01:00
David Perez
ba49a3e91f PM-23553: Replace Environment toasts with snackbars (#5493) 2025-07-09 14:04:22 +00:00
David Perez
965ab67e58 PM-14063: SDK persistance state (#5491) 2025-07-08 21:14:22 +00:00
David Perez
2932ed831b PM-23549: Remove Authenticator app name localizations (#5492) 2025-07-08 20:40:29 +00:00
David Perez
2ff3f3e23d PM-23503: Update Move to Organization toasts to be snackbars (#5489) 2025-07-07 21:43:08 +00:00
David Perez
eb5893dde4 Update Chrome Autofill compatibility mode (#5490) 2025-07-07 21:42:51 +00:00
github-actions[bot]
1165e7002b Update Google privileged browsers list (#5483)
Co-authored-by: GitHub Actions Bot <actions@github.com>
2025-07-07 16:35:35 +00:00
renovate[bot]
5fa7239130 [deps]: Update Azure/login action to v2 (#5484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 16:04:23 +00:00
David Perez
fd9bdfa228 PM-19780: Fix incorrect sub header on authenticator search screen (#5488) 2025-07-07 15:57:25 +00:00
renovate[bot]
7db8f040e4 [deps]: Lock file maintenance (#5485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 15:55:37 +00:00
David Perez
790331e058 PM-23365: Create ToastManager to simplify displaying toasts from a Manager or ViewModel (#5479) 2025-07-07 14:05:20 +00:00
bw-ghapp[bot]
d0640b7e20 Crowdin Pull - Password Manager (#5482)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-04 02:01:23 +00:00
bw-ghapp[bot]
5429e27228 Crowdin Pull - Authenticator (#5481)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-04 02:00:28 +00:00
David Perez
917aaac3a6 PM-23354: Replace Login Approval toasts with snackbar (#5478) 2025-07-03 19:09:13 +00:00
David Perez
0b7209b3c9 Minor-cleanup of StartRegistration classes (#5477) 2025-07-03 18:36:27 +00:00
David Perez
a7b3201015 PM-23320: Replace Export Vault screen toasts with snackbars (#5472) 2025-07-03 18:11:04 +00:00
David Perez
348e14e52d PM-23321: Replace two-factor screen toasts with snackbars (#5473) 2025-07-03 18:10:48 +00:00
David Perez
ef9dda5159 PM-23318: Replace OtherScreen toast with snackbar (#5471) 2025-07-03 18:10:19 +00:00
Patrick Honkonen
b0309e876e [PM-23121] Update privileged app list item subtext (#5475) 2025-07-03 14:41:43 +00:00
David Perez
59a49355fd Clean up lint warnings (#5470) 2025-07-03 13:46:47 +00:00
David Perez
901184db45 PM-23322: Replace VaultItemScreen toasts with snackbars (#5474) 2025-07-03 00:56:32 +00:00
David Perez
a2507c317d PM-23308: Replace Toasts with Snackbar in AttachmentsScreen (#5469) 2025-07-02 19:25:28 +00:00
David Perez
f608852dc7 PM-23305: Replace Vault Screen Toasts with Snackbars (#5468) 2025-07-02 19:13:58 +00:00
David Perez
e44d63229c Update to the latest Bitwarden SDK (#5466) 2025-07-02 19:13:36 +00:00
David Perez
f7b876f204 PM-22972: Replace send Toasts with Snackbars (#5464) 2025-07-02 19:13:14 +00:00
Patrick Honkonen
1268afaef8 [PM-23212] Move bitwarden.pw intent filter to debug and beta builds (#5467) 2025-07-02 19:11:19 +00:00
David Perez
3f1c1dec17 PM-23293: Remove unused Toast events from the app (#5463) 2025-07-02 19:10:03 +00:00
David Perez
5eea55f173 Update various dependencies (#5465) 2025-07-02 19:05:24 +00:00
Amy Galles
1a8cf4055a log inputs to job summary for build workflows (#5453) 2025-07-02 19:03:26 +00:00
aj-rosado
defdf8eb58 [PM-22640] Re-added isScreenCaptureAllowed to the MainViewModel state (#5462) 2025-07-02 18:18:33 +00:00
David Perez
9940c8cf9e Update to AGP v8.11.0 (#5460) 2025-07-02 15:57:32 +00:00
David Perez
e1058f5021 PM-23275: Update the display name for UK English (#5461) 2025-07-02 15:40:42 +00:00
Patrick Honkonen
986cd2ee30 [PM-19779] Make Authenticator TOTP codes collapsible (#5452) 2025-07-02 14:15:03 +00:00
David Perez
eae870cb3a Fix flicker on TextField autocomplete (#5456) 2025-07-02 13:57:00 +00:00
David Perez
79493a55bd Add generic logging to Autofill process (#5457) 2025-07-02 13:56:41 +00:00
David Perez
18bafaba8a PM-22213: Hide current access count when editing and there is not max access count (#5451) 2025-07-01 16:23:50 +00:00
David Perez
896be911a4 Update Junit and Mockk libraries (#5455) 2025-07-01 16:13:08 +00:00
David Perez
85a86106f6 PM-19780: Authenticator source headers (#5450) 2025-07-01 16:12:48 +00:00
David Perez
edb7996c28 PM-23186: Move 'BitwardenSwitch' to the 'ui' module (#5454) 2025-07-01 15:42:24 +00:00
Patrick Honkonen
a806109380 [PM-23132] Update capitalization and wording in privileged apps strings (#5449) 2025-07-01 15:11:54 +00:00
Patrick Honkonen
4f5c28e248 [PM-23131] Make "About privileged apps" screen scrollable (#5448) 2025-06-30 21:37:41 +00:00
Amy Galles
b22f06cbf9 [BRE-768] Rename store publish workflow to avoid confusion (#5439) 2025-06-30 20:28:37 +00:00
Patrick Honkonen
1070c9d46e [PM-23125] Move authenticator drawables to ui module (#5440) 2025-06-30 15:55:24 +00:00
David Perez
b1dc894fe8 PM-23136: Only apply 'always' display cutout mode on API 30 and up (#5446) 2025-06-30 15:31:19 +00:00
aj-rosado
c76945161a [PM-22640] Updating screen capture flag when the setting is changed (#5426) 2025-06-30 13:57:33 +00:00
Patrick Honkonen
789cd80eba [PM-23122] Make BitwardenTextRows in PrivilegedAppsListScreen unclickable (#5441) 2025-06-30 13:21:06 +00:00
Patrick Honkonen
9482890102 [PM-23121] Capitalize "You" in passkey trust string (#5437) 2025-06-27 19:56:34 +00:00
David Perez
ed2d6ca585 Move item listing models to common location for reuse with search (#5438) 2025-06-27 19:06:31 +00:00
Patrick Honkonen
d279f6acae [PM-22786] Migrate BitwardenTextSelectionButton to ui module (#5436) 2025-06-27 17:46:23 +00:00
Andy Pixley
6ebcab7b86 [BRE-848] Add Workflow Permissions (#5389) 2025-06-27 17:00:05 +00:00
David Perez
3ee74d3ec5 PM-19776: Change 'Move to Bitwarden' to 'Copy to Bitwarden vault' (#5435) 2025-06-27 16:50:14 +00:00
Patrick Honkonen
288efb3611 [PM-19108] Fix untrusted privileged app origin validation error handling (#5432) 2025-06-27 15:53:19 +00:00
bw-ghapp[bot]
bbdf8552c9 Crowdin Pull - Password Manager (#5434)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-27 14:49:15 +00:00
bw-ghapp[bot]
44ef598df3 Crowdin Pull - Authenticator (#5433)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-27 14:48:48 +00:00
David Perez
73a8e241d4 Update Androidx Room and WorkManager libraries (#5430) 2025-06-26 20:43:29 +00:00
David Perez
4d6260ea02 Update Robolectric to the latest version (#5428) 2025-06-26 20:43:07 +00:00
David Perez
569bb4f110 Update Compose BOM to latest version (2025.06.01) (#5431) 2025-06-26 20:42:51 +00:00
Patrick Honkonen
ffc71371a9 [BWA-156] Allow TOTP syncing with Authenticator release APKs (#5429) 2025-06-26 20:40:19 +00:00
David Perez
8d0b23d166 PM-23092: Update the Autofill settings UI for better communication (#5427) 2025-06-26 17:53:47 +00:00
Patrick Honkonen
5f525d9d95 [BWA-162] Add getPackageInstallationSourceOrNull to BitwardenPackageManager (#5418) 2025-06-25 21:19:17 +00:00
Patrick Honkonen
b94d59ba6b Upgrade KSP to 2.2.0-2.0.2 (#5422) 2025-06-25 19:45:28 +00:00
David Perez
4ff1a9ba94 Improve autofill version checking (#5421) 2025-06-25 17:01:18 +00:00
Patrick Honkonen
9c1673f603 [PM-22998] Fix isBuildVersionAtLeast check (#5420) 2025-06-25 17:00:25 +00:00
Patrick Honkonen
ddc099f727 [PM-19108] Add Privileged Apps List Screen (#5372) 2025-06-25 16:41:48 +00:00
André Bispo
fbfcfcd683 [PM-19309] Handle restrict item types policy (#5357) 2025-06-25 15:46:44 +00:00
Patrick Honkonen
1234898786 [PM-22998] Migrate isBuildVersionBelow to core module (#5417) 2025-06-25 13:55:19 +00:00
David Perez
182e6475c0 PM-22997: Update compatibility versions for Chrome and Brave (#5415) 2025-06-24 19:17:26 +00:00
David Perez
f27590a4d6 Do not allow Bitwarden to autofill itself (#5416) 2025-06-24 18:33:06 +00:00
Patrick Honkonen
807c76f8ec [PM-22831] Migrate IconData and BitwardenIcon to ui module (#5385) 2025-06-24 17:15:28 +00:00
David Perez
3877c4bd64 PM-22213: Update the order of items in the Send and Cipher overflows (#5407) 2025-06-24 14:45:39 +00:00
David Perez
8c88fd9d53 Add Brave integration toggle (#5411) 2025-06-24 14:45:17 +00:00
Patrick Honkonen
b92493611e [PM-22827] Move drawable resources to ui module and enable resource shrinking (#5388) 2025-06-24 14:26:03 +00:00
Nailik
9235f92206 [PM-22903] fix unit test execution (#5401) 2025-06-24 13:16:07 +00:00
David Perez
a3610c22dd Rename Chrome Autofill to Browser Autofill (#5409) 2025-06-23 21:10:52 +00:00
David Perez
1e4fc31ed4 Update Kotlin to v2.2.0 (#5408) 2025-06-23 20:57:10 +00:00
David Perez
ac1a9a2dc0 PM-22875: Done button on keyboard should submit pin or password from dialog (#5392) 2025-06-23 18:14:14 +00:00
David Perez
fe0e6bc67b Replace toObjectRoute with custom ParcelableRouteSerializer (#5393) 2025-06-23 18:13:22 +00:00
David Perez
419e5ca918 Update to latest Bitwarden SDK (#5403) 2025-06-23 16:51:18 +00:00
David Perez
be1a6e2097 Update Turbine to v1.2.1 (#5398) 2025-06-23 13:57:07 +00:00
David Perez
4fe989ce68 Add Room Gradle plugin (#5399) 2025-06-23 13:56:48 +00:00
Maciej Zieniuk
8be7410302 [PM-15087] Update the device push token every 7 days (#4386) 2025-06-20 21:06:41 +00:00
bw-ghapp[bot]
16225f0d68 Crowdin Pull - Password Manager (#5395)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-20 13:55:30 +00:00
bw-ghapp[bot]
08679a8973 Crowdin Pull - Authenticator (#5394)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-20 13:53:46 +00:00
David Perez
4d3e782b69 PM-22874: Fix Events service domain (#5391) 2025-06-20 13:52:33 +00:00
Patrick Honkonen
4d8fe722d1 [PM-22786] Migrate TooltipData to ui module (#5382) 2025-06-18 20:16:55 +00:00
David Perez
c5600c1d84 PM-22551: Update remove password copy (#5387) 2025-06-18 19:19:01 +00:00
David Perez
9816321d93 PM-22835: Update the passkey creation date format style (#5386) 2025-06-18 19:05:57 +00:00
Patrick Honkonen
56e8acf81f [PM-22786] Migrate PersistentListExtensions to core module (#5380) 2025-06-18 18:42:54 +00:00
Patrick Honkonen
08b07a0050 [PM-22778] Migrate BitwardenTextButton to ui module (#5378) 2025-06-18 17:47:43 +00:00
Patrick Honkonen
25d7c1e72c [PM-22786] Migrate BitwardenRowOfActions to ui module (#5381) 2025-06-18 16:17:18 +00:00
Patrick Honkonen
e311a4f618 [PM-19625] Move DataStateExtensionsTest to data module (#5377) 2025-06-18 16:03:59 +00:00
Patrick Honkonen
0eea6b07a3 [PM-22780] Migrate BitwardenHorizontalDivider to ui module (#5379) 2025-06-18 15:33:25 +00:00
Patrick Honkonen
c52e769327 [PM-21363] Migrate ZonedDateTime utils to core module (#5375) 2025-06-18 15:05:36 +00:00
David Perez
292a28d155 PM-22776: Update logic for determining base domains (#5374) 2025-06-18 15:05:24 +00:00
Patrick Honkonen
6c41c358ac [PM-22815] Migrate BitwardenContentBlock to ui module (#5383) 2025-06-18 15:05:21 +00:00
Patrick Honkonen
e7cf5a7efa [PM-22777] Migrate AnimateNullableContentVisibility to ui module (#5376) 2025-06-17 21:38:50 +00:00
Patrick Honkonen
f64364c1b8 [PM-19108] Update passkey prompt for unrecognized browser (#5371) 2025-06-17 01:03:07 +00:00
David Perez
d42b8ecd2d Update version constant names for consistency (#5369) 2025-06-16 18:26:09 +00:00
David Perez
a6f7b1e176 Update AndroidX AppCompat and Autofill libraries (#5368) 2025-06-16 17:16:38 +00:00
David Perez
d56b9fc0ff Update to Junit v5.13.1 (#5367) 2025-06-16 17:09:11 +00:00
Patrick Honkonen
f290ae411b [PM-22552] Update alg type in PasskeyAttestationOptions (#5363) 2025-06-16 16:47:40 +00:00
David Perez
508566f06f Update the Firebase BOM to 33.15.0 (#5366) 2025-06-16 15:52:14 +00:00
Patrick Honkonen
95f146fb3e [PM-21782] Improve create cipher error handling (#5362) 2025-06-16 14:23:30 +00:00
aj-rosado
469df4495a [PM-22568] Change totp seed field to a password field (#5350) 2025-06-13 16:32:12 +00:00
David Perez
053dfc1647 PM-22643: Do not clear error dialogs when updating TOTP data (#5361) 2025-06-13 16:25:39 +00:00
Patrick Honkonen
7de770ca03 [PM-22441] Refactor DigitalAssetLinkService to use source website (#5351) 2025-06-13 16:03:27 +00:00
David Perez
861a4281fa PM-22642, PM-22644: Add MP reprompt for TOTP code and secure note (#5359) 2025-06-13 16:01:58 +00:00
Patrick Honkonen
265014fd64 [PM-22665] Add BitwardenPackageManager abstraction (#5360) 2025-06-13 16:01:56 +00:00
bw-ghapp[bot]
5d32fe9caf Crowdin Pull - Password Manager (#5356)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-13 13:53:17 +00:00
bw-ghapp[bot]
44ba0f548a Crowdin Pull - Authenticator (#5355)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-13 13:52:33 +00:00
David Perez
694443f2e1 Password field tooltip support (#5354) 2025-06-12 21:56:05 +00:00
David Perez
0ade60025c PM-22634: Fix parsing of system language (#5353) 2025-06-12 20:47:12 +00:00
David Perez
5adccca823 PM-22477: Update the timestamp format for ciphers (#5352) 2025-06-12 18:49:20 +00:00
David Perez
3c86bb425b Add tests for the EditItemScreen and EditItemViewModel (#5348) 2025-06-12 14:30:00 +00:00
Patrick Honkonen
3474e0b608 [PM-22461] Add About privileged apps screen (#5335) 2025-06-12 14:19:19 +00:00
Patrick Honkonen
0f2476bebf Update BitwardenContentBlock divider padding logic (#5346) 2025-06-11 19:44:41 +00:00
Patrick Honkonen
edffb8dd6f Add tooltip to BitwardenTextRow (#5344) 2025-06-11 17:36:15 +00:00
David Perez
2dc6c170f5 PM-22551: Update toasts to snackbars for Sends (#5339) 2025-06-11 16:33:59 +00:00
David Perez
2a8a16ab3f BWA-160: Modernize QrCodeScanScreen (#5342) 2025-06-10 19:04:39 +00:00
Patrick Honkonen
76995a28ad [deps] Update googleProtoBufJava to 4.31.1 (#5343) 2025-06-10 19:02:55 +00:00
David Perez
7e146800a8 PM-22522: Update time picker language (#5338) 2025-06-10 19:02:31 +00:00
Patrick Honkonen
7a2f1c294f [deps] Update sonarqube plugin (#5307) 2025-06-10 18:22:15 +00:00
David Perez
44d4926300 Remove unused dialogs (#5337) 2025-06-10 15:49:15 +00:00
Álison Fernandes
e4c160d1e0 [PM-22437] Add product release notes to GitHub Releases (#5318) 2025-06-09 20:46:25 +00:00
Álison Fernandes
0f9f9d9dce [PM-22389] GitHub Release workflow supports releasing BWPM and BWA (#5312) 2025-06-09 19:40:34 +00:00
David Perez
a0c2600517 PM-10286: VerificationCodeScreen should not show MP reprompt if there is no master password (#5336) 2025-06-09 19:32:02 +00:00
Patrick Honkonen
c60df56648 [PM-21458] Add UserManagedPrivilegedApps feature flag (#5325) 2025-06-09 19:04:38 +00:00
David Perez
9cdfe0c5d6 PM-22502: Format dates and times correctly for locale (#5333) 2025-06-09 18:30:30 +00:00
renovate[bot]
d822be62e1 [deps]: Lock file maintenance (#5331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 16:17:25 +00:00
Matt Andreko
7adbfdcc84 Fix permissions for check-run action (#5316) 2025-06-09 13:34:28 +00:00
David Perez
beb4c533c8 Update the SnackbarRelayManager (#5317) 2025-06-06 18:28:08 +00:00
Patrick Honkonen
e1cd813445 [PM-19107] Introduce user-trusted privileged apps for Credential Manager (#4848) 2025-06-06 17:51:06 +00:00
David Perez
f769900976 PM-22456: Move Temporal Accessor Extensions to 'Core' module (#5324) 2025-06-06 17:29:02 +00:00
David Perez
a0ff94195f Update Junit to v5.13.0 (#5323) 2025-06-06 16:31:05 +00:00
bw-ghapp[bot]
9853f137d2 Crowdin Pull - Authenticator (#5319)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-06 14:07:19 +00:00
bw-ghapp[bot]
b591534bd9 Crowdin Pull - Password Manager (#5320)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-06 14:06:57 +00:00
David Perez
d2c329264c PM-22397: Remove custom deletion date (#5311) 2025-06-05 19:20:12 +00:00
David Perez
a9791c3f9f PM-22402: Update File Send error message (#5313) 2025-06-05 18:39:28 +00:00
David Perez
a59eaf5d40 PM-22362: AddSendScreen should include 'Required.' when describing the max file size (#5310) 2025-06-04 13:43:10 +00:00
David Perez
d2129cf507 PM-22357: Delete Send button should use a capital S (#5309) 2025-06-03 21:03:17 +00:00
Patrick Honkonen
903c260ad1 [PM-21891] Migrate filled and outlined button components to ui module (#5302) 2025-06-03 20:09:13 +00:00
Patrick Honkonen
09a8c01824 [deps] Update Google guava library (#5305) 2025-06-03 20:03:37 +00:00
David Perez
a1a4c217de PM-22346: Remove the period from the generic error title (#5308) 2025-06-03 18:40:45 +00:00
David Perez
7fbe3510b5 PM-22345: Flight recorder banner should not dismiss when navigating to settings (#5306) 2025-06-03 16:58:48 +00:00
Patrick Honkonen
0892e0ff1f [PM-21782] Pass encryptedFor to cipher functions (#5297) 2025-06-03 16:34:46 +00:00
Patrick Honkonen
0934d47159 [deps] Update protobuf (#5304) 2025-06-03 16:33:43 +00:00
David Perez
caf1c2eed5 PM-22265: Add Copy Notes button to ViewSendScreen (#5303) 2025-06-03 16:32:34 +00:00
David Perez
803d519c24 Update AGP to 8.10.1 (#5301) 2025-06-03 15:00:36 +00:00
David Perez
a3d2e51c8e PM-22310: Replace Ok with Okay (#5298) 2025-06-02 22:54:47 +00:00
David Perez
891def5e32 PM-22302: Remove unused string resources (#5296) 2025-06-02 19:56:16 +00:00
Álison Fernandes
00ded69a84 [QA-1126] Add placeholder workflow for device farm testing (#5292) 2025-06-02 12:39:16 +00:00
ifernandezdiaz
c5f597aedb [QA-1164] Adding missing testTags for View Send page (#5290) 2025-05-30 21:12:09 +00:00
David Perez
f43367ebfa PM-10286: View Master Password Prompt (#5280) 2025-05-30 21:04:59 +00:00
Patrick Honkonen
997769bb1c [PM-21475] Remove deprecated SSO details endpoint feature flag (#5286) 2025-05-30 16:08:04 +00:00
David Perez
65d1a4f12a BWA-159: Update the ManualCodeEntryScreen to allow scrolling (#5287) 2025-05-30 13:58:23 +00:00
bw-ghapp[bot]
f7c1278805 Crowdin Pull - Authenticator (#5288)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-05-30 13:32:48 +00:00
bw-ghapp[bot]
aa3602a5ce Crowdin Pull - Password Manager (#5289)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-05-30 13:30:29 +00:00
André Bispo
af18848159 [PM-20146] Remove native-create-account-flow feature flag (#5283) 2025-05-29 21:42:16 +00:00
David Perez
f3b7d0f732 PM-21631: Check for Search Screen when navigating after deleting a Send (#5284) 2025-05-29 21:29:56 +00:00
David Perez
e250a8dc1e BWA-158: Authenticator Edit Item should use a single LazyColum to allow for scrolling (#5285) 2025-05-29 21:12:15 +00:00
David Perez
ab2ac60957 PM-15229: Update logic for handling edge-to-edge (#5282) 2025-05-29 18:25:23 +00:00
Patrick Honkonen
b877487ce1 [PM-22169] Migrate app bar components to ui module (#5279) 2025-05-29 14:54:49 +00:00
David Perez
ef68879778 Clean up lint errors and suppressions (#5281) 2025-05-29 14:52:15 +00:00
André Bispo
a4e4d1488b [PM-21577] Fix delete button not showing bug (#5276) 2025-05-29 14:16:05 +00:00
David Perez
cf8578f3ef PM-21135: Fix view send field order (#5277) 2025-05-28 18:14:26 +00:00
David Perez
bdd0660e2b PM-21134: Fix send link title (#5275) 2025-05-28 15:27:28 +00:00
David Perez
294ef674bc PM-13040: Add known username field for the Disney Plus App (#5271) 2025-05-27 21:49:57 +00:00
David Perez
b9a897a9fc Update Firebase BOM to v33.14.0 (#5272) 2025-05-27 21:49:17 +00:00
Patrick Honkonen
6b12b9757f [PM-17686] Correct body text for "Replace existing certificate" dialog (#5270) 2025-05-27 21:28:12 +00:00
David Perez
61411ca73c Update Compose BOM to 2025.05.01 (#5269) 2025-05-27 16:16:50 +00:00
David Perez
3908827a14 PM-17660: Improve the way we remember the annotated string (#5257) 2025-05-27 15:21:19 +00:00
renovate[bot]
e553d7a015 [deps]: Lock file maintenance (#5267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-27 14:54:55 +00:00
renovate[bot]
3015b768c6 [deps]: Update org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0 (#5265)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-27 14:40:24 +00:00
David Perez
21b8ef92ba PM-21952: Move navigation package to UI module (#5260) 2025-05-27 13:48:07 +00:00
David Perez
0e3e6069fa Update Retrofit BOM to 3.0.0 (#5258) 2025-05-27 13:46:55 +00:00
David Perez
8d8dee5171 PM-21916: Move the FAB to the UI module (#5251) 2025-05-27 13:46:38 +00:00
Michał Chęciński
97b6bccd72 Add stub for publishing releases workflow (#5268) 2025-05-27 13:26:01 +00:00
Álison Fernandes
2a6813e4a2 [PM-21336] CI restructuring #1 - Consolidate Crowdin pull and push workflows (#5253) 2025-05-26 14:21:16 +00:00
aj-rosado
29e7899525 [PM-21537] Fix remove individual vault collection selection (#5262) 2025-05-23 19:41:00 +00:00
Amy Galles
4cd603006f [BRE-768] Creating stub workflow for testing (#5259) 2025-05-23 18:37:25 +00:00
bw-ghapp[bot]
1a091e198c Autosync Crowdin Translations (#5252)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-05-23 16:05:23 +00:00
David Perez
3551e75596 PM-16705: Improve the node validation logic (#5250) 2025-05-22 22:10:46 +00:00
André Bispo
6d976bea4c [PM-21577] Handle organization limitItemDeletion from sync response. (#5244) 2025-05-22 22:09:17 +00:00
Patrick Honkonen
d5c04123d9 [PM-21888] Migrate icon buttons to ui module (#5241) 2025-05-22 20:31:12 +00:00
David Perez
000a7d141e PM-17660: Add additional context for the sync feature (#5243) 2025-05-22 17:55:50 +00:00
Patrick Honkonen
c9d4d35f07 [PM-21851] Use rememberVectorPainter from platform UI in Authenticator (#5240) 2025-05-21 20:58:08 +00:00
Patrick Honkonen
4216f3f5a0 Rename all java source dirs to kotlin (#5239) 2025-05-21 19:14:53 +00:00
David Perez
c14545107d PM-21879: Move SpanStyleUtil and StringResExtensions to UI module (#5238) 2025-05-21 17:23:04 +00:00
Patrick Honkonen
d1a8cbf59f Bump authenticatorbridge to 1.0.1 (#5230) 2025-05-21 16:37:41 +00:00
Patrick Honkonen
1acc1a87a6 [PM-21851] Migrate RememberVectorPainter to ui module (#5233) 2025-05-21 16:17:44 +00:00
André Bispo
fd73360539 [PM-21405] Delete account error message (#5237) 2025-05-21 15:34:42 +00:00
Patrick Honkonen
178625222a Update target and compile SDK to 36 (#5229) 2025-05-21 15:01:59 +00:00
Patrick Honkonen
3ea17eb71c [PM-21849] Rename ui module source dir to kotlin (#5232) 2025-05-21 14:18:11 +00:00
Álison Fernandes
6ccb035ffd [PM-21825] Set missing workflow permissions (#5235) 2025-05-21 13:17:01 +00:00
Patrick Honkonen
5c3008d080 [PM-21385] Use flatMapLatest for accountSyncStateFlow (#5231) 2025-05-20 20:37:36 +00:00
Patrick Honkonen
54efc74907 [PM-21385] Defer feature flag check for Bitwarden account sync (#5222) 2025-05-20 18:09:15 +00:00
David Perez
34aed2ac65 Update authenticator compose tests to allow for easier use of local compositions (#5228) 2025-05-20 17:01:23 +00:00
Patrick Honkonen
3d152f5c36 Bump Kotlin to 2.1.21 (#5227) 2025-05-20 15:42:10 +00:00
Patrick Honkonen
4c8e5602dd [PM-21354] Migrate ColorExtensions and its tests to ui module (#5216) 2025-05-20 14:59:13 +00:00
Vince Grassia
6e44ee2eb0 [BRE-552] Fix Actionlint findings (#5223) 2025-05-20 14:02:52 +00:00
David Perez
4895f2a18a Update test fixtures to allow for easier customization (#5224) 2025-05-19 21:27:21 +00:00
David Perez
fc4f02c4d5 Rename AddSend UI to AddEditSend (#5221) 2025-05-19 21:26:31 +00:00
Patrick Honkonen
6719534494 [PM-21357] Migrate ModifierExtensions to ui module (#5215) 2025-05-19 20:02:52 +00:00
Patrick Honkonen
183584f678 [PM-21386] Fix typo in sync with Bitwarden message (#5220) 2025-05-19 17:26:09 +00:00
David Perez
046bb0fa39 PM-21080: Remove the isRemotelyConfigured flag (#5193) 2025-05-19 15:56:19 +00:00
David Perez
9508b4ba90 PM-21701: Remove segmented control from Add Send Screen and update the screen title (#5217) 2025-05-19 14:48:26 +00:00
Patrick Honkonen
07e4e6a806 [PM-21726] Move OmitFromCoverage to annotation module (#5214) 2025-05-19 13:19:16 +00:00
David Perez
4d142a6a5c PM-21133: Add View Send navigation (#5187) 2025-05-16 18:48:22 +00:00
Patrick Honkonen
f02a3a249b [PM-21703] Consolidate Robolectric and Compose test base classes (#5210) 2025-05-16 17:48:23 +00:00
David Perez
28149532a0 PM-21707: Allow nullable captcha token (#5213) 2025-05-16 16:58:12 +00:00
David Perez
7f5426dea0 PM-19770: Fix the verify email domains (#5212) 2025-05-16 15:49:13 +00:00
Patrick Honkonen
d7d703c977 [PM-21692] Move WindowSize and related util to ui module (#5208) 2025-05-16 15:38:45 +00:00
bw-ghapp[bot]
7fda5d799f Autosync Crowdin Translations (#5211)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-05-16 13:42:32 +00:00
Patrick Honkonen
3c1a0a352a [PM-21356] Migrate ui ListExtensions to ui module (#5200) 2025-05-15 21:45:50 +00:00
David Perez
cb0b135429 PM-21696: Make sure environment is up-to-date (#5209) 2025-05-15 21:44:44 +00:00
Patrick Honkonen
7422efd07a [PM-21366] Migrate BitwardenTheme to ui module (#5207) 2025-05-15 19:52:34 +00:00
Patrick Honkonen
27a9fc52b7 [PM-21657] Migrate Typography to the ui module (#5198) 2025-05-15 15:55:51 +00:00
Patrick Honkonen
c105c102a3 [PM-21359] Migrate StringExtensions to ui module (#5202) 2025-05-15 15:51:47 +00:00
Patrick Honkonen
c83bd8f4a8 [PM-21361] Delete unused ToastUtils (#5203) 2025-05-15 14:20:17 +00:00
Patrick Honkonen
d820b3345a [PM-21676] Relocate Authenticator local manager providers (#5206) 2025-05-15 14:09:39 +00:00
Patrick Honkonen
b71b01d48d [PM-21655] Migrate BitwardenShapes to ui module (#5197) 2025-05-15 13:47:05 +00:00
Patrick Honkonen
8a0f67c0e9 [PM-21361] Migrate TopAppBarScrollBehaviorExtensions to ui module (#5204) 2025-05-15 13:45:01 +00:00
Patrick Honkonen
b4d85e07ba [PM-21358] Migrate PaddingValuesExtensions.kt to ui module (#5201) 2025-05-15 13:40:09 +00:00
David Perez
dfd58822b7 PM-21445: Update the Send delete buttons (#5195) 2025-05-14 21:45:59 +00:00
Patrick Honkonen
f14a1404e3 [PM-21654] Migrate ColorScheme to ui module (#5196) 2025-05-14 21:11:28 +00:00
David Perez
f1950600a1 PM-21641: Allow delete and restore logic to be remotely configured (#5194) 2025-05-14 19:23:21 +00:00
Patrick Honkonen
7d6b6a5959 [PM-21575] Migrate AppTheme enum class to ui module (#5182) 2025-05-14 17:53:02 +00:00
David Perez
ea70191429 PM-21631: Update Edit Send Screen to navigate to Vault Unlocked root (#5190) 2025-05-14 17:51:56 +00:00
David Perez
db956b9b91 PM-21634: Update loading Dialog to be a real dialog (#5191) 2025-05-14 17:51:06 +00:00
David Perez
119812507a Remove logging from tests (#5192) 2025-05-14 17:50:55 +00:00
Patrick Honkonen
a97c962428 [DynamicColors] Update toggle button switch dynamic color scheme (#4886) 2025-05-14 14:17:33 +00:00
David Perez
456adf3158 PM-21610: Update SearchScreen and VaultItemListingScreen for better Sends support (#5188) 2025-05-14 13:57:29 +00:00
Patrick Honkonen
62cb962298 [DynamicColors] Add support for dynamic colors (#4850) 2025-05-13 21:59:44 +00:00
Patrick Honkonen
7f4e65d7e4 [PM-21567] Implement CredentialEntryBuilder interface (#5177) 2025-05-13 21:18:16 +00:00
David Perez
860a2e265f PM-21134, PM-21135, PM-21136, PM-21137: Create View Send Screen (#5178) 2025-05-13 18:47:43 +00:00
David Perez
6d68c3ae24 PM-21591: Add navigation routing for the ViewSendScreen (#5185) 2025-05-13 17:28:10 +00:00
David Perez
97b8c51ab3 PM-21598: Update multi-tonal illustrations and icons to support dynamic colors (#5186) 2025-05-13 17:01:47 +00:00
Patrick Honkonen
a2449a2f19 [PM-21574] Migrate CardStyle to the UI module (#5181) 2025-05-13 17:00:43 +00:00
Patrick Honkonen
1d73bbd440 [PM-21585] Display item folder location when only in a single folder (#5184) 2025-05-13 15:13:32 +00:00
Patrick Honkonen
da62244000 [PM-21573] Migrate EventsEffect to ui module (#5180) 2025-05-13 14:13:57 +00:00
Patrick Honkonen
11b767c98f [PM-21572] Migrate NoPersonalizedLearningInterceptor to ui module (#5179) 2025-05-12 21:48:50 +00:00
David Perez
cd4db467e3 Clean up lint warnings (#5176) 2025-05-12 17:52:56 +00:00
David Perez
7fdf165273 PM-21555: Fix crash on older server versions (#5174) 2025-05-12 16:21:55 +00:00
renovate[bot]
e49bab637c [deps]: Lock file maintenance (#5171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 13:37:51 +00:00
renovate[bot]
14ac194cb7 [deps]: Update com.google.devtools.ksp to v2.1.20-2.0.1 (#5170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 13:37:40 +00:00
aj-bw
578f96a944 BRE-609/android-pr-target-change (#5092) 2025-05-12 13:13:24 +00:00
David Perez
2c71ab7d27 PM-21445: Update Add Edit Sends UI (#5166) 2025-05-09 21:30:28 +00:00
David Perez
c5ee389231 PM-21351: Clear scemantics on new send button (#5165) 2025-05-09 20:51:14 +00:00
Patrick Honkonen
5037af07c7 [PM-21367] Support passkey requests with multiple options (#5161) 2025-05-09 18:23:59 +00:00
bw-ghapp[bot]
d6d1e8e97f Autosync Crowdin Translations (#5164)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-05-09 13:24:17 +00:00
David Perez
652168f946 PM-21397: Create initial View Send scaffold (#5163) 2025-05-08 22:55:07 +00:00
Patrick Honkonen
d4d5d2c2a8 [PM-21355] Migrate LifecycleEventEffect to ui module (#5162) 2025-05-08 22:06:59 +00:00
David Perez
ed148c2089 PM-21252: Create mock NavHostController for navigation testing (#5159) 2025-05-08 19:58:03 +00:00
Patrick Honkonen
564304616d [PM-21365] Migrate BitwardenColorScheme to ui module (#5158) 2025-05-08 19:48:51 +00:00
Patrick Honkonen
0d0b8d6780 [PM-21353] Migrate DensityExtensions to ui module (#5157) 2025-05-08 19:32:04 +00:00
David Perez
472e41f6bc PM-21351: Hide new send button from accessibility when on the empty sends screen (#5160) 2025-05-08 18:52:24 +00:00
Patrick Honkonen
fb9c68755a [PM-21328] Migrate BaseViewModelTest and MainDispatcherExtension to test fixtures (#5146) 2025-05-08 16:54:55 +00:00
David Perez
d7671f47ea PM-21348: Type-safe navigation for authenticator (#5156) 2025-05-08 16:31:10 +00:00
Patrick Honkonen
9c7270df69 [PM-21344] Migrate BackgroundEvent to ui module (#5155) 2025-05-08 16:08:07 +00:00
David Perez
733290569c Update Lifecycle library to v2.9.0 (#5150) 2025-05-08 15:21:34 +00:00
David Perez
cbaa8a329e Fix duplicated launched effect key (#5154) 2025-05-08 14:27:22 +00:00
David Perez
f968d7698a PM-21332: Move NavGraphBuilder extensions to common UI module (#5147) 2025-05-08 14:15:42 +00:00
Patrick Honkonen
68cd08b069 [PM-21325] Migrate BaseViewModel to ui module (#5145) 2025-05-08 14:06:16 +00:00
David Perez
84683894a6 Update AGP to 8.10.0 (#5152) 2025-05-08 13:44:09 +00:00
David Perez
ed2d9ecb80 Update Dagger Hilt to v2.56.2 (#5151) 2025-05-08 13:43:05 +00:00
David Perez
8f4d46954e Update the navigation component to v2.9.0 (#5149) 2025-05-08 13:40:13 +00:00
David Perez
a9fc6ff589 Update compose BOM to 2025.05.00 (#5148) 2025-05-08 13:39:42 +00:00
André Bispo
5c8f5670e4 [PM-21203] Old user migration login error. (#5136) 2025-05-07 21:25:32 +00:00
David Perez
eec88d4924 PM-21324: Move common UI transitions to UI module (#5144) 2025-05-07 21:14:47 +00:00
Patrick Honkonen
82da193e55 [PM-21199] Rename FIDO2 objects to reference CredentialManager (#5128) 2025-05-07 20:25:38 +00:00
David Perez
76fb85ac1f Update Compose BOM to 2025.04.01 (#5134) 2025-05-07 19:16:32 +00:00
Patrick Honkonen
625ac0ea5f Update mockk to version 1.14.2 (#5139) 2025-05-07 18:14:01 +00:00
David Perez
4e88833737 Clean up how we handle test coverage on navigation files (#5142) 2025-05-07 17:53:07 +00:00
David Perez
ecea2ef7c1 PM-21285: Ensure route data is serializable (#5141) 2025-05-07 16:53:14 +00:00
Patrick Honkonen
0eccc7197e Update Protocol Buffers library version (#5140) 2025-05-07 15:36:06 +00:00
David Perez
5dd34afe81 PM-19771: Allow forward slashes in emails (#5137) 2025-05-06 22:12:48 +00:00
Patrick Honkonen
5abcc5b1f7 [PM-17222] Enhance autofill accessibility processor (#5116) 2025-05-06 21:44:06 +00:00
David Perez
6fec95cb84 PM-21255: Implement type-safe navigation (#5131) 2025-05-06 20:46:53 +00:00
David Perez
1d68c1fdf6 Update Firebase BOM to v33.13.0 (#5135) 2025-05-06 20:22:23 +00:00
David Perez
0c2de427dc Update WorkManager to 2.10.1 (#5132) 2025-05-06 20:13:28 +00:00
David Perez
f932682949 PM-21110: Add a generate crash button to the debug menu (#5125) 2025-05-06 18:51:03 +00:00
David Perez
e1f432ea5d Update the Navigation component library (#5130) 2025-05-06 14:47:34 +00:00
Patrick Honkonen
31de7fc331 Remove unused FeatureFlagsConfiguration (#5129) 2025-05-06 00:56:18 +00:00
David Perez
07469672ba PM-21156: Fix ConfigService retrofit instance (#5126) 2025-05-05 14:51:47 +00:00
André Bispo
1a2beea770 [PM-18092] Update cipher delete restore permissions (#5075)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-05-05 13:56:58 +00:00
aj-rosado
639ca02739 [PM-14222] Managed user account deletion prevention (#5114)
Co-authored-by: Matt Portune <mportune@macbook-work.lan>
2025-05-02 20:22:58 +00:00
Patrick Honkonen
186bea2d1d [PM-20127] Prevent double UV prompt during FIDO 2 operations (#5124) 2025-05-02 14:52:14 +00:00
bw-ghapp[bot]
3dc187da87 Autosync Crowdin Translations (#5122)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-05-02 13:14:01 +00:00
aj-rosado
69708c1285 [PM-20037] Remove native-carousel-flow feature flag (#5121) 2025-05-01 21:48:25 +00:00
Patrick Honkonen
ad1566f4b0 [PM-14846] Improve IP Address and Port Handling in StringExtensions (#5118) 2025-05-01 20:39:01 +00:00
David Perez
32d0ca7bcd PM-21088: Remove the unused IgnoreEnvironmentCheck feature flag (#5119) 2025-05-01 16:49:58 +00:00
André Bispo
0353f0c153 [PM-20466] Invalid master password returns generic error. (#5100) 2025-05-01 14:37:59 +00:00
Patrick Honkonen
7436122953 [PM-19846] Mark network module implementation details internal (#5115) 2025-05-01 14:26:19 +00:00
Patrick Honkonen
23ef5b38fe [PM-20508] Centralize passkey credential entry creation (#5033) 2025-04-30 15:31:27 +00:00
Patrick Honkonen
fe1fe770c7 Use Google's Digital Asset Links API to verify digital asset links (#5101) 2025-04-30 13:39:04 +00:00
David Perez
240bca3c2f PM-20552: Ensure userState does not emit while the active user is unlocking (#5112) 2025-04-29 20:56:31 +00:00
David Perez
8c7cc27c5d PM-20966: Log app state changes (#5110) 2025-04-29 20:56:15 +00:00
Patrick Honkonen
a4aa9837a6 [PM-17686] Allow overwriting TLS certificates (#5111) 2025-04-28 21:44:41 +00:00
Patrick Honkonen
b901de9ddf Correct indention in app/strings.xml (#5109) 2025-04-28 18:24:33 +00:00
Patrick Honkonen
96df23f0af Drop all tables when performing destructive migration in Authenticator (#5108) 2025-04-28 15:28:59 +00:00
renovate[bot]
0eb149941d [deps]: Update gh minor (#5102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 14:23:36 +00:00
renovate[bot]
6f44e64375 [deps]: Update kotlin (#5103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 14:22:46 +00:00
renovate[bot]
cda86b842e [deps]: Lock file maintenance (#5106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 13:32:05 +00:00
renovate[bot]
e1608b426d [deps]: Update sonarsource/sonarqube-scan-action action to v5 (#5105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 13:28:19 +00:00
renovate[bot]
b6017baf54 [deps]: Update actions/create-github-app-token action to v2 (#5104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 13:21:09 +00:00
Patrick Honkonen
0f6d15d6a6 [PM-20549] Introduce BitwardenServiceClient (#5091) 2025-04-25 20:26:01 +00:00
André Bispo
cd11164544 [PM-18942] Force sync for revoke/restore notification. (#5098) 2025-04-25 16:14:17 +00:00
David Perez
1b9d2bfab4 Omit navigation files from test coverage (#5095) 2025-04-25 15:46:54 +00:00
bw-ghapp[bot]
373b789fbb Autosync Crowdin Translations (#5096)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-04-25 14:08:02 +00:00
André Bispo
985e576a82 [PM-20148] Remove app-review-prompt feature flag. (#5093) 2025-04-24 20:18:50 +00:00
David Perez
b11e4481f9 PM-20365: Add pre-auth settings support (#5094) 2025-04-24 17:48:03 +00:00
David Perez
5ac0f2b111 Update to AGP 8.9.2 (#5089) 2025-04-23 19:54:09 +00:00
David Perez
37a0d19efc PM-20400: Display snackbar confirming log deletion (#5088) 2025-04-23 19:53:44 +00:00
David Perez
54983bc92e Add helper for concurrent map (#5086) 2025-04-23 19:53:05 +00:00
David Perez
36989875a6 Update to Junit 5.12.2 (#5087) 2025-04-23 19:52:48 +00:00
David Perez
88b0fe59bb PM-20516: Update NetworkConnectionManager (#5085) 2025-04-23 19:52:29 +00:00
David Perez
e4d0c48eed PM-20510: Log whenever the screen changes (#5083) 2025-04-23 19:47:06 +00:00
Patrick Honkonen
bd364a1108 Update Room dependency to version 2.7.1 (#5090) 2025-04-23 19:10:19 +00:00
Patrick Honkonen
39b88d6064 [PM-20389] Define and implement network module CertificateProvider (#5073) 2025-04-22 19:14:06 +00:00
David Perez
da709e039b PM-19809: Update flight recorder tooltip url (#5082) 2025-04-22 19:12:09 +00:00
David Perez
31311964d0 Cleanup minor lint warnings (#5081) 2025-04-21 21:35:05 +00:00
David Perez
8cbd7369c5 PM-19594: Add flight recorder banner (#5079) 2025-04-21 14:46:53 +00:00
David Perez
2a1669cf87 PM-20426: Update Block Autofill UI (#5078) 2025-04-21 14:04:05 +00:00
David Perez
4c4007a734 PM-20385: Delete confirmation dialog should dismiss on confirmation (#5077) 2025-04-21 14:03:45 +00:00
David Perez
70dc82d1b6 PM-20422: Update tab navigation (#5076) 2025-04-21 14:03:20 +00:00
bw-ghapp[bot]
021ece138b Autosync Crowdin Translations (#5074)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-04-18 14:30:44 +00:00
2265 changed files with 104302 additions and 105219 deletions

View File

@@ -0,0 +1,133 @@
# Get Release Notes from Jira script
Fetches release notes from Jira issues.
## Prerequisites
- Python dev environment - use [uv](https://github.com/astral-sh/uv)
- Jira API token. Generate one at: https://id.atlassian.com/manage-profile/security/api-tokens
- Install dependencies:
```bash
uv pip install -r pyproject.toml
```
## Usage
```bash
./jira_release_notes.py RELEASE-1762 example@example.com T0k3n123
```
# Output Format
The script retrieves the content from a custom field and handles two types of Jira release notes formats:
1. Bullet Points:
```
• Point 1
• Point 2
• Point 3
```
2. Single Line:
```
Single line of release notes text
```
## Jira JSON format example
### Single line
```json
...
"customfield_10335": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Single line release notes"
}
]
}
]
},
...
```
### Bullet points
```json
...
"customfield_10335": {
"type": "doc",
"version": 1,
"content": [
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 1"
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 2"
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 3"
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 4"
}
]
}
]
}
]
}
]
},
...
```

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import sys
import base64
import json
import requests
def extract_text_from_content(content):
if isinstance(content, list):
texts = [extract_text_from_content(item) for item in content]
return '\n'.join(text for text in texts if text.strip())
if isinstance(content, dict):
if content.get('type') == 'text':
return content.get('text', '')
elif content.get('type') == 'paragraph':
return extract_text_from_content(content.get('content', []))
elif content.get('type') == 'bulletList':
return extract_text_from_content(content.get('content', []))
elif content.get('type') == 'listItem':
item_text = extract_text_from_content(content.get('content', []))
return f"* {item_text.strip()}"
return ''
def parse_release_notes(response_json):
try:
fields = response_json.get('fields', {})
release_notes_field = fields.get('customfield_10335', {})
if not release_notes_field or not release_notes_field.get('content'):
return ''
release_notes = extract_text_from_content(release_notes_field.get('content', []))
return release_notes
except Exception as e:
print(f"Error parsing release notes: {str(e)}", file=sys.stderr)
return ''
def main():
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <issue_id> <jira_email> <jira_api_token>")
sys.exit(1)
jira_issue_id = sys.argv[1]
jira_email = sys.argv[2]
jira_api_token = sys.argv[3]
jira_base_url = "https://bitwarden.atlassian.net"
auth = base64.b64encode(f"{jira_email}:{jira_api_token}".encode()).decode()
headers = {
"Authorization": f"Basic {auth}",
"Content-Type": "application/json"
}
response = requests.get(
f"{jira_base_url}/rest/api/3/issue/{jira_issue_id}",
headers=headers
)
if response.status_code != 200:
print(f"Error fetching Jira issue: {response.status_code}", file=sys.stderr)
sys.exit(1)
release_notes = parse_release_notes(response.json())
print(release_notes)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,9 @@
[project]
name = "jira-get-release-notes"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"requests>=2.32.3",
]

91
.github/scripts/jira-get-release-notes/uv.lock generated vendored Normal file
View File

@@ -0,0 +1,91 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "jira-get-release-notes"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "requests" },
]
[package.metadata]
requires-dist = [{ name = "requests", specifier = ">=2.32.3" }]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]

View File

@@ -29,20 +29,33 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
permissions:
contents: read
packages: read
jobs:
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
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
@@ -52,7 +65,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
@@ -61,13 +74,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
@@ -98,7 +111,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
@@ -109,7 +122,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -162,10 +175,10 @@ jobs:
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
@@ -175,7 +188,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
@@ -184,7 +197,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -224,7 +237,7 @@ jobs:
- name: Upload release Play Store .aab artifact
if: ${{ matrix.variant == 'aab' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.bitwarden.authenticator.aab
path: authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab
@@ -232,7 +245,7 @@ jobs:
- name: Upload release .apk artifact
if: ${{ matrix.variant == 'apk' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.bitwarden.authenticator.apk
path: authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk
@@ -252,7 +265,7 @@ jobs:
- name: Upload .apk SHA file for release
if: ${{ matrix.variant == 'apk' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: authenticator-android-apk-sha256.txt
path: ./authenticator-android-apk-sha256.txt
@@ -260,7 +273,7 @@ jobs:
- name: Upload .aab SHA file for release
if: ${{ matrix.variant == 'aab' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: authenticator-android-aab-sha256.txt
path: ./authenticator-android-aab-sha256.txt

View File

@@ -30,20 +30,33 @@ env:
JAVA_VERSION: 17
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
permissions:
contents: read
packages: read
jobs:
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
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
@@ -53,7 +66,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
@@ -62,13 +75,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
@@ -85,7 +98,7 @@ jobs:
run: bundle exec fastlane assembleDebugApks
- name: Upload test reports on failure
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: failure()
with:
name: test-reports
@@ -106,7 +119,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
@@ -117,7 +130,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -157,10 +170,10 @@ jobs:
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
@@ -170,7 +183,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
@@ -179,7 +192,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -195,7 +208,7 @@ jobs:
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
bundle exec fastlane setBuildVersionInfo \
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
versionName:${{ inputs.version-name }}
@@ -253,7 +266,7 @@ jobs:
- name: Upload release Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
@@ -261,7 +274,7 @@ jobs:
- name: Upload beta Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
@@ -269,7 +282,7 @@ jobs:
- name: Upload release .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
@@ -277,7 +290,7 @@ jobs:
- name: Upload beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
@@ -286,7 +299,7 @@ jobs:
# When building variants other than 'prod'
- name: Upload debug .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
@@ -324,7 +337,7 @@ jobs:
- name: Upload .apk SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.apk-sha256.txt
path: ./com.x8bit.bitwarden.apk-sha256.txt
@@ -332,7 +345,7 @@ jobs:
- name: Upload .apk SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.beta.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
@@ -340,7 +353,7 @@ jobs:
- name: Upload .aab SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.aab-sha256.txt
path: ./com.x8bit.bitwarden.aab-sha256.txt
@@ -348,7 +361,7 @@ jobs:
- name: Upload .aab SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.beta.aab-sha256.txt
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
@@ -356,7 +369,7 @@ jobs:
- name: Upload .apk SHA file for debug
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
@@ -405,7 +418,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
@@ -416,7 +429,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -442,10 +455,10 @@ jobs:
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
@@ -455,7 +468,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
@@ -464,7 +477,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -481,7 +494,7 @@ jobs:
# Start from 11000 to prevent collisions with mobile build version codes
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
@@ -515,7 +528,7 @@ jobs:
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
@@ -527,14 +540,14 @@ jobs:
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
- name: Upload F-Droid SHA file
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.beta-fdroid.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
@@ -546,7 +559,7 @@ jobs:
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
- name: Upload F-Droid Beta SHA file
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt

View File

@@ -1,56 +0,0 @@
name: Crowdin Sync - Authenticator
on:
workflow_dispatch:
inputs: {}
schedule:
- cron: '0 0 * * 5'
jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-24.04
env:
_CROWDIN_PROJECT_ID: "673718"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Log in to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Generate GH App token
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Download translations
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
with:
config: crowdin-bwa.yml
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "bitwarden-devops-bot"
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@@ -1,23 +1,35 @@
name: Crowdin Sync
name: Cron / Crowdin Pull
run-name: Crowdin Pull - ${{ github.event_name == 'workflow_dispatch' && 'Manual' || 'Scheduled' }}
on:
workflow_dispatch:
inputs: {}
schedule:
- cron: '0 0 * * 5'
jobs:
crowdin-sync:
name: Autosync
name: Crowdin Pull - ${{ matrix.name }} - ${{ github.event_name }}
runs-on: ubuntu-24.04
env:
_CROWDIN_PROJECT_ID: "269690"
permissions:
contents: write
pull-requests: write
strategy:
matrix:
include:
- name: Password Manager
project_id: 269690
config: crowdin-bwpm.yml
branch: crowdin-pull-bwpm
- name: Authenticator
project_id: 673718
config: crowdin-bwa.yml
branch: crowdin-pull-bwa
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -29,28 +41,29 @@ jobs:
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Generate GH App token
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Download translations
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
- name: Download translations for ${{ matrix.name }}
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
_CROWDIN_PROJECT_ID: ${{ matrix.project_id }}
with:
config: crowdin.yml
config: ${{ matrix.config }}
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "bitwarden-devops-bot"
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
commit_message: "Crowdin Pull - ${{ matrix.name }}"
localization_branch_name: ${{ matrix.branch }}
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"
pull_request_title: "Crowdin Pull - ${{ matrix.name }}"
pull_request_body: ":inbox_tray: New translations for ${{ matrix.name }} received!"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@@ -1,30 +0,0 @@
name: Crowdin Push - Authenticator
on:
workflow_dispatch:
push:
branches:
- "main"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
jobs:
crowdin-push:
name: Crowdin Push
runs-on: ubuntu-24.04
env:
_CROWDIN_PROJECT_ID: "673718"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Upload sources
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
with:
config: crowdin-bwa.yml
upload_sources: true
upload_translations: false

View File

@@ -1,23 +1,24 @@
name: Crowdin Push
name: CI / Crowdin Push
run-name: Crowdin Push - ${{ github.event_name == 'workflow_dispatch' && 'Manual' || 'CI' }}
on:
workflow_dispatch:
push:
branches:
- "main"
- main
jobs:
crowdin-push:
name: Crowdin Push
name: Crowdin Push - ${{ github.event_name }}
runs-on: ubuntu-24.04
env:
_CROWDIN_PROJECT_ID: "269690"
permissions:
contents: read
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -28,12 +29,24 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
- name: Upload sources for Password Manager
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
_CROWDIN_PROJECT_ID: "269690"
with:
config: crowdin.yml
config: crowdin-bwpm.yml
upload_sources: true
upload_translations: false
- name: Upload sources for Authenticator
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
_CROWDIN_PROJECT_ID: "673718"
with:
config: crowdin-bwa.yml
upload_sources: true
upload_translations: false

View File

@@ -3,45 +3,24 @@ name: Create GitHub Release
on:
workflow_dispatch:
inputs:
version-name:
description: 'Version Name - E.g. "2024.11.1"'
required: true
type: string
version-number:
description: 'Version Number - E.g. "123456"'
required: true
type: string
artifact-run-id:
description: 'GitHub Action Run ID containing artifacts'
required: true
type: string
draft:
description: 'Create as draft release'
type: boolean
default: true
prerelease:
description: 'Mark as pre-release'
type: boolean
default: true
make-latest:
description: 'Set as the latest release'
type: boolean
branch-protection-type:
description: 'Branch protection type'
type: choice
options:
- Branch Name
- GitHub API
default: Branch Name
release-ticket-id:
description: 'Release Ticket ID - e.g. RELEASE-1762'
required: true
type: string
env:
ARTIFACTS_PATH: artifacts
jobs:
create-release:
name: Create GitHub Release
runs-on: ubuntu-24.04
permissions:
contents: write
actions: read
steps:
- name: Check out repository
@@ -54,31 +33,74 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
run: |
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
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)
case "$BRANCH_PROTECTION_TYPE" in
"Branch Name")
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
# branch protection check
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
case "$workflow_name" in
*"Password Manager"* | "Build")
echo "app_name=Password Manager" >> $GITHUB_OUTPUT
echo "app_name_suffix=bwpm" >> $GITHUB_OUTPUT
;;
"GitHub API")
#NOTE requires token with "administration:read" scope
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
exit 1
fi
*"Authenticator"*)
echo "app_name=Authenticator" >> $GITHUB_OUTPUT
echo "app_name_suffix=bwa" >> $GITHUB_OUTPUT
;;
*)
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
echo "::error::Unknown workflow name: $workflow_name"
exit 1
;;
esac
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
- name: Get version info from run logs and set release tag name
id: get_release_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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)
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
version_name_with_trailing_dot=$(grep -m 1 "Setting version name to" <<< "$workflow_log" | sed 's/.*Setting version name to //')
version_name=${version_name_with_trailing_dot%.} # remove trailing dot
if [[ -z "$version_name" ]]; then
echo "::warning::Version name not found. Using default value - 0.0.0"
version_name="0.0.0"
else
echo "✅ Found version name: $version_name"
fi
if [[ -z "$version_number" ]]; then
echo "::warning::Version number not found. Using default value - 0"
version_number="0"
else
echo "✅ Found version number: $version_number"
fi
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
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
- name: Download artifacts
env:
@@ -93,37 +115,106 @@ jobs:
find $ARTIFACTS_PATH -type f
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
tag_name: "v${{ inputs.version-name }}"
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
prerelease: ${{ inputs.prerelease }}
draft: ${{ inputs.draft }}
make_latest: ${{ inputs.make-latest }}
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
generate_release_notes: true
files: |
artifacts/**/*
- name: Update Release Description
- name: Get product release notes
id: get_release_notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.create_release.outputs.id }}
RELEASE_URL: ${{ steps.create_release.outputs.url }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_RELEASE_TICKET_ID: ${{ inputs.release-ticket-id }}
_JIRA_API_EMAIL: ${{ secrets.JIRA_API_EMAIL }}
_JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
run: |
# Get current release body
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
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)
# Append build source to the end
updated_body="${current_body}
if [[ -z "$product_release_notes" || $product_release_notes == "Error checking"* ]]; then
echo "::warning::Failed to fetch release notes from Jira. Output: $product_release_notes"
product_release_notes="<insert product release notes here>"
else
echo "✅ Product release notes:"
echo "$product_release_notes"
fi
echo "$product_release_notes" > product_release_notes.txt
- name: Create Release
id: create_release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
_APP_NAME: ${{ steps.get_release_branch.outputs.app_name }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_VERSION_NUMBER: ${{ steps.get_release_info.outputs.version_number }}
_TARGET_COMMIT: ${{ steps.get_release_branch.outputs.release_branch }}
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
_LAST_RELEASE_TAG: ${{ steps.get_release_info.outputs.last_release_tag }}
run: |
echo "⌛️ Creating release for $_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER) on $_TARGET_COMMIT"
release_url=$(gh release create "$_TAG_NAME" \
--title "$_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER)" \
--target "$_TARGET_COMMIT" \
--generate-notes \
--notes-start-tag "$_LAST_RELEASE_TAG" \
--draft \
$ARTIFACTS_PATH/*/*)
echo "✅ Release created: $release_url"
# Get release info for outputs
release_data=$(gh release view "$_TAG_NAME" --json id)
release_id=$(echo "$release_data" | jq -r .id)
echo "id=$release_id" >> $GITHUB_OUTPUT
echo "url=$release_url" >> $GITHUB_OUTPUT
- name: Update Release Description
id: update_release_description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
run: |
echo "Getting current release body. Tag: $_TAG_NAME"
current_body=$(gh release view "$_TAG_NAME" --json body --jq .body)
product_release_notes=$(cat product_release_notes.txt)
# Update release description with product release notes and builds source
updated_body="# Overview
${product_release_notes}
${current_body}
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
# Update release
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
-f body="$updated_body"
new_release_url=$(gh release edit "$_TAG_NAME" --notes "$updated_body")
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY
# draft release links change after editing
echo "release_url=$new_release_url" >> $GITHUB_OUTPUT
- name: Add Release Summary
env:
_RELEASE_TAG: ${{ steps.get_release_info.outputs.tag_name }}
_LAST_RELEASE_TAG: ${{ steps.get_release_info.outputs.last_release_tag }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_VERSION_NUMBER: ${{ steps.get_release_info.outputs.version_number }}
_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
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
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

View File

@@ -0,0 +1,14 @@
name: Publish GitHub Release as newest
on:
workflow_dispatch:
permissions: {}
jobs:
stub:
runs-on: ubuntu-24.04
name: Stub
steps:
- name: Stub
run: echo "This is a stub job to trigger the workflow."

16
.github/workflows/publish-store.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Publish
on:
workflow_dispatch:
permissions: {}
jobs:
publish:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: TEST STEP
run: exit 0

View File

@@ -21,7 +21,7 @@ jobs:
fetch-depth: 0
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
@@ -34,7 +34,7 @@ jobs:
--output-path .
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
with:
sarif_file: cx_result.sarif
@@ -51,7 +51,7 @@ jobs:
fetch-depth: 0
- name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:

View File

@@ -2,13 +2,23 @@ name: Scan Pull Requests
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target:
types: [opened, synchronize]
types: [opened, synchronize, reopened]
branches:
- main
permissions: {}
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
permissions:
contents: read
sast:
name: SAST scan
@@ -26,7 +36,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -41,7 +51,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
with:
sarif_file: cx_result.sarif
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
@@ -63,7 +73,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:

16
.github/workflows/test-device.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Test Device
on:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
name: Test Device
runs-on: ubuntu-24.04
steps:
- name: Placeholder step
run: echo "Placeholder workflow step"

View File

@@ -9,7 +9,7 @@ on:
pull_request:
types: [opened, synchronize]
merge_group:
type: [checks_requested]
types: [checks_requested]
workflow_dispatch:
env:
@@ -30,10 +30,10 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/caches
@@ -43,7 +43,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
${{ github.workspace }}/build-cache
@@ -52,12 +52,12 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env._JAVA_VERSION }}
@@ -75,7 +75,7 @@ jobs:
bundle exec fastlane check
- name: Upload test reports
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: test-reports
@@ -91,7 +91,7 @@ jobs:
- name: Upload to codecov.io
id: upload-to-codecov
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
if: github.event_name == 'push' || github.event_name == 'pull_request'
continue-on-error: true
with:
@@ -110,7 +110,7 @@ jobs:
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
if [ ! -z "$PR_NUMBER" ]; then
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"
fi

View File

@@ -9,26 +9,26 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1084.0)
aws-sdk-core (3.222.1)
aws-eventstream (1.4.0)
aws-partitions (1.1125.0)
aws-sdk-core (3.226.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.106.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.183.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-s3 (1.192.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
base64 (0.3.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@@ -58,10 +58,10 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@@ -71,7 +71,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.227.1)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -113,7 +113,7 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-firebase_app_distribution (0.10.0)
fastlane-plugin-firebase_app_distribution (0.10.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
@@ -165,8 +165,8 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.10.2)
jwt (2.10.1)
json (2.12.2)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
@@ -175,13 +175,13 @@ GEM
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (6.0.1)
rake (13.2.1)
public_suffix (6.0.2)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -192,7 +192,7 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.19.0)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)

View File

@@ -4,6 +4,7 @@
- [Compatibility](#compatibility)
- [Setup](#setup)
- [Theme](#theme)
- [Dependencies](#dependencies)
## Compatibility
@@ -15,7 +16,6 @@
## Setup
1. Clone the repository:
```sh
@@ -52,6 +52,35 @@
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`:
- 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 your preferred `Vendor`.
- Hit `Download`.
- Hit `Apply`.
## Theme
### Icons & Illustrations
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.
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.
The supported tags are as follows:
* outline
* primary
* secondary
* tertiary
* accent
* logo
* navigation
* navigationActiveAccent
## Dependencies
### Application Dependencies

1
annotation/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,42 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.bitwarden.annotation"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdkBwa.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
compileOptions {
sourceCompatibility(libs.versions.jvmTarget.get())
targetCompatibility(libs.versions.jvmTarget.get())
}
@Suppress("UnstableApiUsage")
testFixtures {
enable = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
}
}

View File

@@ -1,4 +1,4 @@
package com.bitwarden.core.annotation
package com.bitwarden.annotation
/**
* Used to omit the annotated class from test coverage reporting. This should be used sparingly and

View File

@@ -10,6 +10,7 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.androidx.room)
// Crashlytics is enabled for all builds initially but removed for FDroid builds in gradle and
// standardDebug builds in the merged manifest.
alias(libs.plugins.crashlytics)
@@ -46,6 +47,10 @@ android {
namespace = "com.x8bit.bitwarden"
compileSdk = libs.versions.compileSdk.get().toInt()
room {
schemaDirectory("$projectDir/schemas")
}
defaultConfig {
applicationId = "com.x8bit.bitwarden"
minSdk = libs.versions.minSdk.get().toInt()
@@ -55,11 +60,6 @@ android {
setProperty("archivesBaseName", "com.x8bit.bitwarden")
ksp {
// The location in which the generated Room Database Schemas will be stored in the repo.
arg("room.schemaLocation", "$projectDir/schemas")
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField(
@@ -99,6 +99,7 @@ android {
applicationIdSuffix = ".beta"
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
matchingFallbacks += listOf("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -111,6 +112,7 @@ android {
release {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
@@ -193,7 +195,7 @@ android {
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
}
}
@@ -211,8 +213,9 @@ dependencies {
add("standardImplementation", dependencyNotation)
}
implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
implementation(files("libs/authenticatorbridge-1.0.1-release.aar"))
implementation(project(":annotation"))
implementation(project(":core"))
implementation(project(":data"))
implementation(project(":network"))
@@ -275,6 +278,7 @@ dependencies {
// Pull in test fixtures from other modules
testImplementation(testFixtures(project(":data")))
testImplementation(testFixtures(project(":network")))
testImplementation(testFixtures(project(":ui")))
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.google.hilt.android.testing)
@@ -294,8 +298,7 @@ tasks {
useJUnitPlatform()
maxHeapSize = "2g"
maxParallelForks = Runtime.getRuntime().availableProcessors()
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
android.sourceSets["main"].res.srcDirs("src/test/res")
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
}
}

Binary file not shown.

View File

@@ -0,0 +1,38 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ce40856ec88770d11b7afb587c7deabc",
"entities": [
{
"tableName": "privileged_apps",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` TEXT NOT NULL, PRIMARY KEY(`package_name`, `signature`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"signature"
]
}
}
],
"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, 'ce40856ec88770d11b7afb587c7deabc')"
]
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:ignore="MissingApplicationIcon">
<activity
android:name=".MainActivity">
<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:scheme="https" />
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -7,6 +7,20 @@
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<activity
android:name=".MainActivity"
tools:ignore="IntentFilterExportedReceiver">
<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:scheme="https" />
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.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"/>
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
Note that each build type uses a different value for knownCerts.
@@ -37,15 +37,18 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:intentMatchingFlags="enforceIntentFilter"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/LaunchTheme"
tools:ignore="CredentialDependency"
tools:replace="appComponentFactory"
tools:targetApi="33">
tools:targetApi="36">
<activity
android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:launchMode="@integer/launchModeAPIlevel"
android:theme="@style/LaunchTheme"
@@ -76,16 +79,14 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="vault.bitwarden.com" />
<data android:host="vault.bitwarden.eu" />
<data android:host="*.bitwarden.pw" />
<data android:host="*.bitwarden.com" />
<data android:host="*.bitwarden.eu" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />
<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_UNLOCK_ACCOUNT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
@@ -310,6 +311,14 @@
android:exported="true"
android:permission="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE" />
<!-- Firebase SDK initOrder is 100. We use a higher order to initialize first -->
<provider
android:name=".data.platform.contentprovider.UncaughtErrorLoggingContentProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="false"
android:initOrder="101" />
</application>
<queries>
@@ -320,11 +329,19 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<!-- To Query Privileged Apps -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<!-- To Query Chrome Beta: -->
<package android:name="com.chrome.beta" />
<!-- To Query Chrome Stable: -->
<package android:name="com.android.chrome" />
<!-- To Query Brave Stable: -->
<package android:name="com.brave.browser" />
</queries>
</manifest>

View File

@@ -779,6 +779,42 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "cz.seznam.sbrowser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.opera.mini.native",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.opera.mini.native.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
}
]
}
}
]
}

View File

@@ -1,102 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.di
import com.bitwarden.network.service.AccountsService
import com.bitwarden.network.service.AccountsServiceImpl
import com.bitwarden.network.service.AuthRequestsService
import com.bitwarden.network.service.AuthRequestsServiceImpl
import com.bitwarden.network.service.DevicesService
import com.bitwarden.network.service.DevicesServiceImpl
import com.bitwarden.network.service.HaveIBeenPwnedService
import com.bitwarden.network.service.HaveIBeenPwnedServiceImpl
import com.bitwarden.network.service.IdentityService
import com.bitwarden.network.service.IdentityServiceImpl
import com.bitwarden.network.service.NewAuthRequestService
import com.bitwarden.network.service.NewAuthRequestServiceImpl
import com.bitwarden.network.service.OrganizationService
import com.bitwarden.network.service.OrganizationServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import retrofit2.create
import javax.inject.Singleton
/**
* Provides network dependencies in the auth package.
*/
@Module
@InstallIn(SingletonComponent::class)
object AuthNetworkModule {
@Provides
@Singleton
fun providesAccountService(
retrofits: Retrofits,
json: Json,
): AccountsService = AccountsServiceImpl(
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
authenticatedKeyConnectorApi = retrofits
.createStaticRetrofit(isAuthenticated = true)
.create(),
json = json,
)
@Provides
@Singleton
fun providesAuthRequestsService(
retrofits: Retrofits,
): AuthRequestsService = AuthRequestsServiceImpl(
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
)
@Provides
@Singleton
fun providesDevicesService(
retrofits: Retrofits,
): DevicesService = DevicesServiceImpl(
authenticatedDevicesApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedDevicesApi = retrofits.unauthenticatedApiRetrofit.create(),
)
@Provides
@Singleton
fun providesIdentityService(
retrofits: Retrofits,
json: Json,
): IdentityService = IdentityServiceImpl(
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
json = json,
)
@Provides
@Singleton
fun providesHaveIBeenPwnedService(
retrofits: Retrofits,
): HaveIBeenPwnedService = HaveIBeenPwnedServiceImpl(
api = retrofits
.createStaticRetrofit(baseUrl = "https://api.pwnedpasswords.com")
.create(),
)
@Provides
@Singleton
fun providesNewAuthRequestService(
retrofits: Retrofits,
): NewAuthRequestService = NewAuthRequestServiceImpl(
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedAuthRequestsApi = retrofits.unauthenticatedApiRetrofit.create(),
)
@Provides
@Singleton
fun providesOrganizationService(
retrofits: Retrofits,
): OrganizationService = OrganizationServiceImpl(
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
)
}

View File

@@ -1,28 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.model
import java.time.ZonedDateTime
/**
* Response types when checking for an email's claimed domain organization.
*/
sealed class OrganizationDomainSsoDetailsResult {
/**
* The request was successful.
*
* @property isSsoAvailable Indicates if SSO is available for the email address.
* @property organizationIdentifier The claimed organization identifier for the email address.
* @property verifiedDate The date and time when the domain was verified.
*/
data class Success(
val isSsoAvailable: Boolean,
val organizationIdentifier: String,
val verifiedDate: ZonedDateTime?,
) : OrganizationDomainSsoDetailsResult()
/**
* The request failed.
*/
data class Failure(
val error: Throwable,
) : OrganizationDomainSsoDetailsResult()
}

View File

@@ -1,30 +0,0 @@
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.di
import com.bitwarden.network.service.DigitalAssetLinkService
import com.bitwarden.network.service.DigitalAssetLinkServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.create
import javax.inject.Singleton
/**
* Provides network dependencies in the fido2 package.
*/
@Module
@InstallIn(SingletonComponent::class)
object Fido2NetworkModule {
@Provides
@Singleton
fun provideDigitalAssetLinkService(
retrofits: Retrofits,
): DigitalAssetLinkService =
DigitalAssetLinkServiceImpl(
digitalAssetLinkApi = retrofits
.createStaticRetrofit()
.create(),
)
}

View File

@@ -1,89 +0,0 @@
package com.x8bit.bitwarden.data.autofill.fido2.di
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.DigitalAssetLinkService
import com.bitwarden.sdk.Fido2CredentialStore
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManagerImpl
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManagerImpl
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import java.time.Clock
import javax.inject.Singleton
/**
* Provides dependencies within the fido2 package.
*/
@Module
@InstallIn(SingletonComponent::class)
object Fido2ProviderModule {
@RequiresApi(Build.VERSION_CODES.S)
@Provides
@Singleton
fun provideCredentialProviderProcessor(
@ApplicationContext context: Context,
authRepository: AuthRepository,
vaultRepository: VaultRepository,
fido2CredentialStore: Fido2CredentialStore,
fido2CredentialManager: Fido2CredentialManager,
dispatcherManager: DispatcherManager,
intentManager: IntentManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
featureFlagManager: FeatureFlagManager,
clock: Clock,
): Fido2ProviderProcessor =
Fido2ProviderProcessorImpl(
context,
authRepository,
vaultRepository,
fido2CredentialStore,
fido2CredentialManager,
intentManager,
clock,
biometricsEncryptionManager,
featureFlagManager,
dispatcherManager,
)
@Provides
@Singleton
fun provideFido2CredentialManager(
vaultSdkSource: VaultSdkSource,
fido2CredentialStore: Fido2CredentialStore,
json: Json,
): Fido2CredentialManager =
Fido2CredentialManagerImpl(
vaultSdkSource = vaultSdkSource,
fido2CredentialStore = fido2CredentialStore,
json = json,
)
@Provides
@Singleton
fun provideFido2OriginManager(
assetManager: AssetManager,
digitalAssetLinkService: DigitalAssetLinkService,
): Fido2OriginManager =
Fido2OriginManagerImpl(
assetManager = assetManager,
digitalAssetLinkService = digitalAssetLinkService,
)
}

View File

@@ -1,155 +0,0 @@
package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.network.model.DigitalAssetLinkResponseJson
import com.bitwarden.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
import timber.log.Timber
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
/**
* Primary implementation of [Fido2OriginManager].
*/
@Suppress("TooManyFunctions")
class Fido2OriginManagerImpl(
private val assetManager: AssetManager,
private val digitalAssetLinkService: DigitalAssetLinkService,
) : Fido2OriginManager {
override suspend fun validateOrigin(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult {
return if (callingAppInfo.isOriginPopulated()) {
validatePrivilegedAppOrigin(callingAppInfo)
} else {
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
}
}
private suspend fun validateCallingApplicationAssetLinks(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult = digitalAssetLinkService
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
.onFailure {
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
}
.mapCatching { statements ->
statements
.filterMatchingAppStatementsOrNull(
rpPackageName = callingAppInfo.packageName,
)
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
}
.mapCatching { matchingStatements ->
callingAppInfo
.getSignatureFingerprintAsHexString()
?.let { certificateFingerprint ->
matchingStatements
.filterMatchingAppSignaturesOrNull(
signature = certificateFingerprint,
)
}
?: return Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified
}
.fold(
onSuccess = {
Fido2ValidateOriginResult.Success(null)
},
onFailure = {
Fido2ValidateOriginResult.Error.Unknown
},
)
private suspend fun validatePrivilegedAppOrigin(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult {
val googleAllowListResult =
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
return when (googleAllowListResult) {
is Fido2ValidateOriginResult.Success -> {
// Application was found and successfully validated against the Google allow list so
// we can return the result as the final validation result.
googleAllowListResult
}
is Fido2ValidateOriginResult.Error -> {
// Check the community allow list if the Google allow list failed, and return the
// result as the final validation result.
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
}
}
}
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithAllowList(
callingAppInfo: CallingAppInfo,
fileName: String,
): Fido2ValidateOriginResult =
assetManager
.readAsset(fileName)
.mapCatching { allowList ->
callingAppInfo.validatePrivilegedApp(
allowList = allowList,
)
}
.fold(
onSuccess = { it },
onFailure = {
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
Fido2ValidateOriginResult.Error.Unknown
},
)
/**
* Returns statements targeting the calling Android application, or null.
*/
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppStatementsOrNull(
rpPackageName: String,
): List<DigitalAssetLinkResponseJson>? =
filter { statement ->
val target = statement.target
target.namespace == "android_app" &&
target.packageName == rpPackageName &&
statement.relation.containsAll(
listOf(
"delegate_permission/common.handle_all_urls",
),
)
}
.takeUnless { it.isEmpty() }
/**
* Returns statements that match the given [signature], or null.
*/
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppSignaturesOrNull(
signature: String,
): List<DigitalAssetLinkResponseJson>? =
filter { statement ->
statement.target.sha256CertFingerprints
?.contains(signature)
?: false
}
.takeUnless { it.isEmpty() }
}

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Manager which provides whether specific Chrome versions have third party autofill available and
* enabled.
*/
interface ChromeThirdPartyAutofillEnabledManager {
/**
* Combined status for all concerned Chrome versions.
*/
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
/**
* An observable [StateFlow] of the combined third party autofill status of all concerned
* chrome versions.
*/
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
}

View File

@@ -1,52 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
/**
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
*/
class ChromeThirdPartyAutofillEnabledManagerImpl(
private val featureFlagManager: FeatureFlagManager,
) : ChromeThirdPartyAutofillEnabledManager {
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
set(value) {
field = value
mutableChromeThirdPartyAutofillStatusStateFlow.update {
value
}
}
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
chromeThirdPartyAutofillStatus,
)
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
get() = mutableChromeThirdPartyAutofillStatusStateFlow
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
) { data, enabled ->
if (enabled) {
data
} else {
DEFAULT_STATUS
}
}
}
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
)

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
/**
* Manager class used to determine if a device has installed versions of Chrome (either the
* stable release or beta channel) which support and require opt in to third party autofill.
*/
interface ChromeThirdPartyAutofillManager {
/**
* The data representing the status of the stable chrome version
*/
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
/**
* The data representing the status of the beta chrome version
*/
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
}

View File

@@ -1,12 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model
import android.content.Context
/**
* The app information required for the autofill service.
*/
data class AutofillAppInfo(
val context: Context,
val packageName: String,
val sdkInt: Int,
)

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
/**
* Enumerated values of each version of Chrome supported for third party autofill checks.
*
* @property packageName the package name of the release channel for the Chrome version.
*/
enum class ChromeReleaseChannel(val packageName: String) {
STABLE(CHROME_CHANNEL_PACKAGE),
BETA(BETA_CHANNEL_PACKAGE),
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
/**
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
*/
data class ChromeThirdPartyAutoFillData(
val isAvailable: Boolean,
val isThirdPartyEnabled: Boolean,
)
/**
* The overall status for all relevant release channels of Chrome.
*/
data class ChromeThirdPartyAutofillStatus(
val stableStatusData: ChromeThirdPartyAutoFillData,
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
)

View File

@@ -1,73 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.authenticator
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import javax.inject.Singleton
/**
* An authenticator used to refresh the access token when a 401 is returned from an API. Upon
* successfully getting a new access token, the original request is retried.
*/
@Singleton
class RefreshAuthenticator : Authenticator {
/**
* A provider required to update tokens.
*/
var authenticatorProvider: AuthenticatorProvider? = null
override fun authenticate(
route: Route?,
response: Response,
): Request? {
val accessToken = requireNotNull(
response
.request
.header(name = HEADER_KEY_AUTHORIZATION)
?.substringAfter(delimiter = HEADER_VALUE_BEARER_PREFIX),
)
return when (val userId = parseJwtTokenDataOrNull(accessToken)?.userId) {
null -> {
// We unable to get the user ID, let's just let the 401 pass through.
null
}
authenticatorProvider?.activeUserId -> {
// In order to prevent potential deadlocks or thread starvation we want the call
// to refresh the access token to be strictly synchronous with no internal thread
// hopping.
authenticatorProvider
?.refreshAccessTokenSynchronously(userId)
?.fold(
onFailure = {
authenticatorProvider?.logout(
userId = userId,
reason = LogoutReason.TokenRefreshFail,
)
null
},
onSuccess = {
response.request
.newBuilder()
.header(
name = HEADER_KEY_AUTHORIZATION,
value = "$HEADER_VALUE_BEARER_PREFIX${it.accessToken}",
)
.build()
},
)
}
else -> {
// We are no longer the active user, let's just cancel.
null
}
}
}
}

View File

@@ -1,121 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.di
import com.bitwarden.network.interceptor.AuthTokenInterceptor
import com.bitwarden.network.interceptor.BaseUrlInterceptors
import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.interceptor.HeadersInterceptor
import com.bitwarden.network.service.ConfigService
import com.bitwarden.network.service.ConfigServiceImpl
import com.bitwarden.network.service.EventService
import com.bitwarden.network.service.EventServiceImpl
import com.bitwarden.network.service.PushService
import com.bitwarden.network.service.PushServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManager
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsImpl
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManagerImpl
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_NAME
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_VERSION
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT
import com.x8bit.bitwarden.data.platform.manager.KeyManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import retrofit2.create
import javax.inject.Singleton
/**
* This class provides network-related functionality for the application.
* It initializes and configures the networking components.
*/
@Module
@InstallIn(SingletonComponent::class)
object PlatformNetworkModule {
@Provides
@Singleton
fun providesConfigService(
retrofits: Retrofits,
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
@Provides
@Singleton
fun providesEventService(
retrofits: Retrofits,
): EventService = EventServiceImpl(
eventApi = retrofits.authenticatedEventsRetrofit.create(),
)
@Provides
@Singleton
fun providePushService(
retrofits: Retrofits,
authDiskSource: AuthDiskSource,
): PushService = PushServiceImpl(
pushApi = retrofits.authenticatedApiRetrofit.create(),
appId = authDiskSource.uniqueAppId,
)
@Provides
@Singleton
fun providesAuthTokenInterceptor(
authTokenManager: AuthTokenManager,
): AuthTokenInterceptor = AuthTokenInterceptor(
authTokenProvider = authTokenManager,
)
@Provides
@Singleton
fun providesHeadersInterceptor(): HeadersInterceptor = HeadersInterceptor(
userAgent = HEADER_VALUE_USER_AGENT,
clientName = HEADER_VALUE_CLIENT_NAME,
clientVersion = HEADER_VALUE_CLIENT_VERSION,
)
@Provides
@Singleton
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
@Provides
@Singleton
fun provideSslManager(
keyManager: KeyManager,
environmentRepository: EnvironmentRepository,
): SslManager =
SslManagerImpl(
keyManager = keyManager,
environmentRepository = environmentRepository,
)
@Provides
@Singleton
fun providesBaseUrlInterceptors(
baseUrlsProvider: BaseUrlsProvider,
): BaseUrlInterceptors =
BaseUrlInterceptors(baseUrlsProvider = baseUrlsProvider)
@Provides
@Singleton
fun provideRetrofits(
authTokenInterceptor: AuthTokenInterceptor,
baseUrlInterceptors: BaseUrlInterceptors,
headersInterceptor: HeadersInterceptor,
refreshAuthenticator: RefreshAuthenticator,
sslManager: SslManager,
json: Json,
): Retrofits =
RetrofitsImpl(
authTokenInterceptor = authTokenInterceptor,
baseUrlInterceptors = baseUrlInterceptors,
headersInterceptor = headersInterceptor,
refreshAuthenticator = refreshAuthenticator,
sslManager = sslManager,
json = json,
)
}

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
/**
* Interface for managing SSL connections.
*/
interface SslManager {
/**
* The SSL context to use for SSL connections.
*/
val sslContext: SSLContext
/**
* The trust managers to use for SSL connections.
*/
val trustManagers: Array<TrustManager>
}

View File

@@ -1,116 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
import com.x8bit.bitwarden.data.platform.manager.KeyManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import java.net.Socket
import java.security.KeyStore
import java.security.Principal
import java.security.PrivateKey
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
/**
* Primary implementation of [SslManager].
*/
class SslManagerImpl(
private val keyManager: KeyManager,
private val environmentRepository: EnvironmentRepository,
) : SslManager {
/*
This property must only be accessed from a background thread. Accessing this property from
the main thread will result in an exception being thrown when retrieving the mutual TLS
certificate from [KeyManager].
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@get:WorkerThread
internal val mutualTlsCertificate: MutualTlsCertificate?
get() {
val keyUri = getKeyUri()
?: return null
val host = MutualTlsKeyHost
.entries
.find { it.name == keyUri.authority }
?: return null
val alias = keyUri.path
?.trim('/')
?.takeUnless { it.isEmpty() }
?: return null
return keyManager.getMutualTlsCertificateChain(
alias = alias,
host = host,
)
}
override val trustManagers: Array<TrustManager>
get() = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
.apply { init(null as KeyStore?) }
.trustManagers
override val sslContext: SSLContext
get() = SSLContext
.getInstance("TLS")
.apply {
init(
arrayOf(X509ExtendedKeyManagerImpl()),
trustManagers,
null,
)
}
private fun getKeyUri(): Uri? = environmentRepository
.environment
.environmentUrlData
.keyUri
?.toUri()
private inner class X509ExtendedKeyManagerImpl : X509ExtendedKeyManager() {
override fun chooseClientAlias(
keyType: Array<out String>?,
issuers: Array<out Principal>?,
socket: Socket?,
): String = mutualTlsCertificate?.alias ?: ""
override fun getCertificateChain(
alias: String?,
): Array<X509Certificate>? =
mutualTlsCertificate
?.certificateChain
?.toTypedArray()
override fun getPrivateKey(alias: String?): PrivateKey? =
mutualTlsCertificate
?.privateKey
//region Unused server side methods
override fun getServerAliases(
alias: String?,
issuers: Array<out Principal>?,
): Array<String> = arrayOf()
override fun getClientAliases(
keyType: String?,
issuers: Array<out Principal>?,
): Array<String> = emptyArray()
override fun chooseServerAlias(
alias: String?,
issuers: Array<out Principal>?,
socket: Socket?,
): String = ""
//endregion Unused server side methods
}
}

View File

@@ -1,12 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.network
/**
* Manager to detect and handle changes to network connectivity.
*/
interface NetworkConnectionManager {
/**
* Returns `true` if the application has a network connection and access to the Internet is
* available.
*/
val isNetworkConnected: Boolean
}

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/**
* Primary implementation of [NetworkConnectionManager].
*/
class NetworkConnectionManagerImpl(
context: Context,
) : NetworkConnectionManager {
private val connectivityManager: ConnectivityManager = context
.applicationContext
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override val isNetworkConnected: Boolean
get() = connectivityManager
.getNetworkCapabilities(connectivityManager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false
}

View File

@@ -1,13 +0,0 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.data.platform.util
import android.os.Build
import com.bitwarden.core.annotation.OmitFromCoverage
/**
* Returns true if the current OS build version is below the provided [version].
*
* @see Build.VERSION_CODES
*/
internal fun isBuildVersionBelow(version: Int): Boolean = version > Build.VERSION.SDK_INT

View File

@@ -1,87 +0,0 @@
package com.x8bit.bitwarden.data.vault.datasource.network.di
import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.CiphersServiceImpl
import com.bitwarden.network.service.DownloadService
import com.bitwarden.network.service.DownloadServiceImpl
import com.bitwarden.network.service.FolderService
import com.bitwarden.network.service.FolderServiceImpl
import com.bitwarden.network.service.SendsService
import com.bitwarden.network.service.SendsServiceImpl
import com.bitwarden.network.service.SyncService
import com.bitwarden.network.service.SyncServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import retrofit2.create
import java.time.Clock
import javax.inject.Singleton
/**
* Provides network dependencies in the vault package.
*/
@Module
@InstallIn(SingletonComponent::class)
object VaultNetworkModule {
@Provides
@Singleton
fun provideCiphersService(
retrofits: Retrofits,
json: Json,
clock: Clock,
): CiphersService = CiphersServiceImpl(
azureApi = retrofits
.createStaticRetrofit()
.create(),
ciphersApi = retrofits.authenticatedApiRetrofit.create(),
json = json,
clock = clock,
)
@Provides
@Singleton
fun providesFolderService(
retrofits: Retrofits,
json: Json,
): FolderService = FolderServiceImpl(
foldersApi = retrofits.authenticatedApiRetrofit.create(),
json = json,
)
@Provides
@Singleton
fun provideSendsService(
retrofits: Retrofits,
json: Json,
clock: Clock,
): SendsService = SendsServiceImpl(
azureApi = retrofits
.createStaticRetrofit()
.create(),
sendsApi = retrofits.authenticatedApiRetrofit.create(),
json = json,
clock = clock,
)
@Provides
@Singleton
fun provideSyncService(
retrofits: Retrofits,
): SyncService = SyncServiceImpl(
syncApi = retrofits.authenticatedApiRetrofit.create(),
)
@Provides
@Singleton
fun provideDownloadService(
retrofits: Retrofits,
): DownloadService = DownloadServiceImpl(
downloadApi = retrofits
.createStaticRetrofit()
.create(),
)
}

View File

@@ -1,79 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
/**
* Route constant for navigating to the [SetupAutoFillScreen].
*/
private const val SETUP_AUTO_FILL_PREFIX = "setup_auto_fill"
private const val SETUP_AUTO_FILL_AS_ROOT_PREFIX = "${SETUP_AUTO_FILL_PREFIX}_as_root"
private const val SETUP_AUTO_FILL_NAV_ARG = "isInitialSetup"
private const val SETUP_AUTO_FILL_ROUTE = "$SETUP_AUTO_FILL_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
const val SETUP_AUTO_FILL_AS_ROOT_ROUTE =
"$SETUP_AUTO_FILL_AS_ROOT_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
/**
* Arguments for the [SetupAutoFillScreen] using [SavedStateHandle].
*/
@OmitFromCoverage
data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean) {
constructor(savedStateHandle: SavedStateHandle) : this(
isInitialSetup = requireNotNull(savedStateHandle[SETUP_AUTO_FILL_NAV_ARG]),
)
}
/**
* Navigate to the setup auto-fill screen.
*/
fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) {
this.navigate("$SETUP_AUTO_FILL_PREFIX/false", navOptions)
}
/**
* Navigate to the setup auto-fill screen as the root.
*/
fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? = null) {
this.navigate("$SETUP_AUTO_FILL_AS_ROOT_PREFIX/true", navOptions)
}
/**
* Add the setup auto-fil screen to the nav graph.
*/
fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) {
composableWithSlideTransitions(
route = SETUP_AUTO_FILL_ROUTE,
arguments = setupAutofillNavArgs,
) {
SetupAutoFillScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Add the setup autofil screen to the root nav graph.
*/
fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
composableWithPushTransitions(
route = SETUP_AUTO_FILL_AS_ROOT_ROUTE,
arguments = setupAutofillNavArgs,
) {
SetupAutoFillScreen(
onNavigateBack = {
// No-Op
},
)
}
}
private val setupAutofillNavArgs = listOf(
navArgument(SETUP_AUTO_FILL_NAV_ARG) {
type = NavType.BoolType
},
)

View File

@@ -1,88 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
/**
* Route constants for [SetupUnlockScreen]
*/
private const val SETUP_UNLOCK_PREFIX = "setup_unlock"
private const val SETUP_UNLOCK_AS_ROOT_PREFIX = "${SETUP_UNLOCK_PREFIX}_as_root"
private const val SETUP_UNLOCK_INITIAL_SETUP_ARG = "isInitialSetup"
const val SETUP_UNLOCK_AS_ROOT_ROUTE = "$SETUP_UNLOCK_AS_ROOT_PREFIX/" +
"{$SETUP_UNLOCK_INITIAL_SETUP_ARG}"
private const val SETUP_UNLOCK_ROUTE = "$SETUP_UNLOCK_PREFIX/{$SETUP_UNLOCK_INITIAL_SETUP_ARG}"
/**
* Class to retrieve setup unlock arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class SetupUnlockArgs(
val isInitialSetup: Boolean,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
isInitialSetup = requireNotNull(savedStateHandle[SETUP_UNLOCK_INITIAL_SETUP_ARG]),
)
}
/**
* Navigate to the setup unlock screen.
*/
fun NavController.navigateToSetupUnlockScreen(navOptions: NavOptions? = null) {
this.navigate("$SETUP_UNLOCK_PREFIX/false", navOptions)
}
/**
* Navigate to the setup unlock screen as root.
*/
fun NavController.navigateToSetupUnlockScreenAsRoot(navOptions: NavOptions? = null) {
this.navigate("$SETUP_UNLOCK_AS_ROOT_PREFIX/true", navOptions)
}
/**
* Add the setup unlock screen to a nav graph.
*/
fun NavGraphBuilder.setupUnlockDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = SETUP_UNLOCK_ROUTE,
arguments = setupUnlockArguments,
) {
SetupUnlockScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Add the setup unlock screen to the root nav graph.
*/
fun NavGraphBuilder.setupUnlockDestinationAsRoot() {
composableWithPushTransitions(
route = SETUP_UNLOCK_AS_ROOT_ROUTE,
arguments = setupUnlockArguments,
) {
SetupUnlockScreen(
onNavigateBack = {
// No-Op
},
)
}
}
private val setupUnlockArguments = listOf(
navArgument(
name = SETUP_UNLOCK_INITIAL_SETUP_ARG,
builder = {
type = NavType.BoolType
},
),
)

View File

@@ -1,28 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.environment
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val ENVIRONMENT_ROUTE = "environment"
/**
* Add settings destinations to the nav graph.
*/
fun NavGraphBuilder.environmentDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ENVIRONMENT_ROUTE,
) {
EnvironmentScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Navigate to the about screen.
*/
fun NavController.navigateToEnvironment(navOptions: NavOptions? = null) {
navigate(ENVIRONMENT_ROUTE, navOptions)
}

View File

@@ -1,49 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val EMAIL_ADDRESS: String = "email_address"
private const val MASTER_PASSWORD_HINT_ROUTE: String = "master_password_hint/{$EMAIL_ADDRESS}"
/**
* Class to retrieve login arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class MasterPasswordHintArgs(val emailAddress: String) {
constructor(savedStateHandle: SavedStateHandle) : this(
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
)
}
/**
* Navigate to the master password hint screen.
*/
fun NavController.navigateToMasterPasswordHint(
emailAddress: String,
navOptions: NavOptions? = null,
) {
this.navigate("master_password_hint/$emailAddress", navOptions)
}
/**
* Add the master password hint screen to the nav graph.
*/
fun NavGraphBuilder.masterPasswordHintDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = MASTER_PASSWORD_HINT_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
),
) {
MasterPasswordHintScreen(onNavigateBack = onNavigateBack)
}
}

View File

@@ -1,89 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.twofactorlogin
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.network.util.base64UrlDecodeOrNull
import com.bitwarden.network.util.base64UrlEncode
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val EMAIL_ADDRESS = "email_address"
private const val PASSWORD = "password"
private const val ORG_IDENTIFIER = "org_identifier"
private const val TWO_FACTOR_LOGIN_PREFIX = "two_factor_login"
private const val NEW_DEVICE_VERIFICATION = "new_device_verification"
private const val TWO_FACTOR_LOGIN_ROUTE =
"$TWO_FACTOR_LOGIN_PREFIX/{$EMAIL_ADDRESS}?" +
"$PASSWORD={$PASSWORD}&" +
"$ORG_IDENTIFIER={$ORG_IDENTIFIER}&" +
"$NEW_DEVICE_VERIFICATION={$NEW_DEVICE_VERIFICATION}"
/**
* Class to retrieve Two-Factor Login arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class TwoFactorLoginArgs(
val emailAddress: String,
val password: String?,
val orgIdentifier: String?,
val isNewDeviceVerification: Boolean,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
emailAddress = checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
password = savedStateHandle.get<String>(PASSWORD)?.base64UrlDecodeOrNull(),
orgIdentifier = savedStateHandle.get<String>(ORG_IDENTIFIER)?.base64UrlDecodeOrNull(),
isNewDeviceVerification = savedStateHandle.get<Boolean>(NEW_DEVICE_VERIFICATION) ?: false,
)
}
/**
* Navigate to the Two-Factor Login screen.
*/
fun NavController.navigateToTwoFactorLogin(
emailAddress: String,
password: String?,
orgIdentifier: String?,
navOptions: NavOptions? = null,
isNewDeviceVerification: Boolean = false,
) {
this.navigate(
route = "$TWO_FACTOR_LOGIN_PREFIX/$emailAddress?" +
"$PASSWORD=${password?.base64UrlEncode()}&" +
"$ORG_IDENTIFIER=${orgIdentifier?.base64UrlEncode()}&" +
"$NEW_DEVICE_VERIFICATION=$isNewDeviceVerification",
navOptions = navOptions,
)
}
/**
* Add the Two-Factor Login screen to the nav graph.
*/
fun NavGraphBuilder.twoFactorLoginDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = TWO_FACTOR_LOGIN_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
navArgument(PASSWORD) {
type = NavType.StringType
nullable = true
},
navArgument(ORG_IDENTIFIER) {
type = NavType.StringType
nullable = true
},
navArgument(NEW_DEVICE_VERIFICATION) {
type = NavType.BoolType
},
),
) {
TwoFactorLoginScreen(
onNavigateBack = onNavigateBack,
)
}
}

View File

@@ -1,82 +0,0 @@
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
private const val VAULT_UNLOCK_TYPE: String = "unlock_type"
private const val TDE_VAULT_UNLOCK_ROUTE_PREFIX: String = "tde_vault_unlock"
private const val TDE_VAULT_UNLOCK_ROUTE: String =
"$TDE_VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}"
private const val VAULT_UNLOCK_ROUTE_PREFIX: String = "vault_unlock"
const val VAULT_UNLOCK_ROUTE: String = "$VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}"
/**
* Class to retrieve vault unlock arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class VaultUnlockArgs(
val unlockType: UnlockType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
unlockType = checkNotNull(savedStateHandle.get<UnlockType>(VAULT_UNLOCK_TYPE)),
)
}
/**
* Navigate to the Vault Unlock screen.
*/
fun NavController.navigateToVaultUnlock(
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.STANDARD}",
navOptions = navOptions,
)
}
/**
* Add the Vault Unlock screen to the nav graph.
*/
fun NavGraphBuilder.vaultUnlockDestination() {
composable(
route = VAULT_UNLOCK_ROUTE,
arguments = listOf(
navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) },
),
) {
VaultUnlockScreen()
}
}
/**
* Navigate to the Vault Unlock screen for TDE.
*/
fun NavController.navigateToTdeVaultUnlock(
navOptions: NavOptions? = null,
) {
navigate(
route = "$TDE_VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.TDE}",
navOptions = navOptions,
)
}
/**
* Add the Vault Unlock screen to the TDE nav graph.
*/
fun NavGraphBuilder.tdeVaultUnlockDestination() {
composable(
route = TDE_VAULT_UNLOCK_ROUTE,
arguments = listOf(
navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) },
),
) {
VaultUnlockScreen()
}
}

View File

@@ -1,26 +0,0 @@
package com.x8bit.bitwarden.ui.autofill.fido2.manager
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
/**
* A manager for completing the FIDO 2 creation process.
*/
interface Fido2CompletionManager {
/**
* Completes the FIDO 2 registration process with the provided [result].
*/
fun completeFido2Registration(result: RegisterFido2CredentialResult)
/**
* Complete the FIDO 2 credential assertion process with the provided [result].
*/
fun completeFido2Assertion(result: AssertFido2CredentialResult)
/**
* Complete the FIDO 2 "Get credentials" process with the provided [result].
*/
fun completeFido2GetCredentialsRequest(result: GetFido2CredentialsResult)
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.ui.autofill.fido2.manager
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
/**
* A no-op implementation of [Fido2CompletionManagerImpl] provided when the build version is below
* UPSIDE_DOWN_CAKE (34). These versions do not support [androidx.credentials.CredentialProvider].
*/
object Fido2CompletionManagerUnsupportedApiImpl : Fido2CompletionManager {
override fun completeFido2Registration(result: RegisterFido2CredentialResult) = Unit
override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit
override fun completeFido2GetCredentialsRequest(result: GetFido2CredentialsResult) = Unit
}

View File

@@ -1,28 +0,0 @@
package com.x8bit.bitwarden.ui.autofill.fido2.manager.model
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.ui.util.Text
/**
* Represents the result of fetching FIDO2 credentials.
*/
sealed class GetFido2CredentialsResult {
/**
* Represents a successful result with a list of matching credentials, the original request
* option, and associated user ID.
*/
data class Success(
val credentials: List<Fido2CredentialAutofillView>,
val option: BeginGetPublicKeyCredentialOption,
val userId: String,
) : GetFido2CredentialsResult()
/**
* Represents an error result with a message.
*
* @property message The error message.
*/
data class Error(val message: Text) : GetFido2CredentialsResult()
}

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.ui.platform.base.util
import android.content.Context
import android.widget.Toast
import com.bitwarden.core.annotation.OmitFromCoverage
/**
* Shows a [Toast] with a message indicating something is not yet implemented.
*/
@OmitFromCoverage
fun showNotYetImplementedToast(context: Context) {
Toast
.makeText(
context,
"Not yet implemented",
Toast.LENGTH_SHORT,
)
.show()
}

View File

@@ -1,195 +0,0 @@
package com.x8bit.bitwarden.ui.platform.components.dialog
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerColors
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.orNow
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
/**
* A custom composable representing a button that can display the date picker dialog.
*
* This composable displays an [OutlinedTextField] with a dropdown icon as a trailing icon.
* When the field is clicked, a date picker dialog appears.
*
* @param label The displayed label.
* @param currentZonedDateTime The currently displayed time.
* @param formatPattern The pattern to format the displayed time.
* @param onDateSelect The callback to be invoked when a new date is selected.
* @param isEnabled Whether the button is enabled.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun BitwardenDateSelectButton(
label: String,
currentZonedDateTime: ZonedDateTime?,
formatPattern: String,
onDateSelect: (ZonedDateTime) -> Unit,
isEnabled: Boolean,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
) {
var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) }
val formattedDate by remember(currentZonedDateTime) {
mutableStateOf(
currentZonedDateTime
?.toFormattedPattern(formatPattern)
?: "mm/dd/yyyy",
)
}
TextField(
modifier = modifier
.clearAndSetSemantics {
role = Role.DropdownList
contentDescription = "$label, $formattedDate"
}
.defaultMinSize(minHeight = 60.dp)
.cardStyle(
cardStyle = cardStyle,
clickEnabled = isEnabled,
onClick = { shouldShowDialog = !shouldShowDialog },
)
.padding(top = 4.dp),
textStyle = BitwardenTheme.typography.bodyLarge,
readOnly = true,
label = { Text(text = label) },
value = formattedDate,
onValueChange = { },
enabled = shouldShowDialog,
trailingIcon = {
BitwardenRowOfActions(
modifier = Modifier.padding(end = 4.dp),
) {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_down),
contentDescription = null,
modifier = Modifier.minimumInteractiveComponentSize(),
)
}
},
colors = bitwardenTextFieldButtonColors(),
)
if (shouldShowDialog) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = currentZonedDateTime.orNow().toInstant().toEpochMilli(),
)
DatePickerDialog(
shape = BitwardenTheme.shapes.dialog,
colors = bitwardenDatePickerColors(),
onDismissRequest = { shouldShowDialog = false },
confirmButton = {
BitwardenTextButton(
label = stringResource(id = R.string.ok),
onClick = {
onDateSelect(
ZonedDateTime
.ofInstant(
Instant.ofEpochMilli(
requireNotNull(datePickerState.selectedDateMillis),
),
ZoneOffset.UTC,
)
.withZoneSameLocal(currentZonedDateTime.orNow().zone),
)
shouldShowDialog = false
},
modifier = Modifier.testTag(tag = "AcceptAlertButton"),
)
},
dismissButton = {
BitwardenTextButton(
label = stringResource(id = R.string.cancel),
onClick = { shouldShowDialog = false },
modifier = Modifier.testTag(tag = "DismissAlertButton"),
)
},
modifier = Modifier.semantics {
testTagsAsResourceId = true
testTag = "AlertPopup"
},
) {
DatePicker(
state = datePickerState,
colors = bitwardenDatePickerColors(),
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun bitwardenDatePickerColors(): DatePickerColors = DatePickerColors(
containerColor = BitwardenTheme.colorScheme.background.primary,
titleContentColor = BitwardenTheme.colorScheme.text.secondary,
headlineContentColor = BitwardenTheme.colorScheme.text.primary,
weekdayContentColor = BitwardenTheme.colorScheme.text.primary,
subheadContentColor = BitwardenTheme.colorScheme.text.secondary,
navigationContentColor = BitwardenTheme.colorScheme.icon.primary,
yearContentColor = BitwardenTheme.colorScheme.text.primary,
disabledYearContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
currentYearContentColor = BitwardenTheme.colorScheme.filledButton.foreground,
selectedYearContentColor = BitwardenTheme.colorScheme.filledButton.foreground,
disabledSelectedYearContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
selectedYearContainerColor = BitwardenTheme.colorScheme.filledButton.background,
disabledSelectedYearContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
dayContentColor = BitwardenTheme.colorScheme.text.primary,
disabledDayContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
selectedDayContentColor = BitwardenTheme.colorScheme.text.reversed,
disabledSelectedDayContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
selectedDayContainerColor = BitwardenTheme.colorScheme.filledButton.background,
disabledSelectedDayContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
todayContentColor = BitwardenTheme.colorScheme.outlineButton.foreground,
todayDateBorderColor = BitwardenTheme.colorScheme.outlineButton.border,
dayInSelectionRangeContainerColor = BitwardenTheme.colorScheme.filledButton.background,
dividerColor = BitwardenTheme.colorScheme.stroke.divider,
dayInSelectionRangeContentColor = BitwardenTheme.colorScheme.text.primary,
dateTextFieldColors = bitwardenTextFieldColors(
disabledBorderColor = BitwardenTheme.colorScheme.outlineButton.borderDisabled,
focusedBorderColor = BitwardenTheme.colorScheme.stroke.border,
unfocusedBorderColor = BitwardenTheme.colorScheme.stroke.divider,
),
)

View File

@@ -1,113 +0,0 @@
package com.x8bit.bitwarden.ui.platform.components.dialog
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.orNow
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import java.time.ZonedDateTime
/**
* A custom composable representing a button that can display the time picker dialog.
*
* This composable displays an [OutlinedTextField] with a dropdown icon as a trailing icon.
* When the field is clicked, a time picker dialog appears.
*
* @param label The displayed label.
* @param currentZonedDateTime The currently displayed time.
* @param formatPattern The pattern to format the displayed time.
* @param onTimeSelect The callback to be invoked when a new time is selected.
* @param isEnabled Whether the button is enabled.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param is24Hour Indicates if the time selector should use a 24 hour format or a 12 hour format
* with AM/PM.
*/
@Composable
fun BitwardenTimeSelectButton(
label: String,
currentZonedDateTime: ZonedDateTime?,
formatPattern: String,
onTimeSelect: (hour: Int, minute: Int) -> Unit,
isEnabled: Boolean,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
is24Hour: Boolean = false,
) {
var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) }
val formattedTime by remember(currentZonedDateTime) {
mutableStateOf(
currentZonedDateTime
?.toFormattedPattern(formatPattern)
?: "--:-- --",
)
}
TextField(
modifier = modifier
.clearAndSetSemantics {
role = Role.DropdownList
contentDescription = "$label, $formattedTime"
}
.defaultMinSize(minHeight = 60.dp)
.cardStyle(
cardStyle = cardStyle,
clickEnabled = isEnabled,
onClick = { shouldShowDialog = !shouldShowDialog },
)
.padding(top = 4.dp),
textStyle = BitwardenTheme.typography.bodyLarge,
readOnly = true,
label = { Text(text = label) },
value = formattedTime,
onValueChange = { },
enabled = shouldShowDialog,
trailingIcon = {
BitwardenRowOfActions(
modifier = Modifier.padding(end = 4.dp),
) {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_down),
contentDescription = null,
modifier = Modifier.minimumInteractiveComponentSize(),
)
}
},
colors = bitwardenTextFieldButtonColors(),
)
if (shouldShowDialog) {
BitwardenTimePickerDialog(
initialHour = currentZonedDateTime.orNow().hour,
initialMinute = currentZonedDateTime.orNow().minute,
onTimeSelect = { hour, minute ->
shouldShowDialog = false
onTimeSelect(hour, minute)
},
onDismissRequest = { shouldShowDialog = false },
is24Hour = is24Hour,
)
}
}

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.ui.platform.components.util
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
/**
* Returns a [VectorPainter] built from the given [id] to circumvent issues with painter resources
* recomposing unnecessarily.
*/
@Composable
fun rememberVectorPainter(
@DrawableRes id: Int,
): VectorPainter = rememberVectorPainter(
image = ImageVector.vectorResource(id),
)

View File

@@ -1,148 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.search
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
private const val SEARCH_TYPE: String = "search_type"
private const val SEARCH_TYPE_SEND_ALL: String = "search_type_sends_all"
private const val SEARCH_TYPE_SEND_TEXT: String = "search_type_sends_text"
private const val SEARCH_TYPE_SEND_FILE: String = "search_type_sends_file"
private const val SEARCH_TYPE_VAULT_ALL: String = "search_type_vault_all"
private const val SEARCH_TYPE_VAULT_LOGINS: String = "search_type_vault_logins"
private const val SEARCH_TYPE_VAULT_CARDS: String = "search_type_vault_cards"
private const val SEARCH_TYPE_VAULT_IDENTITIES: String = "search_type_vault_identities"
private const val SEARCH_TYPE_VAULT_SECURE_NOTES: String = "search_type_vault_secure_notes"
private const val SEARCH_TYPE_VAULT_COLLECTION: String = "search_type_vault_collection"
private const val SEARCH_TYPE_VAULT_NO_FOLDER: String = "search_type_vault_no_folder"
private const val SEARCH_TYPE_VAULT_FOLDER: String = "search_type_vault_folder"
private const val SEARCH_TYPE_VAULT_TRASH: String = "search_type_vault_trash"
private const val SEARCH_TYPE_VAULT_VERIFICATION_CODES: String =
"search_type_vault_verification_codes"
private const val SEARCH_TYPE_ID: String = "search_type_id"
private const val SEARCH_TYPE_VAULT_SSH_KEYS: String = "search_type_vault_ssh_keys"
private const val SEARCH_ROUTE_PREFIX: String = "search"
private const val SEARCH_ROUTE: String = "$SEARCH_ROUTE_PREFIX/{$SEARCH_TYPE}/{$SEARCH_TYPE_ID}"
/**
* Class to retrieve search arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class SearchArgs(
val type: SearchType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
type = determineSearchType(
searchTypeString = requireNotNull(savedStateHandle.get<String>(SEARCH_TYPE)),
id = savedStateHandle.get<String>(SEARCH_TYPE_ID),
),
)
}
/**
* Add search destinations to the nav graph.
*/
fun NavGraphBuilder.searchDestination(
onNavigateBack: () -> Unit,
onNavigateToEditSend: (sendId: String) -> Unit,
onNavigateToEditCipher: (args: VaultAddEditArgs) -> Unit,
onNavigateToViewCipher: (args: VaultItemArgs) -> Unit,
) {
composableWithSlideTransitions(
route = SEARCH_ROUTE,
arguments = listOf(
navArgument(SEARCH_TYPE) { type = NavType.StringType },
navArgument(SEARCH_TYPE_ID) {
type = NavType.StringType
nullable = true
},
),
) {
SearchScreen(
onNavigateBack = onNavigateBack,
onNavigateToEditSend = onNavigateToEditSend,
onNavigateToEditCipher = onNavigateToEditCipher,
onNavigateToViewCipher = onNavigateToViewCipher,
)
}
}
/**
* Navigate to the search screen.
*/
fun NavController.navigateToSearch(
searchType: SearchType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$SEARCH_ROUTE_PREFIX/${searchType.toTypeString()}/${searchType.toIdOrNull()}",
navOptions = navOptions,
)
}
private fun determineSearchType(
searchTypeString: String,
id: String?,
): SearchType =
when (searchTypeString) {
SEARCH_TYPE_SEND_ALL -> SearchType.Sends.All
SEARCH_TYPE_SEND_TEXT -> SearchType.Sends.Texts
SEARCH_TYPE_SEND_FILE -> SearchType.Sends.Files
SEARCH_TYPE_VAULT_ALL -> SearchType.Vault.All
SEARCH_TYPE_VAULT_LOGINS -> SearchType.Vault.Logins
SEARCH_TYPE_VAULT_CARDS -> SearchType.Vault.Cards
SEARCH_TYPE_VAULT_IDENTITIES -> SearchType.Vault.Identities
SEARCH_TYPE_VAULT_SECURE_NOTES -> SearchType.Vault.SecureNotes
SEARCH_TYPE_VAULT_COLLECTION -> SearchType.Vault.Collection(requireNotNull(id))
SEARCH_TYPE_VAULT_NO_FOLDER -> SearchType.Vault.NoFolder
SEARCH_TYPE_VAULT_FOLDER -> SearchType.Vault.Folder(requireNotNull(id))
SEARCH_TYPE_VAULT_TRASH -> SearchType.Vault.Trash
SEARCH_TYPE_VAULT_VERIFICATION_CODES -> SearchType.Vault.VerificationCodes
SEARCH_TYPE_VAULT_SSH_KEYS -> SearchType.Vault.SshKeys
else -> throw IllegalArgumentException("Invalid Search Type")
}
private fun SearchType.toTypeString(): String =
when (this) {
SearchType.Sends.All -> SEARCH_TYPE_SEND_ALL
SearchType.Sends.Files -> SEARCH_TYPE_SEND_FILE
SearchType.Sends.Texts -> SEARCH_TYPE_SEND_TEXT
SearchType.Vault.All -> SEARCH_TYPE_VAULT_ALL
SearchType.Vault.Cards -> SEARCH_TYPE_VAULT_CARDS
is SearchType.Vault.Collection -> SEARCH_TYPE_VAULT_COLLECTION
is SearchType.Vault.Folder -> SEARCH_TYPE_VAULT_FOLDER
SearchType.Vault.Identities -> SEARCH_TYPE_VAULT_IDENTITIES
SearchType.Vault.Logins -> SEARCH_TYPE_VAULT_LOGINS
SearchType.Vault.NoFolder -> SEARCH_TYPE_VAULT_NO_FOLDER
SearchType.Vault.SecureNotes -> SEARCH_TYPE_VAULT_SECURE_NOTES
SearchType.Vault.Trash -> SEARCH_TYPE_VAULT_TRASH
SearchType.Vault.VerificationCodes -> SEARCH_TYPE_VAULT_VERIFICATION_CODES
SearchType.Vault.SshKeys -> SEARCH_TYPE_VAULT_SSH_KEYS
}
private fun SearchType.toIdOrNull(): String? =
when (this) {
SearchType.Sends.All -> null
SearchType.Sends.Files -> null
SearchType.Sends.Texts -> null
SearchType.Vault.All -> null
SearchType.Vault.Cards -> null
is SearchType.Vault.Collection -> collectionId
is SearchType.Vault.Folder -> folderId
SearchType.Vault.Identities -> null
SearchType.Vault.Logins -> null
SearchType.Vault.NoFolder -> null
SearchType.Vault.SecureNotes -> null
SearchType.Vault.Trash -> null
SearchType.Vault.VerificationCodes -> null
SearchType.Vault.SshKeys -> null
}

View File

@@ -1,92 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import com.x8bit.bitwarden.ui.platform.feature.settings.about.aboutDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.about.navigateToAbout
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.accountSecurityDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.navigateToAccountSecurity
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.appearanceDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.navigateToAppearance
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.autoFillDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.blockAutoFillDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.navigateToBlockAutoFillScreen
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.navigateToAutoFill
import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther
import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.vaultSettingsDestination
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
const val SETTINGS_GRAPH_ROUTE: String = "settings_graph"
private const val SETTINGS_ROUTE: String = "settings"
/**
* Add settings destinations to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.settingsGraph(
navController: NavController,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPendingRequests: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) {
navigation(
startDestination = SETTINGS_ROUTE,
route = SETTINGS_GRAPH_ROUTE,
) {
composableWithRootPushTransitions(
route = SETTINGS_ROUTE,
) {
SettingsScreen(
onNavigateToAbout = { navController.navigateToAbout() },
onNavigateToAccountSecurity = { navController.navigateToAccountSecurity() },
onNavigateToAppearance = { navController.navigateToAppearance() },
onNavigateToAutoFill = { navController.navigateToAutoFill() },
onNavigateToOther = { navController.navigateToOther() },
onNavigateToVault = { navController.navigateToVaultSettings() },
)
}
aboutDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToFlightRecorder = onNavigateToFlightRecorder,
onNavigateToRecordedLogs = onNavigateToRecordedLogs,
)
accountSecurityDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
onNavigateToPendingRequests = onNavigateToPendingRequests,
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
)
appearanceDestination(onNavigateBack = { navController.popBackStack() })
autoFillDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() },
onNavigateToSetupAutofill = onNavigateToSetupAutoFillScreen,
)
otherDestination(onNavigateBack = { navController.popBackStack() })
vaultSettingsDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToExportVault = onNavigateToExportVault,
onNavigateToFolders = onNavigateToFolders,
onNavigateToImportLogins = onNavigateToImportLogins,
)
blockAutoFillDestination(onNavigateBack = { navController.popBackStack() })
}
}
/**
* Navigate to the settings screen.
*/
fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) {
navigate(SETTINGS_GRAPH_ROUTE, navOptions)
}

View File

@@ -1,34 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.about
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
private const val ABOUT_ROUTE = "settings_about"
/**
* Add settings destinations to the nav graph.
*/
fun NavGraphBuilder.aboutDestination(
onNavigateBack: () -> Unit,
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
) {
composableWithPushTransitions(
route = ABOUT_ROUTE,
) {
AboutScreen(
onNavigateBack = onNavigateBack,
onNavigateToFlightRecorder = onNavigateToFlightRecorder,
onNavigateToRecordedLogs = onNavigateToRecordedLogs,
)
}
}
/**
* Navigate to the about screen.
*/
fun NavController.navigateToAbout(navOptions: NavOptions? = null) {
navigate(ABOUT_ROUTE, navOptions)
}

View File

@@ -1,56 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val FINGERPRINT: String = "fingerprint"
private const val LOGIN_APPROVAL_PREFIX = "login_approval"
private const val LOGIN_APPROVAL_ROUTE = "$LOGIN_APPROVAL_PREFIX?$FINGERPRINT={$FINGERPRINT}"
/**
* Class to retrieve login approval arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class LoginApprovalArgs(val fingerprint: String?) {
constructor(savedStateHandle: SavedStateHandle) : this(
fingerprint = savedStateHandle.get<String>(FINGERPRINT),
)
}
/**
* Add login approval destinations to the nav graph.
*/
fun NavGraphBuilder.loginApprovalDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = LOGIN_APPROVAL_ROUTE,
arguments = listOf(
navArgument(FINGERPRINT) {
type = NavType.StringType
nullable = true
defaultValue = null
},
),
) {
LoginApprovalScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the Login Approval screen.
*/
fun NavController.navigateToLoginApproval(
fingerprint: String?,
navOptions: NavOptions? = null,
) {
navigate("$LOGIN_APPROVAL_PREFIX?$FINGERPRINT=$fingerprint", navOptions)
}

View File

@@ -1,28 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
private const val APPEARANCE_ROUTE = "settings_appearance"
/**
* Add settings destinations to the nav graph.
*/
fun NavGraphBuilder.appearanceDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions(
route = APPEARANCE_ROUTE,
) {
AppearanceScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Navigate to the appearance screen.
*/
fun NavController.navigateToAppearance(navOptions: NavOptions? = null) {
navigate(APPEARANCE_ROUTE, navOptions)
}

View File

@@ -1,377 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.autofill
import android.content.res.Resources
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.chrome.ChromeAutofillSettingsCard
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.displayLabel
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.collections.immutable.toImmutableList
/**
* Displays the auto-fill screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AutoFillScreen(
onNavigateBack: () -> Unit,
viewModel: AutoFillViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
onNavigateToBlockAutoFillScreen: () -> Unit,
onNavigateToSetupAutofill: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
val resources = context.resources
var shouldShowAutofillFallbackDialog by rememberSaveable { mutableStateOf(false) }
EventsEffect(viewModel = viewModel) { event ->
when (event) {
AutoFillEvent.NavigateBack -> onNavigateBack.invoke()
AutoFillEvent.NavigateToAccessibilitySettings -> {
intentManager.startSystemAccessibilitySettingsActivity()
}
AutoFillEvent.NavigateToAutofillSettings -> {
val isSuccess = intentManager.startSystemAutofillSettingsActivity()
shouldShowAutofillFallbackDialog = !isSuccess
}
is AutoFillEvent.ShowToast -> {
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
}
AutoFillEvent.NavigateToBlockAutoFill -> {
onNavigateToBlockAutoFillScreen()
}
AutoFillEvent.NavigateToSettings -> {
intentManager.startCredentialManagerSettings(context)
}
AutoFillEvent.NavigateToSetupAutofill -> onNavigateToSetupAutofill()
is AutoFillEvent.NavigateToChromeAutofillSettings -> {
intentManager.startChromeAutofillSettingsActivity(
releaseChannel = event.releaseChannel,
)
}
}
}
if (shouldShowAutofillFallbackDialog) {
BitwardenBasicDialog(
title = null,
message = stringResource(id = R.string.bitwarden_autofill_go_to_settings),
onDismissRequest = { shouldShowAutofillFallbackDialog = false },
)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.autofill),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.BackClick) }
},
)
},
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
AnimatedVisibility(
visible = state.showAutofillActionCard,
label = "AutofillActionCard",
exit = actionCardExitAnimation(),
) {
BitwardenActionCard(
cardTitle = stringResource(R.string.turn_on_autofill),
actionText = stringResource(R.string.get_started),
onActionClick = remember(viewModel) {
{
viewModel.trySendAction(AutoFillAction.AutofillActionCardCtaClick)
}
},
onDismissClick = remember(viewModel) {
{
viewModel.trySendAction(AutoFillAction.DismissShowAutofillActionCard)
}
},
leadingContent = {
NotificationBadge(notificationCount = 1)
},
modifier = Modifier
.standardHorizontalMargin()
.padding(bottom = 16.dp),
)
}
BitwardenListHeaderText(
label = stringResource(id = R.string.autofill),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.autofill_services),
supportingText = stringResource(id = R.string.autofill_services_explanation_long),
isChecked = state.isAutoFillServicesEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("AutofillServicesSwitch")
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
if (state.showInlineAutofillOption) {
BitwardenSwitch(
label = stringResource(id = R.string.inline_autofill),
supportingText = stringResource(
id = R.string.use_inline_autofill_explanation_long,
),
isChecked = state.isUseInlineAutoFillEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(it)) }
},
enabled = state.canInteractWithInlineAutofillToggle,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("InlineAutofillSwitch")
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
if (state.chromeAutofillSettingsOptions.isNotEmpty()) {
ChromeAutofillSettingsCard(
options = state.chromeAutofillSettingsOptions,
onOptionClicked = remember(viewModel) {
{
viewModel.trySendAction(AutoFillAction.ChromeAutofillSelected(it))
}
},
enabled = state.isAutoFillServicesEnabled,
)
Spacer(modifier = Modifier.height(8.dp))
}
if (state.showPasskeyManagementRow) {
BitwardenExternalLinkRow(
text = stringResource(id = R.string.passkey_management),
description = stringResource(
id = R.string.passkey_management_explanation_long,
),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.PasskeyManagementClick) }
},
dialogTitle = stringResource(id = R.string.continue_to_device_settings),
dialogMessage = stringResource(
id = R.string.set_bitwarden_as_passkey_manager_description,
),
withDivider = false,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
AccessibilityAutofillSwitch(
isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick) }
},
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.copy_totp_automatically),
supportingText = stringResource(id = R.string.copy_totp_automatically_description),
isChecked = state.isCopyTotpAutomaticallyEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.CopyTotpAutomaticallyClick(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("CopyTotpAutomaticallySwitch")
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.ask_to_add_login),
supportingText = stringResource(id = R.string.ask_to_add_login_description),
isChecked = state.isAskToAddLoginEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("AskToAddLoginSwitch")
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
DefaultUriMatchTypeRow(
selectedUriMatchType = state.defaultUriMatchType,
onUriMatchTypeSelect = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.DefaultUriMatchTypeSelect(it)) }
},
modifier = Modifier
.testTag("DefaultUriMatchDetectionChooser")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextRow(
text = stringResource(id = R.string.block_auto_fill),
description = stringResource(
id = R.string.auto_fill_will_not_be_offered_for_these_ur_is,
),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@Composable
private fun AccessibilityAutofillSwitch(
isAccessibilityAutoFillEnabled: Boolean,
onCheckedChange: () -> Unit,
modifier: Modifier = Modifier,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) }
BitwardenSwitch(
label = stringResource(id = R.string.accessibility),
supportingText = stringResource(id = R.string.accessibility_description5),
isChecked = isAccessibilityAutoFillEnabled,
onCheckedChange = {
if (isAccessibilityAutoFillEnabled) {
onCheckedChange()
} else {
shouldShowDialog = true
}
},
cardStyle = CardStyle.Full,
modifier = modifier.testTag(tag = "AccessibilityAutofillSwitch"),
)
if (shouldShowDialog) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.accessibility_service_disclosure),
message = stringResource(id = R.string.accessibility_disclosure_text),
confirmButtonText = stringResource(id = R.string.accept),
dismissButtonText = stringResource(id = R.string.decline),
onConfirmClick = {
onCheckedChange()
shouldShowDialog = false
},
onDismissClick = { shouldShowDialog = false },
onDismissRequest = { shouldShowDialog = false },
)
}
}
@Composable
private fun DefaultUriMatchTypeRow(
selectedUriMatchType: UriMatchType,
onUriMatchTypeSelect: (UriMatchType) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
BitwardenMultiSelectButton(
label = stringResource(id = R.string.default_uri_match_detection),
options = UriMatchType.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = selectedUriMatchType.displayLabel(),
onOptionSelected = { selectedOption ->
onUriMatchTypeSelect(
UriMatchType
.entries
.first { it.displayLabel.toString(resources) == selectedOption },
)
},
supportingText = stringResource(id = R.string.default_uri_match_detection_description),
cardStyle = CardStyle.Full,
modifier = modifier,
)
}

View File

@@ -1,42 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.chrome.model
import android.os.Parcelable
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
import kotlinx.parcelize.Parcelize
/**
* Models an option for an option for each type of supported version of Chrome to enable
* third party autofill. Each [ChromeAutofillSettingsOption] contains the associated
* [ChromeReleaseChannel], the [optionText] to display in any UI component, and
* whether or not the third party autofill [isEnabled].
*/
@Parcelize
sealed class ChromeAutofillSettingsOption(val isEnabled: Boolean) : Parcelable {
abstract val chromeReleaseChannel: ChromeReleaseChannel
abstract val optionText: Text
/**
* Represents the stable Chrome release channel.
*/
@Parcelize
data class Stable(val enabled: Boolean) : ChromeAutofillSettingsOption(isEnabled = enabled) {
override val chromeReleaseChannel: ChromeReleaseChannel
get() = ChromeReleaseChannel.STABLE
override val optionText: Text
get() = R.string.use_chrome_autofill_integration.asText()
}
/**
* Represents the beta Chrome release channel.
*/
@Parcelize
data class Beta(val enabled: Boolean) : ChromeAutofillSettingsOption(isEnabled = enabled) {
override val chromeReleaseChannel: ChromeReleaseChannel
get() = ChromeReleaseChannel.BETA
override val optionText: Text
get() = R.string.use_chrome_beta_autofill_integration.asText()
}
}

View File

@@ -1,30 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
private const val FLIGHT_RECORDER_ROUTE = "flight_recorder_config"
/**
* Add flight recorder destination to the nav graph.
*/
fun NavGraphBuilder.flightRecorderDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions(
route = FLIGHT_RECORDER_ROUTE,
) {
FlightRecorderScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the flight recorder screen.
*/
fun NavController.navigateToFlightRecorder(navOptions: NavOptions? = null) {
navigate(FLIGHT_RECORDER_ROUTE, navOptions)
}

View File

@@ -1,30 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.recordedLogs
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
private const val FLIGHT_RECORDER_RECORDED_LOGS_ROUTE = "flight_recorder_recorded_logs"
/**
* Add recorded logs destination to the nav graph.
*/
fun NavGraphBuilder.recordedLogsDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions(
route = FLIGHT_RECORDER_RECORDED_LOGS_ROUTE,
) {
RecordedLogsScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the flight recorder recorded logs screen.
*/
fun NavController.navigateToRecordedLogs(navOptions: NavOptions? = null) {
navigate(FLIGHT_RECORDER_RECORDED_LOGS_ROUTE, navOptions)
}

View File

@@ -1,93 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType
private const val ADD_TYPE: String = "add"
private const val EDIT_TYPE: String = "edit"
private const val EDIT_ITEM_ID: String = "folder_edit_id"
private const val PARENT_FOLDER_NAME: String = "parent_folder_name"
private const val ADD_EDIT_ITEM_PREFIX: String = "folder_add_edit_item"
private const val ADD_EDIT_ITEM_TYPE: String = "folder_add_edit_type"
private const val ADD_EDIT_ITEM_ROUTE: String =
"$ADD_EDIT_ITEM_PREFIX/{$ADD_EDIT_ITEM_TYPE}" +
"?$EDIT_ITEM_ID={$EDIT_ITEM_ID}&$PARENT_FOLDER_NAME={$PARENT_FOLDER_NAME}"
/**
* Class to retrieve folder add & edit arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class FolderAddEditArgs(
val folderAddEditType: FolderAddEditType,
val parentFolderName: String?,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
folderAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) {
ADD_TYPE -> FolderAddEditType.AddItem
EDIT_TYPE -> FolderAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
else -> throw IllegalStateException("Unknown FolderAddEditType.")
},
parentFolderName = savedStateHandle[PARENT_FOLDER_NAME],
)
}
/**
* Add the folder add & edit screen to the nav graph.
*/
fun NavGraphBuilder.folderAddEditDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ADD_EDIT_ITEM_ROUTE,
arguments = listOf(
navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType },
navArgument(EDIT_ITEM_ID) {
nullable = true
type = NavType.StringType
},
navArgument(PARENT_FOLDER_NAME) {
nullable = true
type = NavType.StringType
},
),
) {
FolderAddEditScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Navigate to the folder add & edit screen.
*/
fun NavController.navigateToFolderAddEdit(
folderAddEditType: FolderAddEditType,
parentFolderName: String? = null,
navOptions: NavOptions? = null,
) {
navigate(
route = "$ADD_EDIT_ITEM_PREFIX/${folderAddEditType.toTypeString()}" +
"?$EDIT_ITEM_ID=${folderAddEditType.toIdOrNull()}" +
"&$PARENT_FOLDER_NAME=$parentFolderName",
navOptions = navOptions,
)
}
private fun FolderAddEditType.toTypeString(): String =
when (this) {
is FolderAddEditType.AddItem -> ADD_TYPE
is FolderAddEditType.EditItem -> EDIT_TYPE
}
private fun FolderAddEditType.toIdOrNull(): String? =
when (this) {
is FolderAddEditType.AddItem -> null
is FolderAddEditType.EditItem -> folderId
}

View File

@@ -1,28 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.other
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
private const val OTHER_ROUTE = "settings_other"
/**
* Add settings destinations to the nav graph.
*/
fun NavGraphBuilder.otherDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions(
route = OTHER_ROUTE,
) {
OtherScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Navigate to the about screen.
*/
fun NavController.navigateToOther(navOptions: NavOptions? = null) {
navigate(OTHER_ROUTE, navOptions)
}

View File

@@ -1,80 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.model
import android.os.Parcelable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.NavigationItem
import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE
import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_GRAPH_ROUTE
import com.x8bit.bitwarden.ui.tools.feature.send.SEND_GRAPH_ROUTE
import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_GRAPH_ROUTE
import kotlinx.parcelize.Parcelize
/**
* Represents the different tabs available in the navigation bar
* for the unlocked portion of the vault.
*
* Each tab is modeled with properties that provide information on:
* - Regular icon resource
* - Icon resource when selected
* and other essential UI and navigational data.
*/
@Parcelize
sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable {
/**
* Show the Generator screen.
*/
@Parcelize
data object Generator : VaultUnlockedNavBarTab() {
override val iconResSelected get() = R.drawable.ic_generator_filled
override val iconRes get() = R.drawable.ic_generator
override val labelRes get() = R.string.generator
override val contentDescriptionRes get() = R.string.generator
override val route get() = GENERATOR_GRAPH_ROUTE
override val testTag get() = "GeneratorTab"
override val notificationCount get() = 0
}
/**
* Show the Send screen.
*/
@Parcelize
data object Send : VaultUnlockedNavBarTab() {
override val iconResSelected get() = R.drawable.ic_send_filled
override val iconRes get() = R.drawable.ic_send
override val labelRes get() = R.string.send
override val contentDescriptionRes get() = R.string.send
override val route get() = SEND_GRAPH_ROUTE
override val testTag get() = "SendTab"
override val notificationCount get() = 0
}
/**
* Show the Vault screen.
*/
@Parcelize
data class Vault(
override val labelRes: Int,
override val contentDescriptionRes: Int,
) : VaultUnlockedNavBarTab() {
override val iconResSelected get() = R.drawable.ic_vault_filled
override val iconRes get() = R.drawable.ic_vault
override val route get() = VAULT_GRAPH_ROUTE
override val testTag get() = "VaultTab"
override val notificationCount get() = 0
}
/**
* Show the Settings screen.
*/
@Parcelize
data class Settings(
override val notificationCount: Int = 0,
) : VaultUnlockedNavBarTab() {
override val iconResSelected get() = R.drawable.ic_settings_filled
override val iconRes get() = R.drawable.ic_settings
override val labelRes get() = R.string.settings
override val contentDescriptionRes get() = R.string.settings
override val route get() = SETTINGS_GRAPH_ROUTE
override val testTag get() = "SettingsTab"
}
}

View File

@@ -1,41 +0,0 @@
package com.x8bit.bitwarden.ui.platform.manager.snackbar
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onCompletion
/**
* The default implementation of the [SnackbarRelayManager] interface.
*/
class SnackbarRelayManagerImpl : SnackbarRelayManager {
private val mutableSnackbarRelayMap =
mutableMapOf<SnackbarRelay, MutableSharedFlow<BitwardenSnackbarData?>>()
override fun sendSnackbarData(data: BitwardenSnackbarData, relay: SnackbarRelay) {
getSnackbarDataFlowInternal(relay).tryEmit(data)
}
override fun getSnackbarDataFlow(relay: SnackbarRelay): Flow<BitwardenSnackbarData> =
getSnackbarDataFlowInternal(relay)
.onCompletion {
// when the subscription is ended, remove the relay from the map.
mutableSnackbarRelayMap.remove(relay)
}
.filterNotNull()
@OptIn(ExperimentalCoroutinesApi::class)
override fun clearRelayBuffer(relay: SnackbarRelay) {
getSnackbarDataFlowInternal(relay).resetReplayCache()
}
private fun getSnackbarDataFlowInternal(
relay: SnackbarRelay,
): MutableSharedFlow<BitwardenSnackbarData?> =
mutableSnackbarRelayMap.getOrPut(relay) {
bufferedMutableSharedFlow(replay = 1)
}
}

View File

@@ -1,31 +0,0 @@
package com.x8bit.bitwarden.ui.platform.util
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
/**
* Creates a new [SpanStyle] from the specified [color] and [textStyle].
*/
fun spanStyleOf(
color: Color,
textStyle: TextStyle,
): SpanStyle =
SpanStyle(
color = color,
fontSize = textStyle.fontSize,
fontWeight = textStyle.fontWeight,
fontStyle = textStyle.fontStyle,
fontSynthesis = textStyle.fontSynthesis,
fontFamily = textStyle.fontFamily,
fontFeatureSettings = textStyle.fontFeatureSettings,
letterSpacing = textStyle.letterSpacing,
baselineShift = textStyle.baselineShift,
textGeometricTransform = textStyle.textGeometricTransform,
localeList = textStyle.localeList,
background = textStyle.background,
textDecoration = textStyle.textDecoration,
shadow = textStyle.shadow,
platformStyle = textStyle.platformStyle?.spanStyle,
drawStyle = textStyle.drawStyle,
)

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.ui.platform.util
import java.time.Clock
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAccessor
/**
* Converts the [TemporalAccessor] to a formatted string based on the provided pattern and timezone.
*/
fun TemporalAccessor.toFormattedPattern(
pattern: String,
zone: ZoneId,
): String = DateTimeFormatter.ofPattern(pattern).withZone(zone).format(this)
/**
* Converts the [TemporalAccessor] to a formatted string based on the provided pattern and timezone.
*/
fun TemporalAccessor.toFormattedPattern(
pattern: String,
clock: Clock = Clock.systemDefaultZone(),
): String = toFormattedPattern(pattern = pattern, zone = clock.zone)

View File

@@ -1,8 +0,0 @@
package com.x8bit.bitwarden.ui.platform.util
import java.time.ZonedDateTime
/**
* Returns the current [ZonedDateTime] or [ZonedDateTime.now] if the current one is null.
*/
fun ZonedDateTime?.orNow(): ZonedDateTime = this ?: ZonedDateTime.now()

View File

@@ -1,102 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.generator
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
/**
* The functions below pertain to entry into the [GeneratorScreen].
*/
private const val GENERATOR_MODAL_ROUTE_PREFIX: String = "generator_modal"
private const val GENERATOR_MODE_TYPE: String = "generator_mode_type"
private const val GENERATOR_WEBSITE: String = "generator_website"
private const val USERNAME_GENERATOR: String = "username_generator"
private const val PASSWORD_GENERATOR: String = "password_generator"
const val GENERATOR_ROUTE: String = "generator"
private const val GENERATOR_MODAL_ROUTE: String =
"$GENERATOR_MODAL_ROUTE_PREFIX/{$GENERATOR_MODE_TYPE}?$GENERATOR_WEBSITE={$GENERATOR_WEBSITE}"
/**
* Class to retrieve vault item listing arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class GeneratorArgs(
val type: GeneratorMode,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
type = when (savedStateHandle.get<String>(GENERATOR_MODE_TYPE)) {
USERNAME_GENERATOR -> GeneratorMode.Modal.Username(
website = savedStateHandle[GENERATOR_WEBSITE],
)
PASSWORD_GENERATOR -> GeneratorMode.Modal.Password
else -> GeneratorMode.Default
},
)
}
/**
* Add generator destination to the root nav graph.
*/
fun NavGraphBuilder.generatorDestination(
onNavigateToPasswordHistory: () -> Unit,
onDimNavBarRequest: (Boolean) -> Unit,
) {
composable(GENERATOR_ROUTE) {
GeneratorScreen(
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
onNavigateBack = {},
onDimNavBarRequest = onDimNavBarRequest,
)
}
}
/**
* Add the generator modal destination to the nav graph.
*/
fun NavGraphBuilder.generatorModalDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = GENERATOR_MODAL_ROUTE,
arguments = listOf(
navArgument(GENERATOR_MODE_TYPE) { type = NavType.StringType },
navArgument(GENERATOR_WEBSITE) {
type = NavType.StringType
nullable = true
},
),
) {
GeneratorScreen(
onNavigateToPasswordHistory = {},
onNavigateBack = onNavigateBack,
onDimNavBarRequest = {},
)
}
}
/**
* Navigate to the generator screen in the given mode with the corresponding website, if one exists.
*/
fun NavController.navigateToGeneratorModal(
mode: GeneratorMode.Modal,
navOptions: NavOptions? = null,
) {
val generatorModeType = when (mode) {
GeneratorMode.Modal.Password -> PASSWORD_GENERATOR
is GeneratorMode.Modal.Username -> USERNAME_GENERATOR
}
val website = (mode as? GeneratorMode.Modal.Username)?.website
navigate(
route = "$GENERATOR_MODAL_ROUTE_PREFIX/$generatorModeType?$GENERATOR_WEBSITE=$website",
navOptions = navOptions,
)
}

View File

@@ -1,86 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode
private const val DEFAULT_MODE: String = "default"
private const val ITEM_MODE: String = "item"
private const val PASSWORD_HISTORY_PREFIX: String = "password_history"
private const val PASSWORD_HISTORY_MODE: String = "password_history_mode"
private const val PASSWORD_HISTORY_ITEM_ID: String = "password_history_id"
private const val PASSWORD_HISTORY_ROUTE: String =
PASSWORD_HISTORY_PREFIX +
"/{$PASSWORD_HISTORY_MODE}" +
"?$PASSWORD_HISTORY_ITEM_ID={$PASSWORD_HISTORY_ITEM_ID}"
/**
* Class to retrieve password history arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class PasswordHistoryArgs(
val passwordHistoryMode: GeneratorPasswordHistoryMode,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
passwordHistoryMode = when (requireNotNull(savedStateHandle[PASSWORD_HISTORY_MODE])) {
DEFAULT_MODE -> GeneratorPasswordHistoryMode.Default
ITEM_MODE -> GeneratorPasswordHistoryMode.Item(
requireNotNull(savedStateHandle[PASSWORD_HISTORY_ITEM_ID]),
)
else -> throw IllegalStateException("Unknown VaultAddEditType.")
},
)
}
/**
* Add password history destination to the graph.
*/
fun NavGraphBuilder.passwordHistoryDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = PASSWORD_HISTORY_ROUTE,
arguments = listOf(
navArgument(PASSWORD_HISTORY_MODE) { type = NavType.StringType },
),
) {
PasswordHistoryScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the Password History Screen.
*/
fun NavController.navigateToPasswordHistory(
passwordHistoryMode: GeneratorPasswordHistoryMode,
navOptions: NavOptions? = null,
) {
navigate(
route = "$PASSWORD_HISTORY_PREFIX/${passwordHistoryMode.toModeString()}" +
"?$PASSWORD_HISTORY_ITEM_ID=${passwordHistoryMode.toIdOrNull()}",
navOptions = navOptions,
)
}
private fun GeneratorPasswordHistoryMode.toModeString(): String =
when (this) {
is GeneratorPasswordHistoryMode.Default -> DEFAULT_MODE
is GeneratorPasswordHistoryMode.Item -> ITEM_MODE
}
private fun GeneratorPasswordHistoryMode.toIdOrNull(): String? =
when (this) {
is GeneratorPasswordHistoryMode.Default -> null
is GeneratorPasswordHistoryMode.Item -> itemId
}

View File

@@ -1,413 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import android.Manifest
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeader
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
/**
* Content view for the [AddSendScreen].
*/
@Suppress("LongMethod")
@Composable
fun AddSendContent(
state: AddSendState.ViewState.Content,
policyDisablesSend: Boolean,
policySendOptionsInEffect: Boolean,
isAddMode: Boolean,
isShared: Boolean,
addSendHandlers: AddSendHandlers,
permissionsManager: PermissionsManager,
modifier: Modifier = Modifier,
) {
val chooseFileCameraPermissionLauncher = permissionsManager.getLauncher { isGranted ->
addSendHandlers.onChooseFileClick(isGranted)
}
Column(
modifier = modifier.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
if (policyDisablesSend) {
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag("SendPolicyInEffectLabel"),
)
Spacer(modifier = Modifier.height(16.dp))
}
if (policySendOptionsInEffect) {
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.send_options_policy_in_effect),
modifier = Modifier
.testTag(tag = "SendPolicyInEffectLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
if (state.selectedType is AddSendState.ViewState.Content.SendType.Text) {
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
label = stringResource(id = R.string.text_to_share),
readOnly = policyDisablesSend,
value = state.selectedType.input,
singleLine = false,
onValueChange = addSendHandlers.onTextChange,
textFieldTestTag = "SendTextContentEntry",
cardStyle = CardStyle.Full,
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
BitwardenListHeaderText(
label = stringResource(id = R.string.send_details),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
label = stringResource(id = R.string.send_name_required),
readOnly = policyDisablesSend,
value = state.common.name,
onValueChange = addSendHandlers.onNameChange,
textFieldTestTag = "SendNameEntry",
cardStyle = CardStyle.Full,
)
when (val type = state.selectedType) {
is AddSendState.ViewState.Content.SendType.File -> {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.file),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
if (isShared) {
Text(
text = type.name.orEmpty(),
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.max_file_size),
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
} else if (isAddMode) {
type.name?.let {
Text(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardStyle(cardStyle = CardStyle.Full, paddingHorizontal = 16.dp)
.testTag(tag = "SendCurrentFileNameLabel"),
text = it,
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodyLarge,
)
Spacer(modifier = Modifier.height(8.dp))
}
BitwardenOutlinedButton(
label = stringResource(id = R.string.choose_file),
onClick = {
if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
addSendHandlers.onChooseFileClick(true)
} else {
chooseFileCameraPermissionLauncher.launch(
Manifest.permission.CAMERA,
)
}
},
modifier = Modifier
.testTag(tag = "SendChooseFileButton")
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
Text(
text = stringResource(id = R.string.max_file_size),
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
} else {
Row(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardStyle(cardStyle = CardStyle.Full, paddingHorizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = type.name.orEmpty(),
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = type.displaySize.orEmpty(),
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodyMedium,
)
}
}
}
is AddSendState.ViewState.Content.SendType.Text -> {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
modifier = Modifier
.testTag(tag = "SendHideTextByDefaultToggle")
.fillMaxWidth()
.standardHorizontalMargin(),
label = stringResource(id = R.string.hide_text_by_default),
isChecked = type.isHideByDefaultChecked,
onCheckedChange = addSendHandlers.onIsHideByDefaultToggle,
readOnly = policyDisablesSend,
cardStyle = CardStyle.Full,
)
}
}
Spacer(modifier = Modifier.height(height = 8.dp))
if (isAddMode) {
SendDeletionDateChooser(
modifier = Modifier
.testTag("SendDeletionOptionsPicker")
.fillMaxWidth()
.standardHorizontalMargin(),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
onDateSelect = addSendHandlers.onDeletionDateChange,
isEnabled = !policyDisablesSend,
)
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardStyle(cardStyle = CardStyle.Full, paddingVertical = 0.dp),
) {
AddSendCustomDateChooser(
modifier = Modifier
.testTag("SendCustomDeletionDatePicker")
.fillMaxWidth(),
dateLabel = stringResource(id = R.string.deletion_date),
timeLabel = stringResource(id = R.string.deletion_time),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
isEnabled = !policyDisablesSend,
onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) },
)
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = R.string.deletion_date_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
}
}
AddSendOptions(
state = state,
sendRestrictionPolicy = policyDisablesSend,
isAddMode = isAddMode,
addSendHandlers = addSendHandlers,
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
/**
* Displays a collapsable set of new send options.
*
* @param state The content state.
* @param sendRestrictionPolicy When `true`, indicates that there's a policy preventing the user
* from editing or creating sends.
* @param isAddMode When `true`, indicates that we are creating a new send and `false` when editing
* an existing send.
* @param addSendHandlers THe handlers various events.
*/
@Suppress("LongMethod")
@Composable
private fun AddSendOptions(
state: AddSendState.ViewState.Content,
sendRestrictionPolicy: Boolean,
isAddMode: Boolean,
addSendHandlers: AddSendHandlers,
) {
var isExpanded by rememberSaveable { mutableStateOf(false) }
BitwardenExpandingHeader(
isExpanded = isExpanded,
onClick = { isExpanded = !isExpanded },
modifier = Modifier
.testTag(tag = "SendShowHideOptionsButton")
.standardHorizontalMargin()
.fillMaxWidth(),
)
// Hide all content if not expanded:
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
modifier = Modifier.clipToBounds(),
) {
Column {
BitwardenStepper(
label = stringResource(id = R.string.maximum_access_count),
supportingContent = {
Text(
text = stringResource(id = R.string.maximum_access_count_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
state.common.currentAccessCount.takeUnless { isAddMode }?.let {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = R.string.current_access_count
.asText()
.concat(": ".asText(), it.toString().asText())
.invoke(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
}
},
value = state.common.maxAccessCount,
onValueChange = addSendHandlers.onMaxAccessCountChange,
isDecrementEnabled = state.common.maxAccessCount != null && !sendRestrictionPolicy,
isIncrementEnabled = !sendRestrictionPolicy,
range = 0..Int.MAX_VALUE,
textFieldReadOnly = sendRestrictionPolicy,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("SendMaxAccessCountEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.new_password),
supportingText = stringResource(id = R.string.password_info),
readOnly = sendRestrictionPolicy,
value = state.common.passwordInput,
onValueChange = addSendHandlers.onPasswordChange,
passwordFieldTestTag = "SendNewPasswordEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
modifier = Modifier
.testTag("SendHideEmailSwitch")
.fillMaxWidth()
.standardHorizontalMargin(),
label = stringResource(id = R.string.hide_email),
isChecked = state.common.isHideEmailChecked,
onCheckedChange = addSendHandlers.onHideEmailToggle,
readOnly = sendRestrictionPolicy,
enabled = state.common.isHideEmailChecked || state.common.isHideEmailAddressEnabled,
cardStyle = CardStyle.Full,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.private_notes),
readOnly = sendRestrictionPolicy,
value = state.common.noteInput,
singleLine = false,
onValueChange = addSendHandlers.onNoteChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}

View File

@@ -1,88 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenDateSelectButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimeSelectButton
import com.x8bit.bitwarden.ui.platform.util.orNow
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
/**
* Displays a UI for selecting a customizable date and time.
*
* @param dateLabel The display label for the date selection field.
* @param timeLabel The display label for the time selection field.
* @param currentZonedDateTime The currently selected time, `null` when no time is selected yet.
* @param dateFormatPattern The pattern to use when displaying the date.
* @param timeFormatPattern The pattern for displaying the time.
* @param onDateSelect The callback for being notified of updates to the selected date and time.
* This will only be `null` when there is no selected time.
* @param isEnabled Whether the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
*/
@Composable
fun AddSendCustomDateChooser(
dateLabel: String,
timeLabel: String,
currentZonedDateTime: ZonedDateTime?,
dateFormatPattern: String,
timeFormatPattern: String,
onDateSelect: (ZonedDateTime?) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
) {
// This tracks the date component (year, month, and day) and ignores lower level
// components.
var date: ZonedDateTime? by remember { mutableStateOf(currentZonedDateTime) }
// This tracks just the time component (hours and minutes) and ignores the higher level
// components. 0 representing midnight and counting up from there.
var timeMillis: Long by remember {
mutableLongStateOf(
currentZonedDateTime.orNow().let {
it.hour.hours.inWholeMilliseconds + it.minute.minutes.inWholeMilliseconds
},
)
}
val derivedDateTimeMillis: ZonedDateTime? by remember {
derivedStateOf { date?.plus(timeMillis, ChronoUnit.MILLIS) }
}
Row(
modifier = modifier,
) {
BitwardenDateSelectButton(
modifier = Modifier.weight(1f),
label = dateLabel,
formatPattern = dateFormatPattern,
currentZonedDateTime = currentZonedDateTime,
isEnabled = isEnabled,
onDateSelect = {
date = it
onDateSelect(derivedDateTimeMillis)
},
cardStyle = null,
)
BitwardenTimeSelectButton(
modifier = Modifier.weight(1f),
label = timeLabel,
formatPattern = timeFormatPattern,
currentZonedDateTime = currentZonedDateTime,
isEnabled = isEnabled,
onTimeSelect = { hour, minute ->
timeMillis = hour.hours.inWholeMilliseconds + minute.minutes.inWholeMilliseconds
onDateSelect(derivedDateTimeMillis)
},
cardStyle = null,
)
}
}

View File

@@ -1,132 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.toImmutableList
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
/**
* Displays UX for choosing deletion date of a send.
*/
@Suppress("LongMethod")
@Composable
fun SendDeletionDateChooser(
currentZonedDateTime: ZonedDateTime,
dateFormatPattern: String,
timeFormatPattern: String,
onDateSelect: (ZonedDateTime) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
) {
val defaultOption = DeletionOptions.SEVEN_DAYS
val options = DeletionOptions.entries.associateWith { it.text() }
var selectedOption: DeletionOptions by rememberSaveable { mutableStateOf(defaultOption) }
Column(
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardStyle(cardStyle = CardStyle.Full, paddingVertical = 0.dp),
) {
BitwardenMultiSelectButton(
label = stringResource(id = R.string.deletion_date),
isEnabled = isEnabled,
options = options.values.toImmutableList(),
selectedOption = selectedOption.text(),
onOptionSelected = { selected ->
selectedOption = options.entries.first { it.value == selected }.key
if (selectedOption != DeletionOptions.CUSTOM) {
onDateSelect(
// Add the appropriate milliseconds offset based on the selected option
ZonedDateTime.now().plus(selectedOption.offsetMillis, ChronoUnit.MILLIS),
)
}
},
insets = PaddingValues(top = 6.dp, bottom = 4.dp),
cardStyle = null,
)
AnimatedVisibility(visible = selectedOption == DeletionOptions.CUSTOM) {
Column {
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
AddSendCustomDateChooser(
dateLabel = stringResource(id = R.string.deletion_date),
timeLabel = stringResource(id = R.string.deletion_time),
currentZonedDateTime = currentZonedDateTime,
dateFormatPattern = dateFormatPattern,
timeFormatPattern = timeFormatPattern,
onDateSelect = { onDateSelect(requireNotNull(it)) },
isEnabled = isEnabled,
)
}
}
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = R.string.deletion_date_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
}
}
private enum class DeletionOptions(
val text: Text,
val offsetMillis: Long,
) {
ONE_HOUR(
text = R.string.one_hour.asText(),
offsetMillis = 1.hours.inWholeMilliseconds,
),
ONE_DAY(
text = R.string.one_day.asText(),
offsetMillis = 1.days.inWholeMilliseconds,
),
TWO_DAYS(
text = R.string.two_days.asText(),
offsetMillis = 2.days.inWholeMilliseconds,
),
THREE_DAYS(
text = R.string.three_days.asText(),
offsetMillis = 3.days.inWholeMilliseconds,
),
SEVEN_DAYS(
text = R.string.seven_days.asText(),
offsetMillis = 7.days.inWholeMilliseconds,
),
THIRTY_DAYS(
text = R.string.thirty_days.asText(),
offsetMillis = 30.days.inWholeMilliseconds,
),
CUSTOM(
text = R.string.custom.asText(),
offsetMillis = -1L,
),
}

View File

@@ -1,78 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
private const val ADD_TYPE: String = "add"
private const val EDIT_TYPE: String = "edit"
private const val EDIT_ITEM_ID: String = "edit_send_id"
private const val ADD_SEND_ITEM_PREFIX: String = "add_send_item"
private const val ADD_SEND_ITEM_TYPE: String = "add_send_item_type"
const val ADD_SEND_ROUTE: String =
"$ADD_SEND_ITEM_PREFIX/{$ADD_SEND_ITEM_TYPE}?$EDIT_ITEM_ID={$EDIT_ITEM_ID}"
/**
* Class to retrieve send add & edit arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class AddSendArgs(
val sendAddType: AddSendType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
sendAddType = when (requireNotNull(savedStateHandle.get<String>(ADD_SEND_ITEM_TYPE))) {
ADD_TYPE -> AddSendType.AddItem
EDIT_TYPE -> AddSendType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
else -> throw IllegalStateException("Unknown VaultAddEditType.")
},
)
}
/**
* Add the new send screen to the nav graph.
*/
fun NavGraphBuilder.addSendDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ADD_SEND_ROUTE,
arguments = listOf(
navArgument(ADD_SEND_ITEM_TYPE) {
type = NavType.StringType
},
),
) {
AddSendScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Navigate to the new send screen.
*/
fun NavController.navigateToAddSend(
sendAddType: AddSendType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$ADD_SEND_ITEM_PREFIX/${sendAddType.toTypeString()}" +
"?${EDIT_ITEM_ID}=${sendAddType.toIdOrNull()}",
navOptions = navOptions,
)
}
private fun AddSendType.toTypeString(): String =
when (this) {
is AddSendType.AddItem -> ADD_TYPE
is AddSendType.EditItem -> EDIT_TYPE
}
private fun AddSendType.toIdOrNull(): String? =
(this as? AddSendType.EditItem)?.sendItemId

View File

@@ -1,62 +0,0 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendAction
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendViewModel
import java.time.ZonedDateTime
/**
* A collection of handler functions for managing actions within the context of adding
* send items.
*/
data class AddSendHandlers(
val onNameChange: (String) -> Unit,
val onFileTypeSelect: () -> Unit,
val onTextTypeSelect: () -> Unit,
val onChooseFileClick: (hasPermission: Boolean) -> Unit,
val onFileChoose: (IntentManager.FileData) -> Unit,
val onTextChange: (String) -> Unit,
val onIsHideByDefaultToggle: (Boolean) -> Unit,
val onMaxAccessCountChange: (Int) -> Unit,
val onPasswordChange: (String) -> Unit,
val onNoteChange: (String) -> Unit,
val onHideEmailToggle: (Boolean) -> Unit,
val onDeactivateSendToggle: (Boolean) -> Unit,
val onDeletionDateChange: (ZonedDateTime) -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Creates an instance of [AddSendHandlers] by binding actions to the provided
* [AddSendViewModel].
*/
fun create(
viewModel: AddSendViewModel,
): AddSendHandlers =
AddSendHandlers(
onNameChange = { viewModel.trySendAction(AddSendAction.NameChange(it)) },
onFileTypeSelect = { viewModel.trySendAction(AddSendAction.FileTypeClick) },
onTextTypeSelect = { viewModel.trySendAction(AddSendAction.TextTypeClick) },
onChooseFileClick = { viewModel.trySendAction(AddSendAction.ChooseFileClick(it)) },
onFileChoose = { viewModel.trySendAction(AddSendAction.FileChoose(it)) },
onTextChange = { viewModel.trySendAction(AddSendAction.TextChange(it)) },
onIsHideByDefaultToggle = {
viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it))
},
onMaxAccessCountChange = {
viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it))
},
onPasswordChange = { viewModel.trySendAction(AddSendAction.PasswordChange(it)) },
onNoteChange = { viewModel.trySendAction(AddSendAction.NoteChange(it)) },
onHideEmailToggle = {
viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it))
},
onDeactivateSendToggle = {
viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it))
},
onDeletionDateChange = {
viewModel.trySendAction(AddSendAction.DeletionDateChange(it))
},
)
}
}

View File

@@ -1,162 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
private const val ADD_TYPE: String = "add"
private const val EDIT_TYPE: String = "edit"
private const val CLONE_TYPE: String = "clone"
private const val EDIT_ITEM_ID: String = "vault_edit_id"
private const val LOGIN: String = "login"
private const val CARD: String = "card"
private const val IDENTITY: String = "identity"
private const val SECURE_NOTE: String = "secure_note"
private const val SSH_KEY: String = "ssh_key"
private const val CIPHER_TYPE: String = "vault_item_type"
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
private const val ADD_EDIT_ITEM_TYPE: String = "vault_add_edit_type"
private const val ADD_SELECTED_FOLDER_ID: String = "vault_add_selected_folder_id"
private const val ADD_SELECTED_COLLECTION_ID: String = "vault_add_selected_collection_id"
private const val ADD_EDIT_ITEM_ROUTE: String =
ADD_EDIT_ITEM_PREFIX +
"/{$ADD_EDIT_ITEM_TYPE}" +
"?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" +
"?$CIPHER_TYPE={$CIPHER_TYPE}" +
"?$ADD_SELECTED_FOLDER_ID={$ADD_SELECTED_FOLDER_ID}" +
"?$ADD_SELECTED_COLLECTION_ID={$ADD_SELECTED_COLLECTION_ID}"
/**
* Class to retrieve vault add & edit arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class VaultAddEditArgs(
val vaultAddEditType: VaultAddEditType,
val vaultItemCipherType: VaultItemCipherType,
val selectedFolderId: String? = null,
val selectedCollectionId: String? = null,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) {
ADD_TYPE -> VaultAddEditType.AddItem
EDIT_TYPE -> VaultAddEditType.EditItem(
vaultItemId = requireNotNull(savedStateHandle[EDIT_ITEM_ID]),
)
CLONE_TYPE -> VaultAddEditType.CloneItem(
vaultItemId = requireNotNull(savedStateHandle[EDIT_ITEM_ID]),
)
else -> throw IllegalStateException("Unknown VaultAddEditType.")
},
vaultItemCipherType = requireNotNull(savedStateHandle.get<String>(CIPHER_TYPE))
.toVaultItemCipherType(),
selectedFolderId = savedStateHandle[ADD_SELECTED_FOLDER_ID],
selectedCollectionId = savedStateHandle[ADD_SELECTED_COLLECTION_ID],
)
}
/**
* Add the vault add & edit screen to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.vaultAddEditDestination(
onNavigateBack: () -> Unit,
onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToQrCodeScanScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String, showOnlyCollections: Boolean) -> Unit,
) {
composableWithSlideTransitions(
route = ADD_EDIT_ITEM_ROUTE,
arguments = listOf(
navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType },
navArgument(CIPHER_TYPE) { type = NavType.StringType },
navArgument(ADD_SELECTED_FOLDER_ID) {
type = NavType.StringType
nullable = true
},
navArgument(ADD_SELECTED_COLLECTION_ID) {
type = NavType.StringType
nullable = true
},
navArgument(ADD_SELECTED_COLLECTION_ID) {
type = NavType.StringType
nullable = true
},
),
) {
VaultAddEditScreen(
onNavigateBack = onNavigateBack,
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
onNavigateToGeneratorModal = onNavigateToGeneratorModal,
onNavigateToAttachments = onNavigateToAttachments,
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
)
}
}
/**
* Navigate to the vault add & edit screen.
*/
fun NavController.navigateToVaultAddEdit(
args: VaultAddEditArgs,
navOptions: NavOptions? = null,
) {
navigate(
route = "$ADD_EDIT_ITEM_PREFIX/${args.vaultAddEditType.toTypeString()}" +
"?$EDIT_ITEM_ID=${args.vaultAddEditType.toIdOrNull()}" +
"?$CIPHER_TYPE=${args.vaultItemCipherType.toTypeString()}" +
"?$ADD_SELECTED_FOLDER_ID=${args.selectedFolderId}" +
"?$ADD_SELECTED_COLLECTION_ID=${args.selectedCollectionId}",
navOptions = navOptions,
)
}
private fun VaultAddEditType.toTypeString(): String =
when (this) {
is VaultAddEditType.AddItem -> ADD_TYPE
is VaultAddEditType.EditItem -> EDIT_TYPE
is VaultAddEditType.CloneItem -> CLONE_TYPE
}
private fun VaultAddEditType.toIdOrNull(): String? =
when (this) {
is VaultAddEditType.AddItem -> null
is VaultAddEditType.CloneItem -> vaultItemId
is VaultAddEditType.EditItem -> vaultItemId
}
private fun VaultItemCipherType.toTypeString(): String =
when (this) {
VaultItemCipherType.LOGIN -> LOGIN
VaultItemCipherType.CARD -> CARD
VaultItemCipherType.IDENTITY -> IDENTITY
VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE
VaultItemCipherType.SSH_KEY -> SSH_KEY
}
private fun String.toVaultItemCipherType(): VaultItemCipherType =
when (this) {
LOGIN -> VaultItemCipherType.LOGIN
CARD -> VaultItemCipherType.CARD
IDENTITY -> VaultItemCipherType.IDENTITY
SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
SSH_KEY -> VaultItemCipherType.SSH_KEY
else -> throw IllegalStateException(
"Edit Item string arguments for VaultAddEditNavigation must match!",
)
}

View File

@@ -1,55 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val ATTACHMENTS_CIPHER_ID = "cipher_id"
private const val ATTACHMENTS_ROUTE_PREFIX = "attachments"
private const val ATTACHMENTS_ROUTE = "$ATTACHMENTS_ROUTE_PREFIX/{$ATTACHMENTS_CIPHER_ID}"
/**
* Class to retrieve arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class AttachmentsArgs(val cipherId: String) {
constructor(savedStateHandle: SavedStateHandle) : this(
cipherId = checkNotNull(savedStateHandle.get<String>(ATTACHMENTS_CIPHER_ID)),
)
}
/**
* Add the attachments screen to the nav graph.
*/
fun NavGraphBuilder.attachmentDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = ATTACHMENTS_ROUTE,
arguments = listOf(
navArgument(ATTACHMENTS_CIPHER_ID) { type = NavType.StringType },
),
) {
AttachmentsScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the attachments screen.
*/
fun NavController.navigateToAttachment(
cipherId: String,
navOptions: NavOptions? = null,
) {
navigate(
route = "$ATTACHMENTS_ROUTE_PREFIX/$cipherId",
navOptions = navOptions,
)
}

View File

@@ -1,58 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
private const val IMPORT_LOGINS_PREFIX = "import-logins"
private const val IMPORT_LOGINS_NAV_ARG = "snackbarRelay"
private const val IMPORT_LOGINS_ROUTE = "$IMPORT_LOGINS_PREFIX/{$IMPORT_LOGINS_NAV_ARG}"
/**
* Arguments for the [ImportLoginsScreen] using [SavedStateHandle].
*/
@OmitFromCoverage
data class ImportLoginsArgs(val snackBarRelay: SnackbarRelay) {
constructor(savedStateHandle: SavedStateHandle) : this(
snackBarRelay = SnackbarRelay.valueOf(
requireNotNull(savedStateHandle[IMPORT_LOGINS_NAV_ARG]),
),
)
}
/**
* Helper function to navigate to the import logins screen.
*/
fun NavController.navigateToImportLoginsScreen(
snackbarRelay: SnackbarRelay,
navOptions: NavOptions? = null,
) {
navigate(route = "$IMPORT_LOGINS_PREFIX/$snackbarRelay", navOptions = navOptions)
}
/**
* Adds the import logins screen to the navigation graph.
*/
fun NavGraphBuilder.importLoginsScreenDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = IMPORT_LOGINS_ROUTE,
arguments = listOf(
navArgument(IMPORT_LOGINS_NAV_ARG) {
type = NavType.StringType
nullable = false
},
),
) {
ImportLoginsScreen(
onNavigateBack = onNavigateBack,
)
}
}

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString
/**
* Models a single instruction step to be displayed in the import login instructions card.
*/
@Immutable
data class InstructionStep(
val stepNumber: Int,
val instructionText: AnnotatedString,
val additionalText: String? = null,
)

View File

@@ -1,101 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
private const val LOGIN: String = "login"
private const val CARD: String = "card"
private const val IDENTITY: String = "identity"
private const val SECURE_NOTE: String = "secure_note"
private const val SSH_KEY: String = "ssh_key"
private const val VAULT_ITEM_CIPHER_TYPE: String = "vault_item_cipher_type"
private const val VAULT_ITEM_PREFIX = "vault_item"
private const val VAULT_ITEM_ID = "vault_item_id"
private const val VAULT_ITEM_ROUTE = "$VAULT_ITEM_PREFIX/{$VAULT_ITEM_ID}" +
"?$VAULT_ITEM_CIPHER_TYPE={$VAULT_ITEM_CIPHER_TYPE}"
/**
* Class to retrieve vault item arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class VaultItemArgs(
val vaultItemId: String,
val cipherType: VaultItemCipherType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
vaultItemId = checkNotNull(savedStateHandle.get<String>(VAULT_ITEM_ID)),
cipherType = requireNotNull(savedStateHandle.get<String>(VAULT_ITEM_CIPHER_TYPE))
.toVaultItemCipherType(),
)
}
/**
* Add the vault item screen to the nav graph.
*/
fun NavGraphBuilder.vaultItemDestination(
onNavigateBack: () -> Unit,
onNavigateToVaultEditItem: (args: VaultAddEditArgs) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
) {
composableWithSlideTransitions(
route = VAULT_ITEM_ROUTE,
arguments = listOf(
navArgument(VAULT_ITEM_ID) { type = NavType.StringType },
navArgument(VAULT_ITEM_CIPHER_TYPE) { type = NavType.StringType },
),
) {
VaultItemScreen(
onNavigateBack = onNavigateBack,
onNavigateToVaultAddEditItem = onNavigateToVaultEditItem,
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
onNavigateToAttachments = onNavigateToAttachments,
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
)
}
}
/**
* Navigate to the vault item screen.
*/
fun NavController.navigateToVaultItem(
args: VaultItemArgs,
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_ITEM_PREFIX/${args.vaultItemId}" +
"?$VAULT_ITEM_CIPHER_TYPE=${args.cipherType.toTypeString()}",
navOptions = navOptions,
)
}
private fun VaultItemCipherType.toTypeString(): String =
when (this) {
VaultItemCipherType.LOGIN -> LOGIN
VaultItemCipherType.CARD -> CARD
VaultItemCipherType.IDENTITY -> IDENTITY
VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE
VaultItemCipherType.SSH_KEY -> SSH_KEY
}
private fun String.toVaultItemCipherType(): VaultItemCipherType =
when (this) {
LOGIN -> VaultItemCipherType.LOGIN
CARD -> VaultItemCipherType.CARD
IDENTITY -> VaultItemCipherType.IDENTITY
SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
SSH_KEY -> VaultItemCipherType.SSH_KEY
else -> throw IllegalStateException(
"Edit Item string arguments for VaultAddEditNavigation must match!",
)
}

View File

@@ -1,34 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.item.component
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Update Text UI common for all item types.
*/
@Composable
fun VaultItemUpdateText(
header: String,
text: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.semantics(mergeDescendants = true) { },
) {
Text(
text = header,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
)
Text(
text = text,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
)
}
}

View File

@@ -1,283 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
private const val CARD: String = "card"
private const val COLLECTION: String = "collection"
private const val FOLDER: String = "folder"
private const val IDENTITY: String = "identity"
private const val LOGIN: String = "login"
private const val SSH_KEY: String = "ssh_key"
private const val SECURE_NOTE: String = "secure_note"
private const val SEND_FILE: String = "send_file"
private const val SEND_TEXT: String = "send_text"
private const val TRASH: String = "trash"
private const val VAULT_ITEM_LISTING_PREFIX: String = "vault_item_listing"
private const val VAULT_ITEM_LISTING_AS_ROOT_PREFIX: String = "vault_item_listing_as_root"
private const val VAULT_ITEM_LISTING_TYPE: String = "vault_item_listing_type"
private const val ID: String = "id"
private const val VAULT_ITEM_LISTING_ROUTE: String =
"$VAULT_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" +
"?$ID={$ID}"
private const val VAULT_ITEM_LISTING_AS_ROOT_ROUTE: String =
"$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" +
"?$ID={$ID}"
private const val SEND_ITEM_LISTING_PREFIX: String = "send_item_listing"
private const val SEND_ITEM_LISTING_ROUTE: String =
"$SEND_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" +
"?$ID={$ID}"
/**
* Class to retrieve vault item listing arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class VaultItemListingArgs(
val vaultItemListingType: VaultItemListingType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
vaultItemListingType = determineVaultItemListingType(
vaultItemListingTypeString = checkNotNull(
savedStateHandle[VAULT_ITEM_LISTING_TYPE],
) as String,
id = savedStateHandle[ID],
),
)
}
/**
* Add the [VaultItemListingScreen] to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.vaultItemListingDestination(
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (args: VaultItemArgs) -> Unit,
onNavigateToVaultEditItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit,
onNavigateToVaultAddItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
) {
internalVaultItemListingDestination(
route = VAULT_ITEM_LISTING_ROUTE,
onNavigateBack = onNavigateBack,
onNavigateToAddSendItem = { },
onNavigateToEditSendItem = { },
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToVaultItemListing = onNavigateToVaultItemListing,
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
)
}
/**
* Add the [VaultItemListingScreen] to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.vaultItemListingDestinationAsRoot(
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (args: VaultItemArgs) -> Unit,
onNavigateToVaultEditItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToVaultAddItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
) {
composableWithStayTransitions(
route = VAULT_ITEM_LISTING_AS_ROOT_ROUTE,
arguments = listOf(
navArgument(
name = VAULT_ITEM_LISTING_TYPE,
builder = {
type = NavType.StringType
},
),
),
) {
VaultItemListingScreen(
onNavigateBack = onNavigateBack,
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
onNavigateToAddFolder = onNavigateToAddFolderScreen,
onNavigateToVaultItemListing = {},
onNavigateToAddSendItem = {},
onNavigateToEditSendItem = {},
)
}
}
/**
* Add the [VaultItemListingScreen] to the nav graph.
*/
fun NavGraphBuilder.sendItemListingDestination(
onNavigateBack: () -> Unit,
onNavigateToAddSendItem: () -> Unit,
onNavigateToEditSendItem: (sendId: String) -> Unit,
onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit,
) {
internalVaultItemListingDestination(
route = SEND_ITEM_LISTING_ROUTE,
onNavigateBack = onNavigateBack,
onNavigateToAddSendItem = onNavigateToAddSendItem,
onNavigateToEditSendItem = onNavigateToEditSendItem,
onNavigateToVaultAddItemScreen = { },
onNavigateToAddFolderScreen = { _ -> },
onNavigateToVaultItemScreen = { },
onNavigateToVaultEditItemScreen = { },
onNavigateToVaultItemListing = { },
onNavigateToSearch = { onNavigateToSearchSend(it as SearchType.Sends) },
)
}
/**
* Add the [VaultItemListingScreen] to the nav graph.
*/
@Suppress("LongParameterList")
private fun NavGraphBuilder.internalVaultItemListingDestination(
route: String,
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (args: VaultItemArgs) -> Unit,
onNavigateToVaultEditItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit,
onNavigateToVaultAddItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
onNavigateToAddSendItem: () -> Unit,
onNavigateToEditSendItem: (sendId: String) -> Unit,
onNavigateToSearch: (searchType: SearchType) -> Unit,
) {
composableWithPushTransitions(
route = route,
arguments = listOf(
navArgument(
name = VAULT_ITEM_LISTING_TYPE,
builder = {
type = NavType.StringType
},
),
navArgument(
name = ID,
builder = {
type = NavType.StringType
nullable = true
},
),
),
) {
VaultItemListingScreen(
onNavigateBack = onNavigateBack,
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToAddSendItem = onNavigateToAddSendItem,
onNavigateToEditSendItem = onNavigateToEditSendItem,
onNavigateToVaultItemListing = onNavigateToVaultItemListing,
onNavigateToSearch = onNavigateToSearch,
onNavigateToAddFolder = onNavigateToAddFolderScreen,
)
}
}
/**
* Navigate to the [VaultItemListingScreen] for vault.
*/
fun NavController.navigateToVaultItemListing(
vaultItemListingType: VaultItemListingType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_ITEM_LISTING_PREFIX/${vaultItemListingType.toTypeString()}" +
"?$ID=${vaultItemListingType.toIdOrNull()}",
navOptions = navOptions,
)
}
/**
* Navigate to the [VaultItemListingScreen] for vault.
*/
fun NavController.navigateToVaultItemListingAsRoot(
vaultItemListingType: VaultItemListingType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/${vaultItemListingType.toTypeString()}" +
"?$ID=${vaultItemListingType.toIdOrNull()}",
navOptions = navOptions,
)
}
/**
* Navigate to the [VaultItemListingScreen] for sends.
*/
fun NavController.navigateToSendItemListing(
vaultItemListingType: VaultItemListingType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$SEND_ITEM_LISTING_PREFIX/${vaultItemListingType.toTypeString()}" +
"?$ID=${vaultItemListingType.toIdOrNull()}",
navOptions = navOptions,
)
}
private fun VaultItemListingType.toTypeString(): String {
return when (this) {
is VaultItemListingType.Card -> CARD
is VaultItemListingType.Collection -> COLLECTION
is VaultItemListingType.Folder -> FOLDER
is VaultItemListingType.Identity -> IDENTITY
is VaultItemListingType.Login -> LOGIN
is VaultItemListingType.SecureNote -> SECURE_NOTE
is VaultItemListingType.Trash -> TRASH
is VaultItemListingType.SendFile -> SEND_FILE
is VaultItemListingType.SendText -> SEND_TEXT
is VaultItemListingType.SshKey -> SSH_KEY
}
}
private fun VaultItemListingType.toIdOrNull(): String? =
when (this) {
is VaultItemListingType.Collection -> collectionId
is VaultItemListingType.Folder -> folderId
is VaultItemListingType.Card -> null
is VaultItemListingType.Identity -> null
is VaultItemListingType.Login -> null
is VaultItemListingType.SecureNote -> null
is VaultItemListingType.Trash -> null
is VaultItemListingType.SendFile -> null
is VaultItemListingType.SendText -> null
is VaultItemListingType.SshKey -> null
}
private fun determineVaultItemListingType(
vaultItemListingTypeString: String,
id: String?,
): VaultItemListingType {
return when (vaultItemListingTypeString) {
LOGIN -> VaultItemListingType.Login
CARD -> VaultItemListingType.Card
IDENTITY -> VaultItemListingType.Identity
SECURE_NOTE -> VaultItemListingType.SecureNote
SSH_KEY -> VaultItemListingType.SshKey
TRASH -> VaultItemListingType.Trash
FOLDER -> VaultItemListingType.Folder(folderId = id)
COLLECTION -> VaultItemListingType.Collection(collectionId = requireNotNull(id))
SEND_FILE -> VaultItemListingType.SendFile
SEND_TEXT -> VaultItemListingType.SendText
// This should never occur, vaultItemListingTypeString must match
else -> throw IllegalStateException()
}
}

View File

@@ -1,41 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import androidx.annotation.StringRes
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
/**
* Returns the string resource ID corresponding to the error message for the given
* [Fido2ValidateOriginResult.Error].
*/
val Fido2ValidateOriginResult.Error.messageResourceId: Int
@StringRes
get() = when (this) {
Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified -> {
R.string.passkey_operation_failed_because_app_could_not_be_verified
}
Fido2ValidateOriginResult.Error.ApplicationNotFound -> {
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
}
Fido2ValidateOriginResult.Error.AssetLinkNotFound -> {
R.string.passkey_operation_failed_because_of_missing_asset_links
}
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp -> {
R.string.passkeys_not_supported_for_this_app
}
Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed -> {
R.string.passkey_operation_failed_because_browser_is_not_privileged
}
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound -> {
R.string.passkey_operation_failed_because_browser_signature_does_not_match
}
Fido2ValidateOriginResult.Error.Unknown -> {
R.string.generic_error_message
}
}

View File

@@ -1,70 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val VAULT_MOVE_TO_ORGANIZATION_PREFIX = "vault_move_to_organization"
private const val VAULT_MOVE_TO_ORGANIZATION_ID = "vault_move_to_organization_id"
private const val VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS =
"vault_move_to_organization_only_collections"
private const val VAULT_MOVE_TO_ORGANIZATION_ROUTE =
VAULT_MOVE_TO_ORGANIZATION_PREFIX +
"/{$VAULT_MOVE_TO_ORGANIZATION_ID}" +
"/{$VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS}"
/**
* Class to retrieve vault move to organization arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class VaultMoveToOrganizationArgs(
val vaultItemId: String,
val showOnlyCollections: Boolean,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
vaultItemId = checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ID]) as String,
showOnlyCollections =
(checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS]) as String)
.toBoolean(),
)
}
/**
* Add the vault move to organization screen to the nav graph.
*/
fun NavGraphBuilder.vaultMoveToOrganizationDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = VAULT_MOVE_TO_ORGANIZATION_ROUTE,
arguments = listOf(
navArgument(VAULT_MOVE_TO_ORGANIZATION_ID) { type = NavType.StringType },
navArgument(VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS) {
type = NavType.StringType
},
),
) {
VaultMoveToOrganizationScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the vault move to organization screen.
*/
fun NavController.navigateToVaultMoveToOrganization(
vaultItemId: String,
showOnlyCollections: Boolean,
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_MOVE_TO_ORGANIZATION_PREFIX/$vaultItemId/$showOnlyCollections",
navOptions = navOptions,
)
}

View File

@@ -2,7 +2,7 @@ package com.x8bit.bitwarden
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.annotation.OmitFromCoverage
/**
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed

View File

@@ -4,7 +4,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.annotation.OmitFromCoverage
import dagger.hilt.android.AndroidEntryPoint
/**

View File

@@ -1,13 +1,13 @@
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.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
import com.x8bit.bitwarden.data.auth.util.getYubiKeyResultOrNull
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

View File

@@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn

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