mirror of
https://github.com/bitwarden/android.git
synced 2026-05-12 23:01:13 -05:00
Compare commits
399 Commits
v2025.5.99
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b497156302 | ||
|
|
ab90f5ff95 | ||
|
|
bc7e682941 | ||
|
|
517829e7b0 | ||
|
|
a1c6276092 | ||
|
|
bc67bf3dff | ||
|
|
bc5788556c | ||
|
|
45e20d8c9e | ||
|
|
a972a40a49 | ||
|
|
717d5665e0 | ||
|
|
bc0a18f250 | ||
|
|
2f72553454 | ||
|
|
e5a1546291 | ||
|
|
d8e319948c | ||
|
|
b3528249e9 | ||
|
|
5f42c9bb39 | ||
|
|
b010c9a29d | ||
|
|
3e55f561c9 | ||
|
|
277e4d8d6f | ||
|
|
32e8fb7d8e | ||
|
|
4a18e57cca | ||
|
|
070ef45087 | ||
|
|
a658cf890a | ||
|
|
d3dea3c9cb | ||
|
|
5ab0517bf3 | ||
|
|
e8b01c2d44 | ||
|
|
b34d873471 | ||
|
|
3c3d8710c9 | ||
|
|
20dea9b5ff | ||
|
|
44410efe56 | ||
|
|
a999592fb6 | ||
|
|
25a78f60ab | ||
|
|
a8546bb4eb | ||
|
|
6c6d4f2d91 | ||
|
|
7347d91fdd | ||
|
|
0a99359978 | ||
|
|
aca4b05b59 | ||
|
|
b0c7995cb7 | ||
|
|
af322b5d1f | ||
|
|
9fcfcc9e41 | ||
|
|
ff6b7b675d | ||
|
|
3164c29184 | ||
|
|
c5663431af | ||
|
|
4fb96cb782 | ||
|
|
36e06cdac7 | ||
|
|
3cf325becf | ||
|
|
584bdb6277 | ||
|
|
b2a9f4b455 | ||
|
|
b0b4379307 | ||
|
|
b9cc664efa | ||
|
|
e30e0ffbb4 | ||
|
|
2ffd71c69a | ||
|
|
3488ad6217 | ||
|
|
58005d908a | ||
|
|
a320e6ea61 | ||
|
|
5a23ceabc1 | ||
|
|
f4102bcd30 | ||
|
|
6d25c12271 | ||
|
|
ef03cdb2db | ||
|
|
474ec4907f | ||
|
|
a68fd8b44f | ||
|
|
3282992221 | ||
|
|
26252ebcdb | ||
|
|
a688693f43 | ||
|
|
3ed63ef5eb | ||
|
|
1e2bc4aa70 | ||
|
|
694865c213 | ||
|
|
29243c8f44 | ||
|
|
4e1dfcaeec | ||
|
|
75f3065085 | ||
|
|
402e399fd4 | ||
|
|
810cbc8da5 | ||
|
|
9bfbe0c087 | ||
|
|
d06c87beb3 | ||
|
|
9b120701eb | ||
|
|
e8f1242744 | ||
|
|
1c525b9dfc | ||
|
|
c613c2df86 | ||
|
|
9a9125321e | ||
|
|
2902b89402 | ||
|
|
93edbb61bf | ||
|
|
85bc76d0a6 | ||
|
|
db18e8012a | ||
|
|
db03c7d703 | ||
|
|
6ee7f9b80f | ||
|
|
fc88ca1ba8 | ||
|
|
3c033d4aa2 | ||
|
|
59c2261e7c | ||
|
|
b6aa0952b1 | ||
|
|
905e3248f2 | ||
|
|
72250dce90 | ||
|
|
60ee129e0b | ||
|
|
911bb40be8 | ||
|
|
308a8a564c | ||
|
|
337e751c05 | ||
|
|
f4c4e06dcc | ||
|
|
38b92133ff | ||
|
|
e381d72d5c | ||
|
|
87a61bbbbd | ||
|
|
7cc3c1c755 | ||
|
|
f614d6039f | ||
|
|
a6d622c3b9 | ||
|
|
b781acb1fa | ||
|
|
45f0ddc60f | ||
|
|
8876418177 | ||
|
|
67b64034ff | ||
|
|
79a232919a | ||
|
|
2fa9ea18b5 | ||
|
|
9b297286e5 | ||
|
|
376a62edaf | ||
|
|
01570c2555 | ||
|
|
4a3db4fea7 | ||
|
|
3c0818232f | ||
|
|
1799d0b716 | ||
|
|
cf896d6bf1 | ||
|
|
40dff74d3f | ||
|
|
ddd2d7fad5 | ||
|
|
b4efc0e59d | ||
|
|
4ffd41c33f | ||
|
|
a70f441064 | ||
|
|
867e2287dc | ||
|
|
912f734cae | ||
|
|
02b5cbb199 | ||
|
|
f589546e6a | ||
|
|
517198b265 | ||
|
|
91f1180be7 | ||
|
|
8589a37e5a | ||
|
|
e4678cc7df | ||
|
|
e665c386ff | ||
|
|
2f2ec71fc4 | ||
|
|
7b115df83a | ||
|
|
edd1763198 | ||
|
|
37d3ff30e4 | ||
|
|
258a58aa25 | ||
|
|
da5dcef41e | ||
|
|
7a578ff2c5 | ||
|
|
355facc36b | ||
|
|
c60f3131b6 | ||
|
|
bb950c8c59 | ||
|
|
c7df80ff00 | ||
|
|
d308b84943 | ||
|
|
79ad18877d | ||
|
|
4f51507e4b | ||
|
|
88fcd35d1a | ||
|
|
987639b2a3 | ||
|
|
d32b4c7c7e | ||
|
|
9ed59e61a3 | ||
|
|
3342ebf139 | ||
|
|
4050215145 | ||
|
|
3e0ee5fcd8 | ||
|
|
fcd7326f2c | ||
|
|
c94fe56b47 | ||
|
|
17287680d9 | ||
|
|
e4935318de | ||
|
|
f22643fec1 | ||
|
|
6454dc1a58 | ||
|
|
411e359600 | ||
|
|
e75d7844de | ||
|
|
25680f9255 | ||
|
|
628cb12081 | ||
|
|
710e35680b | ||
|
|
b5cd0c9d9d | ||
|
|
9995fa92f1 | ||
|
|
44aae70fe4 | ||
|
|
fca4ebe023 | ||
|
|
2d2a5e74da | ||
|
|
b53ca30974 | ||
|
|
8178a61dba | ||
|
|
f0bdc8ede3 | ||
|
|
145c19da22 | ||
|
|
39b1409cbd | ||
|
|
f26d54a2e2 | ||
|
|
33cfaa5e95 | ||
|
|
9274e0f349 | ||
|
|
46656d659e | ||
|
|
811f0f2757 | ||
|
|
8f783a43e4 | ||
|
|
b8f74cdefa | ||
|
|
5e6dcb5b58 | ||
|
|
c5a40a89d9 | ||
|
|
929233081c | ||
|
|
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 |
20
.github/actions/log-inputs/action.yml
vendored
Normal file
20
.github/actions/log-inputs/action.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: 'Log Inputs to Job Summary'
|
||||
description: 'Log workflow inputs to the GitHub Actions job summary'
|
||||
|
||||
inputs:
|
||||
inputs:
|
||||
description: 'Workflow inputs as JSON'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
shell: bash
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ inputs.inputs }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
49
.github/actions/setup-android-build/action.yml
vendored
Normal file
49
.github/actions/setup-android-build/action.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: 'Setup Android Build'
|
||||
description: 'Setup Android build environment with Gradle, Ruby, and Fastlane'
|
||||
inputs:
|
||||
java-version:
|
||||
description: 'Java version to use'
|
||||
required: false
|
||||
default: '17'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
- name: Install Fastlane
|
||||
shell: bash
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
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" },
|
||||
]
|
||||
74
.github/workflows/build-authenticator.yml
vendored
74
.github/workflows/build-authenticator.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/**/*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -29,20 +30,34 @@ env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
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
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -52,7 +67,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -67,7 +82,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -95,10 +110,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -109,9 +124,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "BWA-AAB-KEYSTORE-STORE-PASSWORD,BWA-AAB-KEYSTORE-KEY-PASSWORD,BWA-APK-KEYSTORE-STORE-PASSWORD,BWA-APK-KEYSTORE-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -155,6 +179,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: AZ Logout
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
if: ${{ inputs.publish-to-play-store }}
|
||||
run: |
|
||||
@@ -162,10 +189,10 @@ jobs:
|
||||
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -175,7 +202,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -189,16 +216,25 @@ jobs:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
bundle exec fastlane setAuthenticatorBuildVersionInfo \
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat authenticator/build.gradle.kts)" =~ $regex ]]; then
|
||||
regex='appVersionName = "([^"]+)"'
|
||||
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -209,18 +245,18 @@ jobs:
|
||||
run: |
|
||||
bundle exec fastlane bundleAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_AAB_KEYSTORE_STORE_PASSWORD }}' \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:authenticatorupload \
|
||||
keyPassword:'${{ secrets.BWA_AAB_KEYSTORE_KEY_PASSWORD }}'
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
run: |
|
||||
bundle exec fastlane buildAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_APK_KEYSTORE_STORE_PASSWORD }}' \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:bitwardenauthenticator \
|
||||
keyPassword:'${{ secrets.BWA_APK_KEYSTORE_KEY_PASSWORD }}'
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
|
||||
123
.github/workflows/build.yml
vendored
123
.github/workflows/build.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/**/*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -30,20 +31,34 @@ env:
|
||||
JAVA_VERSION: 17
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
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
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -53,7 +68,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -68,7 +83,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -103,10 +118,10 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -117,9 +132,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -156,11 +180,14 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -170,7 +197,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -195,7 +222,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 }}
|
||||
@@ -203,48 +230,48 @@ jobs:
|
||||
- name: Generate release Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
|
||||
env:
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreRelease \
|
||||
storeFile:app_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:upload \
|
||||
keyPassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store bundle
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
env:
|
||||
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_BETA_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_BETA_KEY_PASSWORD: ${{ secrets.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
|
||||
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreBeta \
|
||||
storeFile:app_beta_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD_BETA_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden-beta-upload \
|
||||
keyPassword:${{ env.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreReleaseApk \
|
||||
storeFile:app_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:${{ env.PLAY_KEYSTORE_PASSWORD }}
|
||||
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY_BETA_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
|
||||
PLAY_BETA_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
|
||||
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreBetaApk \
|
||||
storeFile:app_beta_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY_BETA_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
|
||||
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
@@ -402,10 +429,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -416,9 +443,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "FDROID-KEYSTORE-PASSWORD,FDROID-BETA-KEYSTORE-PASSWORD,FDROID-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -441,11 +477,14 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -455,7 +494,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -481,21 +520,21 @@ 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 \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
|
||||
regex='appVersionName = "([^"]+)"'
|
||||
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Generate F-Droid artifacts
|
||||
env:
|
||||
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidReleaseApk \
|
||||
storeFile:app_fdroid-keystore.jks \
|
||||
@@ -505,14 +544,14 @@ jobs:
|
||||
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
FDROID_BETA_KEY_PASSWORD: ${{ secrets.FDROID_BETA_KEY_PASSWORD }}
|
||||
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
|
||||
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidBetaApk \
|
||||
storeFile:app_beta_fdroid-keystore.jks \
|
||||
storePassword:"${{ env.FDROID_BETA_KEYSTORE_PASSWORD }}" \
|
||||
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Download Google Privileged Browsers List
|
||||
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
|
||||
|
||||
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 }}
|
||||
49
.github/workflows/crowdin-pull.yml
vendored
49
.github/workflows/crowdin-pull.yml
vendored
@@ -1,25 +1,36 @@
|
||||
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 - ${{ github.event_name }}
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -28,18 +39,22 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
@@ -47,10 +62,10 @@ jobs:
|
||||
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"
|
||||
localization_branch_name: "crowdin-pull"
|
||||
create_pull_request: true
|
||||
pull_request_title: "Autosync Crowdin Translations"
|
||||
pull_request_body: "Autosync the updated translations"
|
||||
pull_request_title: "Crowdin Pull"
|
||||
pull_request_body: ":inbox_tray: New translations 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
|
||||
26
.github/workflows/crowdin-push.yml
vendored
26
.github/workflows/crowdin-push.yml
vendored
@@ -1,25 +1,29 @@
|
||||
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
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -29,11 +33,15 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
291
.github/workflows/github-release.yml
vendored
291
.github/workflows/github-release.yml
vendored
@@ -3,82 +3,116 @@ 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
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Get branch from workflow run
|
||||
id: get_release_branch
|
||||
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"
|
||||
echo "🔖 Workflow name: $workflow_name"
|
||||
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
|
||||
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
|
||||
|
||||
case "$workflow_name" in
|
||||
*"Password Manager"* | "Build")
|
||||
app_name="Password Manager"
|
||||
app_name_suffix="bwpm"
|
||||
;;
|
||||
"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"*)
|
||||
app_name="Authenticator"
|
||||
app_name_suffix="bwa"
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
|
||||
echo "::error::Unknown workflow name: $workflow_name"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "🔖 App name: $app_name"
|
||||
echo "🔖 App name suffix: $app_name_suffix"
|
||||
echo "app_name=$app_name" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=$app_name_suffix" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "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 +127,156 @@ 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/**/*
|
||||
# Files that won't be included in any release
|
||||
files_to_remove=(
|
||||
"com.x8bit.bitwarden.aab"
|
||||
"com.x8bit.bitwarden.aab-sha256.txt"
|
||||
|
||||
- name: Update Release Description
|
||||
"com.x8bit.bitwarden.beta.apk"
|
||||
"com.x8bit.bitwarden.beta.apk-sha256.txt"
|
||||
"com.x8bit.bitwarden.beta.aab"
|
||||
"com.x8bit.bitwarden.beta.aab-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.beta-fdroid.apk"
|
||||
"com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.dev.apk"
|
||||
"com.x8bit.bitwarden.dev.apk-sha256.txt"
|
||||
|
||||
"com.bitwarden.authenticator.aab"
|
||||
"authenticator-android-aab-sha256.txt"
|
||||
)
|
||||
|
||||
for file in "${files_to_remove[@]}"; do
|
||||
find $ARTIFACTS_PATH -name "$file" -type f -delete
|
||||
done
|
||||
echo "🔖 Removed internal artifacts."
|
||||
echo ""
|
||||
echo "🔖 Files to be included in the release:"
|
||||
find $ARTIFACTS_PATH -type f
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "JIRA-API-EMAIL,JIRA-API-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- 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: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.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: |
|
||||
is_latest_release=false
|
||||
if [[ "$_APP_NAME" == "Password Manager" ]]; then
|
||||
is_latest_release=true
|
||||
fi
|
||||
|
||||
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" \
|
||||
--latest=$is_latest_release \
|
||||
--draft \
|
||||
$ARTIFACTS_PATH/*/*)
|
||||
|
||||
# Extract release tag from URL
|
||||
release_id_from_url=$(echo "$release_url" | sed 's/.*\/tag\///')
|
||||
echo "release_id_from_url=$release_id_from_url" >> $GITHUB_OUTPUT
|
||||
echo "url=$release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ Release created: $release_url"
|
||||
echo "🔖 Release ID from URL: $release_id_from_url"
|
||||
|
||||
- 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 }}
|
||||
_RELEASE_ID: ${{ steps.create_release.outputs.release_id_from_url }}
|
||||
run: |
|
||||
echo "Getting current release body. Release ID: $_RELEASE_ID"
|
||||
current_body=$(gh release view "$_RELEASE_ID" --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 "$_RELEASE_ID" --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
|
||||
|
||||
36
.github/workflows/publish-github-release.yml
vendored
Normal file
36
.github/workflows/publish-github-release.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1-5'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish-release-password-manager:
|
||||
name: Publish Password Manager Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Password Manager"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "play_creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
|
||||
secrets: inherit
|
||||
|
||||
publish-release-authenticator:
|
||||
name: Publish Authenticator Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Authenticator"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "authenticator_play_store-creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
|
||||
secrets: inherit
|
||||
160
.github/workflows/publish-store.yml
vendored
Normal file
160
.github/workflows/publish-store.yml
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
name: Publish to Google Play
|
||||
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
product:
|
||||
description: "Which app is being released."
|
||||
type: choice
|
||||
options:
|
||||
- Password Manager
|
||||
- Authenticator
|
||||
version-name:
|
||||
description: "Version name to promote to production ex 2025.1.1"
|
||||
type: string
|
||||
version-code:
|
||||
description: "Build number to promote to production."
|
||||
required: true
|
||||
type: string
|
||||
rollout-percentage:
|
||||
description: "Percentage of users who will receive this version update."
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 10%
|
||||
- 30%
|
||||
- 50%
|
||||
- 100%
|
||||
default: 10%
|
||||
release-notes:
|
||||
description: "Change notes to be included with this release."
|
||||
type: string
|
||||
default: "Bug fixes."
|
||||
required: true
|
||||
track-from:
|
||||
description: "Track to promote from."
|
||||
type: choice
|
||||
options:
|
||||
- internal
|
||||
- Fastlane Automation Source
|
||||
required: true
|
||||
default: "internal"
|
||||
track-target:
|
||||
description: "Track to promote to."
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- Fastlane Automation Target
|
||||
required: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Promote build to Production in Play Store
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
mkdir -p ${{ github.workspace }}/app/src/standardRelease
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Format Release Notes
|
||||
run: |
|
||||
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Promote Play Store version to production
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
VERSION_CODE_INPUT: ${{ inputs.version-code }}
|
||||
VERSION_NAME: ${{inputs.version-name}}
|
||||
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
|
||||
PRODUCT: ${{ inputs.product }}
|
||||
TRACK_FROM: ${{ inputs.track-from }}
|
||||
TRACK_TARGET: ${{ inputs.track-target }}
|
||||
run: |
|
||||
if [ "$PRODUCT" = "Password Manager" ]; then
|
||||
PACKAGE_NAME="com.x8bit.bitwarden"
|
||||
elif [ "$PRODUCT" = "Authenticator" ]; then
|
||||
PACKAGE_NAME="com.bitwarden.authenticator"
|
||||
else
|
||||
echo "Unsupported product: $PRODUCT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
|
||||
|
||||
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
|
||||
|
||||
bundle exec fastlane updateReleaseNotes \
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
versionCode:"$VERSION_CODE" \
|
||||
packageName:"$PACKAGE_NAME"
|
||||
|
||||
bundle exec fastlane promoteToProduction \
|
||||
versionCode:"$VERSION_CODE" \
|
||||
versionName:"$VERSION_NAME" \
|
||||
rolloutPercentage:"$decimal" \
|
||||
packageName:"$PACKAGE_NAME" \
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
track:"$TRACK_FROM" \
|
||||
trackPromoteTo:"$TRACK_TARGET"
|
||||
53
.github/workflows/release-branch.yml
vendored
53
.github/workflows/release-branch.yml
vendored
@@ -9,7 +9,9 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- RC
|
||||
- Hotfix
|
||||
- Hotfix Password Manager
|
||||
- Hotfix Authenticator
|
||||
- Test
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
@@ -17,38 +19,54 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create RC Branch
|
||||
if: inputs.release_type == 'RC'
|
||||
- name: Create RC or Test Branch
|
||||
id: rc_branch
|
||||
if: inputs.release_type == 'RC' || inputs.release_type == 'Test'
|
||||
env:
|
||||
RC_PREFIX_DATE: "true" # replace with input if needed
|
||||
_TEST_MODE: ${{ inputs.release_type == 'Test' }}
|
||||
_RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
run: |
|
||||
if [ "$RC_PREFIX_DATE" = "true" ]; then
|
||||
current_date=$(date +'%Y.%m')
|
||||
branch_name="release/${current_date}-rc${{ github.run_number }}"
|
||||
else
|
||||
branch_name="release/rc${{ github.run_number }}"
|
||||
current_date=$(date +'%Y.%-m')
|
||||
branch_name="${current_date}-rc${{ github.run_number }}"
|
||||
|
||||
if [ "$_TEST_MODE" = "true" ]; then
|
||||
branch_name="WORKFLOW-TEST-${branch_name}"
|
||||
fi
|
||||
branch_name="release/${branch_name}"
|
||||
|
||||
git switch main
|
||||
git switch -c $branch_name
|
||||
git push origin $branch_name
|
||||
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Hotfix Branch
|
||||
if: inputs.release_type == 'Hotfix'
|
||||
id: hotfix_branch
|
||||
if: startsWith(inputs.release_type, 'Hotfix')
|
||||
env:
|
||||
_RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
run: |
|
||||
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
|
||||
app_codename="bwpm"
|
||||
if [ "$_RELEASE_TYPE" == "Hotfix Authenticator" ]; then
|
||||
app_codename="bwa"
|
||||
fi
|
||||
echo "🌿 app codename: $app_codename"
|
||||
|
||||
latest_tag=$(git tag -l --sort=-creatordate | grep "$app_codename" | head -n 1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "::error::No tags found in the repository"
|
||||
exit 1
|
||||
fi
|
||||
branch_name="release/hotfix-${latest_tag}"
|
||||
echo "🌿 branch name: $branch_name"
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
|
||||
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
@@ -56,3 +74,12 @@ jobs:
|
||||
git switch -c $branch_name $latest_tag
|
||||
git push origin $branch_name
|
||||
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Trigger CI Workflows
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
_BRANCH_NAME: ${{ steps.rc_branch.outputs.branch_name || steps.hotfix_branch.outputs.branch_name }}
|
||||
run: |
|
||||
echo "🌿 branch name: $_BRANCH_NAME"
|
||||
gh workflow run build.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
|
||||
gh workflow run build-authenticator.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
|
||||
|
||||
61
.github/workflows/scan-ci.yml
vendored
61
.github/workflows/scan-ci.yml
vendored
@@ -6,55 +6,30 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-24.04
|
||||
name: Checkmarx
|
||||
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path .
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
id-token: write
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
name: Sonar
|
||||
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
67
.github/workflows/scan.yml
vendored
67
.github/workflows/scan.yml
vendored
@@ -11,69 +11,38 @@ 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
|
||||
runs-on: ubuntu-24.04
|
||||
name: Checkmarx
|
||||
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
id-token: write
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
name: Sonar
|
||||
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
id-token: write
|
||||
|
||||
167
.github/workflows/sdlc-sdk-update.yml
vendored
Normal file
167
.github/workflows/sdlc-sdk-update.yml
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
name: SDLC / SDK Update
|
||||
run-name: "SDK ${{inputs.run-mode == 'Update' && format('Update - {0}', inputs.sdk-version) || format('Test #{0} - {1}', inputs.pr-id, inputs.sdk-version)}}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-mode:
|
||||
description: "Run Mode"
|
||||
type: choice
|
||||
options:
|
||||
- Test # used for testing sdk-internal repo PRs
|
||||
- Update # opens a PR in this repo updating the SDK
|
||||
default: Test
|
||||
sdk-package:
|
||||
description: "SDK Package ID"
|
||||
required: true
|
||||
default: "com.bitwarden:sdk-android.dev"
|
||||
sdk-version:
|
||||
description: "SDK Version"
|
||||
required: true
|
||||
default: "1.0.0-2686-km-update-kdf-sdk"
|
||||
pr-id:
|
||||
description: "Pull Request ID"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update and PR
|
||||
if: ${{ inputs.run-mode == 'Update' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Switch to branch
|
||||
id: switch-branch
|
||||
run: |
|
||||
BRANCH_NAME="sdlc/sdk-update"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git switch -c $BRANCH_NAME
|
||||
|
||||
- name: Get current SDK version
|
||||
id: get-current-sdk
|
||||
run: |
|
||||
SDK_VERSION=$(grep "bitwardenSdk =" gradle/libs.versions.toml | cut -d'"' -f2)
|
||||
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
|
||||
echo "Current SDK version: $SDK_VERSION"
|
||||
echo "Current SDK git ref: $GIT_REF"
|
||||
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
run: |
|
||||
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
|
||||
|
||||
- name: Create branch and commit
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
run: |
|
||||
echo "👀 Committing SDK version update..."
|
||||
|
||||
git config user.name "bw-ghapp[bot]"
|
||||
git config user.email "178206702+bw-ghapp[bot]@users.noreply.github.com"
|
||||
|
||||
git add gradle/libs.versions.toml
|
||||
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
|
||||
git push origin $_BRANCH_NAME
|
||||
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
_OLD_SDK_VERSION: ${{ steps.get-current-sdk.outputs.version }}
|
||||
_OLD_SDK_GIT_REF: ${{ steps.get-current-sdk.outputs.git_ref }}
|
||||
run: |
|
||||
NEW_SDK_GIT_REF=$(echo "$_SDK_VERSION" | cut -d'-' -f3-)
|
||||
CHANGELOG=$(./scripts/get-repo-changelog.sh "bitwarden/sdk-internal" "$_OLD_SDK_GIT_REF" "$NEW_SDK_GIT_REF")
|
||||
PR_BODY="Updates the SDK version from \`$_OLD_SDK_VERSION\` to \`$_SDK_PACKAGE $_SDK_VERSION\`
|
||||
|
||||
## What's Changed
|
||||
|
||||
$CHANGELOG"
|
||||
|
||||
# Use echo -e to interpret escape sequences and pipe to gh pr create
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file - \
|
||||
--base main \
|
||||
--head $_BRANCH_NAME \
|
||||
--label "automated-pr" \
|
||||
--label "t:ci")
|
||||
|
||||
echo "🚀 Created PR: $PR_URL"
|
||||
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test:
|
||||
name: Test Update
|
||||
if: ${{ inputs.run-mode == 'Test' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Setup Android Build
|
||||
uses: ./.github/actions/setup-android-build
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
run: |
|
||||
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
|
||||
run: |
|
||||
./gradlew assembleDebug --warn
|
||||
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"
|
||||
16
.github/workflows/test.yml
vendored
16
.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:
|
||||
@@ -27,13 +27,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY
|
||||
echo "> Uploading code coverage report failed. Please check the \"Upload to codecov.io\" step of \"Process Test Reports\" job for more details." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ ! -z "$PR_NUMBER" ]; then
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
message=$'> [!WARNING]\n> @'$RUN_ACTOR' Uploading code coverage report failed. Please check the "Upload to codecov.io" step of [Process Test Reports job]('$_GITHUB_ACTION_RUN_URL') for more details.'
|
||||
gh pr comment --repo $GITHUB_REPOSITORY $PR_NUMBER --body "$message"
|
||||
fi
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,6 +3,13 @@
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
|
||||
# Ruby / Bundler
|
||||
.bundle/
|
||||
vendor/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.3.1
|
||||
3.4.2
|
||||
|
||||
9
Gemfile
9
Gemfile
@@ -7,3 +7,12 @@ gem 'time'
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
||||
# Since ruby 3.4.0 these are not included in the standard library
|
||||
gem 'abbrev'
|
||||
gem 'logger'
|
||||
gem 'mutex_m'
|
||||
gem 'csv'
|
||||
|
||||
# Starting with Ruby 3.5.0, these are not included in the standard library
|
||||
gem 'ostruct'
|
||||
|
||||
48
Gemfile.lock
48
Gemfile.lock
@@ -5,35 +5,39 @@ GEM
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.7)
|
||||
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.1139.0)
|
||||
aws-sdk-core (3.228.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.100.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (1.109.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.185.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-s3 (1.195.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.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)
|
||||
bigdecimal (3.2.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
date (3.4.1)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
@@ -58,10 +62,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 +75,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,23 +169,24 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.11.3)
|
||||
jwt (2.10.1)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multi_json (1.17.0)
|
||||
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)
|
||||
ostruct (0.6.3)
|
||||
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)
|
||||
@@ -230,12 +235,17 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
abbrev
|
||||
csv
|
||||
fastlane
|
||||
fastlane-plugin-firebase_app_distribution
|
||||
logger
|
||||
mutex_m
|
||||
ostruct
|
||||
time
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.1p55
|
||||
ruby 3.4.2p28
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.6
|
||||
2.6.9
|
||||
|
||||
41
README.md
41
README.md
@@ -52,6 +52,47 @@
|
||||
|
||||
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`.
|
||||
|
||||
5. Setup `detekt` pre-commit hook (optional):
|
||||
|
||||
Run the following script from the root of the repository to install the hook. This will overwrite any existing pre-commit hook if present.
|
||||
|
||||
```shell
|
||||
echo "Writing detekt pre-commit hook..."
|
||||
cat << 'EOL' > .git/hooks/pre-commit
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Running detekt check..."
|
||||
OUTPUT="/tmp/detekt-$(date +%s)"
|
||||
./gradlew -Pprecommit=true detekt > $OUTPUT
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
cat $OUTPUT
|
||||
rm $OUTPUT
|
||||
echo "***********************************************"
|
||||
echo " detekt failed "
|
||||
echo " Please fix the above issues before committing "
|
||||
echo "***********************************************"
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
rm $OUTPUT
|
||||
EOL
|
||||
echo "detekt pre-commit hook written to .git/hooks/pre-commit"
|
||||
echo "Making the hook executable"
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
echo "detekt pre-commit hook installed successfully to .git/hooks/pre-commit"
|
||||
```
|
||||
|
||||
## 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,26 +47,32 @@ 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()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "2025.4.0"
|
||||
|
||||
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")
|
||||
}
|
||||
versionCode = libs.versions.appVersionCode.get().toInt()
|
||||
versionName = libs.versions.appVersionName.get()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// Set the base archive name for publishing purposes. This is used to derive the APK and AAB
|
||||
// artifact names when uploading to Firebase and Play Store.
|
||||
base.archivesName = "com.x8bit.bitwarden"
|
||||
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "CI_INFO",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"\uD83D\uDCBB local\"")}",
|
||||
)
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "SDK_VERSION",
|
||||
value = "\"${libs.versions.bitwardenSdk.get()}\"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,6 +106,7 @@ android {
|
||||
applicationIdSuffix = ".beta"
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
matchingFallbacks += listOf("release")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
@@ -111,6 +119,7 @@ android {
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
@@ -193,7 +202,7 @@ android {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +220,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"))
|
||||
@@ -255,11 +264,10 @@ dependencies {
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(platform(libs.square.okhttp.bom))
|
||||
implementation(libs.square.okhttp)
|
||||
implementation(libs.square.okhttp.logging)
|
||||
implementation(platform(libs.square.retrofit.bom))
|
||||
implementation(libs.square.retrofit)
|
||||
implementation(libs.square.retrofit.kotlinx.serialization)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.zxing.zxing.core)
|
||||
|
||||
@@ -287,7 +295,6 @@ dependencies {
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.mockk.mockk)
|
||||
testImplementation(libs.robolectric.robolectric)
|
||||
testImplementation(libs.square.okhttp.mockwebserver)
|
||||
testImplementation(libs.square.turbine)
|
||||
}
|
||||
|
||||
@@ -296,8 +303,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +328,7 @@ private fun renameFile(path: String, newName: String) {
|
||||
if (originalFile.renameTo(newFile)) {
|
||||
println("Renamed $originalFile to $newFile")
|
||||
} else {
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
throw RuntimeException("Failed to rename $originalFile to $newFile")
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
27
app/src/debug/res/xml/network_security_config.xml
Normal file
27
app/src/debug/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<base-config
|
||||
cleartextTrafficPermitted="true"
|
||||
tools:ignore="InsecureBaseConfiguration">
|
||||
<trust-anchors>
|
||||
<!-- Trust pre-installed CAs -->
|
||||
<certificates src="system" />
|
||||
<!-- Additionally trust user added CAs -->
|
||||
<certificates
|
||||
src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain includeSubdomains="true">bitwarden.com</domain>
|
||||
<domain includeSubdomains="true">bitwarden.eu</domain>
|
||||
<domain includeSubdomains="true">bitwarden.pw</domain>
|
||||
<trust-anchors>
|
||||
<!-- Only trust pre-installed CAs for Bitwarden domains and all subdomains -->
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
|
||||
</network-security-config>
|
||||
7
app/src/debug/res/xml/provider.xml
Normal file
7
app/src/debug/res/xml/provider.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider>
|
||||
<capabilities>
|
||||
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
@@ -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,12 +81,12 @@
|
||||
<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>
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -112,11 +115,11 @@
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".AutofillTotpCopyActivity"
|
||||
android:name=".AutofillCallbackActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/AutofillTotpCopyTheme" />
|
||||
android:theme="@style/AutofillCallbackTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".AuthCallbackActivity"
|
||||
@@ -130,16 +133,6 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="captcha-callback"
|
||||
android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="duo-callback"
|
||||
android:scheme="bitwarden" />
|
||||
@@ -327,11 +320,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,17 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthTokenManager].
|
||||
*/
|
||||
class AuthTokenManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
) : AuthTokenManager {
|
||||
|
||||
override fun getActiveAccessTokenOrNull(): String? = authDiskSource
|
||||
.userState
|
||||
?.activeUserId
|
||||
?.let { authDiskSource.getAccountTokens(it) }
|
||||
?.accessToken
|
||||
}
|
||||
@@ -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,74 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.net.URLEncoder
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
|
||||
private const val CAPTCHA_HOST: String = "captcha-callback"
|
||||
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
|
||||
|
||||
/**
|
||||
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
|
||||
*/
|
||||
fun generateUriForCaptcha(captchaId: String): Uri {
|
||||
val json = buildJsonObject {
|
||||
put(key = "siteKey", value = captchaId)
|
||||
put(key = "locale", value = Locale.getDefault().toString())
|
||||
put(key = "callbackUri", value = CALLBACK_URI)
|
||||
put(key = "captchaRequiredText", value = "Captcha required")
|
||||
}
|
||||
val base64Data = Base64
|
||||
.getEncoder()
|
||||
.encodeToString(
|
||||
json
|
||||
.toString()
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
|
||||
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=$base64Data&parent=$parentParam&v=1"
|
||||
return Uri.parse(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a [CaptchaCallbackTokenResult] from an Intent. There are three possible cases.
|
||||
*
|
||||
* - `null`: Intent is not a captcha callback, or data is null.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.MissingToken]:
|
||||
* Intent is the captcha callback, but its missing a token value.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.Success]:
|
||||
* Intent is the captcha callback, and it has a token.
|
||||
*/
|
||||
fun Intent.getCaptchaCallbackTokenResult(): CaptchaCallbackTokenResult? {
|
||||
val localData = data
|
||||
return if (
|
||||
action == Intent.ACTION_VIEW && localData != null && localData.host == CAPTCHA_HOST
|
||||
) {
|
||||
localData.getQueryParameter("token")?.let {
|
||||
CaptchaCallbackTokenResult.Success(token = it)
|
||||
} ?: CaptchaCallbackTokenResult.MissingToken
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed class representing the result of captcha callback token extraction.
|
||||
*/
|
||||
sealed class CaptchaCallbackTokenResult {
|
||||
/**
|
||||
* Represents a missing token in the captcha callback.
|
||||
*/
|
||||
data object MissingToken : CaptchaCallbackTokenResult()
|
||||
|
||||
/**
|
||||
* Represents a token present in the captcha callback.
|
||||
*/
|
||||
data class Success(val token: String) : CaptchaCallbackTokenResult()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents data for a TOTP copying during the autofill flow via authentication intents.
|
||||
*
|
||||
* @property cipherId The cipher for which we are copying a TOTP to the clipboard.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AutofillTotpCopyData(
|
||||
val cipherId: String,
|
||||
) : Parcelable
|
||||
@@ -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,123 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.provider
|
||||
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
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
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while waiting for the vault status to not be
|
||||
* 'UNLOCKING' before proceeding.
|
||||
*/
|
||||
private const val VAULT_LOCKED_TIMEOUT_MS: Long = 500L
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while retrieving ciphers before proceeding.
|
||||
*/
|
||||
private const val GET_CIPHERS_TIMEOUT_MS: Long = 2_000L
|
||||
|
||||
/**
|
||||
* The default [AutofillCipherProvider] implementation. This service is used for getting current
|
||||
* [AutofillCipher]s.
|
||||
*/
|
||||
class AutofillCipherProviderImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val cipherMatchingManager: CipherMatchingManager,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : AutofillCipherProvider {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
override suspend fun isVaultLocked(): Boolean {
|
||||
val userId = activeUserId ?: return true
|
||||
|
||||
// Wait for any unlocking actions to finish. This can be relevant on startup for Never lock
|
||||
// accounts.
|
||||
vaultRepository
|
||||
.vaultUnlockDataStateFlow
|
||||
.firstWithTimeoutOrNull(timeMillis = VAULT_LOCKED_TIMEOUT_MS) {
|
||||
it.statusFor(userId = userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
return !vaultRepository.isVaultUnlocked(userId = userId)
|
||||
}
|
||||
|
||||
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
|
||||
return cipherViews
|
||||
.mapNotNull { cipherView ->
|
||||
cipherView
|
||||
// We only care about non-deleted card ciphers.
|
||||
.takeIf {
|
||||
// Must be card type.
|
||||
cipherView.type == CipherType.CARD &&
|
||||
// Must not be deleted.
|
||||
cipherView.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
it.reprompt == CipherRepromptType.NONE
|
||||
}
|
||||
?.let { nonNullCipherView ->
|
||||
AutofillCipher.Card(
|
||||
cipherId = cipherView.id,
|
||||
name = nonNullCipherView.name,
|
||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
||||
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
|
||||
code = nonNullCipherView.card?.code.orEmpty(),
|
||||
expirationMonth = nonNullCipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = nonNullCipherView.card?.expYear.orEmpty(),
|
||||
number = nonNullCipherView.card?.number.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLoginAutofillCiphers(
|
||||
uri: String,
|
||||
): List<AutofillCipher.Login> {
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
// We only care about non-deleted login ciphers.
|
||||
val loginCiphers = cipherViews
|
||||
.filter {
|
||||
// Must be login type
|
||||
it.type == CipherType.LOGIN &&
|
||||
// Must not be deleted.
|
||||
it.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
it.reprompt == CipherRepromptType.NONE
|
||||
}
|
||||
|
||||
return cipherMatchingManager
|
||||
// Filter for ciphers that match the uri in some way.
|
||||
.filterCiphersForMatches(
|
||||
ciphers = loginCiphers,
|
||||
matchUri = uri,
|
||||
)
|
||||
.map { cipherView ->
|
||||
AutofillCipher.Login(
|
||||
cipherId = cipherView.id,
|
||||
isTotpEnabled = cipherView.login?.totp != null,
|
||||
name = cipherView.name,
|
||||
password = cipherView.login?.password.orEmpty(),
|
||||
subtitle = cipherView.subtitle.orEmpty(),
|
||||
username = cipherView.login?.username.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available [CipherView]s if possible.
|
||||
*/
|
||||
private suspend fun getUnlockedCiphersOrNull(): List<CipherView>? =
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.takeUnless { isVaultLocked() }
|
||||
?.firstWithTimeoutOrNull(timeMillis = GET_CIPHERS_TIMEOUT_MS) { it.data != null }
|
||||
?.data
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.View
|
||||
import android.view.autofill.AutofillValue
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
|
||||
/**
|
||||
* Convert this [AutofillView] into a [FilledItem]. Return null if not possible.
|
||||
*/
|
||||
fun AutofillView.buildFilledItemOrNull(
|
||||
value: String,
|
||||
): FilledItem? =
|
||||
when (this.data.autofillType) {
|
||||
View.AUTOFILL_TYPE_DATE -> {
|
||||
value
|
||||
.toLongOrNull()
|
||||
?.let { AutofillValue.forDate(it) }
|
||||
}
|
||||
|
||||
View.AUTOFILL_TYPE_LIST -> this.buildListAutofillValueOrNull(value = value)
|
||||
View.AUTOFILL_TYPE_TEXT -> AutofillValue.forText(value)
|
||||
View.AUTOFILL_TYPE_TOGGLE -> {
|
||||
value
|
||||
.toBooleanStrictOrNull()
|
||||
?.let { AutofillValue.forToggle(it) }
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
?.let { autofillValue ->
|
||||
FilledItem(
|
||||
autofillId = this.data.autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a list [AutofillValue] out of [value] or return null if not possible.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun AutofillView.buildListAutofillValueOrNull(
|
||||
value: String,
|
||||
): AutofillValue? =
|
||||
if (this is AutofillView.Card.ExpirationMonth) {
|
||||
val autofillOptionsSize = this.data.autofillOptions.size
|
||||
// The idea here is that `value` is a numerical representation of a month.
|
||||
val monthIndex = value.toIntOrNull()
|
||||
when {
|
||||
monthIndex == null -> null
|
||||
// We expect there is some placeholder or empty space at the beginning of the list.
|
||||
autofillOptionsSize == 13 -> AutofillValue.forList(monthIndex)
|
||||
autofillOptionsSize >= monthIndex -> AutofillValue.forList(monthIndex - 1)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
this
|
||||
.data
|
||||
.autofillOptions
|
||||
.indexOfFirst { it == value }
|
||||
.takeIf { it != -1 }
|
||||
?.let { AutofillValue.forList(it) }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a password field.
|
||||
*
|
||||
* This function is untestable as [HtmlInfo] contains [android.util.Pair] which requires
|
||||
* instrumentation testing.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun HtmlInfo?.isPasswordField(): Boolean =
|
||||
this
|
||||
?.let { htmlInfo ->
|
||||
if (htmlInfo.isInputField) {
|
||||
htmlInfo
|
||||
.attributes
|
||||
?.any {
|
||||
it.first == "type" && it.second == "password"
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
?: false
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a username field.
|
||||
*
|
||||
* This function is untestable as [HtmlInfo] contains [android.util.Pair] which requires
|
||||
* instrumentation testing.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun HtmlInfo?.isUsernameField(): Boolean =
|
||||
this
|
||||
?.let { htmlInfo ->
|
||||
if (htmlInfo.isInputField) {
|
||||
htmlInfo
|
||||
.attributes
|
||||
?.any {
|
||||
it.first == "type" && it.second == "email"
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
?: false
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents an input field.
|
||||
*/
|
||||
val HtmlInfo?.isInputField: Boolean get() = this?.tag == "input"
|
||||
@@ -1,222 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
|
||||
/**
|
||||
* The default web URI scheme.
|
||||
*/
|
||||
private const val DEFAULT_SCHEME: String = "https"
|
||||
|
||||
/**
|
||||
* The set of raw autofill hints that should be ignored.
|
||||
*/
|
||||
private val IGNORED_RAW_HINTS: List<String> = listOf(
|
||||
"search",
|
||||
"find",
|
||||
"recipient",
|
||||
"edit",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported password autofill hints.
|
||||
*/
|
||||
private val SUPPORTED_RAW_PASSWORD_HINTS: List<String> = listOf(
|
||||
"password",
|
||||
"pswd",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported raw autofill hints.
|
||||
*/
|
||||
private val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
||||
"email",
|
||||
"phone",
|
||||
"username",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported autofill Android View hints.
|
||||
*/
|
||||
private val SUPPORTED_VIEW_HINTS: List<String> = listOf(
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
View.AUTOFILL_HINT_PASSWORD,
|
||||
View.AUTOFILL_HINT_USERNAME,
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether this [AssistStructure.ViewNode] represents an input field.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isInputField: Boolean
|
||||
get() {
|
||||
val isEditText = className
|
||||
?.let {
|
||||
try {
|
||||
Class.forName(it)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.let { EditText::class.java.isAssignableFrom(it) } == true
|
||||
return isEditText || htmlInfo.isInputField
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] into an [AutofillView]. If the view node
|
||||
* doesn't contain a valid autofillId, it isn't an a view setup for autofill, so we return null. If
|
||||
* it doesn't have a supported hint and isn't an input field, we also return null.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
this
|
||||
.autofillId
|
||||
// We only care about nodes with a valid `AutofillId`.
|
||||
?.let { nonNullAutofillId ->
|
||||
val supportedHint = this
|
||||
.autofillHints
|
||||
?.firstOrNull { SUPPORTED_VIEW_HINTS.contains(it) }
|
||||
|
||||
if (supportedHint != null || this.isInputField) {
|
||||
val autofillOptions = this
|
||||
.autofillOptions
|
||||
.orEmpty()
|
||||
.map { it.toString() }
|
||||
|
||||
val autofillViewData = AutofillView.Data(
|
||||
autofillId = nonNullAutofillId,
|
||||
autofillOptions = autofillOptions,
|
||||
autofillType = this.autofillType,
|
||||
isFocused = this.isFocused,
|
||||
textValue = this.autofillValue?.extractTextValue(),
|
||||
hasPasswordTerms = this.hasPasswordTerms(),
|
||||
)
|
||||
buildAutofillView(
|
||||
autofillOptions = autofillOptions,
|
||||
autofillViewData = autofillViewData,
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] and [autofillViewData] into an [AutofillView].
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
autofillOptions: List<String>,
|
||||
autofillViewData: AutofillView.Data,
|
||||
supportedHint: String?,
|
||||
): AutofillView = when {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
val monthValue = this
|
||||
.autofillValue
|
||||
?.extractMonthValue(
|
||||
autofillOptions = autofillOptions,
|
||||
)
|
||||
|
||||
AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = monthValue,
|
||||
)
|
||||
}
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
|
||||
AutofillView.Card.Number(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
this.isPasswordField(supportedHint) -> {
|
||||
AutofillView.Login.Password(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
this.isUsernameField(supportedHint) -> {
|
||||
AutofillView.Login.Username(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
AutofillView.Unused(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a password field.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.isPasswordField(
|
||||
supportedHint: String?,
|
||||
): Boolean {
|
||||
if (supportedHint == View.AUTOFILL_HINT_PASSWORD) return true
|
||||
|
||||
val isInvalidField = this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true
|
||||
val isUsernameField = this.isUsernameField(supportedHint)
|
||||
if (this.inputType.isPasswordInputType && !isInvalidField && !isUsernameField) return true
|
||||
|
||||
return this
|
||||
.htmlInfo
|
||||
.isPasswordField()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] includes any password specific terms.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.hasPasswordTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a username field.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.isUsernameField(
|
||||
supportedHint: String?,
|
||||
): Boolean =
|
||||
supportedHint == View.AUTOFILL_HINT_USERNAME ||
|
||||
supportedHint == View.AUTOFILL_HINT_EMAIL_ADDRESS ||
|
||||
inputType.isUsernameInputType ||
|
||||
idEntry?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
hint?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
htmlInfo.isUsernameField()
|
||||
|
||||
/**
|
||||
* The website that this [AssistStructure.ViewNode] is a part of representing.
|
||||
*/
|
||||
val AssistStructure.ViewNode.website: String?
|
||||
get() = this
|
||||
.webDomain
|
||||
.takeUnless { it?.isBlank() == true }
|
||||
?.let { webDomain ->
|
||||
val webScheme = this
|
||||
.webScheme
|
||||
.orNullIfBlank()
|
||||
?: DEFAULT_SCHEME
|
||||
|
||||
buildUri(
|
||||
domain = webDomain,
|
||||
scheme = webScheme,
|
||||
)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.credentials.builder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Primary implementation of [CredentialEntryBuilder].
|
||||
*/
|
||||
class CredentialEntryBuilderImpl(
|
||||
private val context: Context,
|
||||
private val intentManager: IntentManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
) : CredentialEntryBuilder {
|
||||
|
||||
override fun buildPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
|
||||
beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
|
||||
isUserVerified: Boolean,
|
||||
): List<PublicKeyCredentialEntry> = beginGetPublicKeyCredentialOptions
|
||||
.flatMap { option ->
|
||||
fido2CredentialAutofillViews
|
||||
.toPublicKeyCredentialEntryList(
|
||||
userId = userId,
|
||||
option = option,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
|
||||
userId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
isUserVerified: Boolean,
|
||||
): List<PublicKeyCredentialEntry> = this
|
||||
.map { fido2AutofillView ->
|
||||
PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = fido2AutofillView.userNameForUi
|
||||
?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = fido2AutofillView.credentialId.toString(),
|
||||
cipherId = fido2AutofillView.cipherId,
|
||||
isUserVerified = isUserVerified,
|
||||
requestCode = Random.nextInt(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.setIcon(
|
||||
getCredentialEntryIcon(
|
||||
isPasskey = true,
|
||||
),
|
||||
)
|
||||
.also { builder ->
|
||||
if (!isUserVerified) {
|
||||
builder.setBiometricPromptDataIfSupported(
|
||||
cipher = biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId),
|
||||
isSingleTapAuthEnabled = featureFlagManager
|
||||
.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
// TODO: [PM-20176] Enable web icons in credential entries
|
||||
// Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
|
||||
// are addressed. See https://issuetracker.google.com/issues/355141766 for details.
|
||||
private fun getCredentialEntryIcon(isPasskey: Boolean): Icon = IconCompat
|
||||
.createWithResource(
|
||||
context,
|
||||
if (isPasskey) {
|
||||
R.drawable.ic_bw_passkey
|
||||
} else {
|
||||
R.drawable.ic_globe
|
||||
},
|
||||
)
|
||||
.toIcon(context)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.credentials.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* A wrapper around [ProviderGetCredentialRequest] that includes additional information needed to
|
||||
* fulfill the request.
|
||||
*
|
||||
* @param userId The ID of the user that owns the credential being requested.
|
||||
* @param cipherId The ID of the cipher containing the password to be retrieved.
|
||||
* @param isUserVerified Whether the user has been verified prior to this request.
|
||||
* @param requestData The original request data from the system.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProviderGetPasswordCredentialRequest(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
val isUserVerified: Boolean,
|
||||
val requestData: Bundle,
|
||||
) : Parcelable
|
||||
@@ -1,27 +0,0 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.credentials.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* Sets the biometric prompt data on the [PublicKeyCredentialEntry.Builder] if supported.
|
||||
*/
|
||||
fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher?,
|
||||
isSingleTapAuthEnabled: Boolean,
|
||||
): PublicKeyCredentialEntry.Builder =
|
||||
if (!isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
|
||||
cipher != null &&
|
||||
isSingleTapAuthEnabled
|
||||
) {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Class to hold feature flag keys.
|
||||
*/
|
||||
sealed class FlagKey<out T : Any> {
|
||||
/**
|
||||
* The string value of the given key. This must match the network value.
|
||||
*/
|
||||
abstract val keyName: String
|
||||
|
||||
/**
|
||||
* The value to be used if the flags value cannot be determined or is not remotely configured.
|
||||
*/
|
||||
abstract val defaultValue: T
|
||||
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* List of all flag keys to consider
|
||||
*/
|
||||
val activeFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
AuthenticatorSync,
|
||||
EmailVerification,
|
||||
OnboardingFlow,
|
||||
ImportLoginsFlow,
|
||||
VerifiedSsoDomainEndpoint,
|
||||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
MutualTls,
|
||||
SingleTapPasskeyCreation,
|
||||
SingleTapPasskeyAuthentication,
|
||||
AnonAddySelfHostAlias,
|
||||
SimpleLoginSelfHostAlias,
|
||||
ChromeAutofill,
|
||||
MobileErrorReporting,
|
||||
FlightRecorder,
|
||||
RestrictCipherItemDeletion,
|
||||
PreAuthSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-bwa-sync"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for Email Verification feature.
|
||||
*/
|
||||
data object EmailVerification : FlagKey<Boolean>() {
|
||||
override val keyName: String = "email-verification"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object MobileErrorReporting : FlagKey<Boolean>() {
|
||||
override val keyName: String = "mobile-error-reporting"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for enabling the flught recorder feature.
|
||||
*/
|
||||
data object FlightRecorder : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-flight-recorder"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the new onboarding feature.
|
||||
*/
|
||||
data object OnboardingFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "native-create-account-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the import logins feature.
|
||||
*/
|
||||
data object ImportLoginsFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "import-logins-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the new verified SSO domain endpoint feature.
|
||||
*/
|
||||
data object VerifiedSsoDomainEndpoint : FlagKey<Boolean>() {
|
||||
override val keyName: String = "pm-12337-refactor-sso-details-endpoint"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding hte feature flag key for the Credential Exchange Protocol (CXP) import
|
||||
* feature.
|
||||
*/
|
||||
data object CredentialExchangeProtocolImport : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cxp-import-mobile"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Credential Exchange Protocol (CXP) export
|
||||
* feature.
|
||||
*/
|
||||
data object CredentialExchangeProtocolExport : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cxp-export-mobile"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Cipher Key Encryption feature.
|
||||
*/
|
||||
data object CipherKeyEncryption : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cipher-key-encryption"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Mutual TLS feature.
|
||||
*/
|
||||
data object MutualTls : FlagKey<Boolean>() {
|
||||
override val keyName: String = "mutual-tls"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable single tap passkey creation.
|
||||
*/
|
||||
data object SingleTapPasskeyCreation : FlagKey<Boolean>() {
|
||||
override val keyName: String = "single-tap-passkey-creation"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable single tap passkey authentication.
|
||||
*/
|
||||
data object SingleTapPasskeyAuthentication : FlagKey<Boolean>() {
|
||||
override val keyName: String = "single-tap-passkey-authentication"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable AnonAddy (addy.io) self host alias
|
||||
* generation.
|
||||
*/
|
||||
data object AnonAddySelfHostAlias : FlagKey<Boolean>() {
|
||||
override val keyName: String = "anon-addy-self-host-alias"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable SimpleLogin self-host alias generation.
|
||||
*/
|
||||
data object SimpleLoginSelfHostAlias : FlagKey<Boolean>() {
|
||||
override val keyName: String = "simple-login-self-host-alias"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the checking for Chrome's third party
|
||||
* autofill.
|
||||
*/
|
||||
data object ChromeAutofill : FlagKey<Boolean>() {
|
||||
override val keyName: String = "android-chrome-autofill"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the restriction of cipher item deletion
|
||||
*/
|
||||
data object RestrictCipherItemDeletion : FlagKey<Boolean>() {
|
||||
override val keyName: String = "pm-15493-restrict-item-deletion-to-can-manage-permission"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the settings menu before login.
|
||||
*/
|
||||
data object PreAuthSettings : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-prelogin-settings"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
*/
|
||||
data object DummyBoolean : FlagKey<Boolean>() {
|
||||
override val keyName: String = "dummy-boolean"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for an [Int] flag to be used in tests.
|
||||
*/
|
||||
data object DummyInt : FlagKey<Int>() {
|
||||
override val keyName: String = "dummy-int"
|
||||
override val defaultValue: Int = Int.MIN_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for a [String] flag to be used in tests.
|
||||
*/
|
||||
data object DummyString : FlagKey<String>() {
|
||||
override val keyName: String = "dummy-string"
|
||||
override val defaultValue: String = "defaultValue"
|
||||
}
|
||||
//endregion Dummy keys for testing
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
|
||||
/**
|
||||
* A manager for handling restrictions.
|
||||
*/
|
||||
interface RestrictionManager
|
||||
@@ -1,123 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.RestrictionsManager
|
||||
import android.os.Bundle
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* The default implementation of the [RestrictionManager].
|
||||
*/
|
||||
class RestrictionManagerImpl(
|
||||
appStateManager: AppStateManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val context: Context,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val restrictionsManager: RestrictionsManager,
|
||||
) : RestrictionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
private val intentFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
|
||||
private val restrictionsChangedReceiver = RestrictionsChangedReceiver()
|
||||
private var isReceiverRegistered = false
|
||||
|
||||
init {
|
||||
appStateManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
when (it) {
|
||||
AppForegroundState.BACKGROUNDED -> handleBackground()
|
||||
AppForegroundState.FOREGROUNDED -> handleForeground()
|
||||
}
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
}
|
||||
|
||||
private fun handleBackground() {
|
||||
if (isReceiverRegistered) {
|
||||
context.unregisterReceiver(restrictionsChangedReceiver)
|
||||
}
|
||||
isReceiverRegistered = false
|
||||
}
|
||||
|
||||
private fun handleForeground() {
|
||||
context.registerReceiver(restrictionsChangedReceiver, intentFilter)
|
||||
isReceiverRegistered = true
|
||||
updatePreconfiguredRestrictionSettings()
|
||||
}
|
||||
|
||||
private fun updatePreconfiguredRestrictionSettings() {
|
||||
restrictionsManager
|
||||
.applicationRestrictions
|
||||
?.takeUnless { it.isEmpty }
|
||||
?.let { setPreconfiguredSettings(it) }
|
||||
}
|
||||
|
||||
private fun setPreconfiguredSettings(bundle: Bundle) {
|
||||
bundle
|
||||
.getString(BASE_ENVIRONMENT_URL_RESTRICTION_KEY)
|
||||
?.let { url -> setPreconfiguredUrl(baseEnvironmentUrl = url) }
|
||||
}
|
||||
|
||||
private fun setPreconfiguredUrl(baseEnvironmentUrl: String) {
|
||||
environmentRepository.environment = when (val current = environmentRepository.environment) {
|
||||
Environment.Us -> {
|
||||
when (baseEnvironmentUrl) {
|
||||
// If the base matches the predefined US environment, leave it alone
|
||||
Environment.Us.environmentUrlData.base -> current
|
||||
// If the base does not match the predefined US environment, create a
|
||||
// self-hosted environment with the new base
|
||||
else -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Environment.Eu -> {
|
||||
when (baseEnvironmentUrl) {
|
||||
// If the base matches the predefined EU environment, leave it alone
|
||||
Environment.Eu.environmentUrlData.base -> current
|
||||
// If the base does not match the predefined EU environment, create a
|
||||
// self-hosted environment with the new base
|
||||
else -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
is Environment.SelfHosted -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [BroadcastReceiver] used to listen for [Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED]
|
||||
* updates.
|
||||
*
|
||||
* Note: The `Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED` will only be received if the
|
||||
* `BroadcastReceiver` is dynamically registered, so this cannot be registered in the manifest.
|
||||
*/
|
||||
private inner class RestrictionsChangedReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) {
|
||||
updatePreconfiguredRestrictionSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val BASE_ENVIRONMENT_URL_RESTRICTION_KEY: String = "baseEnvironmentUrl"
|
||||
|
||||
/**
|
||||
* Helper method for creating a new [Environment.SelfHosted] with a new base.
|
||||
*/
|
||||
private fun Environment.toSelfHosted(
|
||||
base: String,
|
||||
): Environment.SelfHosted =
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = environmentUrlData.copy(base = base),
|
||||
)
|
||||
@@ -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,21 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
|
||||
|
||||
/**
|
||||
* Provides a human-readable display label for the given [ClearClipboardFrequency].
|
||||
*/
|
||||
val ClearClipboardFrequency.displayLabel: Text
|
||||
get() = when (this) {
|
||||
ClearClipboardFrequency.NEVER -> R.string.never
|
||||
ClearClipboardFrequency.TEN_SECONDS -> R.string.ten_seconds
|
||||
ClearClipboardFrequency.TWENTY_SECONDS -> R.string.twenty_seconds
|
||||
ClearClipboardFrequency.THIRTY_SECONDS -> R.string.thirty_seconds
|
||||
ClearClipboardFrequency.ONE_MINUTE -> R.string.one_minute
|
||||
ClearClipboardFrequency.TWO_MINUTES -> R.string.two_minutes
|
||||
ClearClipboardFrequency.FIVE_MINUTES -> R.string.five_minutes
|
||||
}
|
||||
.asText()
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.os.Build
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
|
||||
/**
|
||||
* A boolean property that indicates whether the current build flavor is "fdroid".
|
||||
*/
|
||||
val isFdroid: Boolean
|
||||
get() = BuildConfig.FLAVOR == "fdroid"
|
||||
|
||||
/**
|
||||
* A boolean property that indicates whether the current build is a dev build.
|
||||
*/
|
||||
val isDevBuild: Boolean
|
||||
get() = BuildConfig.BUILD_TYPE == "debug"
|
||||
|
||||
/**
|
||||
* A string that represents a displayable app version.
|
||||
*/
|
||||
val versionData: String
|
||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
|
||||
/**
|
||||
* A string that represents device data.
|
||||
*/
|
||||
val deviceData: String get() = "$deviceBrandModel $osInfo $buildInfo"
|
||||
|
||||
/**
|
||||
* A string representing the CI information if available.
|
||||
*/
|
||||
val ciBuildInfo: String? get() = BuildConfig.CI_INFO.takeUnless { it.isBlank() }
|
||||
|
||||
/**
|
||||
* A string representing the build flavor or blank if it is the standard configuration.
|
||||
*/
|
||||
private val buildFlavorName: String
|
||||
get() = when (BuildConfig.FLAVOR) {
|
||||
"standard" -> ""
|
||||
else -> "-${BuildConfig.FLAVOR}"
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representing the build type.
|
||||
*/
|
||||
private val buildTypeName: String
|
||||
get() = when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> "dev"
|
||||
"release" -> "prod"
|
||||
else -> BuildConfig.BUILD_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representing the device brand and model.
|
||||
*/
|
||||
private val deviceBrandModel: String get() = "\uD83D\uDCF1 ${Build.BRAND} ${Build.MODEL}"
|
||||
|
||||
/**
|
||||
* A string representing the operating system information.
|
||||
*/
|
||||
private val osInfo: String get() = "\uD83E\uDD16 ${Build.VERSION.RELEASE}@${Build.VERSION.SDK_INT}"
|
||||
|
||||
/**
|
||||
* A string representing the build information.
|
||||
*/
|
||||
private val buildInfo: String
|
||||
get() = "\uD83D\uDCE6 $buildTypeName" +
|
||||
buildFlavorName.takeUnless { it.isBlank() }?.let { " $it" }.orEmpty()
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.tiles
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A service for handling the Password Generator quick settings tile.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
@Keep
|
||||
@OmitFromCoverage
|
||||
class BitwardenGeneratorTileService : TileService() {
|
||||
@Inject
|
||||
lateinit var intentManager: IntentManager
|
||||
|
||||
override fun onClick() {
|
||||
if (isLocked) {
|
||||
unlockAndRun(Runnable { launchGenerator() })
|
||||
} else {
|
||||
launchGenerator()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun launchGenerator() {
|
||||
val intent = intentManager.createTileIntent("bitwarden://password_generator")
|
||||
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
startActivityAndCollapse(intentManager.createTilePendingIntent(0, intent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.tiles
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A service for handling the My Vault quick settings tile.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
@Keep
|
||||
@OmitFromCoverage
|
||||
class BitwardenVaultTileService : TileService() {
|
||||
@Inject
|
||||
lateinit var intentManager: IntentManager
|
||||
|
||||
override fun onClick() {
|
||||
if (isLocked) {
|
||||
unlockAndRun(Runnable { launchVault() })
|
||||
} else {
|
||||
launchVault()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun launchVault() {
|
||||
val intent = intentManager.createTileIntent("bitwarden://my_vault")
|
||||
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
startActivityAndCollapse(intentManager.createTilePendingIntent(0, intent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the create account screen.
|
||||
*/
|
||||
@Serializable
|
||||
data object CreateAccountRoute
|
||||
|
||||
/**
|
||||
* Navigate to the create account screen.
|
||||
*/
|
||||
fun NavController.navigateToCreateAccount(navOptions: NavOptions? = null) {
|
||||
this.navigate(route = CreateAccountRoute, navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the create account screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.createAccountDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String, captchaToken: String?) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<CreateAccountRoute> {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToLogin = onNavigateToLogin,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||
|
||||
import android.widget.Toast
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
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.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
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.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ContinueWithBreachedPasswordClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ErrorDialogDismiss
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.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.manager.intent.IntentManager
|
||||
|
||||
/**
|
||||
* Top level composable for the create account screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun CreateAccountScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String, captchaToken: String?) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: CreateAccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel) { event ->
|
||||
when (event) {
|
||||
is NavigateToPrivacyPolicy -> {
|
||||
intentManager.launchUri("https://bitwarden.com/privacy/".toUri())
|
||||
}
|
||||
|
||||
is NavigateToTerms -> {
|
||||
intentManager.launchUri("https://bitwarden.com/terms/".toUri())
|
||||
}
|
||||
|
||||
is CreateAccountEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
is CreateAccountEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
is CreateAccountEvent.NavigateToCaptcha -> {
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is CreateAccountEvent.NavigateToLogin -> {
|
||||
onNavigateToLogin(
|
||||
event.email,
|
||||
event.captchaToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show dialog if needed:
|
||||
when (val dialog = state.dialog) {
|
||||
is CreateAccountDialog.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
title = dialog.title?.invoke(),
|
||||
message = dialog.message(),
|
||||
throwable = dialog.error,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is CreateAccountDialog.HaveIBeenPwned -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialog.title(),
|
||||
message = dialog.message(),
|
||||
confirmButtonText = stringResource(id = R.string.yes),
|
||||
dismissButtonText = stringResource(id = R.string.no),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ContinueWithBreachedPasswordClick) }
|
||||
},
|
||||
onDismissClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ErrorDialogDismiss) }
|
||||
},
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
CreateAccountDialog.Loading -> {
|
||||
BitwardenLoadingDialog(text = stringResource(id = R.string.create_account))
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(CloseClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.submit),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SubmitClick) }
|
||||
},
|
||||
modifier = Modifier.testTag("SubmitButton"),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.email_address),
|
||||
value = state.emailInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EmailInputChange(it)) }
|
||||
},
|
||||
keyboardType = KeyboardType.Email,
|
||||
textFieldTestTag = "EmailAddressEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
value = state.passwordInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PasswordInputChange(it)) }
|
||||
},
|
||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||
supportingContent = {
|
||||
PasswordStrengthIndicator(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.passwordStrengthState,
|
||||
currentCharacterCount = state.passwordInput.length,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = state.passwordLengthLabel(),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
)
|
||||
},
|
||||
passwordFieldTestTag = "MasterPasswordEntry",
|
||||
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_master_password),
|
||||
value = state.confirmPasswordInput,
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
|
||||
},
|
||||
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
|
||||
passwordFieldTestTag = "ConfirmMasterPasswordEntry",
|
||||
cardStyle = CardStyle.Middle(dividerPadding = 0.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.master_password_hint),
|
||||
value = state.passwordHintInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PasswordHintChange(it)) }
|
||||
},
|
||||
supportingText = stringResource(id = R.string.master_password_hint_description),
|
||||
textFieldTestTag = "MasterPasswordHintLabel",
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
||||
isChecked = state.isCheckDataBreachesToggled,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ newState ->
|
||||
viewModel.trySendAction(CheckDataBreachesToggle(newState = newState))
|
||||
}
|
||||
},
|
||||
cardStyle = CardStyle.Top(),
|
||||
modifier = Modifier
|
||||
.testTag("CheckExposedMasterPasswordToggle")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
TermsAndPrivacySwitch(
|
||||
isChecked = state.isAcceptPoliciesToggled,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AcceptPoliciesToggle(it)) }
|
||||
},
|
||||
onTermsClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(TermsClick) }
|
||||
},
|
||||
onPrivacyPolicyClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PrivacyPolicyClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TermsAndPrivacySwitch(
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
onTermsClick: () -> Unit,
|
||||
onPrivacyPolicyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val strTerms = stringResource(id = R.string.terms_of_service)
|
||||
val strPrivacy = stringResource(id = R.string.privacy_policy)
|
||||
val annotatedLinkString: AnnotatedString = R.string
|
||||
.by_activating_this_switch_you_agree_to_the_terms_of_service_and_privacy_policy
|
||||
.toAnnotatedString {
|
||||
when (it) {
|
||||
"termsOfService" -> onTermsClick()
|
||||
"privacyPolicy" -> onPrivacyPolicyClick()
|
||||
}
|
||||
}
|
||||
BitwardenSwitch(
|
||||
modifier = modifier.semantics(mergeDescendants = true) {
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction(
|
||||
label = strTerms,
|
||||
action = {
|
||||
onTermsClick()
|
||||
true
|
||||
},
|
||||
),
|
||||
CustomAccessibilityAction(
|
||||
label = strPrivacy,
|
||||
action = {
|
||||
onPrivacyPolicyClick()
|
||||
true
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
label = annotatedLinkString,
|
||||
isChecked = isChecked,
|
||||
contentDescription = "AcceptPoliciesToggle",
|
||||
onCheckedChange = onCheckedChange,
|
||||
cardStyle = CardStyle.Bottom,
|
||||
)
|
||||
}
|
||||
@@ -1,600 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ContinueWithBreachedPasswordClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Internal.ReceivePasswordStrengthResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
private const val MIN_PASSWORD_LENGTH = 12
|
||||
|
||||
/**
|
||||
* Models logic for the create account screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class CreateAccountViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val authRepository: AuthRepository,
|
||||
) : BaseViewModel<CreateAccountState, CreateAccountEvent, CreateAccountAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: CreateAccountState(
|
||||
emailInput = "",
|
||||
passwordInput = "",
|
||||
confirmPasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
isAcceptPoliciesToggled = false,
|
||||
isCheckDataBreachesToggled = true,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
),
|
||||
) {
|
||||
|
||||
/**
|
||||
* Keeps track of async request to get password strength. Should be cancelled
|
||||
* when user input changes.
|
||||
*/
|
||||
private var passwordStrengthJob: Job = Job().apply { complete() }
|
||||
|
||||
init {
|
||||
// As state updates, write to saved state handle:
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
authRepository
|
||||
.captchaTokenResultFlow
|
||||
.onEach {
|
||||
sendAction(
|
||||
CreateAccountAction.Internal.ReceiveCaptchaToken(
|
||||
tokenResult = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: CreateAccountAction) {
|
||||
when (action) {
|
||||
is SubmitClick -> handleSubmitClick()
|
||||
is ConfirmPasswordInputChange -> handleConfirmPasswordInputChanged(action)
|
||||
is EmailInputChange -> handleEmailInputChanged(action)
|
||||
is PasswordHintChange -> handlePasswordHintChanged(action)
|
||||
is PasswordInputChange -> handlePasswordInputChanged(action)
|
||||
is CreateAccountAction.CloseClick -> handleCloseClick()
|
||||
is CreateAccountAction.ErrorDialogDismiss -> handleDialogDismiss()
|
||||
is AcceptPoliciesToggle -> handleAcceptPoliciesToggle(action)
|
||||
is CheckDataBreachesToggle -> handleCheckDataBreachesToggle(action)
|
||||
is PrivacyPolicyClick -> handlePrivacyPolicyClick()
|
||||
is TermsClick -> handleTermsClick()
|
||||
is CreateAccountAction.Internal.ReceiveRegisterResult -> {
|
||||
handleReceiveRegisterAccountResult(action)
|
||||
}
|
||||
|
||||
is CreateAccountAction.Internal.ReceiveCaptchaToken -> {
|
||||
handleReceiveCaptchaToken(action)
|
||||
}
|
||||
|
||||
ContinueWithBreachedPasswordClick -> handleContinueWithBreachedPasswordClick()
|
||||
is ReceivePasswordStrengthResult -> handlePasswordStrengthResult(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePasswordStrengthResult(action: ReceivePasswordStrengthResult) {
|
||||
when (val result = action.result) {
|
||||
is PasswordStrengthResult.Success -> {
|
||||
val updatedState = when (result.passwordStrength) {
|
||||
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
|
||||
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
|
||||
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
|
||||
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
|
||||
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
|
||||
}
|
||||
mutableStateFlow.update { oldState ->
|
||||
oldState.copy(
|
||||
passwordStrengthState = updatedState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is PasswordStrengthResult.Error -> {
|
||||
// Leave UI the same
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceiveCaptchaToken(
|
||||
action: CreateAccountAction.Internal.ReceiveCaptchaToken,
|
||||
) {
|
||||
when (val result = action.tokenResult) {
|
||||
is CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
submitRegisterAccountRequest(
|
||||
shouldCheckForDataBreaches = false,
|
||||
shouldIgnorePasswordStrength = true,
|
||||
captchaToken = result.token,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MaxLineLength")
|
||||
private fun handleReceiveRegisterAccountResult(
|
||||
action: CreateAccountAction.Internal.ReceiveRegisterResult,
|
||||
) {
|
||||
when (val registerAccountResult = action.registerResult) {
|
||||
is RegisterResult.CaptchaRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
sendEvent(
|
||||
CreateAccountEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = registerAccountResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = registerAccountResult.errorMessage?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
error = registerAccountResult.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is RegisterResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
sendEvent(
|
||||
CreateAccountEvent.NavigateToLogin(
|
||||
email = state.emailInput,
|
||||
captchaToken = registerAccountResult.captchaToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
RegisterResult.DataBreachFound -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
||||
title = R.string.exposed_master_password.asText(),
|
||||
message = R.string.password_found_in_a_data_breach_alert_description.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RegisterResult.DataBreachAndWeakPassword -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
||||
title = R.string.weak_and_exposed_master_password.asText(),
|
||||
message = R.string.weak_password_identified_and_found_in_a_data_breach_alert_description.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RegisterResult.WeakPassword -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
||||
title = R.string.weak_master_password.asText(),
|
||||
message = R.string.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePrivacyPolicyClick() = sendEvent(CreateAccountEvent.NavigateToPrivacyPolicy)
|
||||
|
||||
private fun handleTermsClick() = sendEvent(CreateAccountEvent.NavigateToTerms)
|
||||
|
||||
private fun handleAcceptPoliciesToggle(action: AcceptPoliciesToggle) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isAcceptPoliciesToggled = action.newState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCheckDataBreachesToggle(action: CheckDataBreachesToggle) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isCheckDataBreachesToggled = action.newState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDialogDismiss() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(CreateAccountEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleEmailInputChanged(action: EmailInputChange) {
|
||||
mutableStateFlow.update { it.copy(emailInput = action.input) }
|
||||
}
|
||||
|
||||
private fun handlePasswordHintChanged(action: PasswordHintChange) {
|
||||
mutableStateFlow.update { it.copy(passwordHintInput = action.input) }
|
||||
}
|
||||
|
||||
private fun handlePasswordInputChanged(action: PasswordInputChange) {
|
||||
// Update input:
|
||||
mutableStateFlow.update { it.copy(passwordInput = action.input) }
|
||||
// Update password strength:
|
||||
passwordStrengthJob.cancel()
|
||||
if (action.input.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
|
||||
}
|
||||
} else {
|
||||
passwordStrengthJob = viewModelScope.launch {
|
||||
val result = authRepository.getPasswordStrength(
|
||||
email = state.emailInput,
|
||||
password = action.input,
|
||||
)
|
||||
trySendAction(ReceivePasswordStrengthResult(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) {
|
||||
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleSubmitClick() = when {
|
||||
state.emailInput.isBlank() -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
!state.emailInput.isValidEmail() -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_email.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.passwordInput.length < MIN_PASSWORD_LENGTH -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.passwordInput != state.confirmPasswordInput -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
!state.isAcceptPoliciesToggled -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.accept_policies_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
submitRegisterAccountRequest(
|
||||
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
|
||||
shouldIgnorePasswordStrength = false,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueWithBreachedPasswordClick() {
|
||||
submitRegisterAccountRequest(
|
||||
shouldCheckForDataBreaches = false,
|
||||
shouldIgnorePasswordStrength = true,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun submitRegisterAccountRequest(
|
||||
shouldCheckForDataBreaches: Boolean,
|
||||
shouldIgnorePasswordStrength: Boolean,
|
||||
captchaToken: String?,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = CreateAccountDialog.Loading)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.register(
|
||||
shouldCheckDataBreaches = shouldCheckForDataBreaches,
|
||||
isMasterPasswordStrong = shouldIgnorePasswordStrength ||
|
||||
state.isMasterPasswordStrong,
|
||||
email = state.emailInput,
|
||||
masterPassword = state.passwordInput,
|
||||
masterPasswordHint = state.passwordHintInput.ifBlank { null },
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
CreateAccountAction.Internal.ReceiveRegisterResult(
|
||||
registerResult = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI state for the create account screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CreateAccountState(
|
||||
val emailInput: String,
|
||||
val passwordInput: String,
|
||||
val confirmPasswordInput: String,
|
||||
val passwordHintInput: String,
|
||||
val isCheckDataBreachesToggled: Boolean,
|
||||
val isAcceptPoliciesToggled: Boolean,
|
||||
val dialog: CreateAccountDialog?,
|
||||
val passwordStrengthState: PasswordStrengthState,
|
||||
) : Parcelable {
|
||||
|
||||
val passwordLengthLabel: Text
|
||||
// Have to concat a few strings here, resulting string is:
|
||||
// Important: Your master password cannot be recovered if you forget it! 12
|
||||
// characters minimum
|
||||
@Suppress("MaxLineLength")
|
||||
get() = R.string.important.asText()
|
||||
.concat(
|
||||
": ".asText(),
|
||||
R.string.your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether or not the provided master password is considered strong.
|
||||
*/
|
||||
val isMasterPasswordStrong: Boolean
|
||||
get() = when (passwordStrengthState) {
|
||||
PasswordStrengthState.NONE,
|
||||
PasswordStrengthState.WEAK_1,
|
||||
PasswordStrengthState.WEAK_2,
|
||||
PasswordStrengthState.WEAK_3,
|
||||
-> false
|
||||
|
||||
PasswordStrengthState.GOOD,
|
||||
PasswordStrengthState.STRONG,
|
||||
-> true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models dialogs that can be displayed on the create account screen.
|
||||
*/
|
||||
sealed class CreateAccountDialog : Parcelable {
|
||||
/**
|
||||
* Loading dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Loading : CreateAccountDialog()
|
||||
|
||||
/**
|
||||
* Confirm the user wants to continue with potentially breached password.
|
||||
*
|
||||
* @param title The title for the HaveIBeenPwned dialog.
|
||||
* @param message The message for the HaveIBeenPwned dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data class HaveIBeenPwned(
|
||||
val title: Text,
|
||||
val message: Text,
|
||||
) : CreateAccountDialog()
|
||||
|
||||
/**
|
||||
* General error dialog with an OK button.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
val error: Throwable? = null,
|
||||
) : CreateAccountDialog()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the create account screen.
|
||||
*/
|
||||
sealed class CreateAccountEvent {
|
||||
|
||||
/**
|
||||
* Navigate back to previous screen.
|
||||
*/
|
||||
data object NavigateBack : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Placeholder event for showing a toast. Can be removed once there are real events.
|
||||
*/
|
||||
data class ShowToast(val text: String) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the login screen bypassing captcha with token.
|
||||
*/
|
||||
data class NavigateToLogin(
|
||||
val email: String,
|
||||
val captchaToken: String?,
|
||||
) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigate to terms and conditions.
|
||||
*/
|
||||
data object NavigateToTerms : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigate to privacy policy.
|
||||
*/
|
||||
data object NavigateToPrivacyPolicy : CreateAccountEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the create account screen.
|
||||
*/
|
||||
sealed class CreateAccountAction {
|
||||
/**
|
||||
* User clicked submit.
|
||||
*/
|
||||
data object SubmitClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User clicked close.
|
||||
*/
|
||||
data object CloseClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User clicked "Yes" when being asked if they are sure they want to use a breached password.
|
||||
*/
|
||||
data object ContinueWithBreachedPasswordClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* Email input changed.
|
||||
*/
|
||||
data class EmailInputChange(val input: String) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* Password input changed.
|
||||
*/
|
||||
data class PasswordInputChange(val input: String) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* Confirm password input changed.
|
||||
*/
|
||||
data class ConfirmPasswordInputChange(val input: String) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* Password hint input changed.
|
||||
*/
|
||||
data class PasswordHintChange(val input: String) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User dismissed the error dialog.
|
||||
*/
|
||||
data object ErrorDialogDismiss : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped check data breaches toggle.
|
||||
*/
|
||||
data class CheckDataBreachesToggle(val newState: Boolean) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped accept policies toggle.
|
||||
*/
|
||||
data class AcceptPoliciesToggle(val newState: Boolean) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped privacy policy link.
|
||||
*/
|
||||
data object PrivacyPolicyClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped terms link.
|
||||
*/
|
||||
data object TermsClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [CreateAccountViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : CreateAccountAction() {
|
||||
/**
|
||||
* Indicates a captcha callback token has been received.
|
||||
*/
|
||||
data class ReceiveCaptchaToken(
|
||||
val tokenResult: CaptchaCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a [RegisterResult] has been received.
|
||||
*/
|
||||
data class ReceiveRegisterResult(
|
||||
val registerResult: RegisterResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a password strength result has been received.
|
||||
*/
|
||||
data class ReceivePasswordStrengthResult(
|
||||
val result: PasswordStrengthResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
@@ -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,116 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.components.dropdown
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* A custom composable representing a multi-select button.
|
||||
*
|
||||
* This composable displays an [OutlinedTextField] with a dropdown icon as a trailing icon.
|
||||
* When the field is clicked, a dropdown menu appears with a list of options to select from.
|
||||
*
|
||||
* @param label The descriptive text label for the [OutlinedTextField].
|
||||
* @param options A list of strings representing the available options in the dialog.
|
||||
* @param selectedOption The currently selected option that is displayed in the [OutlinedTextField]
|
||||
* (or `null` if no option is selected).
|
||||
* @param onOptionSelected A lambda that is invoked when an option
|
||||
* is selected from the dropdown menu.
|
||||
* @param isEnabled Whether or not 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 supportingText A optional supporting text that will appear below the text field.
|
||||
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
|
||||
* @param insets Inner padding to be applied withing the card.
|
||||
* @param textFieldTestTag The optional test tag associated with the inner text field.
|
||||
* @param actionsPadding Padding to be applied to the [actions] block.
|
||||
* @param actions A lambda containing the set of actions (usually icons or similar) to display
|
||||
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
|
||||
* defining the layout of the actions.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenMultiSelectButton(
|
||||
label: String,
|
||||
options: ImmutableList<String>,
|
||||
selectedOption: String?,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
cardStyle: CardStyle?,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
supportingText: String? = null,
|
||||
tooltip: TooltipData? = null,
|
||||
insets: PaddingValues = PaddingValues(),
|
||||
textFieldTestTag: String? = null,
|
||||
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
BitwardenTextSelectionButton(
|
||||
label = label,
|
||||
selectedOption = selectedOption,
|
||||
onClick = {
|
||||
shouldShowDialog = true
|
||||
},
|
||||
cardStyle = cardStyle,
|
||||
enabled = isEnabled,
|
||||
supportingText = supportingText,
|
||||
tooltip = tooltip,
|
||||
insets = insets,
|
||||
textFieldTestTag = textFieldTestTag,
|
||||
actionsPadding = actionsPadding,
|
||||
actions = actions,
|
||||
semanticRole = Role.DropdownList,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
if (shouldShowDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = label,
|
||||
onDismissRequest = { shouldShowDialog = false },
|
||||
) {
|
||||
options.forEach { optionString ->
|
||||
BitwardenSelectionRow(
|
||||
text = optionString.asText(),
|
||||
isSelected = optionString == selectedOption,
|
||||
onClick = {
|
||||
shouldShowDialog = false
|
||||
onOptionSelected(optionString)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenMultiSelectButton_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenMultiSelectButton(
|
||||
label = "Label",
|
||||
options = persistentListOf("a", "b"),
|
||||
selectedOption = "",
|
||||
onOptionSelected = {},
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.components.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||
|
||||
/**
|
||||
* Defines the possible display options for a bottom divider on a [BitwardenMediumTopAppBar].
|
||||
*/
|
||||
enum class TopAppBarDividerStyle {
|
||||
NONE,
|
||||
STATIC,
|
||||
ON_SCROLL,
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.debugmenu.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
|
||||
/**
|
||||
* Creates a list item for a [FlagKey].
|
||||
*/
|
||||
@Composable
|
||||
fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
currentValue: T,
|
||||
onValueChange: (key: FlagKey<T>, value: T) -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) = when (val flagKey = this) {
|
||||
FlagKey.DummyBoolean,
|
||||
is FlagKey.DummyInt,
|
||||
FlagKey.DummyString,
|
||||
-> {
|
||||
Unit
|
||||
}
|
||||
|
||||
FlagKey.AuthenticatorSync,
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.OnboardingFlow,
|
||||
FlagKey.ImportLoginsFlow,
|
||||
FlagKey.VerifiedSsoDomainEndpoint,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.MutualTls,
|
||||
FlagKey.SingleTapPasskeyCreation,
|
||||
FlagKey.SingleTapPasskeyAuthentication,
|
||||
FlagKey.AnonAddySelfHostAlias,
|
||||
FlagKey.SimpleLoginSelfHostAlias,
|
||||
FlagKey.ChromeAutofill,
|
||||
FlagKey.MobileErrorReporting,
|
||||
FlagKey.FlightRecorder,
|
||||
FlagKey.RestrictCipherItemDeletion,
|
||||
FlagKey.PreAuthSettings,
|
||||
-> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
BooleanFlagItem(
|
||||
label = flagKey.getDisplayLabel(),
|
||||
key = flagKey as FlagKey<Boolean>,
|
||||
currentValue = currentValue as Boolean,
|
||||
onValueChange = onValueChange as (FlagKey<Boolean>, Boolean) -> Unit,
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The UI layout for a boolean backed flag key.
|
||||
*/
|
||||
@Composable
|
||||
private fun BooleanFlagItem(
|
||||
label: String,
|
||||
key: FlagKey<Boolean>,
|
||||
currentValue: Boolean,
|
||||
onValueChange: (key: FlagKey<Boolean>, value: Boolean) -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenSwitch(
|
||||
label = label,
|
||||
isChecked = currentValue,
|
||||
onCheckedChange = { onValueChange(key, it) },
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.DummyBoolean,
|
||||
is FlagKey.DummyInt,
|
||||
FlagKey.DummyString,
|
||||
-> this.keyName
|
||||
|
||||
FlagKey.AuthenticatorSync -> stringResource(R.string.authenticator_sync)
|
||||
FlagKey.EmailVerification -> stringResource(R.string.email_verification)
|
||||
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
|
||||
FlagKey.ImportLoginsFlow -> stringResource(R.string.import_logins_flow)
|
||||
FlagKey.VerifiedSsoDomainEndpoint -> stringResource(R.string.verified_sso_domain_verified)
|
||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(R.string.cxp_import)
|
||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(R.string.cxp_export)
|
||||
FlagKey.CipherKeyEncryption -> stringResource(R.string.cipher_key_encryption)
|
||||
FlagKey.MutualTls -> stringResource(R.string.mutual_tls)
|
||||
FlagKey.SingleTapPasskeyCreation -> stringResource(R.string.single_tap_passkey_creation)
|
||||
FlagKey.SingleTapPasskeyAuthentication -> {
|
||||
stringResource(R.string.single_tap_passkey_authentication)
|
||||
}
|
||||
|
||||
FlagKey.AnonAddySelfHostAlias -> stringResource(R.string.anon_addy_self_hosted_aliases)
|
||||
FlagKey.SimpleLoginSelfHostAlias -> stringResource(R.string.simple_login_self_hosted_aliases)
|
||||
FlagKey.ChromeAutofill -> stringResource(R.string.enable_chrome_autofill)
|
||||
FlagKey.MobileErrorReporting -> stringResource(R.string.enable_error_reporting_dialog)
|
||||
FlagKey.FlightRecorder -> stringResource(R.string.enable_flight_recorder)
|
||||
FlagKey.RestrictCipherItemDeletion -> stringResource(R.string.restrict_item_deletion)
|
||||
FlagKey.PreAuthSettings -> stringResource(R.string.enable_pre_auth_settings)
|
||||
}
|
||||
@@ -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,187 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.manager.intent
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* A manager class for simplifying the handling of Android Intents within a given context.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Immutable
|
||||
interface IntentManager {
|
||||
/**
|
||||
* Start an activity using the provided [Intent].
|
||||
*/
|
||||
fun startActivity(intent: Intent)
|
||||
|
||||
/**
|
||||
* Start a Custom Tabs Activity using the provided [Uri].
|
||||
*/
|
||||
fun startCustomTabsActivity(uri: Uri)
|
||||
|
||||
/**
|
||||
* Attempts to start the system accessibility settings activity.
|
||||
*/
|
||||
fun startSystemAccessibilitySettingsActivity()
|
||||
|
||||
/**
|
||||
* Attempts to start the system autofill settings activity. The return value indicates whether
|
||||
* or not this was successful.
|
||||
*/
|
||||
fun startSystemAutofillSettingsActivity(): Boolean
|
||||
|
||||
/**
|
||||
* Starts the application's settings activity.
|
||||
*/
|
||||
fun startApplicationDetailsSettingsActivity()
|
||||
|
||||
/**
|
||||
* Starts the credential manager settings.
|
||||
*/
|
||||
fun startCredentialManagerSettings(context: Context)
|
||||
|
||||
/**
|
||||
* Starts the Chrome autofill settings activity for the provided [ChromeReleaseChannel].
|
||||
*/
|
||||
fun startChromeAutofillSettingsActivity(releaseChannel: ChromeReleaseChannel): Boolean
|
||||
|
||||
/**
|
||||
* Start an activity to view the given [uri] in an external browser.
|
||||
*/
|
||||
fun launchUri(uri: Uri)
|
||||
|
||||
/**
|
||||
* Start an activity using the provided [Intent] and provides a callback, via [onResult], for
|
||||
* retrieving the [ActivityResult].
|
||||
*/
|
||||
@Composable
|
||||
fun getActivityResultLauncher(
|
||||
onResult: (ActivityResult) -> Unit,
|
||||
): ManagedActivityResultLauncher<Intent, ActivityResult>
|
||||
|
||||
/**
|
||||
* Launches the share sheet with the given [title] and file.
|
||||
*/
|
||||
fun shareFile(title: String? = null, fileUri: Uri)
|
||||
|
||||
/**
|
||||
* Launches the share sheet with the given [text].
|
||||
*/
|
||||
fun shareText(text: String)
|
||||
|
||||
/**
|
||||
* Processes the [activityResult] and attempts to get the relevant file data from it.
|
||||
*/
|
||||
fun getFileDataFromActivityResult(activityResult: ActivityResult): FileData?
|
||||
|
||||
/**
|
||||
* Processes the [intent] and attempts to get the relevant file data from it.
|
||||
*/
|
||||
fun getFileDataFromIntent(intent: Intent): FileData?
|
||||
|
||||
/**
|
||||
* Processes the [intent] and attempts to derive [ShareData] information from it.
|
||||
*/
|
||||
fun getShareDataFromIntent(intent: Intent): ShareData?
|
||||
|
||||
/**
|
||||
* Creates an intent for choosing a file saved to disk.
|
||||
*/
|
||||
fun createFileChooserIntent(withCameraIntents: Boolean): Intent
|
||||
|
||||
/**
|
||||
* Creates an intent to use when selecting to save an item with [fileName] to disk.
|
||||
*/
|
||||
fun createDocumentIntent(fileName: String): Intent
|
||||
|
||||
/**
|
||||
* Creates an intent using [data] when selecting a quick settings tile.
|
||||
*/
|
||||
fun createTileIntent(data: String): Intent
|
||||
|
||||
/**
|
||||
* Creates a pending intent using [requestCode] and [tileIntent] when selecting a quick
|
||||
* settings tile on API 34+.
|
||||
*/
|
||||
fun createTilePendingIntent(requestCode: Int, tileIntent: Intent): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing [androidx.credentials.provider.CreateEntry]
|
||||
* instances for FIDO 2 credential creation.
|
||||
*/
|
||||
fun createFido2CreationPendingIntent(
|
||||
action: String,
|
||||
userId: String,
|
||||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing
|
||||
* [androidx.credentials.provider.CredentialEntry] instances for FIDO 2 credential filling.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun createFido2GetCredentialPendingIntent(
|
||||
action: String,
|
||||
userId: String,
|
||||
credentialId: String,
|
||||
cipherId: String,
|
||||
isUserVerified: Boolean,
|
||||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing
|
||||
* [androidx.credentials.provider.AuthenticationAction] instances for FIDO 2 credential filling.
|
||||
*/
|
||||
fun createFido2UnlockPendingIntent(
|
||||
action: String,
|
||||
userId: String,
|
||||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Open the default email app on device.
|
||||
*/
|
||||
fun startDefaultEmailApplication()
|
||||
|
||||
/**
|
||||
* Represents file information.
|
||||
*/
|
||||
@Parcelize
|
||||
data class FileData(
|
||||
val fileName: String,
|
||||
val uri: Uri,
|
||||
val sizeBytes: Long,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Represents data for a share request coming from outside the app.
|
||||
*/
|
||||
sealed class ShareData : Parcelable {
|
||||
/**
|
||||
* The data required to create a new Text Send.
|
||||
*/
|
||||
@Parcelize
|
||||
data class TextSend(
|
||||
val subject: String?,
|
||||
val text: String,
|
||||
) : ShareData()
|
||||
|
||||
/**
|
||||
* The data required to create a new File Send.
|
||||
*/
|
||||
@Parcelize
|
||||
data class FileSend(
|
||||
val fileData: FileData,
|
||||
) : ShareData()
|
||||
}
|
||||
}
|
||||
@@ -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,54 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
|
||||
|
||||
/**
|
||||
* Determines the initial [AddSendState.ViewState.Content.SendType] based on the data in the
|
||||
* [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.ShareNewSend -> {
|
||||
when (data) {
|
||||
is IntentManager.ShareData.FileSend -> AddSendState.ViewState.Content.SendType.File(
|
||||
uri = data.fileData.uri,
|
||||
name = data.fileData.fileName,
|
||||
sizeBytes = data.fileData.sizeBytes,
|
||||
displaySize = null,
|
||||
)
|
||||
|
||||
is IntentManager.ShareData.TextSend -> AddSendState.ViewState.Content.SendType.Text(
|
||||
input = data.text,
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the initial send name based on the data in the [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance?.toSendName(): String? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.ShareNewSend -> {
|
||||
when (data) {
|
||||
is IntentManager.ShareData.FileSend -> data.fileData.fileName
|
||||
is IntentManager.ShareData.TextSend -> data.subject
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the [SpecialCircumstance] requires the app to be closed after completing the send.
|
||||
*/
|
||||
fun SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
|
||||
when (this) {
|
||||
is SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
|
||||
else -> false
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Represents the types of icons to be displayed with the send.
|
||||
*/
|
||||
enum class SendStatusIcon(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val contentDescription: Text,
|
||||
val testTag: String,
|
||||
) {
|
||||
DISABLED(
|
||||
iconRes = R.drawable.ic_send_disabled,
|
||||
contentDescription = R.string.disabled.asText(),
|
||||
testTag = "DisabledSendIcon",
|
||||
),
|
||||
PASSWORD(
|
||||
iconRes = R.drawable.ic_key,
|
||||
contentDescription = R.string.password.asText(),
|
||||
testTag = "PasswordProtectedSendIcon",
|
||||
),
|
||||
EXPIRED(
|
||||
iconRes = R.drawable.ic_send_expired,
|
||||
contentDescription = R.string.expired.asText(),
|
||||
testTag = "ExpiredSendIcon",
|
||||
),
|
||||
MAX_ACCESS_COUNT_REACHED(
|
||||
iconRes = R.drawable.ic_send_max_access_count_reached,
|
||||
contentDescription = R.string.maximum_access_count_reached.asText(),
|
||||
testTag = "MaxAccessSendIcon",
|
||||
),
|
||||
PENDING_DELETE(
|
||||
iconRes = R.drawable.ic_send_pending_delete,
|
||||
contentDescription = R.string.pending_delete.asText(),
|
||||
testTag = "PendingDeletionSendIcon",
|
||||
),
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
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.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType
|
||||
|
||||
/**
|
||||
* The URI item displayed to the user.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun VaultAddEditUriItem(
|
||||
uriItem: UriItem,
|
||||
onUriItemRemoved: (UriItem) -> Unit,
|
||||
onUriValueChange: (UriItem) -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.website_uri),
|
||||
value = uriItem.uri.orEmpty(),
|
||||
onValueChange = { onUriValueChange(uriItem.copy(uri = it)) },
|
||||
actions = {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = R.drawable.ic_cog,
|
||||
contentDescription = stringResource(id = R.string.options),
|
||||
onClick = { shouldShowOptionsDialog = true },
|
||||
modifier = Modifier.testTag(tag = "LoginUriOptionsButton"),
|
||||
)
|
||||
},
|
||||
textFieldTestTag = "LoginUriEntry",
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
if (shouldShowOptionsDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = R.string.options),
|
||||
onDismissRequest = { shouldShowOptionsDialog = false },
|
||||
) {
|
||||
BitwardenBasicDialogRow(
|
||||
text = stringResource(id = R.string.match_detection),
|
||||
onClick = {
|
||||
shouldShowOptionsDialog = false
|
||||
shouldShowMatchDialog = true
|
||||
},
|
||||
)
|
||||
BitwardenBasicDialogRow(
|
||||
text = stringResource(id = R.string.remove),
|
||||
onClick = {
|
||||
shouldShowOptionsDialog = false
|
||||
onUriItemRemoved(uriItem)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowMatchDialog) {
|
||||
val selectedString = uriItem.match.toDisplayMatchType().text.invoke()
|
||||
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = R.string.uri_match_detection),
|
||||
onDismissRequest = { shouldShowMatchDialog = false },
|
||||
) {
|
||||
UriMatchDisplayType
|
||||
.entries
|
||||
.forEach { matchType ->
|
||||
BitwardenSelectionRow(
|
||||
text = matchType.text,
|
||||
isSelected = matchType.text.invoke() == selectedString,
|
||||
onClick = {
|
||||
shouldShowMatchDialog = false
|
||||
onUriValueChange(
|
||||
uriItem.copy(match = matchType.toUriMatchType()),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.util
|
||||
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultTrailingIcon
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Creates the list of overflow actions to be displayed for a [CipherView].
|
||||
*/
|
||||
fun CipherView.toOverflowActions(
|
||||
hasMasterPassword: Boolean,
|
||||
isPremiumUser: Boolean,
|
||||
): List<ListingItemOverflowAction.VaultAction> =
|
||||
this
|
||||
.id
|
||||
?.let { cipherId ->
|
||||
listOfNotNull(
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = this.type,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = cipherId,
|
||||
cipherType = this.type,
|
||||
requiresPasswordReprompt = hasMasterPassword,
|
||||
)
|
||||
.takeUnless { this.deletedDate != null || !this.edit },
|
||||
this.login?.username?.let {
|
||||
ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = it)
|
||||
},
|
||||
this.login?.password
|
||||
?.let {
|
||||
ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
cipherId = cipherId,
|
||||
password = it,
|
||||
requiresPasswordReprompt = hasMasterPassword,
|
||||
)
|
||||
}
|
||||
.takeIf { this.viewPassword },
|
||||
this.login?.totp
|
||||
?.let { ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode = it) }
|
||||
.takeIf {
|
||||
this.type == CipherType.LOGIN &&
|
||||
(this.organizationUseTotp || isPremiumUser)
|
||||
},
|
||||
this.card?.number?.let {
|
||||
ListingItemOverflowAction.VaultAction.CopyNumberClick(
|
||||
number = it,
|
||||
requiresPasswordReprompt = hasMasterPassword,
|
||||
)
|
||||
},
|
||||
this.card?.code?.let {
|
||||
ListingItemOverflowAction.VaultAction.CopySecurityCodeClick(
|
||||
securityCode = it,
|
||||
cipherId = cipherId,
|
||||
requiresPasswordReprompt = hasMasterPassword,
|
||||
)
|
||||
},
|
||||
this.notes
|
||||
?.let { ListingItemOverflowAction.VaultAction.CopyNoteClick(notes = it) }
|
||||
.takeIf { this.type == CipherType.SECURE_NOTE },
|
||||
this.login?.uris?.firstOrNull { it.uri != null }?.uri?.let {
|
||||
ListingItemOverflowAction.VaultAction.LaunchClick(url = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
/**
|
||||
* Checks if the list is empty and if not returns an icon in a list.
|
||||
*/
|
||||
fun CipherView.toLabelIcons(): ImmutableList<IconData> {
|
||||
return listOfNotNull(
|
||||
VaultTrailingIcon.COLLECTION.takeIf {
|
||||
this.collectionIds.isNotEmpty() || this.organizationId?.isNotEmpty() == true
|
||||
},
|
||||
VaultTrailingIcon.ATTACHMENT.takeIf { this.attachments?.isNotEmpty() == true },
|
||||
)
|
||||
.map {
|
||||
IconData.Local(
|
||||
iconRes = it.iconRes,
|
||||
contentDescription = it.contentDescription,
|
||||
testTag = it.testTag,
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.model
|
||||
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Represents the types for linked fields.
|
||||
*
|
||||
* @param id The ID for the linked field.
|
||||
* @param label A human-readable label for the linked field.
|
||||
*/
|
||||
enum class VaultLinkedFieldType(
|
||||
val id: UInt,
|
||||
val label: Text,
|
||||
) {
|
||||
USERNAME(id = 100.toUInt(), label = R.string.username.asText()),
|
||||
PASSWORD(id = 101.toUInt(), label = R.string.password.asText()),
|
||||
|
||||
CARDHOLDER_NAME(id = 300.toUInt(), label = R.string.cardholder_name.asText()),
|
||||
EXPIRATION_MONTH(id = 301.toUInt(), label = R.string.expiration_month.asText()),
|
||||
EXPIRATION_YEAR(id = 302.toUInt(), label = R.string.expiration_year.asText()),
|
||||
SECURITY_CODE(id = 303.toUInt(), label = R.string.security_code.asText()),
|
||||
BRAND(id = 304.toUInt(), label = R.string.brand.asText()),
|
||||
NUMBER(id = 305.toUInt(), label = R.string.number.asText()),
|
||||
|
||||
TITLE(id = 400.toUInt(), label = R.string.title.asText()),
|
||||
MIDDLE_NAME(id = 401.toUInt(), label = R.string.middle_name.asText()),
|
||||
ADDRESS_1(id = 402.toUInt(), label = R.string.address1.asText()),
|
||||
ADDRESS_2(id = 403.toUInt(), label = R.string.address2.asText()),
|
||||
ADDRESS_3(id = 404.toUInt(), label = R.string.address3.asText()),
|
||||
CITY(id = 405.toUInt(), label = R.string.city_town.asText()),
|
||||
STATE(id = 406.toUInt(), label = R.string.state_province.asText()),
|
||||
POSTAL_CODE(id = 407.toUInt(), label = R.string.zip_postal_code.asText()),
|
||||
COUNTRY(id = 408.toUInt(), label = R.string.country.asText()),
|
||||
COMPANY(id = 409.toUInt(), label = R.string.company.asText()),
|
||||
EMAIL(id = 410.toUInt(), label = R.string.email.asText()),
|
||||
PHONE(id = 411.toUInt(), label = R.string.phone.asText()),
|
||||
SSN(id = 412.toUInt(), label = R.string.ssn.asText()),
|
||||
IDENTITY_USERNAME(id = 413.toUInt(), label = R.string.username.asText()),
|
||||
PASSPORT_NUMBER(id = 414.toUInt(), label = R.string.passport_number.asText()),
|
||||
LICENSE_NUMBER(id = 415.toUInt(), label = R.string.license_number.asText()),
|
||||
FIRST_NAME(id = 416.toUInt(), label = R.string.first_name.asText()),
|
||||
LAST_NAME(id = 417.toUInt(), label = R.string.last_name.asText()),
|
||||
FULL_NAME(id = 418.toUInt(), label = R.string.full_name.asText()),
|
||||
;
|
||||
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Helper function to get the LinkedCustomFieldType from the id
|
||||
*/
|
||||
fun fromId(id: UInt): VaultLinkedFieldType =
|
||||
VaultLinkedFieldType.entries.first { it.id == id }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
|
||||
/**
|
||||
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
|
||||
@@ -11,7 +14,16 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
@OmitFromCoverage
|
||||
class AccessibilityActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
@@ -21,6 +23,7 @@ class AuthCallbackActivity : AppCompatActivity() {
|
||||
private val viewModel: AuthCallbackViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = intent))
|
||||
@@ -35,4 +38,12 @@ class AuthCallbackActivity : AppCompatActivity() {
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@@ -27,7 +26,6 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
|
||||
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
|
||||
val webAuthResult = action.intent.getWebAuthResultOrNull()
|
||||
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
||||
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
|
||||
val ssoCallbackResult = action.intent.getSsoCallbackResult()
|
||||
when {
|
||||
@@ -35,12 +33,6 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
|
||||
}
|
||||
|
||||
captchaCallbackTokenResult != null -> {
|
||||
authRepository.setCaptchaCallbackTokenResult(
|
||||
tokenResult = captchaCallbackTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
duoCallbackTokenResult != null -> {
|
||||
authRepository.setDuoCallbackTokenResult(
|
||||
tokenResult = duoCallbackTokenResult,
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -12,43 +15,43 @@ import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* An activity for copying a TOTP code to the clipboard. This is done when an autofill item is
|
||||
* selected and it requires TOTP authentication. Due to the constraints of the autofill framework,
|
||||
* we also have to re-fulfill the autofill for the views that are being filled.
|
||||
* An activity that is launched to complete Autofill. This is done when an autofill item is selected
|
||||
* and is associated with a valid cipher. Due to the constraints of the autofill framework, we also
|
||||
* have to re-fulfill the autofill for the views that are being filled.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@AndroidEntryPoint
|
||||
class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
class AutofillCallbackActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
|
||||
private val viewModel: AutofillCallbackViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
autofillTotpCopyViewModel.trySendAction(
|
||||
AutofillTotpCopyAction.IntentReceived(
|
||||
intent = intent,
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(AutofillCallbackAction.IntentReceived(intent = intent))
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
autofillTotpCopyViewModel
|
||||
viewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is AutofillTotpCopyEvent.CompleteAutofill -> {
|
||||
handleCompleteAutofill(event)
|
||||
}
|
||||
|
||||
is AutofillTotpCopyEvent.FinishActivity -> {
|
||||
finishActivity()
|
||||
}
|
||||
is AutofillCallbackEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
is AutofillCallbackEvent.FinishActivity -> finishActivity()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
@@ -57,7 +60,7 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
/**
|
||||
* Complete autofill with the provided data.
|
||||
*/
|
||||
private fun handleCompleteAutofill(event: AutofillTotpCopyEvent.CompleteAutofill) {
|
||||
private fun handleCompleteAutofill(event: AutofillCallbackEvent.CompleteAutofill) {
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
@@ -5,14 +5,15 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillCallbackIntentOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
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 timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -21,53 +22,65 @@ import javax.inject.Inject
|
||||
private const val CIPHER_WAIT_TIMEOUT_MILLIS: Long = 500
|
||||
|
||||
/**
|
||||
* A view model that handles logic for the [AutofillTotpCopyActivity].
|
||||
* A view model that handles logic for the [AutofillCallbackActivity].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AutofillTotpCopyViewModel @Inject constructor(
|
||||
class AutofillCallbackViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<Unit, AutofillTotpCopyEvent, AutofillTotpCopyAction>(Unit) {
|
||||
) : BaseViewModel<Unit, AutofillCallbackEvent, AutofillCallbackAction>(Unit) {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
override fun handleAction(action: AutofillTotpCopyAction): Unit = when (action) {
|
||||
is AutofillTotpCopyAction.IntentReceived -> handleIntentReceived(action)
|
||||
override fun handleAction(action: AutofillCallbackAction): Unit = when (action) {
|
||||
is AutofillCallbackAction.IntentReceived -> handleIntentReceived(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the received intent and alert the activity of what to do next.
|
||||
*/
|
||||
private fun handleIntentReceived(action: AutofillTotpCopyAction.IntentReceived) {
|
||||
private fun handleIntentReceived(action: AutofillCallbackAction.IntentReceived) {
|
||||
viewModelScope
|
||||
.launchWithTimeout(
|
||||
timeoutBlock = { finishActivity() },
|
||||
timeoutBlock = {
|
||||
Timber.w("Autofill -- Timeout")
|
||||
finishActivity()
|
||||
},
|
||||
timeoutDuration = CIPHER_WAIT_TIMEOUT_MILLIS,
|
||||
) {
|
||||
// Extract TOTP copy data from the intent.
|
||||
val cipherId = action
|
||||
.intent
|
||||
.getTotpCopyIntentOrNull()
|
||||
.getAutofillCallbackIntentOrNull()
|
||||
?.cipherId
|
||||
|
||||
if (cipherId == null || isVaultLocked()) {
|
||||
if (cipherId == null) {
|
||||
Timber.w("Autofill -- Cipher was not provided")
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
if (isVaultLocked()) {
|
||||
Timber.w("Autofill -- Vault is locked")
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
|
||||
// Try and find the matching cipher.
|
||||
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 -> {
|
||||
Timber.w("Autofill -- Cipher not found")
|
||||
finishActivity()
|
||||
}
|
||||
?: finishActivity()
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.w(result.error, "Autofill -- Get cipher failure")
|
||||
finishActivity()
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> {
|
||||
Timber.d("Autofill -- Cipher found")
|
||||
sendEvent(AutofillCallbackEvent.CompleteAutofill(result.cipherView))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +88,7 @@ class AutofillTotpCopyViewModel @Inject constructor(
|
||||
* Send an event to the activity that signals it to finish.
|
||||
*/
|
||||
private fun finishActivity() {
|
||||
sendEvent(AutofillTotpCopyEvent.FinishActivity)
|
||||
sendEvent(AutofillCallbackEvent.FinishActivity)
|
||||
}
|
||||
|
||||
private suspend fun isVaultLocked(): Boolean {
|
||||
@@ -92,30 +105,30 @@ class AutofillTotpCopyViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions that can be sent to the [AutofillTotpCopyViewModel].
|
||||
* Represents actions that can be sent to the [AutofillCallbackViewModel].
|
||||
*/
|
||||
sealed class AutofillTotpCopyAction {
|
||||
sealed class AutofillCallbackAction {
|
||||
/**
|
||||
* An [intent] has been received and is ready to be processed.
|
||||
*/
|
||||
data class IntentReceived(
|
||||
val intent: Intent,
|
||||
) : AutofillTotpCopyAction()
|
||||
) : AutofillCallbackAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events emitted by the [AutofillTotpCopyViewModel].
|
||||
* Represents events emitted by the [AutofillCallbackViewModel].
|
||||
*/
|
||||
sealed class AutofillTotpCopyEvent {
|
||||
sealed class AutofillCallbackEvent {
|
||||
/**
|
||||
* Complete autofill with the provided [cipherView].
|
||||
*/
|
||||
data class CompleteAutofill(
|
||||
val cipherView: CipherView,
|
||||
) : AutofillTotpCopyEvent()
|
||||
) : AutofillCallbackEvent()
|
||||
|
||||
/**
|
||||
* Finish the activity.
|
||||
*/
|
||||
data object FinishActivity : AutofillTotpCopyEvent()
|
||||
data object FinishActivity : AutofillCallbackEvent()
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -38,6 +39,17 @@ class BitwardenApplication : Application() {
|
||||
@Inject
|
||||
lateinit var restrictionManager: RestrictionManager
|
||||
|
||||
@Inject
|
||||
lateinit var environmentRepository: EnvironmentRepository
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// These must be initialized in order to ensure that the restrictionManager does not
|
||||
// override the environmentRepository values.
|
||||
restrictionManager.initialize()
|
||||
environmentRepository.initialize()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Timber.w("onLowMemory")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user