mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 13:29:18 -05:00
Compare commits
328 Commits
v2025.4.0
...
authentica
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e23a4130 | ||
|
|
3a75ba5110 | ||
|
|
29882c9df8 | ||
|
|
ba49a3e91f | ||
|
|
965ab67e58 | ||
|
|
2932ed831b | ||
|
|
2ff3f3e23d | ||
|
|
eb5893dde4 | ||
|
|
1165e7002b | ||
|
|
5fa7239130 | ||
|
|
fd9bdfa228 | ||
|
|
7db8f040e4 | ||
|
|
790331e058 | ||
|
|
d0640b7e20 | ||
|
|
5429e27228 | ||
|
|
917aaac3a6 | ||
|
|
0b7209b3c9 | ||
|
|
a7b3201015 | ||
|
|
348e14e52d | ||
|
|
ef9dda5159 | ||
|
|
b0309e876e | ||
|
|
59a49355fd | ||
|
|
901184db45 | ||
|
|
a2507c317d | ||
|
|
f608852dc7 | ||
|
|
e44d63229c | ||
|
|
f7b876f204 | ||
|
|
1268afaef8 | ||
|
|
3f1c1dec17 | ||
|
|
5eea55f173 | ||
|
|
1a8cf4055a | ||
|
|
defdf8eb58 | ||
|
|
9940c8cf9e | ||
|
|
e1058f5021 | ||
|
|
986cd2ee30 | ||
|
|
eae870cb3a | ||
|
|
79493a55bd | ||
|
|
18bafaba8a | ||
|
|
896be911a4 | ||
|
|
85a86106f6 | ||
|
|
edb7996c28 | ||
|
|
a806109380 | ||
|
|
4f5c28e248 | ||
|
|
b22f06cbf9 | ||
|
|
1070c9d46e | ||
|
|
b1dc894fe8 | ||
|
|
c76945161a | ||
|
|
789cd80eba | ||
|
|
9482890102 | ||
|
|
ed2d6ca585 | ||
|
|
d279f6acae | ||
|
|
6ebcab7b86 | ||
|
|
3ee74d3ec5 | ||
|
|
288efb3611 | ||
|
|
bbdf8552c9 | ||
|
|
44ef598df3 | ||
|
|
73a8e241d4 | ||
|
|
4d6260ea02 | ||
|
|
569bb4f110 | ||
|
|
ffc71371a9 | ||
|
|
8d0b23d166 | ||
|
|
5f525d9d95 | ||
|
|
b94d59ba6b | ||
|
|
4ff1a9ba94 | ||
|
|
9c1673f603 | ||
|
|
ddc099f727 | ||
|
|
fbfcfcd683 | ||
|
|
1234898786 | ||
|
|
182e6475c0 | ||
|
|
f27590a4d6 | ||
|
|
807c76f8ec | ||
|
|
3877c4bd64 | ||
|
|
8c88fd9d53 | ||
|
|
b92493611e | ||
|
|
9235f92206 | ||
|
|
a3610c22dd | ||
|
|
1e4fc31ed4 | ||
|
|
ac1a9a2dc0 | ||
|
|
fe0e6bc67b | ||
|
|
419e5ca918 | ||
|
|
be1a6e2097 | ||
|
|
4fe989ce68 | ||
|
|
8be7410302 | ||
|
|
16225f0d68 | ||
|
|
08679a8973 | ||
|
|
4d3e782b69 | ||
|
|
4d8fe722d1 | ||
|
|
c5600c1d84 | ||
|
|
9816321d93 | ||
|
|
56e8acf81f | ||
|
|
08b07a0050 | ||
|
|
25d7c1e72c | ||
|
|
e311a4f618 | ||
|
|
0eea6b07a3 | ||
|
|
c52e769327 | ||
|
|
292a28d155 | ||
|
|
6c41c358ac | ||
|
|
e7cf5a7efa | ||
|
|
f64364c1b8 | ||
|
|
d42b8ecd2d | ||
|
|
a6f7b1e176 | ||
|
|
d56b9fc0ff | ||
|
|
f290ae411b | ||
|
|
508566f06f | ||
|
|
95f146fb3e | ||
|
|
469df4495a | ||
|
|
053dfc1647 | ||
|
|
7de770ca03 | ||
|
|
861a4281fa | ||
|
|
265014fd64 | ||
|
|
5d32fe9caf | ||
|
|
44ba0f548a | ||
|
|
694443f2e1 | ||
|
|
0ade60025c | ||
|
|
5adccca823 | ||
|
|
3c86bb425b | ||
|
|
3474e0b608 | ||
|
|
0f2476bebf | ||
|
|
edffb8dd6f | ||
|
|
2dc6c170f5 | ||
|
|
2a8a16ab3f | ||
|
|
76995a28ad | ||
|
|
7e146800a8 | ||
|
|
7a2f1c294f | ||
|
|
44d4926300 | ||
|
|
e4c160d1e0 | ||
|
|
0f9f9d9dce | ||
|
|
a0c2600517 | ||
|
|
c60df56648 | ||
|
|
9cdfe0c5d6 | ||
|
|
d822be62e1 | ||
|
|
7adbfdcc84 | ||
|
|
beb4c533c8 | ||
|
|
e1cd813445 | ||
|
|
f769900976 | ||
|
|
a0ff94195f | ||
|
|
9853f137d2 | ||
|
|
b591534bd9 | ||
|
|
d2c329264c | ||
|
|
a9791c3f9f | ||
|
|
a59eaf5d40 | ||
|
|
d2129cf507 | ||
|
|
903c260ad1 | ||
|
|
09a8c01824 | ||
|
|
a1a4c217de | ||
|
|
7fbe3510b5 | ||
|
|
0892e0ff1f | ||
|
|
0934d47159 | ||
|
|
caf1c2eed5 | ||
|
|
803d519c24 | ||
|
|
a3d2e51c8e | ||
|
|
891def5e32 | ||
|
|
00ded69a84 | ||
|
|
c5f597aedb | ||
|
|
f43367ebfa | ||
|
|
997769bb1c | ||
|
|
65d1a4f12a | ||
|
|
f7c1278805 | ||
|
|
aa3602a5ce | ||
|
|
af18848159 | ||
|
|
f3b7d0f732 | ||
|
|
e250a8dc1e | ||
|
|
ab2ac60957 | ||
|
|
b877487ce1 | ||
|
|
ef68879778 | ||
|
|
a4e4d1488b | ||
|
|
cf8578f3ef | ||
|
|
bdd0660e2b | ||
|
|
294ef674bc | ||
|
|
b9a897a9fc | ||
|
|
6b12b9757f | ||
|
|
61411ca73c | ||
|
|
3908827a14 | ||
|
|
e553d7a015 | ||
|
|
3015b768c6 | ||
|
|
21b8ef92ba | ||
|
|
0e3e6069fa | ||
|
|
8d8dee5171 | ||
|
|
97b6bccd72 | ||
|
|
2a6813e4a2 | ||
|
|
29e7899525 | ||
|
|
4cd603006f | ||
|
|
1a091e198c | ||
|
|
3551e75596 | ||
|
|
6d976bea4c | ||
|
|
d5c04123d9 | ||
|
|
000a7d141e | ||
|
|
c9d4d35f07 | ||
|
|
4216f3f5a0 | ||
|
|
c14545107d | ||
|
|
d1a8cbf59f | ||
|
|
1acc1a87a6 | ||
|
|
fd73360539 | ||
|
|
178625222a | ||
|
|
3ea17eb71c | ||
|
|
6ccb035ffd | ||
|
|
5c3008d080 | ||
|
|
54efc74907 | ||
|
|
34aed2ac65 | ||
|
|
3d152f5c36 | ||
|
|
4c8e5602dd | ||
|
|
6e44ee2eb0 | ||
|
|
4895f2a18a | ||
|
|
fc4f02c4d5 | ||
|
|
6719534494 | ||
|
|
183584f678 | ||
|
|
046bb0fa39 | ||
|
|
9508b4ba90 | ||
|
|
07e4e6a806 | ||
|
|
4d142a6a5c | ||
|
|
f02a3a249b | ||
|
|
28149532a0 | ||
|
|
7f5426dea0 | ||
|
|
d7d703c977 | ||
|
|
7fda5d799f | ||
|
|
3c1a0a352a | ||
|
|
cb0b135429 | ||
|
|
7422efd07a | ||
|
|
27a9fc52b7 | ||
|
|
c105c102a3 | ||
|
|
c83bd8f4a8 | ||
|
|
d820b3345a | ||
|
|
b71b01d48d | ||
|
|
8a0f67c0e9 | ||
|
|
b4d85e07ba | ||
|
|
dfd58822b7 | ||
|
|
f14a1404e3 | ||
|
|
f1950600a1 | ||
|
|
7d6b6a5959 | ||
|
|
ea70191429 | ||
|
|
db956b9b91 | ||
|
|
119812507a | ||
|
|
a97c962428 | ||
|
|
456adf3158 | ||
|
|
62cb962298 | ||
|
|
7f4e65d7e4 | ||
|
|
860a2e265f | ||
|
|
6d68c3ae24 | ||
|
|
97b8c51ab3 | ||
|
|
a2449a2f19 | ||
|
|
1d73bbd440 | ||
|
|
da62244000 | ||
|
|
11b767c98f | ||
|
|
cd4db467e3 | ||
|
|
7fdf165273 | ||
|
|
e49bab637c | ||
|
|
14ac194cb7 | ||
|
|
578f96a944 | ||
|
|
2c71ab7d27 | ||
|
|
c5ee389231 | ||
|
|
5037af07c7 | ||
|
|
d6d1e8e97f | ||
|
|
652168f946 | ||
|
|
d4d5d2c2a8 | ||
|
|
ed148c2089 | ||
|
|
564304616d | ||
|
|
0d0b8d6780 | ||
|
|
472e41f6bc | ||
|
|
fb9c68755a | ||
|
|
d7671f47ea | ||
|
|
9c7270df69 | ||
|
|
733290569c | ||
|
|
cbaa8a329e | ||
|
|
f968d7698a | ||
|
|
68cd08b069 | ||
|
|
84683894a6 | ||
|
|
ed2d9ecb80 | ||
|
|
8f4d46954e | ||
|
|
a9fc6ff589 | ||
|
|
5c8f5670e4 | ||
|
|
eec88d4924 | ||
|
|
82da193e55 | ||
|
|
76fb85ac1f | ||
|
|
625ac0ea5f | ||
|
|
4e88833737 | ||
|
|
ecea2ef7c1 | ||
|
|
0eccc7197e | ||
|
|
5dd34afe81 | ||
|
|
5abcc5b1f7 | ||
|
|
6fec95cb84 | ||
|
|
1d68c1fdf6 | ||
|
|
0c2de427dc | ||
|
|
f932682949 | ||
|
|
e1f432ea5d | ||
|
|
31de7fc331 | ||
|
|
07469672ba | ||
|
|
1a2beea770 | ||
|
|
639ca02739 | ||
|
|
186bea2d1d | ||
|
|
3dc187da87 | ||
|
|
69708c1285 | ||
|
|
ad1566f4b0 | ||
|
|
32d0ca7bcd | ||
|
|
0353f0c153 | ||
|
|
7436122953 | ||
|
|
23ef5b38fe | ||
|
|
fe1fe770c7 | ||
|
|
240bca3c2f | ||
|
|
8c7cc27c5d | ||
|
|
a4aa9837a6 | ||
|
|
b901de9ddf | ||
|
|
96df23f0af | ||
|
|
0eb149941d | ||
|
|
6f44e64375 | ||
|
|
cda86b842e | ||
|
|
e1608b426d | ||
|
|
b6017baf54 | ||
|
|
0f6d15d6a6 | ||
|
|
cd11164544 | ||
|
|
1b9d2bfab4 | ||
|
|
373b789fbb | ||
|
|
985e576a82 | ||
|
|
b11e4481f9 | ||
|
|
5ac0f2b111 | ||
|
|
37a0d19efc | ||
|
|
54983bc92e | ||
|
|
36989875a6 | ||
|
|
88b0fe59bb | ||
|
|
e4d0c48eed | ||
|
|
bd364a1108 | ||
|
|
39b88d6064 | ||
|
|
da709e039b | ||
|
|
31311964d0 | ||
|
|
8cbd7369c5 | ||
|
|
2a1669cf87 | ||
|
|
4c4007a734 | ||
|
|
70dc82d1b6 | ||
|
|
021ece138b |
133
.github/scripts/jira-get-release-notes/README.md
vendored
Normal file
133
.github/scripts/jira-get-release-notes/README.md
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
...
|
||||
```
|
||||
70
.github/scripts/jira-get-release-notes/jira_release_notes.py
vendored
Executable file
70
.github/scripts/jira-get-release-notes/jira_release_notes.py
vendored
Executable 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()
|
||||
9
.github/scripts/jira-get-release-notes/pyproject.toml
vendored
Normal file
9
.github/scripts/jira-get-release-notes/pyproject.toml
vendored
Normal 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
91
.github/scripts/jira-get-release-notes/uv.lock
generated
vendored
Normal 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" },
|
||||
]
|
||||
43
.github/workflows/build-authenticator.yml
vendored
43
.github/workflows/build-authenticator.yml
vendored
@@ -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
|
||||
|
||||
81
.github/workflows/build.yml
vendored
81
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
56
.github/workflows/crowdin-pull-authenticator.yml
vendored
56
.github/workflows/crowdin-pull-authenticator.yml
vendored
@@ -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 }}
|
||||
41
.github/workflows/crowdin-pull.yml
vendored
41
.github/workflows/crowdin-pull.yml
vendored
@@ -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 }}
|
||||
|
||||
30
.github/workflows/crowdin-push-authenticator.yml
vendored
30
.github/workflows/crowdin-push-authenticator.yml
vendored
@@ -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
|
||||
31
.github/workflows/crowdin-push.yml
vendored
31
.github/workflows/crowdin-push.yml
vendored
@@ -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
|
||||
|
||||
229
.github/workflows/github-release.yml
vendored
229
.github/workflows/github-release.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/publish-github-release.yml
vendored
Normal file
14
.github/workflows/publish-github-release.yml
vendored
Normal 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
16
.github/workflows/publish-store.yml
vendored
Normal 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
|
||||
6
.github/workflows/scan-ci.yml
vendored
6
.github/workflows/scan-ci.yml
vendored
@@ -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:
|
||||
|
||||
18
.github/workflows/scan.yml
vendored
18
.github/workflows/scan.yml
vendored
@@ -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
16
.github/workflows/test-device.yml
vendored
Normal 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"
|
||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
38
Gemfile.lock
38
Gemfile.lock
@@ -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)
|
||||
|
||||
31
README.md
31
README.md
@@ -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
1
annotation/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
42
annotation/build.gradle.kts
Normal file
42
annotation/build.gradle.kts
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
BIN
app/libs/authenticatorbridge-1.0.1-release.aar
Normal file
BIN
app/libs/authenticatorbridge-1.0.1-release.aar
Normal file
Binary file not shown.
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
21
app/src/beta/AndroidManifest.xml
Normal file
21
app/src/beta/AndroidManifest.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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!",
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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!",
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user