Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
|
|
@ -1,167 +0,0 @@
|
|||
name: Build and Publish Alpha Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
name: Build Android APK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.47.x'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run Code Generation
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Decode Android Keystore & Key Properties
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
KEY_PROPERTIES: ${{ secrets.ANDROID_KEY_PROPERTIES }}
|
||||
run: |
|
||||
if [ -n "$KEYSTORE_BASE64" ]; then
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks
|
||||
echo "$KEY_PROPERTIES" > android/key.properties
|
||||
fi
|
||||
|
||||
- name: Build Split APKs
|
||||
run: flutter build apk --release --split-per-abi
|
||||
|
||||
- name: Rename and Stage APK
|
||||
run: |
|
||||
mkdir -p dist
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk "dist/GLP1Companion-Android-arm64-${TAG_NAME}.apk"
|
||||
|
||||
- name: Upload Artifact for Release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-release-apk
|
||||
path: dist/*.apk
|
||||
retention-days: 1
|
||||
|
||||
build-windows:
|
||||
name: Build Windows MSIX & Portable
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.47.x'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: pwsh
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run Code Generation
|
||||
shell: pwsh
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Build Windows Native Release
|
||||
shell: pwsh
|
||||
run: flutter build windows --release
|
||||
|
||||
- name: Generate MSIX Installer
|
||||
shell: pwsh
|
||||
run: dart run msix:create --install-certificate false
|
||||
|
||||
- name: Package Portable Zip & Stage MSIX
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
$tag = $env:GITHUB_REF -replace 'refs/tags/', ''
|
||||
|
||||
# Copy MSIX
|
||||
Copy-Item "build\windows\x64\runner\Release\*.msix" "dist\GLP1Companion-Windows-${tag}.msix" -Force
|
||||
|
||||
# Compress standalone runner folder
|
||||
Compress-Archive -Path "build\windows\x64\runner\Release\*" -DestinationPath "dist\GLP1Companion-Windows-x64-${tag}-Portable.zip" -Force
|
||||
|
||||
- name: Upload Artifacts for Release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-release-packages
|
||||
path: dist/*
|
||||
retention-days: 1
|
||||
|
||||
publish-release:
|
||||
name: Attach Assets to Forgejo Release
|
||||
needs: [build-android, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Staged Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: staged-assets
|
||||
|
||||
- name: Create Forgejo Release and Upload Assets
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
REPO="${{ github.repository }}"
|
||||
SERVER="${{ github.server_url }}"
|
||||
|
||||
echo "Target Release: $TAG_NAME on $REPO"
|
||||
|
||||
# 1. Fetch or Create Release
|
||||
RELEASE_JSON=$(curl -s -X POST "$SERVER/api/v1/repos/$REPO/releases" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"$TAG_NAME\",
|
||||
\"name\": \"GLP-1 Companion $TAG_NAME\",
|
||||
\"body\": \"Automated alpha release build for $TAG_NAME.\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": true
|
||||
}")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Release may already exist, fetching existing release ID..."
|
||||
RELEASE_JSON=$(curl -s "$SERVER/api/v1/repos/$REPO/releases/tags/$TAG_NAME" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN")
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
fi
|
||||
|
||||
echo "Release ID: $RELEASE_ID"
|
||||
|
||||
# 2. Upload All Staged Artifacts to Release
|
||||
find staged-assets -type f \( -name "*.apk" -o -name "*.msix" -o -name "*.zip" \) | while read -r FILE; do
|
||||
FILENAME=$(basename "$FILE")
|
||||
echo "Uploading $FILENAME to release $RELEASE_ID..."
|
||||
|
||||
curl -s -X POST "$SERVER/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$FILENAME" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$FILE" > /dev/null
|
||||
done
|
||||
|
||||
echo "All release assets uploaded successfully."
|
||||
|
|
@ -34,7 +34,6 @@ migrate_working_dir/
|
|||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/dist/
|
||||
|
||||
# Web specific
|
||||
|
||||
|
|
@ -55,8 +54,6 @@ macos/Flutter/ephemeral/
|
|||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
**/android/key.properties
|
||||
**/android/build/
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
## Active Target
|
||||
- **Current Milestone:** Phase 7: Navigation & Domain Expansion (Exercise, Recipe Vault, Settings) — COMPLETE
|
||||
- **Status:** Phase 7 Complete (all phases 1–7 done); alpha release pipeline landed 2026-10-18
|
||||
- **Latest Addendum:** Alpha Release Pipeline & Scripting (2026-10-18)
|
||||
- **Status:** Phase 7 Complete (release-ready: all phases 1–7 done)
|
||||
- **Latest Addendum:** Phase 4B — Private Recipe Sources & Mealie Integration (2026-10-17)
|
||||
|
||||
## Phase Checklist
|
||||
- [x] Phase 1: Foundation & Identity (Passwordless SSO & Encrypted Drift DB)
|
||||
|
|
@ -197,50 +197,3 @@
|
|||
- [x] Codegen: `dart run build_runner build` (`flutter pub run` is retired on current Flutter; `--delete-conflicting-outputs` is now ignored by build_runner) — drift re-emits `app_database.g.dart` byte-identically (no schema change).
|
||||
- [x] `flutter test` — 184/184 green (170 pre-existing + 14 new Mealie tests), including the 390 dp mobile navigation suite.
|
||||
- [ ] Manual Mealie round trip on a reachable instance: Settings → Private Recipe Integrations → Connect Mealie (URL + API token) → Test & Save → badge flips to Connected → Sync Now (or the Recipe Vault AppBar sync action) → vault shows the Mealie badge and the Mealie/Web Scraped filter chips isolate each source.
|
||||
|
||||
## Alpha Release Pipeline & Tooling (2026-10-18)
|
||||
Release engineering for alpha (pre-release) builds across Android and Windows, driven either locally (one-shot PowerShell script) or by CI when a version tag is pushed.
|
||||
|
||||
- [x] CI: `.forgejo/workflows/release.yaml` — "Build and Publish Alpha Releases", triggered on `push` of tags matching `v*`. Three jobs:
|
||||
- `build-android` (ubuntu): Java 17 (Temurin) + Flutter 3.47.x → `flutter pub get` → `dart run build_runner build --delete-conflicting-outputs` → decode `ANDROID_KEYSTORE_BASE64` / `ANDROID_KEY_PROPERTIES` repo secrets into `android/app/upload-keystore.jks` + `android/key.properties` (step skipped when secrets are absent) → `flutter build apk --release --split-per-abi` → stages `GLP1Companion-Android-arm64-<tag>.apk` under `dist/`.
|
||||
- `build-windows` (windows): Flutter 3.47.x → `flutter pub get` + codegen → `flutter build windows --release` → `dart run msix:create --install-certificate false` → stages `GLP1Companion-Windows-<tag>.msix` and `GLP1Companion-Windows-x64-<tag>-Portable.zip` (runner Release folder zipped via `Compress-Archive`).
|
||||
- `publish-release` (needs both build jobs): downloads the staged artifacts and drives the Forgejo REST API (`POST /api/v1/repos/{owner}/{repo}/releases`, then `/releases/{id}/assets?name=`) with `GITHUB_TOKEN` to create the release with `prerelease: true` — falling back to the existing release for the tag if creation fails — and uploads every `.apk` / `.msix` / `.zip`.
|
||||
- Flutter pinned to 3.47.x to match the dev toolchain: `pubspec.lock` declares `flutter: ">=3.44.0"`, so an older CI toolchain (e.g. the originally drafted 3.24.x) fails at `flutter pub get`.
|
||||
- [x] Local script: `scripts/publish_alpha.ps1 -Version <x.y.z> [-ForgejoToken <token>]` (token defaults to `$env:FORGEJO_TOKEN`; server `https://git.kellsupport.com`, repo `mtkell/glp-companion`) — same flow end to end: codegen → arm64 APK → portable zip → MSIX → Forgejo release (`prerelease: true`) with asset upload; artifacts land in `dist/v<version>/`.
|
||||
- [x] Forgejo API probe: `scripts/test.ps1` verifies release lookup/creation and `multipart/form-data` asset upload against a throwaway draft release (`v0.0.1-test`) with a dummy text asset; the token is read from `$env:FORGEJO_TOKEN` only (the previously hardcoded token was removed — rotation tracked in the gates below).
|
||||
- [x] Android release signing: `android/app/build.gradle.kts` loads `android/key.properties` from the project root when present and wires a `release` signing config (alias / password / store) with `isMinifyEnabled` + `isShrinkResources` and `proguard-rules.pro`; when the file is absent the properties are empty (CI decodes them from secrets; local release builds need their own `key.properties`).
|
||||
- [x] ProGuard rules (`android/app/proguard-rules.pro`): keep Flutter embedding/plugin/util/view classes, `net.sqlcipher.**` + `net.sqlcipher.database.**` (SQLCipher backing the encrypted drift DB), annotation/enum/signature metadata for JSON serialization, plus `-dontwarn` suppressions for Play Core deferred components, conscrypt, and javax.annotation.
|
||||
- [x] MSIX packaging: `msix: ^3.16.7` dev dependency (resolved 3.18.0) + `msix_config` block in `pubspec.yaml` — display name "GLP-1 Companion", publisher "Kell Engineering LLC" (`CN=Kell Engineering LLC, O=..., L=Valrico, S=Florida, C=US`), identity `KellEngineering.GLP1Companion`, `msix_version 0.1.0.0`, `logo_path: web/icons/Icon-512.png`, `internetClient` capability, execution alias `glp-companion`.
|
||||
- [x] Secret & artifact hygiene: `/dist/` (local release artifacts, ~107 MB), `**/android/build/` (Gradle problems report), and `**/android/key.properties` added to `.gitignore`; the hardcoded Forgejo token stripped from `scripts/test.ps1` before it could ever reach git history.
|
||||
|
||||
## Alpha Release Verification Gates (2026-10-18, to run)
|
||||
- [ ] **Rotate the Forgejo token** that was hardcoded in `scripts/test.ps1`: Forgejo → Settings → Applications → delete the exposed token, mint a new one, export it as `FORGEJO_TOKEN` locally, and store it as a repo secret. (The old token never entered git history, but it existed in plaintext on disk.)
|
||||
- [ ] Configure Forgejo Actions secrets on `mtkell/glp-companion`: `ANDROID_KEYSTORE_BASE64` (base64 of `upload-keystore.jks`) and `ANDROID_KEY_PROPERTIES` (keyAlias / keyPassword / storeFile=`upload-keystore.jks` / storePassword). Without them the Android job's release build fails at signing.
|
||||
- [ ] Push a `v*` tag (e.g. `git tag v0.1.0 && git push origin v0.1.0`) and confirm all three workflow jobs go green and the prerelease at `https://git.kellsupport.com/mtkell/glp-companion/releases` carries the APK, MSIX, and portable zip assets.
|
||||
- [ ] Install-gate: sideload the arm64 APK on an Android device and install the MSIX on Windows; launch both and confirm the encrypted drift DB opens (the minify/shrink build is exactly why the SQLCipher keep-rules exist).
|
||||
|
||||
## Post-Alpha Fixes (2026-10-18)
|
||||
|
||||
- [x] Android Google Health connect fix (field-reported alpha: authorization completed in the browser, but on return the app "only shows the localhost call" — Connect stayed highlighted and the row never flipped to Connected). Root cause: `GoogleHealthConnector.authorize()` ran the RFC 8252 **loopback redirect** on Android too — it bound `HttpServer` on the device loopback and needed the *backgrounded* app to serve Chrome's `http://localhost:52789/callback` after consent, which Android's cached-app freezer prevents; the callback never arrived, so `authorize()` hung (button `busy`) until the 5-minute timeout. Google deprecates loopback for mobile client types, and custom-scheme redirects are rejected on the existing Web-type client ("Custom scheme URIs are not allowed for 'WEB' client type"), so the fix follows Google's recommended Android path (Google Identity Services):
|
||||
- Connector (`google_health_connector_io.dart`): on `Platform.isAndroid` only, `authorize()` now runs **`package:google_sign_in`** (native account chooser + consent sheet — the app stays foreground, no HTTP callback) with `serverClientId = GOOGLE_HEALTH_CLIENT_ID` and `forceCodeForRefreshToken: true` (the `prompt=consent` equivalent), then exchanges the returned one-time `serverAuthCode` on the shared token endpoint (GIS codes carry no `redirect_uri`) — secure-storage tokens, transparent refresh, REST reads, and the parser are untouched. A 5-minute timeout mirrors the loopback path; a missing code logs the Android-client/SHA-1 checklist. `disconnect()` additionally signs the SDK out (best-effort) so Connect re-prompts for an account. Desktop/iOS keep the proven loopback flow.
|
||||
- UX: the Google Health row's failure copy is now OAuth-accurate — "Google Health sign-in was cancelled or did not complete. Tap Connect to try again." — instead of pointing the user at device settings (integrations controller branches on `HealthProviderType.googleHealth`).
|
||||
- Dependency: `google_sign_in: 6.3.0` added (Android implementation 6.2.1).
|
||||
- Verified: `flutter analyze` clean; `flutter test` 184/184 green (including `integrations_controller_test.dart` and `settings_screen_test.dart`).
|
||||
- [x] Android post-connect sync fix (field-reported right after the GIS fix: Google Health **connected**, but the app then showed "Health permissions were denied."): `BiometricController.syncFromHealth()` unconditionally demanded the **native** Health Connect grant first and bailed on denial — never reaching the cloud connector, which needs no OS permission. The bail is now `!granted && !await gateway.isCloudSyncReady()` — a native denial is fatal only when no cloud partner holds a grant; with Google Health connected, cloud reads proceed (native degrades to empty while ungranted) and the status reports `Synced N weight sample(s)…`. Tests: `biometric_controller_test.dart` gains a `cloudReady` fake override plus "native denial with a connected cloud partner still syncs partner data" (11 tests in file); full suite 185/185, analyze clean. Still prompts for Health Connect once — a denial there no longer breaks the cloud sync.
|
||||
|
||||
## Google Health on Android — Verification Gates (2026-10-18, manual)
|
||||
- [x] Google Cloud Android OAuth client(s) — DONE 2026-10-18 (the on-device connect succeeded, proving a matching package/SHA-1 client exists): Google Cloud → the same project as `GOOGLE_HEALTH_CLIENT_ID` → APIs & Services → Credentials → **Create OAuth client ID → Application type Android**: package name `com.companionglp.glpcompanion` with the SHA-1 of the release `upload-keystore.jks` (`keytool -list -v -keystore upload-keystore.jks -alias <keyAlias>`); add a second Android client with the debug keystore SHA-1 (`%USERPROFILE%\.android\debug.keystore`, alias `androiddebugkey`, storepass `android`) for debug builds. The GIS SDK resolves the caller by package/SHA-1 — without a match it returns no `serverAuthCode` and Connect fails (the connector logs the checklist in debug). Fingerprints computed 2026-10-18 (re-verify against the installed APK with `keytool -printcert -jarfile <apk>`): release `upload-keystore.jks` alias `upload` = `79:8B:73:52:70:77:C3:46:C7:12:06:88:42:0E:97:67:CC:B8:E2:85`; debug `~/.android/debug.keystore` alias `androiddebugkey` = `67:BA:FE:4A:15:5B:52:73:E1:8F:0D:F7:44:81:7B:5C:72:C3:A1:36`.
|
||||
- [x] On-device connect round trip (confirmed 2026-10-18): install the rebuilt arm64 APK → Settings → Google Health → Connect → native account chooser → Google consent (activity & fitness + health metrics read scopes) → returns straight into the app → row flips **Connected**.
|
||||
- [ ] Data round trip (pending an APK rebuild containing the post-connect sync fix above): after Connect, status reads `Synced N weight sample(s)…` instead of "Health permissions were denied.", and weight/active energy appear on the Nutrition dashboard (Android imports persist under the `healthConnect` platform source).
|
||||
|
||||
## Health Connect (Samsung & Pixel) Row — Fix (2026-10-18)
|
||||
|
||||
- [x] Native Health Connect permission request failed silently (field-reported: tapping the "Health Connect (Samsung & Pixel)" row showed "Health permissions were denied. Enable them in your device settings" with **no permission dialog ever appearing**). Root cause per the `package:health` v13.3.1 README: `requestAuthorization()` registers its permission launcher via `(activity as ComponentActivity).registerForActivityResult(...)` in `onAttachedToActivity` — the host activity must therefore extend **`FlutterFragmentActivity`**, but `MainActivity` extended plain `FlutterActivity` (`android.app.Activity`, not androidx `ComponentActivity`). The cast fails → the launcher stays `null` → the plugin answers `success(false)` immediately (HealthPlugin.kt:317) → our gateway maps it to "denied". Three fixes:
|
||||
- `MainActivity.kt` now extends `FlutterFragmentActivity` (with a comment explaining the cast requirement). Drop-in for all other plugins (app_links, supabase, url_launcher, notifications, google_sign_in all bind to any Activity).
|
||||
- `AndroidManifest.xml`: added the README-required `<intent-filter>` for `androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE` on `.MainActivity` (the `<queries>` block and `ViewPermissionUsageActivity` alias were already present; `health_connect_config.xml` is optional restore tooling and was not added).
|
||||
- Read-set hygiene: `STEPS` removed from `_androidReadTypes` and `READ_STEPS` from the manifest — steps are **never fetched** anywhere in the app, and the README ties step access to the `ACTIVITY_RECOGNITION` permission this app does not declare; an ungrantable entry risks poisoning the whole permission request. Remaining set: weight, total calories burned, active energy burned, blood glucose (all four declared in the manifest).
|
||||
- Verified: `flutter analyze` clean; `flutter test` 185/185 green; **`flutter build apk --debug` → `Built build\app\outputs\flutter-apk\app-debug.apk`** (also clears the standing build-gate item for the debug APK; Gradle 135.1 s; pre-existing KGP warning for device_info_plus/health/mobile_scanner is non-fatal today).
|
||||
- [ ] On-device Health Connect round trip: tap the Samsung/Pixel row → the Health Connect permission screen **appears** (weight, calories, glucose toggles) → grant → row flips **Connected** → status shows a `Synced …` result. If a stale denial exists from earlier attempts, clear it once under Android Settings → Apps → glpcompanion → Permissions (or the Health Connect app's App permissions screen), then retry in-app.
|
||||
- [ ] Disconnect then Connect again re-prompts for an account (SDK sign-out path), and the row recovers cleanly if the sign-in sheet is dismissed (cancel → the OAuth-specific error banner, button un-busy).
|
||||
- [ ] Follow-up (not part of this fix): iOS still uses the loopback redirect and carries the same background fragility — migrate it to the SDK path when iOS alpha starts (needs an iOS-type OAuth client + reversed-client-ID URL scheme).
|
||||
- [ ] Web-client housekeeping (field-checked 2026-10-18: the console screenshot showed the WRONG client — the project has **two** Web clients): `GOOGLE_HEALTH_CLIENT_ID` = `305941286539-2bqe3cte3gg6b2m64k0uosdp3tvrcfhq…` is the **Google Health** client — confirm it holds `http://localhost:52789/callback` under Authorized redirect URIs (desktop/iOS loopback only; the Android GIS flow uses **no** redirect URI) and that its secret matches `GOOGLE_HEALTH_CLIENT_SECRET`. The other client, `305941286539-8p9itkud4m0r9luiu53rg2s9liqon7lm…` ("GLP-1 Companion Web", redirect `https://pyrgjdvqbjzakuwvbpxw.supabase.co/auth/v1/callback`), is the **Supabase sign-in** client — leave it untouched. Secrets are no longer viewable in the console — rotate via **Add secret** + `.env` update if mismatched.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
|
|
@ -28,43 +19,36 @@ android {
|
|||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.companionglp.glpcompanion"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
//
|
||||
// API 26 (Android 8.0) floor required by the `health` plugin (Health
|
||||
// Connect / HealthKit bridge), which declares minSdk 26 in its
|
||||
// manifest; Health Connect itself only ships on Android 8.0+.
|
||||
minSdk = maxOf(flutter.minSdkVersion, 26)
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
// Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION
|
||||
// is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions)
|
||||
// You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true`
|
||||
// flag during build.
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
storeFile = file(keystoreProperties.getProperty("storeFile"))
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
# Flutter Wrapper
|
||||
-keep class io.flutter.app.** { *; }
|
||||
-keep class io.flutter.plugin.** { *; }
|
||||
-keep class io.flutter.util.** { *; }
|
||||
-keep class io.flutter.view.** { *; }
|
||||
-keep class io.flutter.** { *; }
|
||||
-keep class io.flutter.plugins.** { *; }
|
||||
|
||||
# Drift / SQLCipher
|
||||
-keep class net.sqlcipher.** { *; }
|
||||
-keep class net.sqlcipher.database.** { *; }
|
||||
|
||||
# OpenFoodFacts & JSON Serialization
|
||||
-keepattributes *Annotation*,EnclosingMethod,Signature
|
||||
-keepclassmembers enum * { *; }
|
||||
|
||||
# Flutter Play Store Split / Deferred Components (referenced by engine, not used here)
|
||||
-dontwarn com.google.android.play.core.**
|
||||
|
||||
# Additional standard Flutter & third-party suppressions
|
||||
-dontwarn io.flutter.embedding.engine.deferredcomponents.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn org.conscrypt.**
|
||||
|
|
@ -4,12 +4,11 @@
|
|||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<!-- Health Connect (Android 14+ / Play services): read permissions matching
|
||||
the HealthGateway request set (weight, total calories burned, active
|
||||
energy burned, blood glucose). STEPS was removed from the set: it is
|
||||
never fetched, and the package:health README ties step access to the
|
||||
ACTIVITY_RECOGNITION permission this app does not declare. Health
|
||||
Connect federates Samsung Health and Pixel sensor feeds. -->
|
||||
the HealthGateway request set (weight, steps, total calories burned,
|
||||
active energy burned, blood glucose). Health Connect federates
|
||||
Samsung Health and Pixel sensor feeds through this runtime. -->
|
||||
<uses-permission android:name="android.permission.health.READ_WEIGHT"/>
|
||||
<uses-permission android:name="android.permission.health.READ_STEPS"/>
|
||||
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
|
||||
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
|
||||
<uses-permission android:name="android.permission.health.READ_BLOOD_GLUCOSE"/>
|
||||
|
|
@ -47,12 +46,6 @@
|
|||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="com.companionglp.glpcompanion"/>
|
||||
</intent-filter>
|
||||
<!-- Health Connect (package:health README): lets the Health
|
||||
Connect permissions screen resolve this app for the
|
||||
rationale step of the permission flow. -->
|
||||
<intent-filter>
|
||||
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
package com.companionglp.glpcompanion
|
||||
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
// FlutterFragmentActivity (not the default FlutterActivity) is required by
|
||||
// package:health v13: it casts the host activity to
|
||||
// androidx.activity.ComponentActivity to call registerForActivityResult for
|
||||
// the Health Connect permission flow. With plain FlutterActivity that cast
|
||||
// fails, the permission launcher stays null, and requestAuthorization()
|
||||
// silently returns false ("Health permissions were denied" with no dialog).
|
||||
class MainActivity : FlutterFragmentActivity()
|
||||
class MainActivity : FlutterActivity()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:crypto/crypto.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
@ -55,30 +54,18 @@ class GoogleHealthTokens {
|
|||
}
|
||||
}
|
||||
|
||||
/// Google Health cloud connector: OAuth2 through Google Identity Services on
|
||||
/// Android (native account chooser + consent, `serverAuthCode` exchange) and
|
||||
/// authorization-code + PKCE through the system browser with a loopback
|
||||
/// redirect (`http://localhost:<port>/callback`) on desktop/iOS, backed by
|
||||
/// secure-storage tokens with transparent refresh, and normalized weight /
|
||||
/// active-energy reads from the Google Health API — the official successor to
|
||||
/// the retired Fitbit Web API (shutdown 2026-10-30); Fitbit, Pixel and Wear
|
||||
/// OS data flows into Google Health.
|
||||
/// Google Health cloud connector: OAuth2 (authorization-code + PKCE) through
|
||||
/// the system browser with a loopback redirect
|
||||
/// (`http://localhost:<port>/callback`), secure-storage-backed tokens with
|
||||
/// transparent refresh, and normalized weight / active-energy reads from the
|
||||
/// Google Health API — the official successor to the retired Fitbit Web API
|
||||
/// (shutdown 2026-10-30); Fitbit, Pixel and Wear OS data flows into Google
|
||||
/// Health.
|
||||
///
|
||||
/// Requires `GOOGLE_HEALTH_CLIENT_ID` (a **Web application** OAuth client)
|
||||
/// and `GOOGLE_HEALTH_CLIENT_SECRET` in `.env` (optionally
|
||||
/// `GOOGLE_HEALTH_REDIRECT_PORT`, default 52789). Create them in a Google
|
||||
/// Cloud project with the Google Health API enabled; the loopback redirect
|
||||
/// must be an Authorized redirect URI on the OAuth client. Android also
|
||||
/// needs an **Android** OAuth client (package `com.companionglp.glpcompanion`
|
||||
/// + the signing certificate's SHA-1) in the same project, because the SDK
|
||||
/// resolves the caller by package/SHA-1 before issuing a server auth code.
|
||||
///
|
||||
/// Android deliberately avoids the loopback redirect: the callback requires
|
||||
/// the *backgrounded* app to serve HTTP to the browser, and Android's
|
||||
/// cached-app freezer prevents that — the browser only ever showed the
|
||||
/// `localhost` URL and the Connect button hung until the 5-minute timeout.
|
||||
/// Google likewise deprecates loopback for mobile client types and
|
||||
/// recommends this SDK path instead.
|
||||
/// Requires `GOOGLE_HEALTH_CLIENT_ID` and `GOOGLE_HEALTH_CLIENT_SECRET` in
|
||||
/// `.env` (optionally `GOOGLE_HEALTH_REDIRECT_PORT`, default 52789). Create
|
||||
/// them in a Google Cloud project with the Google Health API enabled; the
|
||||
/// loopback redirect must be an Authorized redirect URI on the OAuth client.
|
||||
class GoogleHealthConnector implements ICloudHealthConnector {
|
||||
GoogleHealthConnector({http.Client? client, FlutterSecureStorage? storage})
|
||||
: _client = client ?? http.Client(),
|
||||
|
|
@ -151,14 +138,6 @@ class GoogleHealthConnector implements ICloudHealthConnector {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Android runs the Google Identity Services SDK path instead of the
|
||||
// loopback redirect: that flow needs the browser to reach an HTTP server
|
||||
// held by this (backgrounded) app, which Android's process freezer
|
||||
// prevents — the callback never arrived and authorize() hung.
|
||||
if (Platform.isAndroid) {
|
||||
return _authorizeViaGoogleSignIn();
|
||||
}
|
||||
|
||||
final verifier = _randomUrlSafe(64);
|
||||
final authorizeUrl = Uri.parse(
|
||||
'$_authorizeEndpoint?response_type=code'
|
||||
|
|
@ -200,73 +179,9 @@ class GoogleHealthConnector implements ICloudHealthConnector {
|
|||
}
|
||||
}
|
||||
|
||||
/// Android authorization through the Google Identity Services SDK
|
||||
/// (`package:google_sign_in`): the consent sheet renders natively inside a
|
||||
/// foreground activity, so the round trip no longer depends on serving a
|
||||
/// loopback HTTP callback from the backgrounded app.
|
||||
///
|
||||
/// The SDK hands back a one-time [GoogleSignInAccount.serverAuthCode]
|
||||
/// issued against the same Web client (`serverClientId`), which is
|
||||
/// exchanged on the shared token endpoint — yielding the identical
|
||||
/// access/refresh token pair the desktop loopback flow produces, so token
|
||||
/// storage, transparent refresh, and the REST layer stay unchanged.
|
||||
/// [forceCodeForRefreshToken] mirrors `prompt=consent` by guaranteeing a
|
||||
/// refresh token on every re-consent.
|
||||
Future<bool> _authorizeViaGoogleSignIn() async {
|
||||
try {
|
||||
final signIn = GoogleSignIn(
|
||||
scopes: _scopes.split(' '),
|
||||
serverClientId: _clientId,
|
||||
forceCodeForRefreshToken: true,
|
||||
);
|
||||
final account = await signIn.signIn().timeout(_callbackTimeout);
|
||||
if (account == null) {
|
||||
debugPrint('GoogleHealthConnector: Google sign-in was cancelled.');
|
||||
return false;
|
||||
}
|
||||
final code = account.serverAuthCode;
|
||||
if (code == null || code.isEmpty) {
|
||||
debugPrint(
|
||||
'GoogleHealthConnector: Google sign-in returned no authorization '
|
||||
'code - verify the Android OAuth client (package '
|
||||
'com.companionglp.glpcompanion + signing-cert SHA-1) exists in the '
|
||||
'same Google Cloud project as GOOGLE_HEALTH_CLIENT_ID.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Google-issued SDK codes are exchanged without a redirect_uri (per
|
||||
// the GIS server-auth-code flow); everything else matches the shared
|
||||
// [_requestTokens] path used by desktop.
|
||||
final tokens = await _requestTokens({
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
});
|
||||
await _writeTokens(tokens);
|
||||
return true;
|
||||
} catch (error, stackTrace) {
|
||||
debugPrintStack(
|
||||
stackTrace: stackTrace,
|
||||
label: 'GoogleHealthConnector: Android sign-in failed ($error)',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
await _storage.delete(key: tokenStorageKey);
|
||||
if (Platform.isAndroid) {
|
||||
// Best-effort: drop the SDK session too, so the next Connect re-prompts
|
||||
// for an account instead of silently reusing the previous grant.
|
||||
try {
|
||||
await GoogleSignIn(
|
||||
scopes: _scopes.split(' '),
|
||||
serverClientId: _clientId,
|
||||
).signOut();
|
||||
} catch (error) {
|
||||
debugPrint('GoogleHealthConnector: SDK sign-out failed ($error)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class MobileHealthGateway extends IHealthGateway {
|
|||
/// Pixel sensor feeds into this single runtime.
|
||||
static const List<HealthDataType> _androidReadTypes = [
|
||||
HealthDataType.WEIGHT,
|
||||
HealthDataType.STEPS,
|
||||
HealthDataType.TOTAL_CALORIES_BURNED,
|
||||
HealthDataType.ACTIVE_ENERGY_BURNED,
|
||||
HealthDataType.BLOOD_GLUCOSE,
|
||||
|
|
|
|||
|
|
@ -219,9 +219,9 @@ class BiometricController extends AsyncNotifier<BiometricState> {
|
|||
/// same source/window is replaced, not duplicated).
|
||||
///
|
||||
/// Returns `false` when no sync source exists on this platform (Web with
|
||||
/// no cloud connector provisioned), when native permission is denied
|
||||
/// *and* no cloud partner holds a valid grant, or when the sync throws;
|
||||
/// the reason is reflected in [BiometricState.statusMessage].
|
||||
/// no cloud connector provisioned), when permission is denied, or when
|
||||
/// the sync throws; the reason is reflected in
|
||||
/// [BiometricState.statusMessage].
|
||||
Future<bool> syncFromHealth({Duration window = syncWindow}) async {
|
||||
final gateway = ref.read(healthGatewayProvider);
|
||||
if (!gateway.supportsNativeHealth && !gateway.supportsCloudSync) {
|
||||
|
|
@ -245,14 +245,10 @@ class BiometricController extends AsyncNotifier<BiometricState> {
|
|||
|
||||
try {
|
||||
// Native stores need an OS permission grant first; cloud connectors
|
||||
// manage their own OAuth grant at connect time. A native denial is
|
||||
// therefore fatal only when no cloud partner can supply the data —
|
||||
// with Google Health connected, `isCloudSyncReady()` is true and the
|
||||
// cloud reads proceed without any OS permission (the gateway merges
|
||||
// both sources; native degrades to empty while ungranted).
|
||||
// manage their own OAuth grant at connect time.
|
||||
if (gateway.supportsNativeHealth) {
|
||||
final granted = await gateway.requestPermissions();
|
||||
if (!granted && !await gateway.isCloudSyncReady()) {
|
||||
if (!granted) {
|
||||
await _refresh(
|
||||
statusMessage: 'Health permissions were denied.',
|
||||
error: StateError('Health permissions denied'),
|
||||
|
|
|
|||
|
|
@ -203,12 +203,6 @@ class IntegrationsController extends Notifier<IntegrationsState> {
|
|||
isRequestingPermissions: false,
|
||||
errorMessage: granted
|
||||
? null
|
||||
: provider == HealthProviderType.googleHealth
|
||||
// OAuth round trip (SDK on Android, browser loopback on
|
||||
// desktop): nothing to enable in device settings — the flow
|
||||
// was cancelled or the redirect never completed.
|
||||
? 'Google Health sign-in was cancelled or did not complete. '
|
||||
'Tap Connect to try again.'
|
||||
: 'Health permissions were denied. Enable them in your device '
|
||||
'settings to sync biometrics.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import app_links
|
|||
import device_info_plus
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import google_sign_in_ios
|
||||
import mobile_scanner
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
|
@ -19,7 +18,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
|
|
|||
96
pubspec.lock
96
pubspec.lock
|
|
@ -49,14 +49,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0d58d1fa1fddf6588251ccf2819f3fb3b6d0452c1b2813ac7413a05592bfbefe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -193,14 +185,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
console:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: console
|
||||
sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -453,14 +437,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
get_it:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get_it
|
||||
sha256: "2b5c6b077aad30bba9782178baff6076a2044141f1d5f2f15b43bca3fbe74874"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -477,54 +453,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.8.1"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
google_sign_in:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in
|
||||
sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_sign_in_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_android
|
||||
sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_ios
|
||||
sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
google_sign_in_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_platform_interface
|
||||
sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.4+4"
|
||||
gotrue:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -589,14 +517,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.12.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -733,14 +653,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.3"
|
||||
msix:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: msix
|
||||
sha256: "61415c352e8aea084332b8a5514e6408c961c8cdeca202804fff1040eb555ac7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.18.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -877,14 +789,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.3"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.2"
|
||||
postgrest:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
12
pubspec.yaml
12
pubspec.yaml
|
|
@ -55,7 +55,6 @@ dependencies:
|
|||
sqlite3: ^3.4.0
|
||||
# Scheduling support for flutter_local_notifications (zonedSchedule).
|
||||
timezone: ^0.9.4
|
||||
google_sign_in: 6.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
@ -68,17 +67,6 @@ dev_dependencies:
|
|||
# Riverpod 3 migration.
|
||||
build_runner: ^2.16.0
|
||||
drift_dev: ^2.35.1
|
||||
msix: ^3.16.7
|
||||
|
||||
msix_config:
|
||||
display_name: GLP-1 Companion
|
||||
publisher_display_name: Kell Engineering LLC
|
||||
identity_name: KellEngineering.GLP1Companion
|
||||
publisher: CN=Kell Engineering LLC, O=Kell Engineering LLC, L=Valrico, S=Florida, C=US
|
||||
msix_version: 0.1.0.0
|
||||
logo_path: web/icons/Icon-512.png
|
||||
capabilities: internetClient
|
||||
execution_alias: glp-companion
|
||||
|
||||
# Compile SQLCipher into the SQLite engine bundled by package:sqlite3's build
|
||||
# hooks (the sqlcipher_flutter_libs plugin is retired on sqlite3 3.x).
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Version,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ForgejoToken =$env:FORGEJO_TOKEN,
|
||||
|
||||
[string]$Server = "https://git.kellsupport.com",
|
||||
[string]$Owner = "mtkell",
|
||||
[string]$Repo = "glp-companion"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $ForgejoToken) {
|
||||
Write-Error "Forgejo Token missing! Pass -ForgejoToken <token> or set `$env:FORGEJO_TOKEN."
|
||||
}
|
||||
|
||||
$DistDir = "dist\v$Version"
|
||||
$Tag = "v$Version"
|
||||
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host " Building & Publishing Alpha Release: $Tag" -ForegroundColor Cyan
|
||||
Write-Host " Target: $Server/$Owner/$Repo" -ForegroundColor Cyan
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
|
||||
|
||||
Write-Host "`n[1/5] Running Drift build_runner..." -ForegroundColor Yellow
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
Write-Host "`n[2/5] Building Android arm64 APK..." -ForegroundColor Yellow
|
||||
flutter build apk --release --split-per-abi
|
||||
$ApkSrc = "build\app\outputs\flutter-apk\app-arm64-v8a-release.apk"
|
||||
$ApkDest = "$DistDir\GLP1Companion-Android-arm64-$Tag.apk"
|
||||
Copy-Item $ApkSrc $ApkDest -Force
|
||||
|
||||
Write-Host "`n[3/5] Building Windows Release & Packaging Standalone Zip..." -ForegroundColor Yellow
|
||||
flutter build windows --release
|
||||
$ZipDest = "$DistDir\GLP1Companion-Windows-x64-$Tag-Portable.zip"
|
||||
Compress-Archive -Path "build\windows\x64\runner\Release\*" -DestinationPath $ZipDest -Force
|
||||
|
||||
Write-Host "`n[4/5] Building Windows MSIX..." -ForegroundColor Yellow
|
||||
dart run msix:create --install-certificate false
|
||||
$MsixSrc = Get-ChildItem "build\windows\x64\runner\Release\*.msix" | Select-Object -First 1
|
||||
$MsixDest = "$DistDir\GLP1Companion-Windows-$Tag.msix"
|
||||
Copy-Item $MsixSrc.FullName $MsixDest -Force
|
||||
|
||||
Write-Host "`n[5/5] Publishing Release & Assets to Forgejo..." -ForegroundColor Yellow
|
||||
$Headers = @{
|
||||
"Authorization" = "token $ForgejoToken"
|
||||
"Accept" = "application/json"
|
||||
}
|
||||
|
||||
$ReleaseBody = @{
|
||||
tag_name = $Tag
|
||||
name = "GLP-1 Companion $Tag"
|
||||
body = "Automated alpha release build for $Tag containing Android APK and Windows packages."
|
||||
draft = $false
|
||||
prerelease = $true
|
||||
} | ConvertTo-Json
|
||||
|
||||
$ReleaseUrl = "$Server/api/v1/repos/$Owner/$Repo/releases"
|
||||
|
||||
try {
|
||||
$ReleaseResp = Invoke-RestMethod -Uri $ReleaseUrl -Method Post -Headers $Headers -Body $ReleaseBody -ContentType "application/json"
|
||||
$ReleaseId = $ReleaseResp.id
|
||||
Write-Host "Created release $ReleaseId for$Tag" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Release for $Tag may already exist, fetching existing ID..." -ForegroundColor Gray
|
||||
$Existing = Invoke-RestMethod -Uri "$ReleaseUrl/tags/$Tag" -Method Get -Headers $Headers
|
||||
$ReleaseId = $Existing.id
|
||||
Write-Host "Using existing release ID: $ReleaseId" -ForegroundColor Green
|
||||
}
|
||||
|
||||
$Assets = @($ApkDest, $ZipDest,$MsixDest)
|
||||
|
||||
foreach ($FilePath in $Assets) {
|
||||
if (Test-Path $FilePath) {
|
||||
$FileName = [System.IO.Path]::GetFileName($FilePath)
|
||||
Write-Host "Uploading $FileName..." -ForegroundColor Cyan
|
||||
|
||||
& curl.exe -s -X POST "$Server/api/v1/repos/$Owner/$Repo/releases/$ReleaseId/assets?name=$FileName" `
|
||||
-H "Authorization: token $ForgejoToken" `
|
||||
-H "Content-Type: multipart/form-data" `
|
||||
-F "attachment=@$FilePath" | Out-Null
|
||||
|
||||
Write-Host " -> Uploaded $FileName" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nSuccessfully released $Tag to$Server/$Owner/$Repo/releases!" -ForegroundColor Green
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
$Server = "https://git.kellsupport.com"
|
||||
$Owner = "mtkell"
|
||||
$Repo = "glp-companion"
|
||||
$Tag = "v0.0.1-test"
|
||||
|
||||
# Token is read from the environment only — never hardcode credentials here.
|
||||
$Token = $env:FORGEJO_TOKEN
|
||||
if (-not $Token) {
|
||||
Write-Error "FORGEJO_TOKEN environment variable is not set. Export it before running this script."
|
||||
}
|
||||
|
||||
$DummyFile = "dummy-asset-test.txt"
|
||||
"Forgejo API asset upload verification payload - $(Get-Date)" | Set-Content -Path $DummyFile -Encoding utf8
|
||||
|
||||
$Headers = @{
|
||||
"Authorization" = "token $Token"
|
||||
"Accept" = "application/json"
|
||||
}
|
||||
|
||||
Write-Host "Checking for release for tag: $Tag..." -ForegroundColor Cyan
|
||||
$ReleaseUrl = "$Server/api/v1/repos/$Owner/$Repo/releases"
|
||||
|
||||
try {
|
||||
$Release = Invoke-RestMethod -Uri "$ReleaseUrl/tags/$Tag" -Headers $Headers -Method Get
|
||||
$ReleaseId =$Release.id
|
||||
Write-Host "Found existing release ID: $ReleaseId" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Creating new draft test release for tag $Tag..." -ForegroundColor Yellow
|
||||
$Release = Invoke-RestMethod -Uri $ReleaseUrl -Headers $Headers -Method Post -ContentType "application/json" -Body (@{
|
||||
tag_name = $Tag
|
||||
name = "Test Asset Upload Release"
|
||||
body = "Testing Forgejo attachment upload endpoint."
|
||||
draft = $true
|
||||
prerelease = $true
|
||||
} | ConvertTo-Json)
|
||||
$ReleaseId =$Release.id
|
||||
Write-Host "Created test release ID: $ReleaseId" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "Uploading $DummyFile to release ID$ReleaseId via curl..." -ForegroundColor Cyan
|
||||
$UploadUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/$ReleaseId/assets?name=$DummyFile"
|
||||
|
||||
& curl.exe -s -X POST $UploadUrl `
|
||||
-H "Authorization: token $Token" `
|
||||
-F "attachment=@$DummyFile" `
|
||||
-o "curl_response.json"
|
||||
|
||||
Write-Host "`nServer Response:" -ForegroundColor Gray
|
||||
Get-Content -Path "curl_response.json"
|
||||
|
||||
Remove-Item -Path $DummyFile -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "curl_response.json" -Force -ErrorAction SilentlyContinue
|
||||
|
|
@ -86,7 +86,6 @@ class _FakeHealthSyncRepository implements IHealthSyncRepository {
|
|||
class _FakeHealthGateway extends IHealthGateway {
|
||||
bool nativeSupported = true;
|
||||
bool granted = true;
|
||||
bool cloudReady = false;
|
||||
List<HealthMetric> weights = [];
|
||||
List<HealthMetric> energies = [];
|
||||
int permissionRequests = 0;
|
||||
|
|
@ -105,9 +104,6 @@ class _FakeHealthGateway extends IHealthGateway {
|
|||
return granted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isCloudSyncReady() async => cloudReady;
|
||||
|
||||
@override
|
||||
Future<List<HealthMetric>> fetchWeightHistory({
|
||||
required DateTime start,
|
||||
|
|
@ -290,32 +286,6 @@ void main() {
|
|||
expect(repository.entries, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'native denial with a connected cloud partner still syncs partner '
|
||||
'data', () async {
|
||||
gateway.granted = false;
|
||||
gateway.cloudReady = true;
|
||||
gateway.weights = [
|
||||
HealthMetric(
|
||||
timestamp: DateTime.now().subtract(const Duration(hours: 1)),
|
||||
value: 182,
|
||||
unit: 'lb',
|
||||
),
|
||||
];
|
||||
await load();
|
||||
|
||||
final ok = await notifier().syncFromHealth();
|
||||
|
||||
expect(ok, isTrue);
|
||||
// The native grant is still requested exactly once…
|
||||
expect(gateway.permissionRequests, 1);
|
||||
final state = container.read(biometricControllerProvider).valueOrNull!;
|
||||
// …but the denial no longer blocks the cloud-backed sync.
|
||||
expect(state.statusMessage, contains('Synced 1 weight sample(s)'));
|
||||
expect(state.statusMessage, isNot(contains('denied')));
|
||||
expect(repository.entries, hasLength(1));
|
||||
});
|
||||
|
||||
test(
|
||||
'imports weights and per-day active energy, then re-syncs without '
|
||||
'duplicates', () async {
|
||||
|
|
|
|||
Loading…
Reference in New Issue