diff --git a/.fvmrc b/.fvmrc
new file mode 100644
index 00000000..457360f8
--- /dev/null
+++ b/.fvmrc
@@ -0,0 +1,3 @@
+{
+ "flutter": "3.44.0"
+}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7e42569e..3ec84ab6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,7 +5,12 @@
name: Flutter build and test
-on: push
+# CI validation runs only on pull requests into the integration branches.
+# Feature-branch pushes don't trigger anything; nothing runs on push to a
+# branch, so there are no duplicate runs.
+on:
+ pull_request:
+ branches: [develop, test, master]
env:
FLUTTER_FOLDER: '$HOME/flutter'
@@ -30,7 +35,10 @@ jobs:
uses: flutter-actions/setup-flutter@v4
with:
channel: stable
- version: 3.32.1
+ version: 3.44.0
+
+ - name: Clean
+ run: flutter clean
- name: Install dependencies
run: flutter pub get
diff --git a/.github/workflows/create_release_on_tag.yml b/.github/workflows/create_release_on_tag.yml
index 61407422..a530bae4 100644
--- a/.github/workflows/create_release_on_tag.yml
+++ b/.github/workflows/create_release_on_tag.yml
@@ -1,7 +1,7 @@
on:
push:
tags:
- - 'v*'
+ - "v*"
name: Create Release
@@ -10,34 +10,28 @@ jobs:
name: Create Release and Upload APK
runs-on: ubuntu-latest
steps:
- - name: Checkout code
- uses: actions/checkout@master
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: "adopt" # See 'Supported distributions' for available options
+ java-version: "17"
- - name: Set up JDK 1.8
- uses: actions/setup-java@v4
+ - name: Set up Flutter
+ uses: subosito/flutter-action@v2
with:
- distribution: 'adopt' # See 'Supported distributions' for available options
- java-version: '17'
+ flutter-version: "3.44.0"
+ channel: stable
+ cache: true
+ pub-cache: true
- - name: Cache Flutter
- id: cache-flutter
- uses: actions/cache@v3
+ - name: Cache Gradle
+ id: cache-gradle
+ uses: actions/cache@v4
with:
path: |
- flutter
- ~/.pub-cache
- key: flutter-${{ hashFiles('**/pubspec.lock') }}
-
- - if: ${{ steps.cache-flutter.outputs.cache-hit != 'true' }}
- name: Install Flutter
- run: git clone https://github.com/flutter/flutter.git --depth 1 -b stable $FOLDER
-
- - name: Add Flutter to PATH
- run: echo "$GITHUB_WORKSPACE/flutter/bin" >> $GITHUB_PATH
-
- - if: ${{ steps.cache-flutter.outputs.cache-hit != 'true' }}
- name: Install dependencies
- run: flutter pub get
+ ~/.gradle/caches
+ ~/.gradle/wrapper/
+ key: gradle-ubuntu-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Configure Keystore
run: |
@@ -59,20 +53,35 @@ jobs:
run: |
flutter --version
pwd
-
+
- name: Build APK
run: |
flutter clean
flutter pub get
- flutter pub upgrade
flutter build apk --release --no-tree-shake-icons
- name: Create Release and upload apk
uses: underwindfall/create-release-with-debugapk@v2.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
+ with:
tag_name: ${{ github.ref }}
asset_path: build/app/outputs/flutter-apk/app-release.apk
asset_name: carp-studies-app-${{ github.ref_name }}.apk
asset_content_type: application/vnd.android.package-archive
+
+ # Setup Ruby/Bundler so fastlane is available
+ - name: Setup Fastlane
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.3"
+ bundler-cache: false
+ working-directory: android
+
+ # Production release to Google Play runs only on tags (like the iOS
+ # App Store workflow on Xcode Cloud).
+ - if: startsWith(github.ref, 'refs/tags/')
+ run: fastlane release
+ env:
+ PLAY_STORE_JSON_KEY: PLAY_STORE_CONFIG.json
+ working-directory: android
diff --git a/.github/workflows/deploy_to_google_play_store_fastlane_release.yml b/.github/workflows/deploy_to_google_play_store_fastlane_release.yml
index db9f3d43..48eeb2ce 100644
--- a/.github/workflows/deploy_to_google_play_store_fastlane_release.yml
+++ b/.github/workflows/deploy_to_google_play_store_fastlane_release.yml
@@ -2,112 +2,62 @@ name: Upload to Google Store
run-name: ${{github.actor}} is pushing to branch and deploying to Google Store
on:
- push:
- branches:
- - master
- - test
+ push:
+ branches:
+ - test
jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-java@v4
- with:
- distribution: 'adopt' # See 'Supported distributions' for available options
- java-version: '17'
-
- - name: Cache Flutter
- id: cache-flutter
- uses: actions/cache@v3
- with:
- path: |
- flutter
- ~/.pub-cache
- key: flutter-${{ hashFiles('**/pubspec.lock') }}
-
- - name: Cache Gradle
- id: cache-gradle
- uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper/
- key: gradle-ubuntu-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
-
- - if: ${{ steps.cache-flutter.outputs.cache-hit != 'true' }}
- name: Install Flutter
- run: git clone https://github.com/flutter/flutter.git --depth 1 -b stable $FOLDER
-
- - name: Add Flutter to PATH
- run: echo "$GITHUB_WORKSPACE/flutter/bin" >> $GITHUB_PATH
-
- - if: ${{ steps.cache-flutter.outputs.cache-hit != 'true' }}
- name: Install dependencies
- run: flutter pub get
-
- # Setup Ruby, Bundler, and Gemfile dependencies
- - name: Setup Fastlane
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: '3.3'
- bundler-cache: false
- working-directory: android
-
- - name: Extract last commit message
- id: extract-commit-message
- run: |
- COMMIT_MESSAGE=$(git log -1 --pretty=%B | tr -d '\n')
- COMMIT_MESSAGE="${COMMIT_MESSAGE}"
- echo "Commit Message: $COMMIT_MESSAGE"
- echo "commit_message=$COMMIT_MESSAGE" >> $GITHUB_ENV
-
- - name: Extract version code and version name from pubspec.yaml
- id: extract-version
- run: |
- # Navigate to the root directory (if not already there)
- pwd
- cd $GITHUB_WORKSPACE
-
- # Extract version name and version code from pubspec.yaml
- VERSION_NAME=$(grep 'version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f1)
- VERSION_CODE=$(grep 'version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f2)
-
- echo "Version Name: $VERSION_NAME"
- echo "Version Code: $VERSION_CODE"
-
- echo "version_name=$VERSION_NAME" >> $GITHUB_ENV
- echo "version_code=$VERSION_CODE" >> $GITHUB_ENV
-
- - name: Create or update changelog file
- run: |
- pwd
- mkdir -p ./android/fastlane/metadata/android/en-GB/changelogs
- echo "${{ env.commit_message }}" > "./android/fastlane/metadata/android/en-GB/changelogs/${{ env.version_code }}.txt"
-
- - name: Configure Keystore
- run: |
- echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > app/upload-keystore.jks
- echo "$PLAY_STORE_CONFIG_JSON" > PLAY_STORE_CONFIG.json
- echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> key.properties
- echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> key.properties
- echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> key.properties
- echo "storeFile=upload-keystore.jks" >> key.properties
- env:
- PLAY_STORE_UPLOAD_KEY: ${{ secrets.PLAY_STORE_UPLOAD_KEY }}
- KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }}
- KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
- KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
- PLAY_STORE_CONFIG_JSON: ${{secrets.PLAY_STORE_CONFIG_JSON}}
- working-directory: android
-
- - if: github.ref == 'refs/heads/master'
- run: fastlane release
- env:
- PLAY_STORE_JSON_KEY: PLAY_STORE_CONFIG.json
- working-directory: android
-
- - if: github.ref == 'refs/heads/test'
- run: fastlane test
- env:
- PLAY_STORE_JSON_KEY: PLAY_STORE_CONFIG.json
- working-directory: android
\ No newline at end of file
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: "adopt" # See 'Supported distributions' for available options
+ java-version: "17"
+
+ - name: Set up Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: "3.44.0"
+ channel: stable
+ cache: true
+ pub-cache: true
+
+ - name: Cache Gradle
+ id: cache-gradle
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper/
+ key: gradle-ubuntu-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+
+ # Setup Ruby, Bundler, and Gemfile dependencies
+ - name: Setup Fastlane
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.3"
+ bundler-cache: false
+ working-directory: android
+
+ - name: Configure Keystore
+ run: |
+ echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > app/upload-keystore.jks
+ echo "$PLAY_STORE_CONFIG_JSON" > PLAY_STORE_CONFIG.json
+ echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> key.properties
+ echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> key.properties
+ echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> key.properties
+ echo "storeFile=upload-keystore.jks" >> key.properties
+ env:
+ PLAY_STORE_UPLOAD_KEY: ${{ secrets.PLAY_STORE_UPLOAD_KEY }}
+ KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }}
+ KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
+ KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
+ PLAY_STORE_CONFIG_JSON: ${{secrets.PLAY_STORE_CONFIG_JSON}}
+ working-directory: android
+
+ - if: github.ref == 'refs/heads/test'
+ run: fastlane test
+ env:
+ PLAY_STORE_JSON_KEY: PLAY_STORE_CONFIG.json
+ working-directory: android
diff --git a/.gitignore b/.gitignore
index 02b68057..701e4dce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -122,6 +122,15 @@ android/fastlane/report.xml
# CARP related
carp/carpspec.yaml
-assets/carp/
-assets/carp/lang/_da.json
-assets/carp/lang/_en.json
+# Ignore local study config but keep the asset dirs (via .gitkeep) so release
+# builds don't fail on missing/empty asset directories.
+assets/carp/*
+!assets/carp/lang/
+assets/carp/lang/*
+!assets/carp/lang/.gitkeep
+!assets/carp/resources/
+assets/carp/resources/*
+!assets/carp/resources/.gitkeep
+
+# FVM Version Cache
+.fvm/
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 570ef544..a3b7a4a7 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -27,18 +27,44 @@
],
"flutterMode": "debug"
},
+ {
+ // profile mode using the DEV CAWS instance - for measuring jank/perf
+ "name": "DEV (profile) - carp-studies-app",
+ "request": "launch",
+ "type": "dart",
+ "toolArgs": [
+ "--dart-define",
+ "deployment-mode=dev",
+ "--dart-define",
+ "debug-level=info"
+ ],
+ "flutterMode": "profile"
+ },
+ {
+ // release mode using the DEV CAWS instance - production-speed sanity check
+ "name": "DEV (release) - carp-studies-app",
+ "request": "launch",
+ "type": "dart",
+ "toolArgs": [
+ "--dart-define",
+ "deployment-mode=dev",
+ "--dart-define",
+ "debug-level=info"
+ ],
+ "flutterMode": "release"
+ },
{
// debug mode using the TEST CAWS instance
- "name": "TEST - carp-studies-app",
+ "name": "TEST - carp-studies-app (release)",
"request": "launch",
"type": "dart",
"toolArgs": [
"--dart-define",
"deployment-mode=test",
"--dart-define",
- "debug-level=debug"
+ "debug-level=none"
],
- "flutterMode": "debug"
+ "flutterMode": "release"
},
{
// debug mode using COMPUTEROME CAWS instance
@@ -52,6 +78,22 @@
"debug-level=debug"
],
"flutterMode": "debug"
+ },
+ {
+ // end-to-end join-study flow on a device/simulator (local deployment).
+ // The test also forces local + debug internally, so it runs correctly
+ // even without these defines.
+ "name": "Integration - join study flow (LOCAL)",
+ "request": "launch",
+ "type": "dart",
+ "program": "integration_test/join_study_flow_test.dart",
+ "toolArgs": [
+ "--dart-define",
+ "deployment-mode=local",
+ "--dart-define",
+ "debug-level=debug"
+ ],
+ "flutterMode": "debug"
}
]
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1760de8a..80549beb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 4.2.0
+
+* moving to carp_themes_package instead of research package themes
+* partially transitioning from pods to spm
+* health connect flow update
+* informed consent accepted is now checked via API instead of local settings
+* fixing hasSeenBluetoothInstructions bool logic
+* small UI changes
+
## 4.1.1
- small visual fixes
diff --git a/analysis_options.yaml b/analysis_options.yaml
index b449163e..6a7acd6c 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -3,6 +3,9 @@
include: package:lints/recommended.yaml
+formatter:
+ page_width: 120
+
analyzer:
exclude: [build/**]
language:
diff --git a/android/Gemfile.lock b/android/Gemfile.lock
index 3d9a5e6f..0e43bde2 100644
--- a/android/Gemfile.lock
+++ b/android/Gemfile.lock
@@ -1,43 +1,49 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.6)
- rexml
- addressable (2.8.4)
- public_suffix (>= 2.0.2, < 6.0)
- artifactory (3.0.15)
+ CFPropertyList (3.0.8)
+ abbrev (0.1.2)
+ addressable (2.9.0)
+ public_suffix (>= 2.0.2, < 8.0)
+ artifactory (3.0.17)
atomos (0.1.3)
- aws-eventstream (1.2.0)
- aws-partitions (1.779.0)
- aws-sdk-core (3.174.0)
- aws-eventstream (~> 1, >= 1.0.2)
- aws-partitions (~> 1, >= 1.651.0)
- aws-sigv4 (~> 1.5)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1255.0)
+ aws-sdk-core (3.250.0)
+ aws-eventstream (~> 1, >= 1.3.0)
+ aws-partitions (~> 1, >= 1.992.0)
+ aws-sigv4 (~> 1.9)
+ base64
+ bigdecimal
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.66.0)
- aws-sdk-core (~> 3, >= 3.174.0)
- aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.124.0)
- aws-sdk-core (~> 3, >= 3.174.0)
+ logger
+ aws-sdk-kms (1.128.0)
+ aws-sdk-core (~> 3, >= 3.248.0)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.224.0)
+ aws-sdk-core (~> 3, >= 3.248.0)
aws-sdk-kms (~> 1)
- aws-sigv4 (~> 1.4)
- aws-sigv4 (1.5.2)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
+ base64 (0.3.0)
+ benchmark (0.5.0)
+ bigdecimal (4.1.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
+ csv (3.3.5)
declarative (0.0.20)
- digest-crc (0.6.4)
+ digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
- domain_name (0.5.20190701)
- unf (>= 0.0.5, < 1.0.0)
+ domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
- excon (0.100.0)
- faraday (1.10.3)
+ excon (0.112.0)
+ faraday (1.10.5)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -49,32 +55,36 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
- faraday-cookie_jar (0.0.7)
+ faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
- http-cookie (~> 1.0.0)
+ http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
+ faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
- faraday-multipart (1.0.4)
- multipart-post (~> 2)
- faraday-net_http (1.0.1)
+ faraday-multipart (1.2.0)
+ multipart-post (~> 2.0)
+ faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
- faraday-retry (1.0.3)
- faraday_middleware (1.2.0)
+ faraday-retry (1.0.4)
+ faraday_middleware (1.2.1)
faraday (~> 1.0)
- fastimage (2.2.7)
- fastlane (2.213.0)
- CFPropertyList (>= 2.3, < 4.0.0)
+ fastimage (2.4.1)
+ fastlane (2.235.0)
+ CFPropertyList (>= 2.3, < 5.0.0)
+ abbrev (~> 0.1)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
- aws-sdk-s3 (~> 1.0)
+ aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
- bundler (>= 1.12.0, < 3.0.0)
- colored
+ base64 (~> 0.2)
+ benchmark (>= 0.1.0)
+ bundler (>= 2.4.0, < 5.0.0)
+ colored (~> 1.2)
commander (~> 4.6)
+ csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -82,134 +92,147 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.3.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
+ http-cookie (~> 1.0.5)
json (< 3.0.0)
- jwt (>= 2.1.0, < 3)
+ jwt (>= 2.1.0, < 4)
+ logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3)
naturally (~> 2.2)
- optparse (~> 0.1.1)
+ nkf (~> 0.2)
+ optparse (>= 0.1.1, < 1.0.0)
+ ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
- security (= 0.1.3)
+ security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
- terminal-table (>= 1.4.5, < 2.0.0)
+ terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
- xcpretty (~> 0.3.0)
- xcpretty-travis-formatter (>= 0.0.3)
+ xcpretty (~> 0.4.1)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
- google-apis-androidpublisher_v3 (0.43.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-core (0.11.0)
+ google-apis-androidpublisher_v3 (0.101.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
- googleauth (>= 0.16.2, < 2.a)
- httpclient (>= 2.8.1, < 3.a)
+ googleauth (~> 1.9)
+ httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
+ mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
- rexml
- webrick
- google-apis-iamcredentials_v1 (0.17.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-playcustomapp_v1 (0.13.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-storage_v1 (0.19.0)
- google-apis-core (>= 0.9.0, < 2.a)
- google-cloud-core (1.6.0)
- google-cloud-env (~> 1.0)
+ google-apis-iamcredentials_v1 (0.27.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.17.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-apis-storage_v1 (0.63.0)
+ google-apis-core (>= 0.15.0, < 2.a)
+ google-cloud-core (1.8.0)
+ google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.6.0)
- faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.3.1)
- google-cloud-storage (1.44.0)
+ google-cloud-env (2.2.2)
+ base64 (~> 0.2)
+ faraday (>= 1.0, < 3.a)
+ google-cloud-errors (1.6.0)
+ google-cloud-storage (1.60.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
- google-apis-iamcredentials_v1 (~> 0.1)
- google-apis-storage_v1 (~> 0.19.0)
+ google-apis-core (>= 0.18, < 2)
+ google-apis-iamcredentials_v1 (~> 0.18)
+ google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
- googleauth (>= 0.16.2, < 2.a)
+ googleauth (~> 1.9)
mini_mime (~> 1.0)
- googleauth (1.5.2)
- faraday (>= 0.17.3, < 3.a)
- jwt (>= 1.4, < 3.0)
- memoist (~> 0.16)
+ google-logging-utils (0.2.0)
+ googleauth (1.16.2)
+ faraday (>= 1.0, < 3.a)
+ google-cloud-env (~> 2.2)
+ google-logging-utils (~> 0.1)
+ jwt (>= 1.4, < 4.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
- http-cookie (1.0.5)
+ http-cookie (1.0.8)
domain_name (~> 0.5)
- httpclient (2.8.3)
+ httpclient (2.9.0)
+ mutex_m
jmespath (1.6.2)
- json (2.6.3)
- jwt (2.7.1)
- memoist (0.16.2)
- mini_magick (4.12.0)
- mini_mime (1.1.2)
- multi_json (1.15.0)
- multipart-post (2.3.0)
- nanaimo (0.3.0)
- naturally (2.2.1)
- optparse (0.1.1)
+ json (2.19.7)
+ jwt (3.2.0)
+ base64
+ logger (1.7.0)
+ mini_magick (4.13.2)
+ mini_mime (1.1.5)
+ multi_json (1.21.1)
+ multipart-post (2.4.1)
+ mutex_m (0.3.0)
+ nanaimo (0.4.0)
+ naturally (2.3.0)
+ nkf (0.2.0)
+ optparse (0.8.1)
os (1.1.4)
- plist (3.7.0)
- public_suffix (5.0.1)
- rake (13.0.6)
+ ostruct (0.6.3)
+ plist (3.7.2)
+ public_suffix (7.0.5)
+ rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
- retriable (3.1.2)
- rexml (3.2.5)
- rouge (2.0.7)
+ retriable (3.8.0)
+ rexml (3.4.4)
+ rouge (3.28.0)
ruby2_keywords (0.0.5)
- rubyzip (2.3.2)
- security (0.1.3)
- signet (0.17.0)
+ rubyzip (2.4.1)
+ security (0.1.5)
+ signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
- jwt (>= 1.5, < 3.0)
+ jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
- terminal-table (1.8.0)
- unicode-display_width (~> 1.1, >= 1.1.1)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
- tty-screen (0.8.1)
+ tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
- unf (0.1.4)
- unf_ext
- unf_ext (0.0.8.2)
- unicode-display_width (1.8.0)
- webrick (1.8.1)
+ unicode-display_width (2.6.0)
word_wrap (1.0.0)
- xcodeproj (1.22.0)
+ xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
- nanaimo (~> 0.3.0)
- rexml (~> 3.2.4)
- xcpretty (0.3.0)
- rouge (~> 2.0.7)
+ nanaimo (~> 0.4.0)
+ rexml (>= 3.3.6, < 4.0)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-22
+ arm64-darwin-25
x86_64-linux
DEPENDENCIES
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e70b25af..bc2192c5 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -5,7 +5,7 @@ plugins {
}
def localProperties = new Properties()
-def localPropertiesFile = rootProject.file('local.properties')
+def localPropertiesFile = project.rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
@@ -13,7 +13,7 @@ if (localPropertiesFile.exists()) {
}
def keyProperties = new Properties()
-def keyPropertiesFile = rootProject.file('key.properties')
+def keyPropertiesFile = project.rootProject.file('key.properties')
def signingConfigExists = false
if (keyPropertiesFile.exists()) {
@@ -23,15 +23,8 @@ if (keyPropertiesFile.exists()) {
}
}
-def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
-if (flutterVersionCode == null) {
- flutterVersionCode = '1'
-}
-
-def flutterVersionName = localProperties.getProperty('flutter.versionName')
-if (flutterVersionName == null) {
- flutterVersionName = '1.0'
-}
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode') ?: '1'
+def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
android {
namespace "dk.carp.studies_app"
@@ -88,10 +81,10 @@ android {
debugSymbolLevel 'SYMBOL_TABLE'
}
if (signingConfigExists) {
- logger.error('storeFile found, signing with release build.')
+ logger.info('storeFile found, signing with release build.')
signingConfig signingConfigs.release
} else {
- logger.error('No storeFile found or null. Skipping signing of release build.')
+ logger.info('No storeFile found or null. Skipping signing of release build.')
signingConfig signingConfigs.debug
}
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 303b83a4..ea000c7f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -56,8 +56,6 @@
-
-
@@ -68,29 +66,37 @@
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
-
+
+
+
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
current_production_version_code && version_number > current_internal_version_code
- sh "flutter clean"
- sh "flutter pub get"
- sh "flutter pub upgrade"
- sh "flutter build appbundle --release --no-deferred-components --no-tree-shake-icons"
+ # Auto-increment: one above the highest code on either track. This mirrors
+ # the iOS Xcode Cloud CI_BUILD_NUMBER flow, so the build number never needs
+ # a manual bump in pubspec.yaml.
+ codes = google_play_track_version_codes(track: 'production') +
+ google_play_track_version_codes(track: 'internal')
+ build_number = (codes.map(&:to_i).max || 0) + 1
+ build_name = YAML.load_file('../../pubspec.yaml')['version'].split('+').first
+ UI.message("Building production versionCode #{build_number} (#{build_name})")
+
+ sh "flutter clean"
+ sh "flutter pub get"
+ sh "flutter build appbundle --release --no-deferred-components --no-tree-shake-icons --build-name=#{build_name} --build-number=#{build_number}"
+
+ write_play_changelog(build_number)
- upload_to_play_store(
- track: 'production',
- aab: '../build/app/outputs/bundle/release/app-release.aab',
- )
- else
- UI.user_error!("Version code must be greater than or equal to the current version code: #{current_production_version_code} and #{current_internal_version_code}")
- end
- end
+ upload_to_play_store(
+ track: 'production',
+ aab: '../build/app/outputs/bundle/release/app-release.aab',
+ )
end
desc "Upload to internal testing track"
lane :test do
- current_production_version_code = google_play_track_version_codes(track: 'production').first.to_i
- current_internal_version_code = google_play_track_version_codes(track: 'internal').first.to_i
- pubspec_path = '../../pubspec.yaml'
- pubspec_content = YAML.load_file(pubspec_path)
- version_string = pubspec_content['version']
- version_parts = version_string.split('+')
+ # Auto-increment from the live Play state (see :release for rationale).
+ codes = google_play_track_version_codes(track: 'production') +
+ google_play_track_version_codes(track: 'internal')
+ build_number = (codes.map(&:to_i).max || 0) + 1
+ base_version = YAML.load_file('../../pubspec.yaml')['version'].split('+').first
+ build_name = "#{base_version}-test"
+ UI.message("Building internal versionCode #{build_number} (#{build_name})")
- if version_parts.size == 2
- version_number = version_parts[1].to_i
- base_version = version_parts[0]
+ sh "flutter clean"
+ sh "flutter pub get"
+ sh "flutter build appbundle --release --no-deferred-components --no-tree-shake-icons --dart-define='deployment-mode=test' --build-name=#{build_name} --build-number=#{build_number}"
- if version_number > current_production_version_code && version_number > current_internal_version_code
- test_version = "#{base_version}-test+#{version_number}"
- sh "sed -i 's/^version: .*/version: #{test_version}/' #{pubspec_path}"
- sh "flutter clean"
- sh "flutter pub get"
- sh "flutter pub upgrade"
- sh "flutter build appbundle --release --no-deferred-components --no-tree-shake-icons --dart-define='deployment-mode=test'"
+ write_play_changelog(build_number)
- upload_to_play_store(
- track: 'internal',
- aab: '../build/app/outputs/bundle/release/app-release.aab',
- )
- else
- UI.user_error!("Version code must be greater than or equal to the current version code: #{current_production_version_code} and #{current_internal_version_code}")
- end
- end
+ upload_to_play_store(
+ track: 'internal',
+ aab: '../build/app/outputs/bundle/release/app-release.aab'
+ )
end
-end
\ No newline at end of file
+end
diff --git a/android/gradle.properties b/android/gradle.properties
index 7d40b1c3..e7b01d3c 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -3,4 +3,8 @@ org.gradle.caching=true
org.gradle.parallel=true
org.gradle.daemon=true
android.enableJetifier=true
-android.useAndroidX=true
\ No newline at end of file
+android.useAndroidX=true
+# This builtInKotlin flag was added automatically by Flutter migrator
+android.builtInKotlin=false
+# This newDsl flag was added automatically by Flutter migrator
+android.newDsl=false
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 2733ed5d..3ae1e2f1 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/android/settings.gradle b/android/settings.gradle
index 52bd901a..5b46a604 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -18,9 +18,9 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.2"
- id "com.android.application" version "8.9.0" apply false // this is AGP?
- id 'com.android.library' version '7.4.2' apply false
- id 'org.jetbrains.kotlin.android' version '2.1.0' apply false
+ id "com.android.application" version "8.11.1" apply false
+ id 'com.android.library' version '8.11.1' apply false
+ id 'org.jetbrains.kotlin.android' version '2.2.20' apply false
}
include ":app", ":mdsflutter", ":mdslib"
diff --git a/assets/carp/lang/.gitkeep b/assets/carp/lang/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/assets/carp/resources/.gitkeep b/assets/carp/resources/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/assets/instructions/apple_health_preview.png b/assets/instructions/apple_health_preview.png
new file mode 100644
index 00000000..89113a45
Binary files /dev/null and b/assets/instructions/apple_health_preview.png differ
diff --git a/assets/instructions/google_health_connect_preview.png b/assets/instructions/google_health_connect_preview.png
new file mode 100644
index 00000000..b0cb0528
Binary files /dev/null and b/assets/instructions/google_health_connect_preview.png differ
diff --git a/assets/instructions/health_permission_allow_all.png b/assets/instructions/health_permission_allow_all.png
new file mode 100644
index 00000000..7990000a
Binary files /dev/null and b/assets/instructions/health_permission_allow_all.png differ
diff --git a/assets/lang/da.json b/assets/lang/da.json
index e08855bb..61fcabba 100644
--- a/assets/lang/da.json
+++ b/assets/lang/da.json
@@ -97,9 +97,9 @@
"pages.profile.device_role": "Engedsrolle",
"pages.profile.contact": "Kontakt forsker",
"pages.profile.privacy": "Fortrolighedspolitik",
- "pages.profile.study_website": "Studiw Hjemmeside",
+ "pages.profile.study_website": "Hjemmeside for studiet",
"pages.profile.leave_study": "Forlad studie",
- "pages.profile.log_out": "Log ud",
+ "pages.profile.log_out": "Forlad studie & Log ud",
"pages.profile.log_out.confirmation": "Du er ved at forlade dette studie og logge af. Operativsystemet vil åbne en browser for at logge dig ud. Er du sikker?",
"pages.profile.leave_study.confirmation": "Du er ved at forlade studiet. Du vil ikke længere deltage i dette studie. Er du sikker?",
"announcements": "Annonceringer",
@@ -124,6 +124,12 @@
"pages.about.study.privacy": "Fortrolighedspolitik",
"pages.about.install_health_connect.title": "Health Connect påkrævet",
"pages.about.install_health_connect.description": "Denne app kræver, at Google Health Connect er installeret. Vil du installere det?",
+ "pages.devices.type.health.instructions.data.title": "Dette studie indsamler sundhedsrelaterede data fra din enhed. For at deltage skal du tillade alle de anmodede tilladelser på næste skærm.",
+ "pages.devices.type.health.access_denied.title": "Adgang ikke givet",
+ "pages.devices.type.health.access_denied.message": "Der blev ikke givet adgang. For at tillade det skal du gå til: Indstillinger → Apps → CARP Studies → Tilladelser → Sundhed, fitness og wellness → Tillad alle. Træk derefter ned på enhedsskærmen for at opdatere.",
+ "pages.devices.type.health.instructions.data.steps": "Skridttælling",
+ "pages.devices.type.health.instructions.data.heart_rate": "Puls",
+ "pages.devices.type.health.instructions.data.exercise": "Træningssessioner",
"dialog.location.permission": "Lokaliseringstjeneste",
"dialog.location.info": "Denne app indsamler lokaliseringsdata.\n\nVi beder dig om at aktivere lokaliseringstjenesten på telefonen ved at tillade sporing af placering 'Altid'. Lokaliseringstjenesten holder appen kørende i baggrunden, hvilket hjælper os med at indsamle data.\n\nVi bruger placeringsdata i vores efterfølgende dataanalyse for at udregne mobilitetsmønstre, såsom hvor meget tid du er hjemme.",
"dialog.location.continue": "Fortsæt",
@@ -216,5 +222,6 @@
"tasks.participant_data.phone_number.title": "Tilføj telefonnummer",
"tasks.participant_data.phone_number.country": "Landekode",
"tasks.participant_data.phone_number.phone_number": "Telefonnummer",
- "tasks.participant_data.review.title": "Gennemse"
+ "tasks.participant_data.review.title": "Gennemse",
+ "pages.home.setup_failed": "Studiet kunne ikke konfigureres. Tjek din internetforbindelse og prøv igen."
}
\ No newline at end of file
diff --git a/assets/lang/en.json b/assets/lang/en.json
index aae2fb09..c7355678 100644
--- a/assets/lang/en.json
+++ b/assets/lang/en.json
@@ -110,9 +110,9 @@
"pages.profile.device_id": "Device ID",
"pages.profile.contact": "Contact researcher",
"pages.profile.privacy": "Privacy policy",
- "pages.profile.study_website": "Study Website",
+ "pages.profile.study_website": "Study website",
"pages.profile.leave_study": "Leave study",
- "pages.profile.log_out": "Log out",
+ "pages.profile.log_out": "Leave study & Log out",
"pages.profile.log_out.confirmation": "You are about to leave this study and log out. The operating system will open a browser to log you out. Are you sure?",
"pages.profile.leave_study.confirmation": "You are about to leave the study. You will no longer participate in this study. Are you sure?",
"announcements": "Announcements",
@@ -211,6 +211,12 @@
"pages.devices.type.health.instructions.page2.android.allow_all": "Allow all",
"pages.devices.type.health.instructions.page2.ios.turn_on_all": "Turn On All",
"pages.devices.type.health.instructions.page2.allow": "Allow",
+ "pages.devices.type.health.instructions.data.title": "This study collects health-related data from your device. To take part, please allow all the requested permissions on the next screen.",
+ "pages.devices.type.health.access_denied.title": "Access not granted",
+ "pages.devices.type.health.access_denied.message": "Access was not granted. To allow it, go to: Settings → Apps → CARP Studies → Permissions → Health, fitness and wellness → Allow all. Then pull down on the devices screen to refresh.",
+ "pages.devices.type.health.instructions.data.steps": "Step count",
+ "pages.devices.type.health.instructions.data.heart_rate": "Heart rate",
+ "pages.devices.type.health.instructions.data.exercise": "Exercise sessions",
"widgets.logoutmessage.title": "Log out",
"widgets.logoutmessage.message": "The operating system will open a browser to log you out. Is this okay?",
"pages.login.login": "Log in",
@@ -243,5 +249,6 @@
"tasks.participant_data.phone_number.title": "Add phone number",
"tasks.participant_data.phone_number.country": "Country code",
"tasks.participant_data.phone_number.phone_number": "Phone No.",
- "tasks.participant_data.review.title": "Review"
-}
+ "tasks.participant_data.review.title": "Review",
+ "pages.home.setup_failed": "Could not set up the study. Please check your internet connection and try again."
+}
\ No newline at end of file
diff --git a/assets/lang/es.json b/assets/lang/es.json
index b4f811b1..da6cfe57 100644
--- a/assets/lang/es.json
+++ b/assets/lang/es.json
@@ -1,4 +1,11 @@
{
+ "pages.devices.type.health.instructions.data.title": "Este estudio recopila datos relacionados con la salud de tu dispositivo. Para participar, permite todos los permisos solicitados en la siguiente pantalla.",
+ "pages.devices.type.health.access_denied.title": "Acceso no concedido",
+ "pages.devices.type.health.access_denied.message": "No se concedió acceso. Para permitirlo, ve a: Ajustes → Aplicaciones → CARP Studies → Permisos → Salud, actividad física y bienestar → Permitir todo. Luego desliza hacia abajo en la pantalla de dispositivos para actualizar.",
+ "settings": "Ajustes",
+ "pages.devices.type.health.instructions.data.steps": "Recuento de pasos",
+ "pages.devices.type.health.instructions.data.heart_rate": "Frecuencia cardíaca",
+ "pages.devices.type.health.instructions.data.exercise": "Sesiones de ejercicio",
"app_home.nav_bar_item.tasks": "Tareas",
"app_home.nav_bar_item.about": "Acerca de",
"app_home.nav_bar_item.data": "Datos",
@@ -58,8 +65,9 @@
"pages.profile.account_id": "Id Usuario",
"pages.profile.contact": "Contactar investigador",
"pages.profile.privacy": "Política de privacidad",
+ "pages.profile.study_website": "Página web del estudio",
"pages.profile.leave_study": "Abandonar el estudio",
- "pages.profile.log_out": "Cerrar sesión",
+ "pages.profile.log_out": "Cerrar estudio & sesión",
"pages.profile.log_out.confirmation": "Estás a punto de cerrar sesión et abandonar el estudio. ¿Estás seguro?",
"pages.profile.leave_study.confirmation": "Estás a punto de abandonar el estudio. Dejarás de participar en el es estudio. ¿Estás seguro?",
"announcement": "Anuncio",
@@ -120,5 +128,6 @@
"pages.devices.connection.step.start.3": "Si el dispositivo no está en la lista, asegúrese de que esté cargado y encendido. Leer más en las 'Instrucciones'.",
"pages.devices.connection.step.confirm.title": "está conectado!",
"pages.devices.connection.step.confirm.1": "El",
- "pages.devices.connection.step.confirm.2": " ha sido connectado satisfactoriamente al estudio y está listo para empezar a detectar."
-}
+ "pages.devices.connection.step.confirm.2": " ha sido connectado satisfactoriamente al estudio y está listo para empezar a detectar.",
+ "pages.home.setup_failed": "No se pudo configurar el estudio. Comprueba tu conexión a internet e inténtalo de nuevo."
+}
\ No newline at end of file
diff --git a/carp_study_app.code-workspace b/carp_study_app.code-workspace
index 29fcf7e1..a2f866b8 100644
--- a/carp_study_app.code-workspace
+++ b/carp_study_app.code-workspace
@@ -21,7 +21,7 @@
"dart.debugExternalPackageLibraries": false,
"dart.debugSdkLibraries": false,
"dart.openDevTools": "never",
- "java.configuration.updateBuildConfiguration": "automatic",
+ "dart.flutterSdkPath": ".fvm/flutter_sdk",
},
"extensions": {
"recommendations": [
diff --git a/integration_test/join_study_flow_test.dart b/integration_test/join_study_flow_test.dart
new file mode 100644
index 00000000..4ffe33c2
--- /dev/null
+++ b/integration_test/join_study_flow_test.dart
@@ -0,0 +1,206 @@
+// End-to-end test of the join-study flow in LOCAL deployment mode:
+// deploy a protocol locally, gate on informed consent, configure the study
+// (Sensing initialize -> addStudy -> tryDeployment -> verify deployed), and
+// start sensing.
+//
+// Run on a simulator or device with:
+//
+// flutter test integration_test --dart-define=deployment-mode=local
+import 'package:carp_core/carp_core.dart' hide Smartphone;
+import 'package:carp_mobile_sensing/carp_mobile_sensing.dart';
+import 'package:carp_backend/carp_backend.dart';
+import 'package:cognition_package/cognition_package.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'package:carp_study_app/main.dart';
+
+/// Mock the platform channels a simulator cannot serve: OS permission
+/// dialogs (which a test cannot answer) and battery hardware (which a
+/// simulator does not have). Everything else - deployment, sensing runtime,
+/// routing, consent - runs for real.
+void mockSimulatorPlatformChannels() {
+ final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+
+ messenger.setMockMethodCallHandler(const MethodChannel('flutter.baseflow.com/permissions/methods'), (call) async {
+ const granted = 1;
+ switch (call.method) {
+ case 'requestPermissions':
+ return {for (final permission in (call.arguments as List).cast()) permission: granted};
+ case 'checkPermissionStatus':
+ case 'checkServiceStatus':
+ return granted;
+ default:
+ return null;
+ }
+ });
+
+ messenger.setMockMethodCallHandler(const MethodChannel('dexterous.com/flutter/local_notifications'), (call) async {
+ switch (call.method) {
+ case 'initialize':
+ case 'requestPermissions':
+ return true;
+ case 'pendingNotificationRequests':
+ return