Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/workflows/mobile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Mobile App (Flutter)

on:
push:
branches: [feat/flutter-mobile-app]
paths: ['mobile/**']
pull_request:
branches: [develop]
paths: ['mobile/**']
workflow_dispatch:
inputs:
upload_testflight:
description: 'Upload to TestFlight'
type: boolean
default: false

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test & Analyze
runs-on: ubuntu-latest
defaults:
run:
working-directory: mobile
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- run: flutter analyze --no-fatal-infos
- run: flutter test

build-ios:
name: Build iOS
needs: test
runs-on: macos-latest
if: |
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/mobile-v')
defaults:
run:
working-directory: mobile
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable

- run: flutter pub get
- run: cd ios && pod install

# Uses same secret names as immich-fork for reuse.
- name: Create API Key
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }}
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8

- name: Import Certificate
env:
IOS_CERTIFICATE_P12: ${{ secrets.IOS_DISTRIBUTION_CERT_P12 }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERT_PASSWORD }}
run: |
echo "$IOS_CERTIFICATE_P12" | base64 --decode > /tmp/certificate.p12

security create-keychain -p "$IOS_CERTIFICATE_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$IOS_CERTIFICATE_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain

security import /tmp/certificate.p12 -k build.keychain \
-P "$IOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$IOS_CERTIFICATE_PASSWORD" build.keychain

security find-identity -v -p codesigning build.keychain

- name: Build IPA
run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist

- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: huly-mobile-ios
path: mobile/build/ios/ipa/*.ipa

- name: Upload to TestFlight
if: |
startsWith(github.ref, 'refs/tags/mobile-v') ||
github.event.inputs.upload_testflight == 'true'
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file build/ios/ipa/*.ipa \
--apiKey "$API_KEY_ID" \
--apiIssuer "$API_KEY_ISSUER_ID"

- name: Cleanup keychain
if: always()
run: security delete-keychain build.keychain 2>/dev/null || true

build-android:
name: Build Android
needs: test
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/mobile-v')
defaults:
run:
working-directory: mobile
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- run: flutter build appbundle --release
- name: Upload AAB
uses: actions/upload-artifact@v4
with:
name: huly-mobile-android
path: mobile/build/app/outputs/bundle/release/*.aab
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.heft/
lib/
!mobile/lib/
_api-extractor-temp/
temp/
.idea
Expand Down
166 changes: 166 additions & 0 deletions .ideas/mobile-app-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Huly Mobile App (Flutter) — Status & Continuation Plan

**Branch:** `feat/flutter-mobile-app`
**Fork PR:** https://github.com/ledoent/platform/pull/2
**Repo location:** `mobile/` directory in huly-platform monorepo
**Target:** Self-hosted Huly deployment (ledoweb.com)

---

## What's Done (11 commits on branch)

### Phase 1 — Auth + Issue Tracking
- [x] OTP, password, OAuth (Google/GitHub/OIDC) via `huly://` custom scheme
- [x] 2FA/TOTP verification (verify2fa RPC, TfaScreen, tfaPending state)
- [x] Issue list/detail/create/edit (TxUpdateDoc)
- [x] Status management (fetch, display, edit via dropdown)
- [x] Client-side search/filter on issue list
- [x] Auto-refresh (45s timer, lifecycle-aware)
- [x] Server-side `mobileRedirect` in authProviders

### Phase 2 — Polish + Navigation
- [x] Member/assignee name resolution (contact:class:Person)
- [x] Activity feed + comment posting on issue detail
- [x] File attachments (image_picker → blob upload → attachment doc)
- [x] Bottom nav bar (Issues / Chat / Settings)
- [x] Settings screen (server info, workspace, biometric toggle, sign out)

### Phase 3 — Chunter Chat
- [x] Channel + DirectMessage list with DM name resolution
- [x] Message thread with send, shared MessageBubble widget
- [x] 10s polling (lifecycle-aware)

### Phase 4 — Real-Time
- [x] WebSocket client (Huly protocol: hello handshake, ping/pong, Tx streaming)
- [x] dataVersionProvider — auto-refreshes providers on Tx events
- [x] Polling kept as fallback
- [x] FCM push notifications (register token as PushSubscription, foreground display)

### Phase 5 — Advanced
- [x] Biometric auth (Face ID / fingerprint, lock screen, settings toggle)

### Infrastructure
- [x] GitHub Actions CI (`.github/workflows/mobile.yml`)
- Test on push/PR, build iOS+Android on tags/manual
- Uses same secrets as `dnplkndll/immich` repo
- [x] App Store Connect: bundle ID `com.ledoweb.hulyMobile` registered
- [x] Capabilities enabled: Push Notifications, App Groups, Associated Domains

### Code Quality
- [x] Shared widgets extracted (MessageBubble, PriorityChip)
- [x] Shared utilities (escapeHtml, stripHtml, formatTimestamp)
- [x] No duplication across screens
- [x] `flutter build ios --no-codesign` passes
- [x] `flutter test` — 16/16 tests pass

---

## Blocked / In Progress

### TestFlight Upload (blocked)
- IPA built locally (21.8MB) at `mobile/build/ios/ipa/`
- Upload failed: app `com.ledoweb.hulyMobile` needs to be **created in App Store Connect**
- Bundle ID is registered, capabilities enabled
- **TODO:** Go to https://appstoreconnect.apple.com/apps → New App → iOS
- Name: `Huly Mobile`
- Bundle ID: `com.ledoweb.hulyMobile` (select from dropdown)
- SKU: `com.ledoweb.hulyMobile`
- Primary Language: English (U.S.)
- Then upload: `xcrun altool --upload-app --type ios --file mobile/build/ios/ipa/huly_mobile.ipa --apiKey H37UAAFAVA --apiIssuer 7f122bfe-f415-4590-9229-35a8e80a1826`

### CI Secrets (done)
All 5 secrets set on `ledoent/platform`:
- `APP_STORE_CONNECT_API_KEY_ID` — H37UAAFAVA
- `APP_STORE_CONNECT_API_KEY_ISSUER_ID` — 7f122bfe-f415-4590-9229-35a8e80a1826
- `APP_STORE_CONNECT_API_KEY_P8` — from ~/.appstoreconnect/private_keys/
- `IOS_DISTRIBUTION_CERT_P12` — from ~/.apple-certs/immich_distribution.p12
- `IOS_DISTRIBUTION_CERT_PASSWORD` — empty string

---

## TODO — Remaining Work

### Immediate (unblock TestFlight)
- [ ] Create app in App Store Connect (manual — API key lacks CREATE permission)
- [ ] Upload IPA to TestFlight (command above, or `git tag mobile-v0.3.0` to trigger CI)
- [ ] Add custom Huly app icon (currently Flutter default placeholder)
- [ ] Add custom launch screen / splash (currently default)

### Android (untested)
- [ ] Test Android build on device or emulator
- [ ] Chrome Custom Tabs for OAuth (vs ASWebAuthenticationSession)
- [ ] Verify Android share intent works
- [ ] Verify Android Keystore via flutter_secure_storage
- [ ] Add google-services.json for FCM on Android

### Firebase Setup (for push notifications)
- [ ] Create Firebase project for Huly Mobile
- [ ] Add GoogleService-Info.plist to `mobile/ios/Runner/`
- [ ] Add google-services.json to `mobile/android/app/`
- [ ] Test push notification delivery end-to-end
- [ ] Verify FCM token registration as PushSubscription in workspace

### WebSocket Hardening
- [ ] Test WebSocket against live Huly instance
- [ ] Verify Tx events are received and providers refresh
- [ ] Test reconnection after network loss
- [ ] Add connection status indicator in UI (connected/disconnected)
- [ ] Consider adding snappy compression for bandwidth (optional)

### Chat Improvements
- [ ] Thread replies (tap message → reply in thread)
- [ ] Unread indicators on channel list (DocNotifyContext queries)
- [ ] Message reactions
- [ ] Rich markup rendering (`flutter_widget_from_html` for TipTap HTML)
- [ ] Typing indicators

### Issue Improvements
- [ ] Issue deletion
- [ ] Assignee picker (select from workspace members)
- [ ] Sub-tasks / related issues
- [ ] Due dates
- [ ] Labels/tags

### UX Polish
- [ ] Workspace switcher (currently requires logout/re-login)
- [ ] Notification badge on bottom nav for unread chat
- [ ] Dark/light theme toggle (currently dark only)
- [ ] Error states and empty states with illustrations
- [ ] Loading skeletons instead of spinners

### Future Modules
- [ ] Documents (read-only markdown rendering)
- [ ] Time tracking (log entries against issues)
- [ ] HR module (leave requests, PTO)
- [ ] Kanban board view for issues

---

## Key Files Reference

| File | Purpose |
|------|---------|
| `mobile/lib/app.dart` | Router, bottom nav shell, realtime init |
| `mobile/lib/core/api/rest_client.dart` | REST API client (find-all, tx, blob) |
| `mobile/lib/core/api/websocket_client.dart` | WebSocket protocol implementation |
| `mobile/lib/core/api/realtime_provider.dart` | WebSocket + push + dataVersion providers |
| `mobile/lib/features/auth/auth_provider.dart` | Auth state machine, all login methods |
| `mobile/lib/features/issues/issue_provider.dart` | Issue, status, member, attachment providers |
| `mobile/lib/features/chat/chat_provider.dart` | Channel + message providers |
| `mobile/lib/services/push_notification_service.dart` | FCM init, foreground display, subscription |
| `mobile/pubspec.yaml` | Dependencies |
| `.github/workflows/mobile.yml` | CI: test, build iOS/Android, upload TestFlight |
| `.ideas/mobile-app-plan.md` | This file |

## Apple Developer Details

| Item | Value |
|------|-------|
| Bundle ID | `com.ledoweb.hulyMobile` |
| Team ID | `6ZJTLNKLQR` |
| ASC Key ID | `H37UAAFAVA` |
| ASC Issuer ID | `7f122bfe-f415-4590-9229-35a8e80a1826` |
| API Key | `~/.appstoreconnect/private_keys/AuthKey_H37UAAFAVA.p8` |
| Dist Cert | `~/.apple-certs/immich_distribution.p12` |
| ASC Bundle Internal ID | `9F22F9BGXB` |
| Capabilities | Push Notifications, App Groups, Associated Domains, In-App Purchase |
45 changes: 45 additions & 0 deletions mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
Loading
Loading