mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:54:26 -05:00
Compare commits
219 Commits
v2025.5.99
...
v2025.7.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a721744a6b | ||
|
|
37af6a1773 | ||
|
|
557c5b46a5 | ||
|
|
390ef34398 | ||
|
|
d2f7d52132 | ||
|
|
0feac46711 | ||
|
|
bc50c0d873 | ||
|
|
fb3b9c9ea7 | ||
|
|
9a81e18cb4 | ||
|
|
f9914e5b46 | ||
|
|
e193661f5f | ||
|
|
532fcbb40e | ||
|
|
187d50faa2 | ||
|
|
8f5376c2de | ||
|
|
56192a7e8b | ||
|
|
70350746ce | ||
|
|
febfc82a53 | ||
|
|
5f5c71979f | ||
|
|
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 |
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" },
|
||||
]
|
||||
15
.github/workflows/build-authenticator.yml
vendored
15
.github/workflows/build-authenticator.yml
vendored
@@ -29,12 +29,25 @@ 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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -30,12 +30,25 @@ 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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 \
|
||||
|
||||
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@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@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
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 }}
|
||||
37
.github/workflows/crowdin-pull.yml
vendored
37
.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 }}
|
||||
|
||||
@@ -35,22 +47,23 @@ jobs:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
- 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@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||
with:
|
||||
config: crowdin-bwa.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
29
.github/workflows/crowdin-push.yml
vendored
29
.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
|
||||
- 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@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
||||
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
|
||||
4
.github/workflows/scan.yml
vendored
4
.github/workflows/scan.yml
vendored
@@ -11,10 +11,14 @@ on:
|
||||
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
|
||||
|
||||
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"
|
||||
4
.github/workflows/test.yml
vendored
4
.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:
|
||||
@@ -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
|
||||
|
||||
32
Gemfile.lock
32
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.1102.0)
|
||||
aws-sdk-core (3.223.0)
|
||||
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.100.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.185.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.2)
|
||||
fastlane (2.228.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -165,8 +165,8 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.11.3)
|
||||
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.2)
|
||||
rake (13.2.1)
|
||||
rake (13.3.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
|
||||
10
README.md
10
README.md
@@ -52,6 +52,16 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -37,6 +37,6 @@ android {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +213,7 @@ 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"))
|
||||
@@ -296,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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "4c6ad1f5268d7e8add7407201788aa2e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `has_totp` INTEGER NOT NULL DEFAULT 1, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasTotp",
|
||||
"columnName": "has_totp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c6ad1f5268d7e8add7407201788aa2e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
@@ -78,7 +81,6 @@
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*.bitwarden.com" />
|
||||
<data android:host="*.bitwarden.eu" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -327,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,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,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,137 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthenticatorBridgeRepository].
|
||||
*/
|
||||
class AuthenticatorBridgeRepositoryImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
) : AuthenticatorBridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
get() {
|
||||
val doAnyAccountsHaveAuthenticatorSyncEnabled = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.accounts
|
||||
?.any {
|
||||
// Authenticator sync is enabled if any accounts have an authenticator
|
||||
// sync key stored:
|
||||
authDiskSource.getAuthenticatorSyncUnlockKey(it.userId) != null
|
||||
}
|
||||
?: false
|
||||
return if (doAnyAccountsHaveAuthenticatorSyncEnabled) {
|
||||
authDiskSource.authenticatorSyncSymmetricKey
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
|
||||
|
||||
return allAccounts
|
||||
.mapNotNull { account ->
|
||||
val userId = account.userId
|
||||
|
||||
// Grab the user's authenticator sync unlock key. If it is null,
|
||||
// the user has not enabled authenticator sync.
|
||||
val decryptedUserKey = authDiskSource.getAuthenticatorSyncUnlockKey(userId)
|
||||
?: return@mapNotNull null
|
||||
|
||||
// Wait for any unlocking actions to finish:
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
// Unlock vault if necessary:
|
||||
val isVaultAlreadyUnlocked = vaultRepository.isVaultUnlocked(userId = userId)
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
val unlockResult = vaultRepository
|
||||
.unlockVaultWithDecryptedUserKey(
|
||||
userId = userId,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
)
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
val totpUris = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.first()
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers
|
||||
.filter { it.login?.totp != null && it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
val decryptedCipher = vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
)
|
||||
.getOrNull()
|
||||
|
||||
val rawTotp = decryptedCipher?.login?.totp
|
||||
val cipherName = decryptedCipher?.name
|
||||
val username = decryptedCipher?.login?.username
|
||||
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(
|
||||
userId = userId,
|
||||
isUserInitiated = false,
|
||||
)
|
||||
}
|
||||
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
.let {
|
||||
SharedAccountData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,194 +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.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.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
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.row.BitwardenRowOfActions
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
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)
|
||||
@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.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
|
||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
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,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.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
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.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,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,25 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.util
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.toRoute
|
||||
import com.bitwarden.ui.platform.util.toObjectKClassNavigationRoute
|
||||
|
||||
/**
|
||||
* Determines if the [SavedStateHandle] contains a route for the specified object class.
|
||||
*
|
||||
* This will return the object instance if the route is correct, `null` otherwise.
|
||||
*/
|
||||
inline fun <reified T : Any> SavedStateHandle.toObjectRoute(): T? =
|
||||
this
|
||||
.get<Intent>(key = NavController.KEY_DEEP_LINK_INTENT)
|
||||
?.data
|
||||
?.pathSegments
|
||||
.orEmpty()
|
||||
.takeIf { segments -> segments.any { it == T::class.toObjectKClassNavigationRoute() } }
|
||||
?.let { _ ->
|
||||
// This will get the instance for us. We only do this after the checks above as it
|
||||
// will always return the object instance even if it is not the correct one.
|
||||
this.toRoute<T>()
|
||||
}
|
||||
@@ -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,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.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
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,60 +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 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,
|
||||
val onDeleteClick: () -> 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)) },
|
||||
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))
|
||||
},
|
||||
onDeleteClick = { viewModel.trySendAction(AddSendAction.DeleteClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,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.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -55,19 +55,13 @@ class AutofillTotpCopyViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
// Try and find the matching cipher.
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.mapNotNull { it.data }
|
||||
.first()
|
||||
.find { it.id == cipherId }
|
||||
?.let { cipherView ->
|
||||
sendEvent(
|
||||
AutofillTotpCopyEvent.CompleteAutofill(
|
||||
cipherView = cipherView,
|
||||
),
|
||||
)
|
||||
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
|
||||
GetCipherResult.CipherNotFound -> finishActivity()
|
||||
is GetCipherResult.Failure -> finishActivity()
|
||||
is GetCipherResult.Success -> {
|
||||
sendEvent(AutofillTotpCopyEvent.CompleteAutofill(result.cipherView))
|
||||
}
|
||||
?: finishActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,18 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.setupEdgeToEdge
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
@@ -35,6 +38,7 @@ import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.util.appLanguage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -62,66 +66,29 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = intent,
|
||||
),
|
||||
)
|
||||
mainViewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = intent))
|
||||
}
|
||||
|
||||
// Within the app the theme will change dynamically and will be managed by the
|
||||
// OS, but we need to ensure we properly set the values when upgrading from older versions
|
||||
// that handle this differently or when the activity restarts.
|
||||
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
|
||||
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberBitwardenNavController(name = "MainActivity")
|
||||
EventsEffect(viewModel = mainViewModel) { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAccessibilityAutofill -> {
|
||||
handleCompleteAccessibilityAutofill(event)
|
||||
}
|
||||
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(
|
||||
baseContext,
|
||||
event.message.invoke(resources),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
)
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppTheme -> {
|
||||
AppCompatDelegate.setDefaultNightMode(event.osTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
SetupEventsEffect(navController = navController)
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(it),
|
||||
)
|
||||
}
|
||||
{ mainViewModel.trySendAction(MainAction.ResumeScreenDataReceived(it)) }
|
||||
},
|
||||
)
|
||||
BitwardenTheme(
|
||||
@@ -148,11 +115,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
mainViewModel.trySendAction(
|
||||
action = MainAction.ReceiveNewIntent(
|
||||
intent = intent,
|
||||
),
|
||||
)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = intent))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -199,6 +162,34 @@ class MainActivity : AppCompatActivity() {
|
||||
.takeIf { it }
|
||||
?: super.dispatchKeyEvent(event)
|
||||
|
||||
@Composable
|
||||
private fun SetupEventsEffect(navController: NavController) {
|
||||
EventsEffect(viewModel = mainViewModel) { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAccessibilityAutofill -> {
|
||||
handleCompleteAccessibilityAutofill(event)
|
||||
}
|
||||
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(baseContext, event.message.invoke(resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
)
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppTheme -> AppCompatDelegate.setDefaultNightMode(event.osTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOpenDebugMenuEvent() {
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
|
||||
@@ -66,7 +67,7 @@ class AuthRequestNotificationManagerImpl(
|
||||
?.let { context.getString(R.string.confim_log_in_attemp_for_x, it) }
|
||||
?: context.getString(R.string.confirm_log_in),
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSmallIcon(BitwardenDrawable.ic_notification)
|
||||
.setColor(Color.White.value.toInt())
|
||||
.setAutoCancel(true)
|
||||
.setTimeoutAfter(NOTIFICATION_DEFAULT_TIMEOUT_MILLIS)
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.R
|
||||
@@ -27,15 +26,15 @@ import timber.log.Timber
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class UserLogoutManagerImpl(
|
||||
private val context: Context,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val generatorDiskSource: GeneratorDiskSource,
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
private val pushDiskSource: PushDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val toastManager: ToastManager,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : UserLogoutManager {
|
||||
private val scope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
@@ -117,7 +116,7 @@ class UserLogoutManagerImpl(
|
||||
}
|
||||
|
||||
private fun showToast(@StringRes message: Int) {
|
||||
mainScope.launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
|
||||
mainScope.launch { toastManager.show(messageId = message) }
|
||||
}
|
||||
|
||||
private fun switchUserIfAvailable(
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
@@ -107,23 +108,23 @@ object AuthManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserLogoutManager(
|
||||
@ApplicationContext context: Context,
|
||||
authDiskSource: AuthDiskSource,
|
||||
generatorDiskSource: GeneratorDiskSource,
|
||||
passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
pushDiskSource: PushDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
toastManager: ToastManager,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): UserLogoutManager =
|
||||
UserLogoutManagerImpl(
|
||||
context = context,
|
||||
authDiskSource = authDiskSource,
|
||||
generatorDiskSource = generatorDiskSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
pushDiskSource = pushDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
toastManager = toastManager,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user