The KMP shared layer's task-completion-with-images path now exclusively
uses the presigned-URL flow: each image is compressed, uploaded directly
to B2 via APILayer.uploadImage, and the resulting upload_ids are passed
to /api/task-completions/ as JSON. Bytes never traverse our API server.
Changes:
- TaskCompletionViewModel.createTaskCompletionWithImages now does the
presign→POST→collect-ids dance internally. The signature stays the
same so the three Android UI call sites (TasksScreen, AllTasksScreen,
ResidenceDetailScreen, CompleteTaskDialog, CompleteTaskScreen) need
no changes.
- APILayer.createTaskCompletionWithImages removed (dead).
- TaskCompletionApi.createCompletionWithImages removed (the multipart
HTTP helper that posted to the legacy POST /api/task-completions/
multipart endpoint).
- TaskCompletionCreateRequest.imageUrls field removed.
- Three Swift call sites (CompleteTaskView, WidgetActionProcessor,
PushNotificationManager) updated to drop the imageUrls argument.
- Two Kotlin call sites (CompleteTaskDialog, CompleteTaskScreen) updated.
Image uploads now match WhatsApp/Slack-class architecture: client-side
compression + direct-to-storage upload + lightweight JSON entity create.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS (Swift) — primary path, since iOS is the live platform:
- ImageDownsampler.swift: ImageIO/CGImageSourceCreateThumbnailAtIndex
based resize. Pays only the cost of the resized bitmap rather than
decoding the full source — a 12 MP iPhone photo previously
materialized ~50 MB regardless of JPEG size. Profiles: completion
(2048 px / quality 0.85), document_image (2560 px / 0.90).
- PresignedUploader.swift: three-step orchestration (POST /uploads/presign
→ multipart POST direct to B2 with the signed policy fields → return
upload_id). Maps HTTP errors to user-facing copy. Concurrent uploads
via TaskGroup.
- CompleteTaskView.swift: replaces the multipart-with-images path with
downsample → upload-to-B2 → create-completion-with-upload_ids[]. The
no-image branch unchanged.
Android (Kotlin) — parity:
- composeApp/.../media/ImageDownsampler.kt: BitmapFactory inSampleSize
+ proportional scale + JPEG compress. Same profiles as iOS.
- composeApp/.../network/UploadApi.kt: Ktor-based presign + direct-to-B2
POST. Preserves form-field order so the S3 policy signature validates.
- APILayer.uploadImage(category, contentType, bytes, fileName) → upload_id.
UI integration to follow.
Shared (Kotlin):
- models/TaskCompletion.kt: added uploadIds: List<Int>? to
TaskCompletionCreateRequest and a new PresignUploadRequest /
PresignUploadResponse pair matching the Go API DTOs.
- Existing call sites (WidgetActionProcessor, PushNotificationManager)
explicitly pass uploadIds: nil for backwards compatibility — Swift's
bridge to Kotlin doesn't honor Kotlin defaults for required-positional
parameters.
The legacy multipart path remains functional alongside the new one for
soak-test purposes; per-platform feature flags can flip between them at
any time. After zero multipart traffic in production for 7 consecutive
days, the legacy paths can be dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth: `com.tt.honeyDue.testing.GalleryScreens` lists
every user-reachable screen with its category (DataCarrying / DataFree)
and per-platform reachability. Both platforms' test harnesses are
CI-gated against it — `GalleryManifestParityTest` on each side fails
if the surface list drifts from the manifest.
Variant matrix by category: DataCarrying captures 4 PNGs
(empty/populated × light/dark), DataFree captures 2 (light/dark only).
Empty variants for DataCarrying use `FixtureDataManager.empty(seedLookups = false)`
so form screens that only read DM lookups can diff against populated.
Detail-screen rendering fixed on both platforms. Root cause: VM
`stateIn(Eagerly, initialValue = …)` closures evaluated
`_selectedX.value` before screen-side `LaunchedEffect` / `.onAppear`
could set the id, leaving populated captures byte-identical to empty.
Kotlin: `ContractorViewModel` + `DocumentViewModel` accept
`initialSelectedX: Int? = null` so the id is set in the primary
constructor before `stateIn` computes its seed.
Swift: `ContractorViewModel`, `DocumentViewModelWrapper`,
`ResidenceViewModel`, `OnboardingTasksViewModel` gained pre-seed
init params. `ContractorDetailView`, `DocumentDetailView`,
`ResidenceDetailView`, `OnboardingFirstTaskContent` gained
test/preview init overloads that accept the pre-seeded VM.
Corresponding view bodies prefer cached success state over
loading/error — avoids a spinner flashing over already-visible
content during background refreshes (production benefit too).
Real production bug fixed along the way: `DataManager.clear()` was
missing `_contractorDetail`, `_documentDetail`, `_contractorsByResidence`,
`_taskCompletions`, `_notificationPreferences`. On logout these maps
leaked across user sessions; in the gallery they leaked the previous
surface's populated state into the next surface's empty capture.
`ImagePicker.android.kt` guards `rememberCameraPicker` with
`LocalInspectionMode` — `FileProvider.getUriForFile` can't resolve the
Robolectric test-cache path, so `add_document` / `edit_document`
previously failed the entire capture.
Honest reclassifications: `complete_task`, `manage_users`, and
`task_suggestions` moved to DataFree. Their first-paint visible state
is driven by static props or APILayer calls, not by anything on
`IDataManager` — populated would be byte-identical to empty without
a significant production rewire. The manifest comments call this out.
Manifest counts after all moves: 43 screens = 12 DataCarrying + 31
DataFree, 37 on both platforms + 3 Android-only (home, documents,
biometric_lock) + 3 iOS-only (documents_warranties, add_task,
profile_edit).
Test results after full record:
Android: 11/11 DataCarrying diff populated vs empty
iOS: 12/12 DataCarrying diff populated vs empty
Also in this change:
- `scripts/build_parity_gallery.py` parses the Kotlin manifest
directly, renders rows in product-flow order, shows explicit
`[missing — <platform>]` placeholders for expected-but-absent
captures and muted `not on <platform>` placeholders for
platform-specific screens. Docs regenerated.
- `scripts/cleanup_orphan_goldens.sh` safely removes PNGs from prior
test configurations (theme-named, compare artifacts, legacy
empty/populated pairs for what is now DataFree). Dry-run by default.
- `docs/parity-gallery.md` rewritten: canonical-manifest workflow,
adding-a-screen guide, variant matrix explained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
testTagsAsResourceId is Android-only; its use in commonMain broke
compileKotlinIosSimulatorArm64. Wrap behind expect fun — Android impl
sets the semantic, other platforms return Modifier unchanged. Blocks
P3 iOS parity gallery otherwise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Material 3 AutoMirrored variants flip correctly in Arabic/Hebrew.
Previous Icons.Default.ArrowBack pointed wrong direction in RTL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds acceptResidenceInvite / declineResidenceInvite to ResidenceApi
(POST /api/residences/{id}/invite/{accept|decline}) and exposes them via
APILayer. On accept success, myResidences is force-refreshed so the
newly-joined residence appears without a manual pull.
Wires NotificationActionReceiver's ACCEPT_INVITE / DECLINE_INVITE
handlers to the new APILayer calls, replacing the log-only TODOs left
behind by P4 Stream O. Notifications are now cleared only on API
success so a failed accept stays actionable.
Tests:
- ResidenceApiInviteTest covers correct HTTP method/path + error surfacing.
- NotificationActionReceiverTest invite cases updated to assert the new
APILayer calls (were previously asserting the log-only path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream M replaced the widget's task-complete path with CompleteTaskAction +
WidgetActionProcessor. Grepping confirmed the only references to
WidgetTaskActionReceiver were its own class file and the manifest entry.
Remove both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the legacy MyFirebaseMessagingService.onNewToken() device-registration
path onto the new iOS-parity FcmService. The legacy service is already
unwired from the manifest MESSAGING_EVENT filter; this removes its last
functional responsibility. Behaviour preserved: auth-token guard,
DeviceRegistrationRequest with ANDROID_ID + Build.MODEL, log-only on error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Navigation wiring (per follow-ups flagged by Streams G/H/I):
- Add TaskTemplatesBrowserRoute (new) + App.kt composable<TaskTemplatesBrowserRoute>
- Wire composable<TaskSuggestionsRoute> (declared by Stream H but unwired)
- Wire composable<AddTaskWithResidenceRoute> (declared by Stream I but unwired)
MainActivity.onCreate now calls HapticsInit.install(applicationContext) so the
Vibrator fallback path works on non-View call-sites (flagged by Stream S).
Deferred cleanup (tracked, not done here):
- Port push-token registration from legacy MyFirebaseMessagingService.kt to
new FcmService (Stream N TODO).
- Remove legacy WidgetTaskActionReceiver + manifest entry (Stream M flag).
- Residence invite accept/decline APILayer methods (Stream O TODO).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BiometricPrompt wrapper with 3-strike lockout + NO_HARDWARE bypass.
BiometricLockScreen with auto-prompt on mount + PIN fallback after 3 failures.
PIN wiring marked TODO for secure-storage follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port iOS TaskSuggestionsView as a standalone route reachable outside
onboarding. Uses shared suggestions API + accept/skip analytics in
non-onboarding variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream M's WidgetActionProcessorTest.kt references this predicate but
Stream J's initial repo only exposed mark/clear mutators. Trivial addition:
reads the pending-completion set and checks membership.
Unblocks :composeApp:testDebugUnitTest (now green across all streams).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Glance ActionCallback wires to WidgetActionProcessor: premium marks pending +
calls API + refreshes; free tier opens paywall deep link instead. Idempotent,
rollback-safe on API failure.
Also fixes a one-line compile error in WidgetTaskActionReceiver.kt where
updateAllWidgets() had been removed by Stream L — swapped for forceRefresh()
so the build stays green. The legacy receiver is now redundant (replaced by
CompleteTaskAction) but deletion is deferred to a Stream K follow-up so the
AndroidManifest entry can be removed in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Android: uncaught exception handler sends $exception events with stack
trace to PostHog, flushes before delegating to default handler.
iOS: NSSetUncaughtExceptionHandler captures crashes via PostHogSDK,
avoids @MainActor deadlock by calling SDK directly.
Common: captureException() available for non-fatal catches app-wide.
Platform stubs for jvm/js/wasmJs.
Biometric lock: opt-in Face ID/Touch ID/fingerprint app lock with toggle
in ProfileScreen. Locks on background, requires auth on foreground return.
Platform implementations: BiometricPrompt (Android), LAContext (iOS).
Rate limit: 429 responses parsed with Retry-After header, user-friendly
error messages in all 10 locales, retry plugin respects 429.
ErrorMessageParser updated for both iOS Swift and KMM.
Remediate all P0-S priority findings from cross-platform architecture audit:
- Harden token storage with EncryptedSharedPreferences (Android) and Keychain (iOS)
- Add SSL pinning and certificate validation to API clients
- Fix subscription cache race conditions and add thread-safe access
- Add input validation for document uploads and file type restrictions
- Refactor DocumentApi to use proper multipart upload flow
- Add rate limiting awareness and retry logic to API layer
- Harden subscription tier enforcement in SubscriptionHelper
- Add biometric prompt for sensitive actions (Login, Onboarding)
- Fix notification permission handling and device registration
- Add UI test infrastructure (page objects, fixtures, smoke tests)
- Add CI workflow for mobile builds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add customIntervalDays field to Kotlin models (TaskResponse, TaskCreateRequest, TaskUpdateRequest)
- Update Android AddTaskDialog to show interval field only for "Custom" frequency
- Update Android EditTaskScreen for custom frequency support
- Update iOS TaskFormView for custom frequency support
- Fix preview data in TaskCard and TasksSection to include new field
- Add customIntervalDays to OnboardingFirstTaskView
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add platform haptic feedback abstraction (HapticFeedback.kt) with
implementations for Android, iOS, JVM, JS, and WASM
- Enhance CompleteTaskDialog with interactive 5-star rating, image
thumbnails, and haptic feedback
- Add ImageBitmap platform abstraction for displaying selected images
- Localize TaskTemplatesBrowserSheet with string resources
- Add Android widgets infrastructure (small, medium, large sizes)
- Add Google Sign-In button components and auth flow preparation
- Update strings.xml with new localization keys for completions,
templates, and document features
- Integrate haptic feedback into ThemePickerDialog
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Send the device's IANA timezone identifier (e.g., "America/Los_Angeles")
with every API request to enable timezone-aware overdue task detection.
Platform implementations:
- Android/JVM: TimeZone.getDefault().id
- iOS: NSTimeZone.localTimeZone.name
- JS/WASM: Intl.DateTimeFormat().resolvedOptions().timeZone
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SharedResidence model and package type detection for .casera files
- Add generateSharePackage API endpoint integration
- Create ResidenceSharingManager for iOS and Android
- Add share button to residence detail screens (owner only)
- Add residence import handling with confirmation dialogs
- Update Quick Look extensions to show house icon for residence packages
- Route .casera imports by type (contractor vs residence)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- DataManager now persists lookup data (residence types, task categories,
priorities, statuses, specialties, templates) to disk
- Loads cached lookups on app startup for faster launch
- iOS: Refresh lookups when app becomes active, refresh widget on background
- Android: Initialize DataManager in onCreate, already had onResume refresh
- Only send ETag if lookup data is actually in memory to avoid 304 with no data
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Contractor Sharing:
- Add .casera file format for sharing contractors between users
- Create SharedContractor model with JSON serialization
- Implement ContractorSharingManager for iOS (Swift) and Android (Kotlin)
- Register .casera file type in iOS Info.plist and Android manifest
- Add share button to ContractorDetailView (iOS) and ContractorDetailScreen (Android)
- Add import confirmation, success, and error dialogs
- Create expect/actual platform implementations for sharing and import handling
Navigation Changes:
- Remove Profile tab from bottom tab bar (iOS and Android)
- Add settings gear icon to left side of "My Properties" title
- Settings gear opens Profile/Settings screen as sheet (iOS) or navigates (Android)
- Add property button to top right action bar
Bug Fixes:
- Fix ResidenceUsersResponse to match API's flat array response format
- Fix GenerateShareCodeResponse handling to access nested shareCode property
- Update ManageUsersDialog to accept residenceOwnerId parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
iOS:
- Add notification categories with action buttons (complete, view, cancel, etc.)
- Handle notification actions in AppDelegate with API calls
- Add navigation to specific task from notification tap
- Register UNNotificationCategory for each task state
Android:
- Add NotificationActionReceiver BroadcastReceiver for handling actions
- Update MyFirebaseMessagingService to show action buttons
- Add deep link handling in MainActivity for task navigation
- Register receiver in AndroidManifest.xml
Shared:
- Add navigateToTaskId parameter to App for cross-platform navigation
- Add notification observers in MainTabView/AllTasksView for refresh
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Create DataManager.kt with StateFlows for all cached data:
- Authentication (token, user)
- Residences, tasks, documents, contractors
- Subscription status and upgrade triggers
- All lookup data (residence types, task categories, etc.)
- Theme preferences and state metadata
- Add PersistenceManager with platform-specific implementations:
- Android: SharedPreferences
- iOS: NSUserDefaults
- JVM: Properties file
- WasmJS: localStorage
- Migrate APILayer to update DataManager on every API response
- Update Kotlin ViewModels to use DataManager for token access
- Deprecate LookupsRepository (delegates to DataManager)
- Create iOS DataManagerObservable Swift wrapper for SwiftUI
- Update iOS auth flow to use DataManager.isAuthenticated()
Data flow: User Action → API Call → DataManager Updated → All Screens React
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
KMM (Android/Shared):
- Add strings.xml with 200+ localized strings
- Add translation files for es, fr, de, pt languages
- Update all screens to use stringResource() for i18n
- Add Accept-Language header to API client for all platforms
iOS:
- Add L10n.swift helper with type-safe string accessors
- Add Localizable.xcstrings with translations for all 5 languages
- Update all SwiftUI views to use L10n.* for localized strings
- Localize Auth, Residence, Task, Contractor, Document, and Profile views
Supported languages: English, Spanish, French, German, Portuguese
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add NotificationPreferencesScreen (Android) and NotificationPreferencesView (iOS)
- Add NotificationPreferencesViewModel for shared business logic
- Wire up notification preferences from ProfileScreen on both platforms
- Add subscription verification on app launch for iOS (StoreKit) and Android (Google Play Billing)
- Update SubscriptionApi to match Go backend endpoints (/subscription/purchase/)
- Update StoreKit Configuration with correct product IDs and pricing ($2.99/month, $27.99/year)
- Update Android placeholder prices to match App Store pricing
- Fix NotificationPreference model to match Go backend schema
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Rename Kotlin package from com.example.mycrib to com.example.casera
- Update Android app name, namespace, and application ID
- Update iOS bundle identifiers and project settings
- Rename iOS directories (MyCribTests -> CaseraTests, etc.)
- Update deep link schemes from mycrib:// to casera://
- Update app group identifiers
- Update subscription product IDs
- Update all UI strings and branding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implemented Android subscription UI components:
- UpgradeFeatureScreen: Full-screen view for restricted features (contractors, documents)
* Shows feature icon, name, and description
* Displays "This feature is available with Pro" badge
* Opens UpgradePromptDialog on button click
- UpgradePromptDialog: Modal dialog with upgrade options
* Trigger-specific title and message from backend
* Feature preview list with Material icons
* "Upgrade to Pro" button with loading state
* "Compare Free vs Pro" button
* "Maybe Later" cancel option
- FeatureComparisonDialog: Full-screen comparison table
* Free vs Pro feature comparison
* Displays data from SubscriptionCache.featureBenefits
* Default features if no data loaded
* Upgrade button
- BillingManager: Google Play Billing Library placeholder
* Singleton manager for in-app purchases
* Product query placeholder
* Purchase flow placeholder
* Backend receipt verification placeholder
* Restore purchases placeholder
All components follow Material3 design system with theme-aware colors and spacing constants.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit adds persistent theme storage and comprehensive documentation for Android development.
Theme Persistence:
- Created ThemeStorage with platform-specific implementations (SharedPreferences/UserDefaults)
- Updated ThemeManager.initialize() to load saved theme on app start
- Integrated ThemeStorage initialization in MainActivity and MainViewController
- Theme selection now persists across app restarts
Documentation (CLAUDE.md):
- Added comprehensive Android Design System section
- Documented all 11 themes and theme management
- Provided color system guidelines (use MaterialTheme.colorScheme)
- Documented spacing system (AppSpacing/AppRadius constants)
- Added standard component usage examples (StandardCard, FormTextField, etc.)
- Included screen patterns (Scaffold, pull-to-refresh, lists)
- Provided button and dialog patterns
- Listed key design principles for Android development
Build Status:
- ✅ Android builds successfully
- ✅ iOS builds successfully
- ✅ Theme persistence works on both platforms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit adds a comprehensive theming system to Android matching iOS, and fixes package declarations throughout the codebase to match directory structure.
Theme System Additions:
- Added 11 themes matching iOS: Default, Teal, Ocean, Forest, Sunset, Monochrome, Lavender, Crimson, Midnight, Desert, Mint
- Created ThemeColors.kt with exact iOS color values for light/dark modes
- Added ThemeManager.kt for dynamic theme switching
- Created Spacing.kt with standardized spacing constants (xs/sm/md/lg/xl)
- Added ThemePickerDialog.kt for theme selection UI
- Integrated theme switching in ProfileScreen.kt
- Updated App.kt to observe ThemeManager for reactive theming
Component Library:
- Added StandardCard.kt and CompactCard.kt for consistent card styling
- Added FormTextField.kt with error/helper text support
- Added FormSection.kt for grouping related form fields
- Added StandardEmptyState.kt for empty state UI
Package Migration:
- Fixed all package declarations to match directory structure (com.example.mycrib.*)
- Updated package declarations in commonMain, androidMain, and iosMain
- Fixed all import statements across entire codebase
- Ensures compilation on both Android and iOS platforms
iOS Theme Rename:
- Renamed "Default" theme to "Teal" in iOS
- Renamed "Bright" theme to "Default" in iOS to make vibrant colors the default
Build Status:
- ✅ Android builds successfully
- ✅ iOS builds successfully
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated both iOS and Android to build residence task summary UI entirely
from API response data, with no hardcoded categories, icons, colors, or labels.
**Changes:**
**Backend Integration:**
- Updated TaskSummary model to use dynamic categories list instead of static fields
- Added TaskColumnCategory and TaskColumnIcon models for metadata
- Categories now include: name, displayName, icons (ios/android/web), color, count
**Android (ResidencesScreen.kt):**
- Removed hardcoded category extraction (overdue_tasks, current_tasks, in_progress_tasks)
- Now dynamically iterates over first 3 categories from API
- Added getIconForCategory() helper to map icon names to Material Icons
- Added parseHexColor() helper that works in commonMain (no Android-specific code)
- Uses category.displayName, category.icons.android, category.color from API
**iOS (ResidenceCard.swift):**
- Removed hardcoded category extraction and SF Symbol names
- Now dynamically iterates over first 3 categories using ForEach
- Uses category.displayName, category.icons.ios, category.color from API
- Leverages existing Color(hex:) extension for color parsing
**Component Organization:**
- Moved TaskSummaryCard.kt from commonMain to androidMain (uses Android-specific APIs)
- Created TaskSummaryCard.swift for iOS with dynamic category rendering
**Benefits:**
✅ Backend controls all category metadata (icons, colors, display names)
✅ Apps automatically reflect backend changes without redeployment
✅ No platform-specific hardcoded values
✅ Single source of truth in task/constants.py TASK_COLUMNS
**Files Changed:**
- Residence.kt: Added TaskColumnCategory, TaskColumnIcon models
- ResidencesScreen.kt: Dynamic category rendering with helpers
- ResidenceCard.swift: Dynamic category rendering with ForEach
- TaskSummaryCard.kt: Moved to androidMain
- TaskSummaryCard.swift: New iOS dynamic component
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Integrated Firebase Cloud Messaging (FCM) for Android
- Integrated Apple Push Notification Service (APNs) for iOS
- Created shared notification models and API client
- Added device registration and token management
- Added notification permission handling for Android
- Created PushNotificationManager for iOS with AppDelegate
- Added placeholder google-services.json (needs to be replaced with actual config)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Swift ImageCompression helper to enforce 200KB limit on iOS
- Update iOS views to use compression for all image uploads
- Fix iOS EditDocumentView to show success only after all uploads complete
- Add AsyncImage thumbnails to Android EditDocumentScreen
- Fix Android success message visibility with delay before state reset
- Add proper error handling for failed image uploads
- Add resetUpdateState on error for consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>