Compare commits

...

433 Commits

Author SHA1 Message Date
774559f854 fix 2026-05-13 18:35:07 -05:00
82f05d8375 added scheduledActivities capability to deviceCapabilities model 2026-05-13 18:33:28 -05:00
592bea9ead fix 2026-05-13 05:32:01 -05:00
74fbf0084e fix(ui): align deposit button with text field and shrink primary button row 2026-05-13 05:22:13 -05:00
ccef0fd186 fix 2026-05-12 05:46:01 -05:00
6ff2d77a32 added filters to getPositionHistory and getLastPosition in location feature 2026-05-12 04:05:12 -05:00
ecfaaec161 payments child wallet available balance changed to solde 2026-05-12 03:35:39 -05:00
4f6e3684bf release: 1.0.0+10 — chat: state in Riverpod + dispose fix
Chat refactor — no setState
- ChatConversationState: new isReconciling flag in the view state
- reconcileFromRemote sets isReconciling true/false with idempotent guard and try/catch error path; isReconciling is now the single source of truth for the AppBar refresh button and the inverted pull-to-refresh spinner
- ChatConversationScreen drops the local _isRefreshing field; AppBar e inverted pull-to-refresh read state.isReconciling and dispatch the controller directly
- ChatImageBubble migrated to a pure ConsumerWidget; local file existence check and remote file resolution are now FutureProvider.family.autoDispose providers (_localFileExistsProvider, _remoteFileProvider)

Chat dispose fix
- ChatContextNotifier exposes releaseList() and releaseConversation(chatId) so callers no longer need to read .state (which is @protected)
- ChatConversationScreen and ChatListScreen schedule the release inside Future.microtask from dispose(); Riverpod 3.x prohibits mutating providers during widget lifecycle, the microtask defers the mutation past tree finalization and resolves the "Tried to modify a provider while the widget tree was building" assertion
2026-05-11 03:55:14 -05:00
abdbc2bf2e release: 1.0.0+6 — chat notifications + videocall refactor
Chat notifications (production-ready)
- ChatDeeplinkService: resolves client chat ID from incoming push/WS payloads using the (senderId, chatId) matrix and switches selectedDeviceProvider when the payload references a different watch (multi-device deeplink)
- IncomingChatResolver in domain layer with full unit coverage of the 4-case matrix
- ChatContext provider (sealed: outsideChat / list / conversation) wired into ChatListScreen and ChatConversationScreen via initState/dispose to enable WhatsApp-style suppression
- notifications_init refactor: foreground CHAT_MESSAGE notifications are suppressed on chat list and matching conversation; tap navigation goes through ChatDeeplinkService
- ChatSyncService.subscribeToReconnect: reconciles from REST when the WebSocket comes back from a disconnect (recovers messages missed in background)
- 5s message-id dedup window in the WS listener (mitigates server-side duplicate chat-message-received events)
- Reconcile from remote on conversation mount (covers cached controller from background)
- AppBar refresh button + inverted pull-to-refresh in the conversation (overscroll either edge of the reverse list)

WS event parser fix
- chat-message-received normalises to chat_message_received; parser now accepts both that and chat_message so the conversation reactively refreshes when a watch sends a message

Chat application layer
- Split the conversation controller into services: chat_send_service, chat_sync_service, chat_participants_service, chat_permission_flow_service, chat_media_cleanup_service
- chat_conversation_config centralises page size, polling interval, dedup window
- chat_bubble_shell extracted; input bar split into smaller widgets
- emoji picker sheet + emoji blocking input formatter + watch_emoji_catalog
- Multipart upload header race fixed via synchronized.Lock around the shared Dio instance

Videocall (carryover from earlier work in this branch)
- Application services: incoming, outgoing, session
- Domain entities: VideocallIncomingArgs, VideocallRoom, VideocallUserId, parseDeviceIdFromRoom helper
- Views split: idle, incoming, active call, group call
- Widgets: picture_in_picture_video, remote_or_fallback_video, video_call_header
- videocall_config centralises timeouts, ringing duration, battery threshold
- Incoming via push (channel mode) with full-screen notification + ringtone
- Hangup-on-remote-left moved to controller; redirect on participant update documented
- treezor_token_interceptor: distinguish session expiry from operation-denied 401s

Localization & misc
- New keys for chat conversation, refresh, errors across en/es/de/fr/it/pt
- Location map: dispose ref-after-unmount fix; route history layer cleanup
- Legacy device view model: position update event handling
- AndroidManifest: notification channel + permissions for incoming-call full-screen intent
2026-05-08 07:20:40 -05:00
54b81818ec feat(chat): add 1:1 and family group chat module for legacy dashboard
- Module structure mirrors location pattern (core/data, core/domain, core/providers, features)
- Supports text, emoji, image and audio messages over POST /chat-messages multipart
- Optimistic UI with status reconciliation via 4s polling and circuit breaker after 3 errors
- Offline queue persisted in SharedPreferences, drained on conversation re-open
- WhatsApp-style audio recorder with long-press, slide-to-cancel, haptics and animated overlay
- Image picker (camera/gallery) with on-device 1024px JPEG compression
- Single-audio playback coordinator across bubbles
- Family group fan-out: N parallel POSTs sharing chatId, members derived from delegationId
- Reuses LegacyOptionCard extracted from videocall idle screen
- Tracking events legacy_chat_opened, message_sent, image_sent, audio_sent, permission_denied (no PII)
- WebSocket ChatMessageEvent parser added for future backend support
- Push command CHAT_MESSAGE handled in notifications_init for deep-linking
- 15 unit tests covering id resolver, file url builder and repository

Pending backend coordination: GET /chat-messages 500 (parseQueryParams), push routing heuristic, file size/mime limits.
2026-05-05 23:32:54 -05:00
62b38acab4 chore: pending staged changes 2026-05-05 10:31:43 -05:00
924ecd83ee france localizations corrections 2026-04-30 14:55:15 +02:00
4510d2bb28 fix(location): add orderBy to positions query to show latest position 2026-04-27 18:09:30 +02:00
f54bf24417 feat(videocall): handle device response via WebSocket and show ringing state 2026-04-27 14:12:39 +02:00
b5d12f31a1 chore(ui): replace loading gif with birds animation and add loading text 2026-04-27 01:14:59 +02:00
74f470219a feat(friends): add friends list screen with delete support 2026-04-27 01:14:48 +02:00
24ddbf34e4 fix(videocall): fix SDK re-entry lifecycle and add logout on session clear
- Skip login when SDK client is already logged in (early return with isSdkReady)
- Set isSdkReady immediately when login() returns true instead of waiting for async callback
- Add ref.mounted check after userInfo future to prevent state updates on disposed provider
- Logout VideocallClient on session clear to prevent stale SDK sessions
- Use getActiveCallItem() in onCallItemUpdate for fresh call state (matches JC demo app)
2026-04-27 00:22:48 +02:00
69297f826e fix: regenerate freezed files with system and imei fields 2026-04-26 23:28:46 +02:00
390f2501b4 merge: sync feature/videocall-sdk-integration with fusion-app 2026-04-26 22:03:00 +02:00
555a668481 feat(videocall): complete signaling integration, group call support, and UI polish
- Wire VIDEO_CALL_REQUEST/CANCEL/REFUSE/ROOM_COUNT commands via CommandsRepository
- Add VideocallChatType enum (single/multi) with chatType stored in state
- Implement auto-login to Juphoon SDK using sanitized email + user UUID
- Add runtime camera/microphone permissions before call start
- Add RetryInterceptor for transient TLS/socket errors in Dio
- Migrate VideocallItem to Freezed with isTalking extension
- Implement startGroupCall/leaveGroupCall using ChannelService with participant grid
- Add PopScope to intercept back navigation during active calls
- Redesign idle screen with device option cards and group call button
- Redesign active call UI with video overlay, PiP local view, and new controls layout
- Clean up SDK wrapper: remove unused streams, merge destroy+dispose into shutdown
- Add i18n keys for videocall UI across 6 locales
2026-04-26 21:53:32 +02:00
5aa0c0acc7 fix(videocall): apply PR review fixes and add i18n translations
- Replace setState with FutureBuilder in VideoViewWidget
- Remove comments from datasource impl
- Replace GetIt.I with ref.read(provider) in controllers
- Fix state comparison using isTalking getter
- Add hangUp/stopAudio/stopCamera cleanup on dispose
- Remove duplicate SafeArea from IncomingCallOverlay
- Add missedCall error event to enum
- Replace Colors.white70 with theme colorScheme
- Move VIDEOCALL_INTEGRATION.md to apps/mobile_app/docs/
- Gitignore config JSON files with app keys
- Add 28 i18n keys in 6 locales for all videocall strings
- Map error events to specific i18n messages
2026-04-26 21:53:32 +02:00
57f0f64d08 refactor(videocall): migrate to @riverpod controller pattern and add device config
- Rename ViewModel → Controller, ViewState → State with @riverpod codegen
- Move state/ → providers/ folder for consistency
- Add post-init device configuration (camera, speaker, maxCallNum, mediaConfig)
- Use MediaConfig.generateByMode based on device capabilities.system (rtos/android)
- Add CallParam.ticket for smartwatch wake-up protocol
- Start audio/camera before call, stop on hangup
- Granular video management based on uploadVideoStreamSelf/Other flags
- Build watch userId from device.imei (w_ prefix)
- Add imei field to DeviceEntity
- Add system field to DeviceCapabilitiesEntity with isRtos/isAndroid helpers
2026-04-26 21:53:32 +02:00
9f23ecb42e feat(videocall): integrate videocall feature with architecture refactor
Merges videocall stash, resolves conflicts with installed_apps routes,
and refactors UI to match current legacy patterns:
- Replace themePortProvider with context.sfColors and Theme.of(context)
- Replace showTopSnackbar with feedback dialogs
- Replace hardcoded colors with theme-aware colorScheme
- Wrap test login button in kDebugMode
- Rename error enum values to be more descriptive
2026-04-26 21:53:32 +02:00
8ff94a1e92 feat(legacy): add connection status indicator to device banners 2026-04-26 21:53:05 +02:00
a87c7e8732 refactor(heartbeat): refresh devices every 2min instead of pinging /auth/me 2026-04-26 21:53:05 +02:00
4be46f71c7 fix(control-panel): use iso_sf.png in map preview banner 2026-04-26 21:53:05 +02:00
105211e334 fix(control-panel): prevent device name from overlapping header logo 2026-04-26 21:53:05 +02:00
6d2d16d8cb fix 2026-04-26 21:53:05 +02:00
424f2af0cc fix(tests): update feedback_dialogs test for queue implementation 2026-04-26 21:53:05 +02:00
41f1797dcf test(device-entity): add queueCommands and isDisconnected tests (11 cases) 2026-04-26 21:53:05 +02:00
cf6a9dd6df test(do-not-disturb): add enabled field and toggle tests (4 new cases) 2026-04-26 21:53:04 +02:00
0d677afd32 test(app-usage-schedules): add controller and editor unit tests (6 cases) 2026-04-26 21:53:04 +02:00
323d28bb17 test(installed-apps): add InstalledAppsController unit tests (3 cases) 2026-04-26 21:53:04 +02:00
92f6035b84 chore(i18n): add GPS functions, history and delete confirmation keys 2026-04-26 21:53:04 +02:00
c5c85c4fda fix(settings): capitalize language names in selector 2026-04-26 21:53:04 +02:00
4cfee8ced3 fix(legacy): replace hardcoded colors for dark mode contrast 2026-04-26 21:53:04 +02:00
376e6bc13a fix(legacy): add dark mode support for pickers and dialogs 2026-04-26 21:53:04 +02:00
4422f93903 fix(feedback-dialogs): add queue system to prevent dialog stacking 2026-04-26 21:53:03 +02:00
a9a34fbbc5 test(location): add LocationMapController unit tests (22 cases) 2026-04-26 21:53:03 +02:00
04b8d1609c feat(location): add history playback, GPS functions dialog and UX improvements 2026-04-26 21:53:03 +02:00
e901b22981 feat(location): redesign device banner to match reference UI 2026-04-26 21:53:03 +02:00
2d87cd5aee fix(location): add refresh success feedback and fix queueCommands null crash 2026-04-26 21:53:03 +02:00
bef87262b1 chore(i18n): improve positionUpdated message text 2026-04-26 21:53:03 +02:00
3f28aa95bc chore(i18n): add positionUpdated key in 6 locales 2026-04-26 21:53:03 +02:00
dd8faa9f35 feat(device): add queueCommands to DeviceEntity and command guard 2026-04-26 21:53:03 +02:00
bc946cfc39 refactor(legacy): replace showTopSnackbar with feedback dialogs 2026-04-26 21:53:03 +02:00
c4bdf707e1 chore: regenerate health controller codegen 2026-04-26 21:53:03 +02:00
17d931ffda chore(i18n): add localization keys for new features and fixes 2026-04-26 21:53:03 +02:00
a21a8af9b1 fix(tracking): shorten event names to fit Firebase 40-char limit 2026-04-26 21:53:03 +02:00
05c96bc10e fix(call-history): sort calls by most recent first 2026-04-26 21:53:03 +02:00
bbdaa25e12 feat(navigation): add routes for installed apps and app usage schedules 2026-04-26 21:53:03 +02:00
440adcdf8d feat(device-management): add app usage schedules feature 2026-04-26 21:53:02 +02:00
6193c97802 feat(device-management): add installed apps enable/disable feature 2026-04-26 21:53:02 +02:00
a985ced85c feat(do-not-disturb): add enabled toggle, redesign UI and duplicate validation 2026-04-26 21:53:02 +02:00
51901cc639 fix(contacts): validate duplicate phone numbers before adding 2026-04-26 21:53:02 +02:00
c7fefe2a8b feat(remote-camera): add delete, share, download and redesign photo viewer 2026-04-26 21:53:02 +02:00
cb897a3243 feat(location): add manual frequency option and optimize position loading 2026-04-26 21:53:02 +02:00
3325df94f7 refactor(legacy-auth): simplify device setup to single carrier name field 2026-04-26 21:53:02 +02:00
0aefed0163 feat(legacy-auth): improve 2FA sheet UX and signup layout 2026-04-26 21:53:02 +02:00
516197b819 fix(splash): distinguish network errors from auth errors in session check 2026-04-26 21:53:02 +02:00
d9465996d7 feat(legacy-ui): add animated transitions for dialogs and navigation 2026-04-26 21:53:02 +02:00
309c7b1d87 fix payments 2026-04-26 21:53:01 +02:00
ab1a0a88f9 removede shadow and blurs 2026-04-26 21:53:01 +02:00
5be9136e06 feat(legacy): replace CircularProgressIndicator with animated GIF loading
Add LegacyLoadingIndicator widget using a transparent GIF animation
for all full-page loading states across legacy modules. Also fix
HealthController crash by deferring provider mutation during build.
2026-04-26 21:53:01 +02:00
ac493725cf test(location): add LocationController unit tests (15 cases, 25% coverage) 2026-04-26 21:53:01 +02:00
99da6e12fe todo 2026-04-26 21:53:01 +02:00
383ea3d053 refactor(settings): rename alerts_* data layer to notifications_* 2026-04-26 21:53:01 +02:00
929ccadb0f refactor(legacy): move notifications feature from control_panel to settings 2026-04-26 21:53:01 +02:00
144e48f370 chore(legacy_dashboard_shell): remove dead control_panel dependency 2026-04-26 21:53:01 +02:00
53edf0b7e1 refactor(legacy): remove remaining setState usages + tap-to-center on device banner 2026-04-26 21:53:01 +02:00
773312d5f9 refactor(location): migrate module to Riverpod codegen + replace snackbars with dialogs 2026-04-26 21:53:01 +02:00
003604444c warning icon 2026-04-26 21:53:01 +02:00
c7ed2f16c1 feat(ui): use pill style for offline and technical error dialogs 2026-04-26 21:53:00 +02:00
92c922a130 feat(ui): redesign feedback dialogs with pill style for legacy mode 2026-04-26 21:53:00 +02:00
f0666d9848 refactor(legacy_auth): migrate onboarding to Riverpod codegen 2026-04-26 21:53:00 +02:00
6e6225d6b6 refactor(legacy_auth): migrate device_setup to Riverpod + polish QR scanner UX 2026-04-26 21:53:00 +02:00
c23ae39b87 refactor(legacy_auth): migrate sign_up to Riverpod 2026-04-26 21:53:00 +02:00
cff71245ae refactor(legacy_auth): migrate login to Riverpod 2026-04-26 21:53:00 +02:00
3065b78779 refactor(legacy_auth): migrate recover_password to Riverpod 2026-04-26 21:53:00 +02:00
c1e498b1ab refactor(legacy_auth): migrate link_phone to Riverpod 2026-04-26 21:53:00 +02:00
81c3eaec70 refactor(device_management): migrate health to Riverpod + improve charts 2026-04-26 21:53:00 +02:00
0f2d9ba601 refactor(device_management): migrate apps_use to Riverpod 2026-04-26 21:53:00 +02:00
82571e6035 refactor(device_management): migrate activity_meter to Riverpod 2026-04-26 21:53:00 +02:00
ba76348936 refactor(device_management): migrate scheduled_activities to Riverpod 2026-04-26 21:53:00 +02:00
a181ae4724 refactor(device_management): migrate remote_connection to Riverpod 2026-04-26 21:52:59 +02:00
4d2cd62267 refactor(device_management): migrate contacts to Riverpod 2026-04-26 21:52:59 +02:00
fb281caf99 refactor(device_management): migrate background_image to Riverpod 2026-04-26 21:52:59 +02:00
a2ef28a1b5 refactor(device_management): migrate do_not_disturb to Riverpod + add delete button 2026-04-26 21:52:59 +02:00
cbaee6d597 refactor(device_management): migrate volume_control to Riverpod 2026-04-26 21:52:59 +02:00
e37adc1f78 refactor(device_management): migrate call_history to Riverpod 2026-04-26 21:52:59 +02:00
3485e430f7 refactor(device_management): migrate locate_device to Riverpod + fix rewards padding 2026-04-26 21:52:59 +02:00
731787b002 refactor(device_management): migrate rewards to Riverpod 2026-04-26 21:52:59 +02:00
c9629c32e3 refactor(device_management): migrate call_watch to Riverpod 2026-04-26 21:52:59 +02:00
5c6eb97c1f icons 2026-04-26 21:52:59 +02:00
1961be3805 feat(notifications): category list with tap-to-filter navigation 2026-04-26 21:52:59 +02:00
6ff11b8c1e fix(router): rename duplicate notifications route name 2026-04-26 21:52:59 +02:00
460fbffcad fix(websocket): parse wifi-current event type for wifi settings 2026-04-26 21:52:58 +02:00
b93fac4614 fix(control-panel): use legacyPrimary color in device dropdown for dark mode 2026-04-26 21:52:58 +02:00
9622cc2d64 feat(control-panel): custom animated device dropdown 2026-04-26 21:52:58 +02:00
79e8c0fe74 refactor(settings): migrate wifi_settings God VM to Riverpod 2026-04-26 21:52:58 +02:00
8d5a2c8e56 refactor(settings): migrate disable_functions to Riverpod 2026-04-26 21:52:58 +02:00
ad0b8d209a refactor(settings): migrate block_phone to Riverpod CRUD pattern 2026-04-26 21:52:58 +02:00
653ea9ab56 refactor(legacy-settings): migrate sos_contacts CRUD to AsyncNotifier 2026-04-26 21:52:58 +02:00
8c269e8c47 fix(legacy-settings): keep alarm list after save for consistency 2026-04-26 21:52:58 +02:00
a197d5bc28 refactor(legacy-settings): migrate alarm CRUD to AsyncNotifier 2026-04-26 21:52:58 +02:00
3f9c298b6f fix(legacy-settings): go back after timezone save for consistency 2026-04-26 21:52:58 +02:00
3b1534d3b3 refactor(legacy-settings): migrate alerts to AsyncNotifier 2026-04-26 21:52:57 +02:00
eb2bde8d40 refactor(legacy-settings): migrate remote_management to AsyncNotifier 2026-04-26 21:52:57 +02:00
c3dcc6febc feat(legacy-settings): DST-aware timezone with phone auto-detect 2026-04-26 21:52:57 +02:00
c4d328d92c chore(legacy-theme): remove reserved timeframe dead code 2026-04-26 21:52:57 +02:00
2eee3489cd refactor(legacy-settings): migrate battery to AsyncNotifier 2026-04-26 21:52:57 +02:00
3b57d0e70d refactor(legacy-settings): migrate timezone, sound, sync_clock to AsyncNotifier 2026-04-26 21:52:57 +02:00
fe9476d417 refactor(legacy-customer-service): move contact form state to provider (no setState) 2026-04-26 21:52:56 +02:00
79d0f72f08 refactor(legacy-account): move delete_account dialog state to provider (no setState) 2026-04-26 21:52:56 +02:00
5925a97b01 refactor(legacy-account): move personal_data form state to provider (no setState) 2026-04-26 21:52:56 +02:00
41b22ad457 refactor(legacy-account): move change_password local error to provider (no setState) 2026-04-26 21:52:56 +02:00
66a08c8016 refactor(legacy-settings): move language selection state to provider (no setState) 2026-04-26 21:52:56 +02:00
3449ff9afd refactor(legacy-settings): migrate language to AsyncNotifier 2026-04-26 21:52:56 +02:00
d4fbbb8d4b refactor(legacy-customer-service): migrate contact form to AsyncNotifier 2026-04-26 21:52:56 +02:00
3147566241 refactor(sf_shared): add help center URL to BrandLinks 2026-04-26 21:52:55 +02:00
f7e69b1184 refactor(sf_shared): move brand links to Firebase Remote Config 2026-04-26 21:52:55 +02:00
41324c61bd refactor(legacy-account): use .select for AsyncValue field reads 2026-04-26 21:52:55 +02:00
b8bf71fbe3 refactor(legacy-account): migrate account_settings to AsyncNotifier 2026-04-26 21:52:55 +02:00
6d49e604be refactor(legacy-account): migrate delete_account to AsyncNotifier + fix A1 2026-04-26 21:52:55 +02:00
21dcafec26 refactor(legacy-account): migrate linked_devices to AsyncNotifier 2026-04-26 21:52:55 +02:00
8e8243345a fix(sf_shared): distinguish 401 vs 403 error UX 2026-04-26 21:52:55 +02:00
fe38e477e3 refactor(legacy-account): migrate personal_data to AsyncNotifier 2026-04-26 21:52:54 +02:00
db47543252 fix(legacy-account): show success dialog after password change 2026-04-26 21:52:54 +02:00
caf77b1fd9 refactor(legacy-account): migrate change_password to AsyncNotifier 2026-04-26 21:52:54 +02:00
dcc786d376 refactor(legacy-account): migrate app_users to FutureProvider 2026-04-26 21:52:54 +02:00
8cd01c6f3b feat(sf_tracking): consent-aware crashlytics wrapper 2026-04-26 21:52:54 +02:00
1c98c0842d feat(sf_shared): add FailureType + handleFailure + feedback dialogs 2026-04-26 21:52:54 +02:00
59cced7b17 chore(legacy): add test infrastructure + mocktail 2026-04-26 21:52:54 +02:00
4e14534b1b fix 2026-04-26 21:52:54 +02:00
72d0c79c74 Revert "docs: add snackbar messages reference in Spanish"
This reverts commit 7ea415cb6e.
2026-04-26 21:52:53 +02:00
5b1826a10d docs: add snackbar messages reference in Spanish 2026-04-26 21:52:53 +02:00
039f2bb051 chore(i18n): add missing translation keys and device debug logging 2026-04-26 21:52:53 +02:00
9d6953dbf5 feat(wifi): enable wifi settings entry point and add debug logging 2026-04-26 21:52:53 +02:00
90048ac159 fix(sos-contacts): prevent deleting last emergency contact 2026-04-26 21:52:53 +02:00
982dee6c7a feat(block-phone): add edit contact functionality 2026-04-26 21:52:53 +02:00
09897b7f69 fix(location): add sharePositionOrigin for iPad share 2026-04-26 21:52:53 +02:00
09a625530e fix(control-panel): match dropdown value by device id to prevent crash 2026-04-26 21:52:53 +02:00
c60761adab fix(do-not-disturb): return empty schedule on 404 instead of error 2026-04-26 21:52:53 +02:00
6694a4b0ce fix(personal-data): send all required fields in PUT and autofill form 2026-04-26 21:52:53 +02:00
850796e1ca fix(rewards): send rewards key instead of amount in command data 2026-04-26 21:52:52 +02:00
051424f58b fix(volume): use capabilities max per slider and fix 0-10 scale 2026-04-26 21:52:52 +02:00
ac986ac360 feat(capabilities): add volume limits (media, ringtone, alarm) 2026-04-26 21:52:52 +02:00
cf86570e4c feat(settings): add falldown, falldownLevel and rename gps to location 2026-04-26 21:52:52 +02:00
e30f5dabcc fix(notifications): handle deep linking on cold start without crash 2026-04-26 21:52:52 +02:00
79039b99e2 refactor(legacy): align with Riverpod architecture (DTOs, repos, drop UseCases) 2026-04-26 21:52:52 +02:00
14720b66bf refactor(legacy): split legacy_shared into cohesive packages
legacy_shared was the junk drawer of the legacy app mode — 37 files
mixing device entities, command infrastructure, UI primitives, generic
formatters, and a duplicate of sf_infrastructure's dio_error_mapper.
Any module needing one piece pulled the whole bag into its graph.

Split it by responsibility, following Mandamiento 4 of Real-World Flutter:

- legacy_ui (new): 6 widget/layout primitives (PageLayout, MenuButton,
  SectionButton, PulsingLocationMarker, RefreshableErrorState,
  WeekDayChips) plus mapStyleProvider — shared UI state that was the
  only reason two modules needed a common package.
- legacy_device_state (expanded 9 → 30): absorbed device entities,
  commands infrastructure (datasource + repo + provider + guard),
  device settings update flow, and the CSV exporter. Now one package
  owns the device domain end-to-end.
- packages/utils: absorbed battery_utils and date_format_utils as pure
  formatters that never belonged in a legacy-scoped package.
- legacy_shared: deleted entirely.

The duplicate dio_error_mapper in legacy_shared is gone; callers now use
the sf_infrastructure version (which was always the superset — it adds
ApiException and the dart:io socket handling).

DeviceEntity note: legacy_device_state keeps its own DeviceEntity (with
int timestamps and typed paymentOptions) separate from sf_shared's
DeviceEntity (String timestamps, untyped paymentOptions). The legacy
one is intentionally not exported from the barrel to avoid the
ambiguous_import collision that legacy_shared quietly hid by never
exporting it in the first place. Unifying the two is a domain-model
refactor out of scope here.

0 cross-module imports remain among legacy feature modules.
2026-04-26 21:52:51 +02:00
d7308229a0 chore(sf_localizations): switch i18n source to es.json and add parity check
Spanish is the app default (SFLocalizations.testInit uses 'es',
localeResolutionCallback falls back to the first supported locale), so
make that explicit by pointing the code generator at es.json instead of
en.json. Regenerating picked up 12 activity-meter keys that were already
present in every locale file but had drifted out of I18n.

Add scripts/check_i18n_parity.dart: treats es.json as the template and
reports any missing or orphan keys in en/fr/de/it/pt. Exits non-zero so
it can gate CI or a pre-commit hook later.
2026-04-26 21:52:51 +02:00
f7d3dbfd27 refactor(legacy): migrate theming to Material 3 + SfColors extension
Replace the ThemePort/ThemeCode abstraction (GetIt-registered adapter)
with a Riverpod-driven Material 3 ColorScheme, an SfColors ThemeExtension
for brand tokens, and a user-facing appearance selector for light/dark/
system modes. Persisted via SharedPreferences, reacts to system
brightness changes. Payments mode keeps the existing ThemePort API.

Highlights
- New legacy_theme package: LegacyAppTheme (light/dark), LegacyColorSchemes,
  SfColors ThemeExtension, LegacyThemePreferences, LegacyThemeNotifier,
  LegacyThemeSelector. Timeframe-based variants scaffolded but disabled.
- New /legacy/dashboard/control_panel/settings/appearance route + screen.
- MaterialApp.router picks the legacy theme only when isLegacyMode.
- ~90 ThemeCode.* usages migrated to colorScheme.* / context.sfColors.*.
- 25 widgets dropped the 'ThemePort theme' constructor param.
- ~145 hardcoded colors migrated (exact hex 1:1, grey.shade tiers,
  destructive red -> colorScheme.error, background whites -> surface).
  Content-over-color whites, transparents, and brand semantic reds/
  oranges/greens intentionally preserved.
- sf_localizations updated with appearance / appearanceDescription keys
  in all six locales.
2026-04-26 21:52:50 +02:00
bd7c47351f chore: ignore flutter auto-generated files
These files are regenerated by flutter pub get and contain machine-
specific paths (FLUTTER_ROOT, FLUTTER_APPLICATION_PATH). They should
not be tracked — the file header itself states "do not check into
version control".

- Update .gitignore: prepend **/ to .flutter-plugins-dependencies so
  it matches in sub-packages, add Generated.xcconfig and
  flutter_export_environment.sh patterns.
- git rm --cached the 4 files that were already tracked (keeps them
  on disk for local builds but removes from repo).
2026-04-26 21:52:50 +02:00
6cf994cd5d refactor(legacy): extract shared device state into legacy_device_state package
- New package legacy_device_state hosts shared device/position state
  consumed by both control_panel and location screens. ControlPanel*
  classes renamed to LegacyDevice* in 8 moved files.
- Move PositionEntity + AddressEntity + NetworkEntity +
  LatestPositionsResponseModel + BatteryUtils + DateFormatUtils from
  control_panel to legacy_shared (shared domain entities).
- Remove anti-pattern re-export of sf_infrastructure/dio_error_mapper
  from legacy_shared barrel. Update consumers to import
  sf_infrastructure directly.
- Clean 14 unused legacy_shared imports across view models in
  account, device_management, legacy_auth and settings modules.

Eliminates cross-module dependency location -> control_panel.
0 errors, 0 warnings in flutter analyze across 11 packages/modules.
2026-04-26 21:52:50 +02:00
fa3d7aa1fd fix(location): keep map visible on transient network errors
Show snackbar instead of replacing the entire map with an error state
when a reload fails. Only show RefreshableErrorState on first load failure.
2026-04-26 21:52:50 +02:00
5b4d31e2f1 fix(location): guard frequency timer minimum and filter zero options
Prevent position polling timer from firing with frequency < 60s (protects
against 0s values that saturate the API). Also filter out 0 from location
capability options in the frequency selector.
2026-04-26 21:52:50 +02:00
2e6769f18f feat(legacy): add connection status indicator to device banners 2026-04-26 09:05:09 +02:00
7d20b56583 refactor(heartbeat): refresh devices every 2min instead of pinging /auth/me 2026-04-26 09:01:26 +02:00
40d55b0b43 fix(control-panel): use iso_sf.png in map preview banner 2026-04-26 08:53:49 +02:00
5aa45b3d01 fix(control-panel): prevent device name from overlapping header logo 2026-04-26 08:52:13 +02:00
ecfb4cc7d2 fix 2026-04-26 08:39:26 +02:00
6b1e571341 fix(tests): update feedback_dialogs test for queue implementation 2026-04-26 08:38:18 +02:00
f6ff53bcd7 test(device-entity): add queueCommands and isDisconnected tests (11 cases) 2026-04-26 08:29:17 +02:00
d84edc11a9 test(do-not-disturb): add enabled field and toggle tests (4 new cases) 2026-04-26 08:29:17 +02:00
45b2842e5a test(app-usage-schedules): add controller and editor unit tests (6 cases) 2026-04-26 08:29:17 +02:00
a4f57f780b test(installed-apps): add InstalledAppsController unit tests (3 cases) 2026-04-26 08:29:16 +02:00
e130f4a037 chore(i18n): add GPS functions, history and delete confirmation keys 2026-04-26 08:25:55 +02:00
5c30dd9224 fix(settings): capitalize language names in selector 2026-04-26 08:25:47 +02:00
836aea707a fix(legacy): replace hardcoded colors for dark mode contrast 2026-04-26 08:25:41 +02:00
e0e7815ad8 fix(legacy): add dark mode support for pickers and dialogs 2026-04-26 08:25:33 +02:00
e0dde50eba fix(feedback-dialogs): add queue system to prevent dialog stacking 2026-04-26 08:25:24 +02:00
54ddf68c22 test(location): add LocationMapController unit tests (22 cases) 2026-04-26 08:25:18 +02:00
b5bba037f1 feat(location): add history playback, GPS functions dialog and UX improvements 2026-04-26 08:25:05 +02:00
6ed36dba75 feat(location): redesign device banner to match reference UI 2026-04-26 05:52:27 +02:00
4deb263c7e fix(location): add refresh success feedback and fix queueCommands null crash 2026-04-26 05:41:21 +02:00
82786b3577 chore(i18n): improve positionUpdated message text 2026-04-26 05:29:38 +02:00
ff48b873e9 chore(i18n): add positionUpdated key in 6 locales 2026-04-26 05:27:29 +02:00
35948998f6 feat(device): add queueCommands to DeviceEntity and command guard 2026-04-26 05:27:23 +02:00
c034d781af refactor(legacy): replace showTopSnackbar with feedback dialogs 2026-04-26 05:27:17 +02:00
5bebe110fc chore: regenerate health controller codegen 2026-04-26 05:13:42 +02:00
107a4ec593 chore(i18n): add localization keys for new features and fixes 2026-04-26 05:13:19 +02:00
03effaed13 fix(tracking): shorten event names to fit Firebase 40-char limit 2026-04-26 05:13:10 +02:00
32eb4e0d52 fix(call-history): sort calls by most recent first 2026-04-26 05:13:05 +02:00
6f5855e2fd feat(navigation): add routes for installed apps and app usage schedules 2026-04-26 05:13:01 +02:00
412cb96888 feat(device-management): add app usage schedules feature 2026-04-26 05:12:52 +02:00
853b6f20a3 feat(device-management): add installed apps enable/disable feature 2026-04-26 05:12:42 +02:00
01cb4c9427 feat(do-not-disturb): add enabled toggle, redesign UI and duplicate validation 2026-04-26 05:12:34 +02:00
82123a6d5f fix(contacts): validate duplicate phone numbers before adding 2026-04-26 05:12:23 +02:00
3956a87862 feat(remote-camera): add delete, share, download and redesign photo viewer 2026-04-26 05:12:18 +02:00
7251349e1d feat(location): add manual frequency option and optimize position loading 2026-04-26 05:12:10 +02:00
cf2dbbeb63 refactor(legacy-auth): simplify device setup to single carrier name field 2026-04-26 05:12:05 +02:00
e9cceae485 feat(legacy-auth): improve 2FA sheet UX and signup layout 2026-04-26 05:11:55 +02:00
6de01b62ae fix(splash): distinguish network errors from auth errors in session check 2026-04-26 05:11:48 +02:00
7c7ffb8f3d feat(legacy-ui): add animated transitions for dialogs and navigation 2026-04-26 05:11:42 +02:00
80f95bae5a fix payments 2026-04-24 15:06:30 +02:00
63547b0f37 removede shadow and blurs 2026-04-23 15:02:25 +02:00
9ab78ac965 feat(legacy): replace CircularProgressIndicator with animated GIF loading
Add LegacyLoadingIndicator widget using a transparent GIF animation
for all full-page loading states across legacy modules. Also fix
HealthController crash by deferring provider mutation during build.
2026-04-23 14:54:38 +02:00
375e613caf test(location): add LocationController unit tests (15 cases, 25% coverage) 2026-04-23 05:09:37 +02:00
9b253dd545 todo 2026-04-23 03:49:36 +02:00
3f3fb3d5d0 refactor(settings): rename alerts_* data layer to notifications_* 2026-04-23 03:38:02 +02:00
e48dec979c refactor(legacy): move notifications feature from control_panel to settings 2026-04-23 03:33:44 +02:00
1c30318e06 chore(legacy_dashboard_shell): remove dead control_panel dependency 2026-04-23 03:16:03 +02:00
d5d38637a7 refactor(legacy): remove remaining setState usages + tap-to-center on device banner 2026-04-23 03:06:32 +02:00
ac5219f389 refactor(location): migrate module to Riverpod codegen + replace snackbars with dialogs 2026-04-23 02:55:18 +02:00
4fbdce3c8c warning icon 2026-04-23 02:13:36 +02:00
5ad0a7acc5 feat(ui): use pill style for offline and technical error dialogs 2026-04-23 02:06:43 +02:00
065433ff61 feat(ui): redesign feedback dialogs with pill style for legacy mode 2026-04-23 02:04:33 +02:00
c06fb06d03 refactor(legacy_auth): migrate onboarding to Riverpod codegen 2026-04-23 00:20:09 +02:00
04c26e83cf refactor(legacy_auth): migrate device_setup to Riverpod + polish QR scanner UX 2026-04-23 00:16:05 +02:00
dc7325ea65 refactor(legacy_auth): migrate sign_up to Riverpod 2026-04-22 23:29:38 +02:00
76782fbfd4 refactor(legacy_auth): migrate login to Riverpod 2026-04-22 23:21:44 +02:00
c17e94ff7f refactor(legacy_auth): migrate recover_password to Riverpod 2026-04-22 23:14:29 +02:00
c84287e803 refactor(legacy_auth): migrate link_phone to Riverpod 2026-04-22 23:09:20 +02:00
44c8949c07 refactor(device_management): migrate health to Riverpod + improve charts 2026-04-22 23:04:14 +02:00
aaecc38461 refactor(device_management): migrate apps_use to Riverpod 2026-04-22 22:49:22 +02:00
3470e1bfef refactor(device_management): migrate activity_meter to Riverpod 2026-04-22 22:45:18 +02:00
0530f892f2 refactor(device_management): migrate scheduled_activities to Riverpod 2026-04-22 22:35:27 +02:00
734bd79af7 refactor(device_management): migrate remote_connection to Riverpod 2026-04-22 22:21:35 +02:00
94e2fcbf7d refactor(device_management): migrate contacts to Riverpod 2026-04-22 22:14:06 +02:00
35a943c066 refactor(device_management): migrate background_image to Riverpod 2026-04-22 21:56:07 +02:00
5193e6ada2 refactor(device_management): migrate do_not_disturb to Riverpod + add delete button 2026-04-22 21:44:16 +02:00
2052fdcf85 refactor(device_management): migrate volume_control to Riverpod 2026-04-22 21:26:18 +02:00
4e50384dd9 refactor(device_management): migrate call_history to Riverpod 2026-04-22 21:11:32 +02:00
9f5ec3f1da refactor(device_management): migrate locate_device to Riverpod + fix rewards padding 2026-04-22 21:07:04 +02:00
db3197a93a refactor(device_management): migrate rewards to Riverpod 2026-04-22 20:59:25 +02:00
b90eed2a54 refactor(device_management): migrate call_watch to Riverpod 2026-04-22 20:53:52 +02:00
118be4c6c0 icons 2026-04-22 20:47:38 +02:00
62de343dae feat(notifications): category list with tap-to-filter navigation 2026-04-22 20:29:49 +02:00
df92c51344 fix(router): rename duplicate notifications route name 2026-04-22 20:21:35 +02:00
221d053d5f fix(websocket): parse wifi-current event type for wifi settings 2026-04-22 11:23:30 +02:00
e5cf5fcb61 fix(control-panel): use legacyPrimary color in device dropdown for dark mode 2026-04-22 03:21:24 +02:00
3e427f44d7 feat(control-panel): custom animated device dropdown 2026-04-22 02:57:10 +02:00
746230a541 refactor(settings): migrate wifi_settings God VM to Riverpod 2026-04-22 02:37:21 +02:00
86642b9587 refactor(settings): migrate disable_functions to Riverpod 2026-04-22 02:26:25 +02:00
71ffc52993 refactor(settings): migrate block_phone to Riverpod CRUD pattern 2026-04-22 02:19:33 +02:00
d355ee2442 refactor(legacy-settings): migrate sos_contacts CRUD to AsyncNotifier 2026-04-22 02:02:18 +02:00
cc5159fc56 fix(legacy-settings): keep alarm list after save for consistency 2026-04-22 01:54:48 +02:00
d6d82d20c6 refactor(legacy-settings): migrate alarm CRUD to AsyncNotifier 2026-04-22 01:52:22 +02:00
f2d2385f24 fix(legacy-settings): go back after timezone save for consistency 2026-04-22 01:31:15 +02:00
e6974c7be7 refactor(legacy-settings): migrate alerts to AsyncNotifier 2026-04-22 01:31:14 +02:00
20cebc8bc7 refactor(legacy-settings): migrate remote_management to AsyncNotifier 2026-04-22 01:20:55 +02:00
2247833203 feat(legacy-settings): DST-aware timezone with phone auto-detect 2026-04-22 01:10:10 +02:00
92e93a2b69 chore(legacy-theme): remove reserved timeframe dead code 2026-04-22 00:56:47 +02:00
691dfc0472 refactor(legacy-settings): migrate battery to AsyncNotifier 2026-04-22 00:44:35 +02:00
2b9b6aa215 refactor(legacy-settings): migrate timezone, sound, sync_clock to AsyncNotifier 2026-04-22 00:42:33 +02:00
4cd4be24e6 refactor(legacy-customer-service): move contact form state to provider (no setState) 2026-04-22 00:34:53 +02:00
a547f7a786 refactor(legacy-account): move delete_account dialog state to provider (no setState) 2026-04-22 00:30:20 +02:00
42698631a3 refactor(legacy-account): move personal_data form state to provider (no setState) 2026-04-22 00:16:48 +02:00
69fdc2233f refactor(legacy-account): move change_password local error to provider (no setState) 2026-04-22 00:10:43 +02:00
75b47e2c25 refactor(legacy-settings): move language selection state to provider (no setState) 2026-04-22 00:00:14 +02:00
1c0a8b7bb7 refactor(legacy-settings): migrate language to AsyncNotifier 2026-04-21 23:51:38 +02:00
417b6660fc refactor(legacy-customer-service): migrate contact form to AsyncNotifier 2026-04-21 23:45:41 +02:00
b8ac786146 refactor(sf_shared): add help center URL to BrandLinks 2026-04-21 23:33:12 +02:00
dd1617939b refactor(sf_shared): move brand links to Firebase Remote Config 2026-04-21 23:32:26 +02:00
4c85af38aa refactor(legacy-account): use .select for AsyncValue field reads 2026-04-21 23:32:01 +02:00
309ff8b8b7 refactor(legacy-account): migrate account_settings to AsyncNotifier 2026-04-21 23:31:56 +02:00
e040944965 refactor(legacy-account): migrate delete_account to AsyncNotifier + fix A1 2026-04-21 22:12:51 +02:00
b6526f20ee refactor(legacy-account): migrate linked_devices to AsyncNotifier 2026-04-21 21:47:36 +02:00
0418f16f87 fix(sf_shared): distinguish 401 vs 403 error UX 2026-04-21 21:34:32 +02:00
f36ad5e4a6 refactor(legacy-account): migrate personal_data to AsyncNotifier 2026-04-21 21:00:42 +02:00
0a50941c2b fix(legacy-account): show success dialog after password change 2026-04-21 20:49:13 +02:00
7746d08759 refactor(legacy-account): migrate change_password to AsyncNotifier 2026-04-21 20:35:18 +02:00
72c88cc4b0 refactor(legacy-account): migrate app_users to FutureProvider 2026-04-21 20:03:45 +02:00
b21b234b9a feat(sf_tracking): consent-aware crashlytics wrapper 2026-04-21 19:33:54 +02:00
f89bca99b3 feat(sf_shared): add FailureType + handleFailure + feedback dialogs 2026-04-21 19:32:49 +02:00
1056895c31 chore(legacy): add test infrastructure + mocktail 2026-04-21 19:32:40 +02:00
424b8d9034 fix 2026-04-21 18:24:25 +02:00
4aa91c355e Revert "docs: add snackbar messages reference in Spanish"
This reverts commit 7ea415cb6e.
2026-04-21 18:15:07 +02:00
7ea415cb6e docs: add snackbar messages reference in Spanish 2026-04-21 17:59:58 +02:00
21fd1e0197 chore(i18n): add missing translation keys and device debug logging 2026-04-21 17:59:50 +02:00
d618ed76d0 feat(wifi): enable wifi settings entry point and add debug logging 2026-04-21 17:59:39 +02:00
dfd7ba9c41 fix(sos-contacts): prevent deleting last emergency contact 2026-04-21 17:59:29 +02:00
b8f5c5d6f8 feat(block-phone): add edit contact functionality 2026-04-21 17:59:16 +02:00
d470ed470a fix(location): add sharePositionOrigin for iPad share 2026-04-21 17:59:08 +02:00
a400fef77d fix(control-panel): match dropdown value by device id to prevent crash 2026-04-21 17:59:00 +02:00
febc21a590 fix(do-not-disturb): return empty schedule on 404 instead of error 2026-04-21 17:58:52 +02:00
29fca859fc fix(personal-data): send all required fields in PUT and autofill form 2026-04-21 17:58:44 +02:00
d92fe887fd fix(rewards): send rewards key instead of amount in command data 2026-04-21 17:58:27 +02:00
315e5b2908 fix(volume): use capabilities max per slider and fix 0-10 scale 2026-04-21 17:58:20 +02:00
244e5bbd03 feat(capabilities): add volume limits (media, ringtone, alarm) 2026-04-21 17:58:12 +02:00
a86041885c feat(settings): add falldown, falldownLevel and rename gps to location 2026-04-21 17:58:03 +02:00
12011ce525 fix(notifications): handle deep linking on cold start without crash 2026-04-21 17:57:53 +02:00
c92e2fb67f refactor(legacy): align with Riverpod architecture (DTOs, repos, drop UseCases) 2026-04-19 23:20:38 +02:00
7e1ead9cae refactor(legacy): split legacy_shared into cohesive packages
legacy_shared was the junk drawer of the legacy app mode — 37 files
mixing device entities, command infrastructure, UI primitives, generic
formatters, and a duplicate of sf_infrastructure's dio_error_mapper.
Any module needing one piece pulled the whole bag into its graph.

Split it by responsibility, following Mandamiento 4 of Real-World Flutter:

- legacy_ui (new): 6 widget/layout primitives (PageLayout, MenuButton,
  SectionButton, PulsingLocationMarker, RefreshableErrorState,
  WeekDayChips) plus mapStyleProvider — shared UI state that was the
  only reason two modules needed a common package.
- legacy_device_state (expanded 9 → 30): absorbed device entities,
  commands infrastructure (datasource + repo + provider + guard),
  device settings update flow, and the CSV exporter. Now one package
  owns the device domain end-to-end.
- packages/utils: absorbed battery_utils and date_format_utils as pure
  formatters that never belonged in a legacy-scoped package.
- legacy_shared: deleted entirely.

The duplicate dio_error_mapper in legacy_shared is gone; callers now use
the sf_infrastructure version (which was always the superset — it adds
ApiException and the dart:io socket handling).

DeviceEntity note: legacy_device_state keeps its own DeviceEntity (with
int timestamps and typed paymentOptions) separate from sf_shared's
DeviceEntity (String timestamps, untyped paymentOptions). The legacy
one is intentionally not exported from the barrel to avoid the
ambiguous_import collision that legacy_shared quietly hid by never
exporting it in the first place. Unifying the two is a domain-model
refactor out of scope here.

0 cross-module imports remain among legacy feature modules.
2026-04-19 05:42:31 +02:00
e59ce36033 chore(sf_localizations): switch i18n source to es.json and add parity check
Spanish is the app default (SFLocalizations.testInit uses 'es',
localeResolutionCallback falls back to the first supported locale), so
make that explicit by pointing the code generator at es.json instead of
en.json. Regenerating picked up 12 activity-meter keys that were already
present in every locale file but had drifted out of I18n.

Add scripts/check_i18n_parity.dart: treats es.json as the template and
reports any missing or orphan keys in en/fr/de/it/pt. Exits non-zero so
it can gate CI or a pre-commit hook later.
2026-04-19 04:58:09 +02:00
aa3ffdb6a7 refactor(legacy): migrate theming to Material 3 + SfColors extension
Replace the ThemePort/ThemeCode abstraction (GetIt-registered adapter)
with a Riverpod-driven Material 3 ColorScheme, an SfColors ThemeExtension
for brand tokens, and a user-facing appearance selector for light/dark/
system modes. Persisted via SharedPreferences, reacts to system
brightness changes. Payments mode keeps the existing ThemePort API.

Highlights
- New legacy_theme package: LegacyAppTheme (light/dark), LegacyColorSchemes,
  SfColors ThemeExtension, LegacyThemePreferences, LegacyThemeNotifier,
  LegacyThemeSelector. Timeframe-based variants scaffolded but disabled.
- New /legacy/dashboard/control_panel/settings/appearance route + screen.
- MaterialApp.router picks the legacy theme only when isLegacyMode.
- ~90 ThemeCode.* usages migrated to colorScheme.* / context.sfColors.*.
- 25 widgets dropped the 'ThemePort theme' constructor param.
- ~145 hardcoded colors migrated (exact hex 1:1, grey.shade tiers,
  destructive red -> colorScheme.error, background whites -> surface).
  Content-over-color whites, transparents, and brand semantic reds/
  oranges/greens intentionally preserved.
- sf_localizations updated with appearance / appearanceDescription keys
  in all six locales.
2026-04-19 04:47:22 +02:00
2eddb99c47 chore: ignore flutter auto-generated files
These files are regenerated by flutter pub get and contain machine-
specific paths (FLUTTER_ROOT, FLUTTER_APPLICATION_PATH). They should
not be tracked — the file header itself states "do not check into
version control".

- Update .gitignore: prepend **/ to .flutter-plugins-dependencies so
  it matches in sub-packages, add Generated.xcconfig and
  flutter_export_environment.sh patterns.
- git rm --cached the 4 files that were already tracked (keeps them
  on disk for local builds but removes from repo).
2026-04-19 01:58:45 +02:00
c461519597 refactor(legacy): extract shared device state into legacy_device_state package
- New package legacy_device_state hosts shared device/position state
  consumed by both control_panel and location screens. ControlPanel*
  classes renamed to LegacyDevice* in 8 moved files.
- Move PositionEntity + AddressEntity + NetworkEntity +
  LatestPositionsResponseModel + BatteryUtils + DateFormatUtils from
  control_panel to legacy_shared (shared domain entities).
- Remove anti-pattern re-export of sf_infrastructure/dio_error_mapper
  from legacy_shared barrel. Update consumers to import
  sf_infrastructure directly.
- Clean 14 unused legacy_shared imports across view models in
  account, device_management, legacy_auth and settings modules.

Eliminates cross-module dependency location -> control_panel.
0 errors, 0 warnings in flutter analyze across 11 packages/modules.
2026-04-19 01:55:11 +02:00
919ee55c45 fix(location): keep map visible on transient network errors
Show snackbar instead of replacing the entire map with an error state
when a reload fails. Only show RefreshableErrorState on first load failure.
2026-04-17 17:49:25 +02:00
f5350f5e78 fix(location): guard frequency timer minimum and filter zero options
Prevent position polling timer from firing with frequency < 60s (protects
against 0s values that saturate the API). Also filter out 0 from location
capability options in the frequency selector.
2026-04-17 17:11:01 +02:00
b9b49f0b26 feat(videocall): complete channel wrapper + add integration plan doc
Add 11 missing JCMediaChannel methods to VideocallChannelService:
getSelfParticipant, subscribeParticipantAudio, startScreenShareVideo,
stopScreenShareVideo, enableSelfVideoRatio, getMaxResolution,
sendCommandToDelivery, enableVolumeChangeNotify, getCaptureScreen,
getScreenRenderId, getScreenUserId.

Add VIDEOCALL_INTEGRATION.md with full implementation checklist
tracking phases 1-10.
2026-04-17 15:51:36 +02:00
afa916a30d feat(videocall): add native permissions for video calling
Android: RECORD_AUDIO, ACCESS_WIFI_STATE, MODIFY_AUDIO_SETTINGS,
BLUETOOTH permissions + camera/bluetooth uses-feature + ProGuard
rules for Juphoon/JusTalk.

iOS: NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription,
updated NSCameraUsageDescription + Podfile GCC_PREPROCESSOR_DEFINITIONS
for camera, photos and microphone permissions.
2026-04-17 15:51:27 +02:00
4347cefaed feat(videocall): add videocall_sdk package wrapping Juphoon jc_sdk
Full wrapper around jc_sdk v2.16.5 with clean architecture:
- 7 services covering 100% of jc_sdk public API (Client, Call, Device, Channel, Push, Net, Log)
- Constructor injection with GetIt DI module (follows sca_treezor pattern)
- VideocallSdkManager orchestrator for init/destroy lifecycle
- VideocallSdkConfig abstract for environment-specific AppKey
- Stream-based callbacks for reactive UI consumption
- Riverpod providers (service + stream) for feature layer
- AppKey configured per environment via dart-define-from-file
- Integrated in init_app.dart alongside scaTreezorModule
2026-04-17 15:17:56 +02:00
e7ebe7f403 fix 2026-04-17 15:13:34 +02:00
ed41b82076 chore(legacy): hide wifi settings and forgot password entry points
Comment out the navigation entry points for the wifi settings flow
from the device settings list and for the forgot-password link from
the login screen while these flows wait for product sign-off. The
underlying screens and providers stay in place so re-enabling them is
a single uncomment.
2026-04-17 11:16:08 +02:00
9470f54867 chore(notifications): log push token registration outcome for debugging
Add debugPrint entries before and after POST /universal-notifications/push
so we can trace whether a given token was accepted by the backend
during debugging of push delivery issues.
2026-04-17 11:13:59 +02:00
f82d222df3 style(device_setup): redesign activation code dialog with icon and OK action
Replace the bare centered text with a rounded dialog that leads with a
mail icon, keeps the activation-code message, and adds an explicit OK
button to dismiss. Aligns the dialog with the rest of the legacy
design system.
2026-04-17 11:13:53 +02:00
fd8ef27185 feat(splash): route to device setup when a logged-in user has zero devices
Extend CheckSessionUseCase to fetch /devices after confirming the
session and return InitialRoute.deviceSetup when the list is empty
(or the call fails). The splash builder now injects a
SharedDevicesRepository alongside the user repository, and the app
router maps the new route to legacyDeviceSetup / deviceSetup per
shell. The legacy builder reads legacyDevicesProvider to derive
isFirstDevice so the setup screen adapts its copy accordingly.
2026-04-17 11:13:48 +02:00
5b6ed5cf16 fix(config): swap production apiBaseUrl and apiOrigin back to correct hosts
The two fields had been flipped, pointing API calls at the origin host
(platform.savefamily.app) and sending the Origin header as the gateway
host (api-platform.savefamily.app). Restore the intended wiring:
gateway handles /api, and Origin is the platform domain.
2026-04-17 11:13:40 +02:00
bf1032245a refactor(device_setup): type API errors with status-code-based enum
Introduce LegacyDeviceSetupErrorEvent and two mappers for the
generate-activation-key and create-device endpoints. 403 on
activation-key maps to invalidIdentificator; 404 to deviceNotFound;
401 (already activated or B2B) unifies as deviceNotAvailable. On
create-device, 403 → invalidField and 404 → invalidActivationKey.
The view state now splits validation vs API errors with a
displayErrorKey extension, replacing the previous e.toString() leak
that could surface stack traces to users.
2026-04-17 11:12:37 +02:00
fad2c8792c refactor(recover_password): type API errors and hide email enumeration
Map PUT /auth/reset-password and PUT /auth/recovery-password failures
into LegacyRecoverPasswordErrorEvent. Reset-password now treats 404
(email not found) as success and surfaces a generic sent-if-exists
flow, closing an account enumeration vector. Recovery-password
differentiates 401 (tokenExpired), 404 (tokenNotFound), 403+Property
(invalidField) from 403 without Property (weakPassword). The view
state splits validation vs API errors with a displayErrorKey extension
for the inline error text.
2026-04-17 11:12:14 +02:00
73d9de45a2 refactor(signup): type API errors with status-code-based enum
Introduce LegacySignupErrorEvent to map backend failures from
POST /auth/signup: 400 → emailAlreadyExists, 403 → invalidField,
429 → tooManyAttempts, timeout → network. The view state now
separates validationErrorKey (pre-submit i18n keys) from apiErrorEvent
(typed API outcomes), and the screen listens to both to show proper
i18n messages instead of leaking raw backend text.
2026-04-17 11:11:52 +02:00
56d89fcdc4 refactor(login): type auth errors with status-code-based enum
Replace the raw errorMessage string in the login view state with a
typed LegacyAuthErrorEvent that classifies backend failures by HTTP
status (403/404 → invalidCredentials, 423 → accountLocked, 401 +
NOT_VERIFIED/expired → dedicated events, 429 → tooManyAttempts,
timeout → network). The login screen and 2FA sheet now switch on the
enum to show specific i18n messages instead of surfacing raw backend
text. Adds auth i18n keys for the full set of mapped states.
2026-04-17 11:11:23 +02:00
eff6f01924 feat(infra): preserve status code and network flag in ApiException
Replace the untyped Exception(msg) thrown by mapDioError with an
ApiException that exposes statusCode and isNetworkError alongside the
message. Callers can now classify failures by HTTP status without
string-matching on the error message — this unblocks typed error
mapping in the auth feature modules.
2026-04-17 11:10:13 +02:00
72d44b81df feat(legacy): add pull-to-refresh for control_panel and location
Introduce a shared RefreshableErrorState widget that wraps the retry
hint in a RefreshIndicator with an explicit 'pull down to retry'
caption, so users can recover from load failures without navigating
away. Wire it into the location screen's error fallbacks and make the
control_panel body pull-to-refresh at any time, invalidating the device
list so the dashboard picks up fresh data.
2026-04-17 09:44:29 +02:00
2942d7393e feat(wifi_settings): support setting saved networks as current
Add a setNetwork action that sends the setWifi command for a saved
network and waits for wifiCurrent confirmation with a 15s timeout.
Saved network cards are now tappable to trigger the switch, and
dedicated success/error states surface the result via snackbar.
2026-04-17 09:44:16 +02:00
0b160758e2 refactor(legacy): make guardDeviceCommand async with stale TTL refetch
Convert the shared command guard to an async check that refetches
/devices when the cached state is older than 30s, so the isDisconnect
flag reflects reality before a command runs. A TopSnackBar explains the
check only if the fetch takes longer than 400ms, avoiding noise on fast
responses. Update all 44 call sites to await the guard.
2026-04-17 09:43:52 +02:00
ecbb6d1e76 feat(notifications): handle tap navigation to device alerts
Route FCM and local notification taps to the device alerts screen
when the user is already inside the legacy dashboard. Adds payload
parsing with command-based routing and richer debug logs.
2026-04-17 09:42:07 +02:00
e7a4653c01 refactor(shared): move device providers and data layer from legacy_shared to sf_shared
Device providers (legacyDevicesProvider, selectedDeviceProvider), repository,
datasource, and GetDevicesResponseModel now live in sf_shared. Also moved
dio_error_mapper (safeCall, mapDioError, formatErrorMessage) to sf_infrastructure.
Consumers import directly from sf_shared instead of re-exporting through legacy_shared.
2026-04-17 08:48:38 +02:00
05ffe572c8 fix(logout): invalidate device providers to clear stale data on user switch 2026-04-17 04:10:33 +02:00
cbc40f7d95 feat(alerts): add device alerts screen with pagination, filters, and WebSocket 2026-04-17 04:00:25 +02:00
27e26ca921 refactor(utils): extract query params builder, migrate health and location 2026-04-17 04:00:12 +02:00
e83adbfdbf feat(firebase): add production config and push token refresh listener 2026-04-17 03:07:57 +02:00
973fc2490c feat(wifi-settings): redesign with device commands, WebSocket scan, and connect flow 2026-04-17 03:06:23 +02:00
e148b9fdfa fix(websocket): handle notification wrapper, hyphenated types, and stringified data 2026-04-17 03:06:09 +02:00
238c15888b fix(interceptor): disable 500 token-expired handler in legacy mode 2026-04-17 03:05:59 +02:00
ddc5086b3b feat(legacy): block device commands when watch is disconnected 2026-04-16 23:48:07 +02:00
769e8fea27 refactor(do-not-disturb): move data layer to core, remove delete action 2026-04-16 21:26:07 +02:00
297fa8241a fix(control-panel): make position model fields nullable to prevent null cast crash 2026-04-16 20:35:48 +02:00
984a87f200 feat(alarm): changed alarm endpoint and modified models, entities and presentation 2026-04-16 18:42:29 +02:00
cda889a15b feat(websocket): add WebSocket service with typed events and auto-reconnect 2026-04-16 16:47:34 +02:00
1230a27d94 fix(location): single-color route, contrasting arrows, time labels on endpoints 2026-04-16 14:52:07 +02:00
bc46f31434 fix(contacts): use dynamic max limits from device capabilities 2026-04-16 13:55:07 +02:00
514daf9c7c feat(do-not-disturb): add DND schedule feature with capabilities-driven UI 2026-04-16 13:54:13 +02:00
51a3979c03 feat(location-map): auto-center on first fix, follow toggle, refresh feedback 2026-04-15 22:13:12 +02:00
c7e32d1399 refactor(activity-meter): redesign screen with honest per-range stats 2026-04-15 21:51:08 +02:00
4e21e8d698 chore(gitignore): untrack IDE and flutter plugin artifacts 2026-04-15 17:13:52 +02:00
703b1e9fba docs(analytics): update catalog for signup and recover password changes 2026-04-15 17:07:02 +02:00
2fe5a2399d fix(app): scope user analytics listener to authenticated shells 2026-04-15 17:06:57 +02:00
9e41090712 refactor(settings): unify SOS and block_phone sheets with shared ContactFormSheet 2026-04-15 17:06:51 +02:00
648d0fc04b refactor(account): migrate personal_data to SfPhoneNumber and fix change_password state 2026-04-15 17:06:46 +02:00
56e437ff13 refactor(device-management): migrate call_watch and remote_connection to SfPhoneNumber 2026-04-15 17:06:41 +02:00
88c1111bd5 refactor(device-management): split contacts feature into list/new/edit view models and migrate to SfPhoneNumber 2026-04-15 17:05:56 +02:00
85be483c4e refactor(legacy-auth): migrate link_phone to SfPhoneNumber with E.164 2026-04-15 17:05:50 +02:00
08e099fc37 refactor(legacy-auth): simplify signup to 2 steps and remove phone from recover password 2026-04-15 17:04:43 +02:00
8a97304ff5 feat(design_system): add shared contacts permission dialog 2026-04-15 17:04:28 +02:00
8c1ca94a08 feat(sf_shared): add SfPhoneNumber value object and DeviceContactPicker service 2026-04-15 17:03:18 +02:00
cbaff2e763 analytics catalog 2026-04-15 12:00:36 +02:00
f36bc9afc1 fix(router): unique navigatorKey per StatefulShellBranch to avoid duplicate GlobalKey crash 2026-04-15 11:32:23 +02:00
95a03434ca fix(antelop): route FCM pushes through SDK, per-flavor plist on iOS, unify applicationId
- Add AntelopAwareMessagingService so the Antelop SDK gets first dibs on every
  FCM push before delegating to firebase_messaging. Unblocks SCA wallet
  activation on Android, which was waiting forever for pushes that the
  FlutterFirebaseMessagingService was swallowing ever since Firebase was
  integrated. Remove the stock Antelop and firebase_messaging services via
  manifest-merge so only the wrapper handles MESSAGING_EVENT.
- Add Copy AntelopRelease Build Phase in Xcode to copy
  Runner/AntelopRelease-{flavor}.plist over the fixed AntelopRelease.plist
  inside the .app bundle based on CONFIGURATION. Without this the iOS SDK
  silently used the production plist on every flavor.
- Revert the six applicationId values (3 AndroidManifest + 3 AntelopRelease
  plist) to the sample id 4713640103500149457, which is the only one
  provisioned in Antelop's backend today. The four env+platform ids they
  provided all fail with 9999 / cryptography: Error while decrypting data.
  To be updated once Treezor confirms the real ids.
- Add packages/flutter_treezor_entrust_sdk_bridge/example as a pub workspace
  member and bump its Dart SDK and flutter_lints constraints so
  'melos bootstrap' stops failing with 'dependencies for
  flutter_treezor_entrust_sdk_bridge_example missing'.
2026-04-15 11:25:06 +02:00
6b2034612a removed "+" symbol from sign up email 2026-04-14 15:39:01 +02:00
ec14ad49e5 bump build to 9 2026-04-14 15:16:39 +02:00
03998f9cf1 router fixes 2026-04-14 14:44:03 +02:00
811e92defc bump build to 8 2026-04-14 11:03:10 +02:00
1e60b38087 changed fcm token path 2026-04-14 10:20:24 +02:00
693f55369c feat(version-check): add in-app update prompt with Remote Config
Adds a production-grade in-app version check that prompts users to update when a new build is available. Soft updates are dismissable. Force updates block the app entirely. Configured via FirebaseRemote Config so rollouts can be triggered without redeploying.

- Sealed result types (NoUpdate / SoftUpdate / ForceUpdate) for type-safe pattern matching
- AppVersionCheckService
- AppUpdateGate widget encapsulates listener, route guard and dialog wireup, isolated from save_family_app
- Serialized notifier operations prevent race between dismiss and refresh, mounted checks blindar disposal edge cases                                                                                                  - Build-aware dismiss persistence via SharedPreferences
2026-04-09 14:52:37 +02:00
506dd5a80f chore: ignore .DS_Store metadata files 2026-04-08 16:30:50 +02:00
7445021cf3 bump build to 7 2026-04-08 16:30:38 +02:00
63a4113d81 refactor(legacy): single source of truth for devices + persisted selection
Introduces legacyDevicesProvider as the canonical AsyncNotifier owning
the devices list, with semantic mutation methods (removeDevice,
renameDevice, refresh) instead of ref.invalidate from outside.

selectedDeviceProvider becomes an AsyncNotifier that persists the
selected device id in SharedPreferences and resolves it against the
shared list at build time, surviving cold starts.

ControlPanelViewModel and LocationViewModel are converted to
AsyncNotifier and react to selection changes, refetching positions /
geofences / frequent_places automatically. Screens use .when with
skipLoadingOnReload to avoid flicker.

Multi-device fixes:
- DeviceBanner cards now look up positions per device by identificator
- LocationMap recenters the camera on device swipe
- Country-level fallback zoom when no position is available
- Banner shows each device's own info while swiping

linked_devices and device_setup feed mutations through the new notifier
methods. 30+ legacy view models updated to read selectedDeviceProvider
via .value (AsyncNotifier shape).
2026-04-08 16:06:44 +02:00
d352aec5be feat(notifications): register FCM token with backend post-login
Adds NotificationsRemoteDatasource in sf_shared that POSTs the current
Firebase Messaging token to /notifications/push, wired into the legacy
login flow right after getUserInfo so the device can receive targeted
push notifications.

Fire-and-forget with catchError so a failure never blocks login.
2026-04-08 15:40:37 +02:00
60558a4fcf fix(payment): correct hardcoded legacyLogin in onboarding redirect 2026-04-08 15:39:26 +02:00
29887818f9 melos format 2026-04-07 17:07:47 +02:00
42ec003b05 refactor(tracking): tighten sf_tracking package
- Lazy-init sfTracking to avoid touching Firebase at import time
- DRY SfTrackingRepository with a single _broadcast helper
- Drop empty DashboardTracking, fix double step_completed in device_setup
- Move yearsBetween to packages/utils
- Add 5 unit tests for SfTrackingRepository
- Strip noisy comments from mixins and view models
2026-04-07 16:59:38 +02:00
4728e25803 feat(tracking): expand legacy module analytics coverage 2026-04-07 16:17:53 +02:00
7b91447cad feat(tracking): add sf_tracking package and instrument legacy module
Introduces packages/sf_tracking — a multi-client, GDPR-first analytics layer with feature mixins, a GoRouter listener for automatic screen views, and a user properties helper that runs on login.
Wires the package into the legacy module 61 events
2026-04-07 13:47:07 +02:00
d84c856ce7 chore: add ios ephemeral folder to gitignore 2026-04-07 03:36:49 +02:00
81284d7efe feat(firebase): integrate Firebase + APNs/FCM push notifications
Phase 2 of multi-environment setup. Adds Firebase core, Crashlytics,
Analytics, Remote Config, Performance, Messaging and flutter_local_notifications,
plus full APNs configuration for iOS push.

- Wire setupFirebase(env) and setupNotifications() in initApp
- Add firebase_options for dev and staging via flutterfire (sf-platform-pre)
- Register google-services / firebase-perf / crashlytics gradle plugins
- Add per-flavor GoogleService-Info.plist with Build Phase script that
  copies the right plist into the .app bundle based on \$CONFIGURATION
- Bump iOS deployment target 13.0 -> 15.0 (required by firebase_analytics)
- Pin flutter_local_notifications to ^19.4.2 (v20+ needs Dart SDK >=3.10)
- Add aps-environment to staging (development) and production entitlements;
  development flavor intentionally excluded (no App Store Connect entry)
- Fix AppDelegate.swift to call super.application after forwarding to
  AntelopAppDelegate, otherwise Firebase Messaging swizzling breaks and
  the APNs token is never captured
- Crashlytics reports in all builds (debug + release) for early detection
- Tag analytics events with env user property per flavor
- App Check intentionally not included (debug-token friction with large
  QA team); can be re-added release-only later
2026-04-07 03:33:25 +02:00
c1954497b8 chore: add root .gitignore and untrack auto-generated cache files
The repo had no root-level .gitignore, so .dart_tool/, ephemeral plugin
symlinks and build artifacts were being tracked across the workspace.
Every flutter pub get regenerated them and they polluted PRs as recurring
noise.

- Add root .gitignore covering .dart_tool/, ephemeral plugin symlinks,
  build/, coverage/, IDE files
- Untrack ~39 existing cache files across root, modules and packages
2026-04-07 03:30:35 +02:00
9cdb4c7724 refactor: rename Questia to SaveFamily across the codebase 2026-04-07 00:47:29 +02:00
a560e19db2 fix: configure Antelop SDK applicationIds per environment 2026-04-07 00:37:39 +02:00
c263e4227e feat: split legacy/payment apps via APP_MODE flag 2026-04-07 00:09:48 +02:00
3a375044b2 fix 2026-04-06 23:16:57 +02:00
4a57bb35c5 style: apply dart format tall style and fix curly braces lints 2026-04-06 23:15:32 +02:00
3c4159ae8c chore: upgrade melos to 7.5.1 with pub workspaces and dep catalog
- Upgrade Melos to 7.5.1 using Dart pub workspaces (Melos 7 paradigm).
- Replace per-package pubspec_overrides.yaml with workspace-native resolution. Add dependencies.yaml as a centralized catalog for all external deps with sync_deps.dart tool to propagate and validate versions across the monorepo.
2026-04-06 23:08:51 +02:00
3d267aff37 chore: clean up flutter analyze warnings
Highlights:
- Add publish_to: 'none' to legacy pubspec.yaml files                                                      - Replace print() with dart:developer log() in Treezor SDK
- Add !context.mounted guards in async callbacks (defensive bug fix)                                                       - Add super.key to widget constructors
- Remove redundant @Default(null) from device_model                                     - Fix implementation_imports in legacy_auth datasources                                        - Add ignore comments for scaffolding code
- Add missing shared_preferences dependency in splash module                                                                                                            Mostly code quality improvements, with one defensive bug fix              context.mounted) and one missing dependency fix (shared_preferences).
2026-04-06 22:21:22 +02:00
b63b06ef14 refactor: group payment app modules under payment/ folder 2026-04-06 21:21:15 +02:00
8ade5ad3d7 refactor(i18n): rename keys for clarity and complete missing translations 2026-04-06 20:33:57 +02:00
26e89fb177 Merge branch 'set-reset-card-pin-and-renew' into fusion-app 2026-04-06 19:44:26 +02:00
1810dc6e2a fix 2026-03-25 18:53:21 +01:00
4d2d25f47b fix 2026-03-25 16:39:47 +01:00
c79cbeffcc bump build to 6 2026-03-25 14:43:27 +01:00
b7614a39f1 feat: sync device settings after updates and improve remote connection features
- Add DeviceSettingsSync extension on Ref to centralize device provider
    updates after settings changes (sound, volume, language, timezone,
    battery, disable functions, alerts, pedometer, heart rate freq,
    location freq, background image)
  - Add photo capture countdown with Lottie animation in remote camera
  - Replace Image.network with Image.memory for photo display
  - Fix commands datasource to handle text/html responses (post<dynamic>)
  - Add typed error/success events to RemoteConnectionViewModel
  - Add background image active indicator and backgroundImageId to device settings
  - Change photos endpoint from /devices/identificator/:id/photos/files to /photos/files
  - Remove dead deviceId param from getBackgroundImage chain
  - Relax PictureEntity required fields to @Default for API compatibility
  - Fix LocationViewModel rebuild crash (ref.watch → ref.read)
  - Use .select() in RemoteCameraScreen for optimized rebuilds
  - Increase health measure countdown to 60s
2026-03-25 13:51:48 +01:00
a05c167f30 fix 2026-03-25 06:11:48 +01:00
c1c903ac93 feat: add country code picker to call watch dialog 2026-03-25 06:06:33 +01:00
a0a782c91b clean 2026-03-25 05:46:49 +01:00
c140daa7ae refactor: extract timezone data, remove duplicate i18n keys 2026-03-25 05:45:05 +01:00
6d30a59651 feat: implement alerts, disable functions, and battery night mode settings
- Rename sms_alert feature to alerts with toggle list from device capabilities
  - Implement disable functions (keyboard, GPS) with device settings update
  - Implement battery night mode with dedicated view model
  - Add keyboard, gps, nightMode fields to DeviceSettingsEntity/Model
  - Fix photos endpoint to use /photos/files for file content
2026-03-25 05:03:40 +01:00
8d453dc980 feature/background-image into fusion-app with fixes
- Fix upload flow: capture photo ID from POST /photos response
  - Fix endpoint: use /devices/identificator/{id}/photos/files for listing
  - Fix setBackgroundImage: use device.id (UUID) instead of identificator
  - Redesign screen as photo gallery with grid view
  - Add image compression on pick (maxWidth: 800, quality: 80%)
  - Fix multipart upload: remove content-type header for FormData auto-detection
  - Replace hardcoded Spanish text with i18n in 6 languages
  - Add typed error/success enums (BackgroundImageErrorEvent, BackgroundImageSuccessEvent)
  - Revert initialLocation to splash
  - Add missing translations for contacts and background-image features
2026-03-25 03:52:24 +01:00
0a50de3d70 Merge remote-tracking branch 'origin/feature/contacts' into fusion-app 2026-03-25 02:30:09 +01:00
02053182db Merge remote-tracking branch 'origin/feature/linked-devices' into fusion-app 2026-03-25 02:22:48 +01:00
33f3dfa252 feat: type device settings and capabilities, centralize device updates, and improve error handling
- Type device.settings and device.capabilities from untyped maps to Freezed models
  - Centralize all device settings updates through shared DeviceSettingsUpdateDatasource
  - Add frequency selectors for location and heart rate, pedometer toggle, and health measurement countdown with Lottie animation
  - Replace raw backend error messages with typed i18n error events across location, health, and activity meter
  - Fix silent error swallowing in commands datasource and stuck dialog in locate device
2026-03-25 02:15:25 +01:00
b6deb4b371 renew card 2026-03-24 20:20:54 +01:00
cb70973d3b feat: change background image 2026-03-24 17:31:49 +01:00
47c7821b0c feat: add card PIN management and card renewal to child wallet
- Add set PIN / change PIN multi-step flow (4-digit card PIN + 6-digit SCA PIN) with Treezor PCI DSS SCA proof generation
  - Add unblock PIN for blocked cards after failed attempts
  - Add renew card with SCA proof (same as wallet creation)
  - Show menu options conditionally based on hasCardPin and isPinBlocked flags
  - Make ScaPinView configurable with pinLength parameter (default 6)
  - Add hasCardPin to ChildProfileEntity and isPinBlocked to WalletCardEntity
  - Add EN/ES localizations for all new screens and messages
2026-03-24 13:47:21 +01:00
a07e9c23ca create wallet witch pci work in progress 2026-03-24 13:46:53 +01:00
1ffeea8b77 background image screen and state 2026-03-24 11:25:45 +01:00
5f484036f8 feat: contacts upper and lower limits 2026-03-23 15:38:47 +01:00
73927557ca fix: show activation code dialog with qr scan 2026-03-23 13:58:59 +01:00
5111d5d65f feat: add call history screen
- Add call history screen with list of incoming/outgoing calls
  - Implement GET /devices/identificator/{id}/call-histories endpoint
  - Add CallHistoryResponseModel with freezed
  - Add Riverpod provider for CallHistoryDatasource
  - Add route, builder, and menu button in device management
2026-03-22 05:50:20 +01:00
ced0895063 feat: merge health feature and add measure command
- Add REQUEST_HEART_RATE command with measure button in health screen
  - Add ref.mounted checks and fix early return in measure()
  - Remove unused SET_LANGUAGE from DeviceCommand enum
2026-03-22 05:15:22 +01:00
34e7a7c60f feat: merge remote-call feature and fix remote connection
- Implement photos API (GET /devices/identificator/{id}/photos)
  - Fix deviceId empty in commands (set before async load)
  - Fix missing await in call() method
  - Add ref.mounted checks on all async operations
  - Reload photos after REQUEST_PHOTO command
  - Add CountryPrefixPicker to spy call dialog
  - Add loading state and topSnackbar feedback on call
  - Handle empty photos list in gallery
  - Fix Expanded overflow in remote camera screen
  - Change keyboard to phone type in spy call
  - Remove unnecessary use cases
  - Add GetPicturesResponseModel with freezed
2026-03-22 04:57:38 +01:00
c89f1c666e feat: add volume control and merge sound mode feature
- Add volume control screen with sliders for media, ringtone, and alarm
- Update device settings via PUT /devices with CSV (same as language)
- Extract DeviceCsvBuilder to legacy_shared (shared between language and volume)
- Create Riverpod provider for DeviceUpdateDatasource
- Extract VolumeThumbShape to separate widget file
- Merge sound mode feature (SET_SOUND_MODE command, pending backend whitelist)
- Fix sound screen overflow with SingleChildScrollView
2026-03-22 04:01:09 +01:00
33c2403aef fix: improve language feature and fix merge issues
- Change language update from POST /commands to PUT /devices with CSV
- Add CSV escape for JSON fields (doubled quotes)
- Move device payload construction to datasource layer
- Add loading indicator on save button
- Fix 401 redirect to legacy login
- Remove debug print from commands datasource
2026-03-22 03:16:07 +01:00
0088d146f0 feat: enhance location map with route history, animations, follow mode, and fix API models
- Fix position address model nullability (province field missing from API)
  - Fix health query order to sortDirection to match backend API
  - Add pagination to health chart queries to prevent backend timeout
  - Align GetDevicesResponseModel with full backend schema
  - Add route history with gradient polyline, direction arrows, and clustering
  - Add animated map movements
  - Add follow mode with auto-refresh
  - Add share location via Google Maps link
  - Add fit bounds on history load
  - Add expandable action buttons panel
  - Add location list bottom sheet with type filters
  - Add whitelist sync alongside secondary contacts
  - Add loading state to linked devices screen
  - Refactor location_map.dart: extract RouteHistoryLayer, MapActionsPanel
  - Migrate setState to LocationMapViewModel
2026-03-22 02:30:21 +01:00
94c042d403 manual health measurement command 2026-03-20 15:35:12 +01:00
48cb23379c remote photo command 2026-03-20 15:14:40 +01:00
e526dce2c9 change language options and fix command 2026-03-20 10:41:03 +01:00
cacc2460f1 Merge branch 'fusion-app' into feature/language 2026-03-20 09:34:47 +01:00
dd53db6795 set language 2026-03-20 09:33:57 +01:00
435a9c04f9 bump build to 5 2026-03-18 21:00:03 +01:00
8e3a27e0d3 feat: add route history, map controls, and geofence/FP management
- Position history with polyline trail and date range picker
  - Map style selector (standard, voyager, light, dark, satellite) persisted via SharedPreferences
  - Geofence and frequent place CRUD with info cards
  - Device banner with swipeable carousel
  - Refresh position button
  - Widget extraction: map controls, info cards, device banner, modal overlay
2026-03-18 19:48:30 +01:00
48d2430c9c remote call command 2026-03-18 17:09:03 +01:00
cf0c55eafe Merge branch 'feature/linked-devices' into fusion-app 2026-03-18 15:09:35 +01:00
03c6633504 fix delete device sizing 2026-03-18 15:09:13 +01:00
b8184f02ec Merge remote-tracking branch 'origin/fusion-app' into fusion-app 2026-03-18 14:56:51 +01:00
c12d1924c4 splash screen fix 2026-03-18 13:52:07 +01:00
869f33f1f1 Merge remote-tracking branch 'origin/feature/change-password' into fusion-app 2026-03-18 13:21:52 +01:00
67aafafd1e hide interceptor for legacy app 2026-03-18 13:15:00 +01:00
c929e1e2d7 beneficiary validation and development api origin fix 2026-03-18 13:13:36 +01:00
990266ba95 delete device command and device setup flow 2026-03-18 11:49:05 +01:00
fa36037aac personal data dial code 2026-03-16 17:45:00 +01:00
c9e2adf692 contacts dial codes 2026-03-16 16:22:45 +01:00
995b69eb65 Merge remote-tracking branch 'origin/fusion-app' into legacy 2026-03-16 15:25:16 +01:00
88269c40f8 Add iOS privacy keys for staging/dev flavors, bump build to 4, hide SF Pay button 2026-03-16 15:09:26 +01:00
f1226b4c18 Merge remote-tracking branch 'origin/fusion-app' into legacy
# Conflicts:
#	apps/mobile_app/lib/save_family_app.dart
#	packages/sf_localizations/assets/l10n/de.json
#	packages/sf_localizations/assets/l10n/en.json
#	packages/sf_localizations/assets/l10n/es.json
#	packages/sf_localizations/assets/l10n/fr.json
#	packages/sf_localizations/assets/l10n/it.json
#	packages/sf_localizations/assets/l10n/pt.json
#	packages/sf_localizations/lib/src/generated/i18n.dart
2026-03-16 13:26:08 +01:00
b636550619 navigation fixes 2026-03-16 13:10:55 +01:00
797d236547 fix translation texts 2026-03-16 12:56:17 +01:00
90447ce9a0 Merge branch 'feature/remote-management' into legacy
# Conflicts:
#	apps/mobile_app/lib/save_family_app.dart
#	packages/sf_localizations/assets/l10n/de.json
#	packages/sf_localizations/assets/l10n/en.json
#	packages/sf_localizations/assets/l10n/es.json
#	packages/sf_localizations/assets/l10n/fr.json
#	packages/sf_localizations/assets/l10n/it.json
#	packages/sf_localizations/assets/l10n/pt.json
#	packages/sf_localizations/lib/src/generated/i18n.dart
2026-03-16 12:54:23 +01:00
22ef648b41 Merge remote-tracking branch 'origin/fusion-app' into legacy
# Conflicts:
#	apps/mobile_app/lib/navigation/app_router.dart
#	packages/sf_localizations/assets/l10n/en.json
#	packages/sf_localizations/assets/l10n/es.json
#	packages/sf_localizations/lib/src/generated/i18n.dart
2026-03-16 12:50:54 +01:00
ae4bc7824a Merge remote-tracking branch 'origin/feature/remote-management' into feature/remote-management 2026-03-16 12:47:18 +01:00
4eb4ac81ce Merge origin/feature/remote-management
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:46:58 +01:00
01d9d4241b remote management commands 2026-03-16 12:46:13 +01:00
907eaf8d33 commented some tabs to deploy in stores ,persistence login and heartbeaat 2026-03-16 12:40:24 +01:00
440bbcac66 device management features, settings module and contact sync
Device management:
  - Activity meter with steps charts and history
  - Apps usage with daily breakdown and top apps
  - Health monitoring (heart rate, oxygen, blood pressure)
  - Scheduled activities with timeline and CRUD
  - Contacts sync to device via contact-lists
  - Locate device, rewards refactor

  Settings (new module):
  - Block phone
  - SOS contacts
  - WiFi networks
  - Alarm refactor with full CRUD
  - Settings menu with feature stubs

  Account:
  - Personal data and account settings refactor

  Shared:
  - 100+ i18n keys in 6 languages
  - New routes in app_router
  - WeekDayChips, TimeRangeSelector shared widgets
  - Legacy dashboard shell simplified
2026-03-16 08:37:52 +01:00
904cfee2a9 comment some widgets 2026-03-13 09:44:59 +01:00
69b3cf358a add edit profile screens (child/parent) with SCA, paginated transactions, and reactive state refresh 2026-03-12 22:42:38 +01:00
ec4e42b408 settings ui 2026-03-12 17:40:06 +01:00
9bf06f2480 contacts endpoints and state fix 2026-03-12 17:11:26 +01:00
903e1991e1 delete account endpoint 2026-03-12 11:12:13 +01:00
f9d8f59195 personal data ui fix and endpoint 2026-03-11 17:35:00 +01:00
ffc0a1f103 legacy device setup screen flow from account settings 2026-03-11 13:11:11 +01:00
53cadd8499 linked devices state and models fix 2026-03-11 11:56:39 +01:00
1900 changed files with 133124 additions and 43722 deletions

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"sf_app_platform_mono_repo","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -1,364 +0,0 @@
{
"configVersion": 2,
"packages": [
{
"name": "ansi_styles",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/ansi_styles-0.3.2+1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "args",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/args-2.7.0",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "async",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/async-2.13.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "characters",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "charcode",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/charcode-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "checked_yaml",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/checked_yaml-2.0.4",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "cli_launcher",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/cli_launcher-0.3.2+1",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "cli_util",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/cli_util-0.4.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "conventional_commit",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/conventional_commit-0.6.1+1",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "ffi",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/ffi-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "file",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/file-7.0.1",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "flutter",
"rootUri": "file:///C:/Program%20Files/Flutter/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_secure_storage",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage-9.2.4",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_linux",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_macos",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_platform_interface",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_web",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_windows",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_web_plugins",
"rootUri": "file:///C:/Program%20Files/Flutter/packages/flutter_web_plugins",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "glob",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/glob-2.1.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "graphs",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/graphs-2.3.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "http",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/http-1.5.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "http_parser",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/http_parser-4.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "io",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/io-1.0.5",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "js",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/js-0.6.7",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "json_annotation",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/json_annotation-4.9.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "material_color_utilities",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "melos",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/melos-6.3.3",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "meta",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.16.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "mustache_template",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/mustache_template-2.0.2",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "path",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "path_provider",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider-2.1.5",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "path_provider_android",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_android-2.2.20",
"packageUri": "lib/",
"languageVersion": "3.9"
},
{
"name": "path_provider_foundation",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_foundation-2.4.3",
"packageUri": "lib/",
"languageVersion": "3.9"
},
{
"name": "path_provider_linux",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "path_provider_platform_interface",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_platform_interface-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "path_provider_windows",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "platform",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/platform-3.1.6",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "plugin_platform_interface",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/plugin_platform_interface-2.1.8",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "pool",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/pool-1.5.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "process",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/process-5.0.5",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "prompts",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/prompts-2.0.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "pub_semver",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/pub_semver-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "pub_updater",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/pub_updater-0.5.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "pubspec_parse",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/pubspec_parse-1.5.0",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "sky_engine",
"rootUri": "file:///C:/Program%20Files/Flutter/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "source_span",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "string_scanner",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "typed_data",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/typed_data-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "vector_math",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/vector_math-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "web",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/web-1.1.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "win32",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/win32-5.15.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "xdg_directories",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/xdg_directories-1.1.0",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "yaml",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/yaml-3.1.3",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "yaml_edit",
"rootUri": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache/hosted/pub.dev/yaml_edit-2.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "sf_app_platform_mono_repo",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.0"
}
],
"generator": "pub",
"generatorVersion": "3.9.2",
"flutterRoot": "file:///C:/Program%20Files/Flutter",
"flutterVersion": "3.35.6",
"pubCache": "file:///C:/Users/Aitor%20Arana/AppData/Local/Pub/Cache"
}

View File

@@ -1,491 +0,0 @@
{
"roots": [
"sf_app_platform_mono_repo"
],
"packages": [
{
"name": "sf_app_platform_mono_repo",
"version": "0.0.0",
"dependencies": [
"flutter_secure_storage"
],
"devDependencies": [
"melos"
]
},
{
"name": "flutter_secure_storage",
"version": "9.2.4",
"dependencies": [
"flutter",
"flutter_secure_storage_linux",
"flutter_secure_storage_macos",
"flutter_secure_storage_platform_interface",
"flutter_secure_storage_web",
"flutter_secure_storage_windows",
"meta"
]
},
{
"name": "melos",
"version": "6.3.3",
"dependencies": [
"ansi_styles",
"args",
"async",
"cli_launcher",
"cli_util",
"collection",
"conventional_commit",
"file",
"glob",
"graphs",
"http",
"meta",
"mustache_template",
"path",
"platform",
"pool",
"prompts",
"pub_semver",
"pub_updater",
"pubspec_parse",
"string_scanner",
"yaml",
"yaml_edit"
]
},
{
"name": "meta",
"version": "1.16.0",
"dependencies": []
},
{
"name": "flutter_secure_storage_windows",
"version": "3.1.2",
"dependencies": [
"ffi",
"flutter",
"flutter_secure_storage_platform_interface",
"path",
"path_provider",
"win32"
]
},
{
"name": "flutter_secure_storage_web",
"version": "1.2.1",
"dependencies": [
"flutter",
"flutter_secure_storage_platform_interface",
"flutter_web_plugins",
"js"
]
},
{
"name": "flutter_secure_storage_platform_interface",
"version": "1.1.2",
"dependencies": [
"flutter",
"plugin_platform_interface"
]
},
{
"name": "flutter_secure_storage_macos",
"version": "3.1.3",
"dependencies": [
"flutter",
"flutter_secure_storage_platform_interface"
]
},
{
"name": "flutter_secure_storage_linux",
"version": "1.2.3",
"dependencies": [
"flutter",
"flutter_secure_storage_platform_interface"
]
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "yaml_edit",
"version": "2.2.2",
"dependencies": [
"collection",
"meta",
"source_span",
"yaml"
]
},
{
"name": "yaml",
"version": "3.1.3",
"dependencies": [
"collection",
"source_span",
"string_scanner"
]
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "pubspec_parse",
"version": "1.5.0",
"dependencies": [
"checked_yaml",
"collection",
"json_annotation",
"pub_semver",
"yaml"
]
},
{
"name": "pub_updater",
"version": "0.5.0",
"dependencies": [
"http",
"json_annotation",
"process",
"pub_semver"
]
},
{
"name": "pub_semver",
"version": "2.2.0",
"dependencies": [
"collection"
]
},
{
"name": "prompts",
"version": "2.0.0",
"dependencies": [
"charcode",
"io"
]
},
{
"name": "pool",
"version": "1.5.2",
"dependencies": [
"async",
"stack_trace"
]
},
{
"name": "platform",
"version": "3.1.6",
"dependencies": []
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "mustache_template",
"version": "2.0.2",
"dependencies": []
},
{
"name": "http",
"version": "1.5.0",
"dependencies": [
"async",
"http_parser",
"meta",
"web"
]
},
{
"name": "graphs",
"version": "2.3.2",
"dependencies": [
"collection"
]
},
{
"name": "glob",
"version": "2.1.3",
"dependencies": [
"async",
"collection",
"file",
"path",
"string_scanner"
]
},
{
"name": "file",
"version": "7.0.1",
"dependencies": [
"meta",
"path"
]
},
{
"name": "conventional_commit",
"version": "0.6.1+1",
"dependencies": []
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "cli_util",
"version": "0.4.2",
"dependencies": [
"meta",
"path"
]
},
{
"name": "cli_launcher",
"version": "0.3.2+1",
"dependencies": [
"path",
"yaml"
]
},
{
"name": "async",
"version": "2.13.0",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "args",
"version": "2.7.0",
"dependencies": []
},
{
"name": "ansi_styles",
"version": "0.3.2+1",
"dependencies": []
},
{
"name": "win32",
"version": "5.15.0",
"dependencies": [
"ffi"
]
},
{
"name": "path_provider",
"version": "2.1.5",
"dependencies": [
"flutter",
"path_provider_android",
"path_provider_foundation",
"path_provider_linux",
"path_provider_platform_interface",
"path_provider_windows"
]
},
{
"name": "ffi",
"version": "2.1.4",
"dependencies": []
},
{
"name": "js",
"version": "0.6.7",
"dependencies": [
"meta"
]
},
{
"name": "flutter_web_plugins",
"version": "0.0.0",
"dependencies": [
"flutter"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "vector_math",
"version": "2.2.0",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "source_span",
"version": "1.10.1",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "json_annotation",
"version": "4.9.0",
"dependencies": [
"meta"
]
},
{
"name": "checked_yaml",
"version": "2.0.4",
"dependencies": [
"json_annotation",
"source_span",
"yaml"
]
},
{
"name": "process",
"version": "5.0.5",
"dependencies": [
"file",
"path",
"platform"
]
},
{
"name": "io",
"version": "1.0.5",
"dependencies": [
"meta",
"path",
"string_scanner"
]
},
{
"name": "charcode",
"version": "1.4.0",
"dependencies": []
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "web",
"version": "1.1.1",
"dependencies": []
},
{
"name": "http_parser",
"version": "4.1.2",
"dependencies": [
"collection",
"source_span",
"string_scanner",
"typed_data"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{
"name": "path_provider_foundation",
"version": "2.4.3",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_android",
"version": "2.2.20",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "typed_data",
"version": "1.4.0",
"dependencies": [
"collection"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
}
],
"configVersion": 1
}

View File

@@ -1 +0,0 @@
3.35.7

View File

@@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_secure_storage","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.3/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_secure_storage","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_android-2.2.20/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_secure_storage_macos","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.3/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_secure_storage_linux","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_secure_storage_windows","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"flutter_secure_storage_web","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2026-02-27 12:35:56.235180","version":"3.35.7","swift_package_manager_enabled":{"ios":false,"macos":false}}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_secure_storage","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.3/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_secure_storage","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_android-2.2.20/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_secure_storage_macos","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.3/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_secure_storage_linux","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_secure_storage_windows","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"flutter_secure_storage_web","path":"/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2026-03-13 09:52:54.635963","version":"3.35.7","swift_package_manager_enabled":{"ios":false,"macos":false}}

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Dart / Flutter workspace caches (regenerated by `flutter pub get` / `dart pub get`)
.dart_tool/
**/.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
# Per-package build outputs
**/build/
**/coverage/
# Flutter ephemeral plugin symlinks and helpers (regenerated on pub get)
**/ios/Flutter/ephemeral/
**/linux/flutter/ephemeral/
**/macos/Flutter/ephemeral/
**/windows/flutter/ephemeral/
# Flutter iOS build config (regenerated on pub get; contains machine-specific paths)
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/flutter_export_environment.sh
# IDE
.idea/
*.iml
.vscode/
# App config (contains API keys, passed via --dart-define-from-file)
apps/mobile_app/config/*.json
# macOS
.DS_Store
**/.DS_Store

2
.idea/.name generated
View File

@@ -1 +1 @@
sf-app-platform
sf_app_platform_mono_repo

38
.idea/modules.xml generated
View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/account/melos_account.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/account/melos_account.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/activity/melos_activity.iml" filepath="$PROJECT_DIR$/modules/activity/melos_activity.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/auth/melos_auth.iml" filepath="$PROJECT_DIR$/modules/auth/melos_auth.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/control_panel/melos_control_panel.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/control_panel/melos_control_panel.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/customer_service/melos_customer_service.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/customer_service/melos_customer_service.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/dashboard_shell/melos_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/dashboard_shell/melos_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/design_system/melos_design_system.iml" filepath="$PROJECT_DIR$/packages/design_system/melos_design_system.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/device_management/melos_device_management.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/device_management/melos_device_management.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/melos_flutter_treezor_entrust_sdk_bridge.iml" filepath="$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/melos_flutter_treezor_entrust_sdk_bridge.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/example/melos_flutter_treezor_entrust_sdk_bridge_example.iml" filepath="$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/example/melos_flutter_treezor_entrust_sdk_bridge_example.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/fonts/melos_fonts.iml" filepath="$PROJECT_DIR$/packages/fonts/melos_fonts.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/home/melos_home.iml" filepath="$PROJECT_DIR$/modules/home/melos_home.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/melos_legacy.iml" filepath="$PROJECT_DIR$/modules/legacy/melos_legacy.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/legacy_auth/melos_legacy_auth.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/legacy_auth/melos_legacy_auth.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/legacy_dashboard_shell/melos_legacy_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/legacy_dashboard_shell/melos_legacy_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/packages/legacy_design_system/melos_legacy_design_system.iml" filepath="$PROJECT_DIR$/modules/legacy/packages/legacy_design_system/melos_legacy_design_system.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/packages/legacy_shared/melos_legacy_shared.iml" filepath="$PROJECT_DIR$/modules/legacy/packages/legacy_shared/melos_legacy_shared.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/location/melos_location.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/location/melos_location.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/navigation/melos_navigation.iml" filepath="$PROJECT_DIR$/packages/navigation/melos_navigation.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/notifications/melos_notifications.iml" filepath="$PROJECT_DIR$/modules/notifications/melos_notifications.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/payments/melos_payments.iml" filepath="$PROJECT_DIR$/packages/payments/melos_payments.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/profile/melos_profile.iml" filepath="$PROJECT_DIR$/modules/profile/melos_profile.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sca_treezor/melos_sca_treezor.iml" filepath="$PROJECT_DIR$/packages/sca_treezor/melos_sca_treezor.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/settings/melos_settings.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/settings/melos_settings.iml" />
<module fileurl="file://$PROJECT_DIR$/apps/mobile_app/melos_sf_app_platform.iml" filepath="$PROJECT_DIR$/apps/mobile_app/melos_sf_app_platform.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_infrastructure/melos_sf_infrastructure.iml" filepath="$PROJECT_DIR$/packages/sf_infrastructure/melos_sf_infrastructure.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_localizations/melos_sf_localizations.iml" filepath="$PROJECT_DIR$/packages/sf_localizations/melos_sf_localizations.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_shared/melos_sf_shared.iml" filepath="$PROJECT_DIR$/packages/sf_shared/melos_sf_shared.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/splash/melos_splash.iml" filepath="$PROJECT_DIR$/modules/splash/melos_splash.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/utils/melos_utils.iml" filepath="$PROJECT_DIR$/packages/utils/melos_utils.iml" />
<module fileurl="file://$PROJECT_DIR$/melos_sf-app-platform.iml" filepath="$PROJECT_DIR$/melos_sf-app-platform.iml" />
</modules>
</component>
</project>

View File

@@ -6,6 +6,7 @@
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos bootstrap" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -6,6 +6,7 @@
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos clean" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Run -&gt; 'flutter_treezor_entrust_sdk_bridge_example'" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/packages//flutter_treezor_entrust_sdk_bridge//example/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Run -&gt; 'sf_app_platform'" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/apps//mobile_app/lib/main.dart" />
<option name="filePath" value="$PROJECT_DIR$/apps/mobile_app/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'auth'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/auth/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/auth/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'dashboard_shell'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/dashboard_shell/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/dashboard_shell/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'design_system'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/packages\\design_system\test" />
<option name="testDir" value="$PROJECT_DIR$/packages/design_system/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'flutter_treezor_entrust_sdk_bridge'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/packages\\flutter_treezor_entrust_sdk_bridge\test" />
<option name="testDir" value="$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'home'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/home/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/home/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'notifications'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/notifications/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/notifications/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'profile'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/profile/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/profile/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'analyze'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run analyze" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'app:dev'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run app:dev" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'app:prod'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run app:prod" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'app:staging'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run app:staging" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'check-deps'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run check-deps" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'clean'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run clean" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'format'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run format" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'format:check'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run format:check" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'generate'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run generate" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'outdated'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run outdated" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'sync-deps'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run sync-deps" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,11 +1,12 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'bootstrap'" type="ShConfigurationType">
<configuration default="false" name="Melos Run -&gt; 'test'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run bootstrap" />
<option name="SCRIPT_TEXT" value="melos run test" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

67
.vscode/launch.json vendored
View File

@@ -2,39 +2,88 @@
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
//
// Configurations are split between (Legacy) and (Payment) variants.
// (Legacy) is the default and matches historical behavior; (Payment)
// boots straight into the Treezor wallet flow via APP_MODE=payment.
"version": "0.2.0",
"configurations": [
{
"name": "SF Development",
"name": "SF Development (Legacy)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"args": [
"program": "lib/main_development.dart",
"toolArgs": [
"--flavor",
"development",
"--dart-define-from-file=config/development.json"
"--dart-define-from-file=config/development.json",
"--dart-define=APP_MODE=legacy"
]
},
{
"name": "SF Staging",
"name": "SF Development (Payment)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"args": [
"program": "lib/main_development.dart",
"toolArgs": [
"--flavor",
"development",
"--dart-define-from-file=config/development.json",
"--dart-define=APP_MODE=payment"
]
},
{
"name": "SF Staging (Legacy)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"toolArgs": [
"--flavor",
"staging",
"--dart-define-from-file=config/staging.json"
"--dart-define-from-file=config/staging.json",
"--dart-define=APP_MODE=legacy"
]
},
{
"name": "SF Production",
"name": "SF Staging (Payment)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"args": [
"program": "lib/main_staging.dart",
"toolArgs": [
"--flavor",
"staging",
"--dart-define-from-file=config/staging.json",
"--dart-define=APP_MODE=payment"
]
},
{
"name": "SF Production (Legacy)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_production.dart",
"toolArgs": [
"--flavor",
"production",
"--dart-define-from-file=config/production.json"
"--dart-define-from-file=config/production.json",
"--dart-define=APP_MODE=legacy"
]
},
{
"name": "SF Production (Payment)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_production.dart",
"toolArgs": [
"--flavor",
"production",
"--dart-define-from-file=config/production.json",
"--dart-define=APP_MODE=payment"
]
}
]

View File

@@ -0,0 +1,195 @@
# Integración Videollamadas Juphoon — Resumen del progreso
## Contexto
SaveFamily S.L (Bizkaia) está integrando videollamadas y chat entre su app móvil Flutter y sus smartwatches infantiles (RTOS/Android), usando el SDK de Juphoon (`jc_sdk`).
**Actores:**
- Grupo SaveFamily S.L — Cliente, dueño de la app y backend
- Shenzhen i365-Tech Co., Limited (Jane Zhang, Carmen) — Fabricante hardware, intermediario comercial
- Juphoon/JUQU (Allen) — Proveedor del SDK de videollamadas
- SeTracker — Proveedor del firmware del reloj y servidores auxiliares
**Cotización aprobada: $8,835** (Integración $2,200 + Chat $2,950 + Cloud Photo Album $735 + Encryption $2,950)
---
## Lo que se hizo
### 1. Análisis de documentación (3 rondas)
- **Ronda 1** (31-03-2026): 50 preguntas técnicas → 27/50 respondidas (54%)
- **Ronda 2** (01-04-2026): 17 preguntas generales → 17/17 respondidas (calidad desigual)
- **Ronda 3** (09-04-2026): Documentación oficial SDK recibida — Quickstart V1.1 Flutter (13 páginas), sequence diagrams, protocolo TCP, connection/mutual dialing process
- Documentos generados: análisis completo, conclusiones, preguntas bilingües ES/EN, análisis cruzado de respuestas
### 2. Cuenta Juphoon Cloud creada (16-04-2026)
- Consola: juphoon.com (+34)
- App creada: "SaveFamily" (tipo IoT, escenario Smartwatch)
- **AppKey:** `9efcf2d889dc8a0320925096`
- **AppSecret:** `ui7pr73ggl5rr0gf01np` (solo backend)
- **AES_KEY IoT:** `8e3637pG7E9144E0` (solo backend)
- Token auth activado en consola
### 3. Paquete `packages/videocall_sdk/` creado
Wrapper 100% del `jc_sdk` v2.16.5 con arquitectura sólida (patrón `sca_treezor` del monorepo):
- **7 servicios** cubriendo toda la API pública del SDK:
- `VideocallClient` → JCClient (auth, login, logout, messaging)
- `VideocallCallService` → JCCall (llamadas 1-to-1)
- `VideocallDeviceService` → JCMediaDevice (cámara, mic, speaker)
- `VideocallChannelService` → JCMediaChannel (llamadas grupales)
- `VideocallPushService` → JCPush (push notifications)
- `VideocallNetService` → JCNet (estado de red)
- `VideocallLogService` → JCLog (logging)
- **Constructor injection** (no singletons estáticos)
- **GetIt module** (`videocallSdkModule(config)`)
- **`VideocallSdkManager`** orquestador de inicialización (Client → Device → Call/Channel/Push)
- **`VideocallSdkConfig`** abstracto para config por entorno
- **Riverpod providers** + StreamProviders para UI reactiva
- **Callbacks del SDK → Dart Streams**
### 4. Permisos nativos configurados
- **Android:** RECORD_AUDIO, ACCESS_WIFI_STATE, MODIFY_AUDIO_SETTINGS, BLUETOOTH + uses-feature (camera, bluetooth) + ProGuard rules (juphoon, justalk)
- **iOS:** NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription, NSCameraUsageDescription actualizado + Podfile GCC_PREPROCESSOR_DEFINITIONS (PERMISSION_CAMERA, PHOTOS, MICROPHONE)
### 5. AppKey configurado por entorno
- `juphoonAppKey` en development.json, staging.json, production.json
- `Environment.juphoonAppKey` via `String.fromEnvironment()`
- `SaveFamilyVideocallConfig` implementa `VideocallSdkConfig`
- `videocallSdkModule(config)` integrado en `init_app.dart`
### 6. Feature `videocall/` creada en device_management
Feature completa siguiendo el patrón del monorepo (builder + domain + data + presentation):
**Domain:**
- `videocall_error.dart` — enums de error/success/screenMode
- `videocall_participant.dart` — entidad Freezed para participantes grupales
- `videocall_signaling_repository.dart` — interface señalización backend
**Data:**
- `videocall_signaling_datasource.dart` — interface
- `videocall_signaling_datasource_impl.dart` — placeholder (TODO cuando backend dé spec)
- `videocall_signaling_repository_impl.dart` — impl
**State:**
- `videocall_view_state.dart` — Freezed state 1-to-1 (screenMode, sdk ready, mic/speaker/camera, canvas, error/success events)
- `videocall_view_model.dart` — Notifier 1-to-1 (init, login, call, answer, hangup, mute, speaker, camera, streams del SDK)
- `group_call_view_state.dart` — Freezed state grupal
- `group_call_view_model.dart` — Notifier grupal (join, leave, participants, streams)
**Widgets:**
- `video_view_widget.dart` — renderiza JCMediaDeviceVideoCanvas (iOS/Android)
- `call_controls_widget.dart` — mic, speaker, camera, hangup (botones circulares)
- `call_status_indicator.dart` — "Llamando...", "Conectando..."
- `incoming_call_overlay.dart` — aceptar/rechazar llamada entrante (fullscreen)
- `participant_tile_widget.dart` — tile individual con video + nombre
- `participant_grid_widget.dart` — grid responsivo de participantes
**Screen:**
- `videocall_screen.dart` — 4 modos: idle (input userID + botón llamar), outgoing (llamando...), incoming (overlay aceptar/rechazar), inCall (video fullscreen + PIP + controles)
**Routing:**
- `videocall_builder.dart` — GoRouter builder
- Ruta: `/legacy/dashboard/device_management/videocall`
### 7. Code review realizado
Score: **6/10 — Request changes**
**Issues identificados (pendientes de corregir):**
1. Hardcoded test credentials (`p_test1/test123`) en UI de producción → guardar con `kDebugMode`
2. `_onCallItemRemove` llama async sin await → race condition
3. Todos los errores mapean a `I18n.errorGeneric` → sin diferenciación para el usuario
4. `videocall_screen.dart` (310 líneas) demasiado grande → extraer `_IdleView` y `_InCallView` a ficheros separados como `ConsumerWidget`
5. `group_call_view_model.dart` es dead code (no lo consume ninguna screen)
6. Signaling placeholder con `throw UnimplementedError` → cambiar a no-op
7. `VideocallParticipant` (domain) expone tipo SDK (`JCMediaDeviceVideoCanvas`) → mover al ViewModel
---
## Dónde quedamos
- **Rama:** `feature/videocall-sdk-integration`
- Los cambios del paquete `videocall_sdk` están **commiteados y pusheados** (3 commits)
- Los cambios de la feature están en disco pero **sin commitear** (necesitan correcciones del code review)
- `fusion-app` avanzó y revirtió algunos cambios compartidos (permisos, rutas) → hay que re-sincronizar
---
## Pendiente
### Correcciones del code review
- [ ] Guardar test credentials con `kDebugMode`
- [ ] Fix async race en `_onCallItemRemove`
- [ ] Implementar mensajes de error diferenciados
- [ ] Extraer `_IdleView` y `_InCallView` a ficheros separados
- [ ] Integrar o excluir group call ViewModel
- [ ] Cambiar signaling placeholder de throw a no-op
- [ ] Remover SDK type de domain entity
### Pruebas APP↔APP (primera llamada real)
- [ ] Login con 2 userIDs de prueba (`p_test1`, `p_test2`)
- [ ] Videollamada entre dos teléfonos físicos
- [ ] Probar incoming call, reject, hangup, mute, camera switch
- [ ] Probar app cerrada en iOS (riesgo #1 — push/background)
### Integración con backend
- [ ] Obtener API REST del backend SaveFamily para señalización
- [ ] Definir formato userID con backend (`p_<cuenta>` + sanitización emails)
- [ ] Implementar datasource de señalización
### Pruebas APP↔Reloj
- [ ] Llamada APP → Reloj
- [ ] Llamada Reloj → APP
- [ ] Llamadas grupales
### Producción
- [ ] Token auth (backend genera tokens con AppSecret)
- [ ] AppKeys separadas por entorno
- [ ] Push/background iOS (PushKit + CallKit si necesario)
---
## 3 riesgos abiertos antes del pago ($8,835)
| # | Riesgo | Estado |
|---|---|---|
| 1 | **Push/background iOS** — la doc no menciona FCM/APNs, CallKit ni ConnectionService. App cerrada = no recibe llamadas. Posible deal-breaker | ❌ Sin respuesta |
| 2 | **GDPR sin DPA** — servidores UE pero sin DPA, sin control routing, datos de menores | ❌ Email enviado 01-04, sin respuesta |
| 3 | **Chat sin spec** — $2,950 sin lista de features, "mira SeTracker2" | ❌ Sin spec |
| + | **Encryption** — $2,950 pagados, cero documentación del módulo | ❌ Sin spec |
---
## Arquitectura confirmada
```
APP (Flutter + jc_sdk) ←→ Juphoon Cloud (solo media)
↕ API REST
Backend SaveFamily ←→ Backend i365/SeTracker ←→ Smartwatch (firmware + jrtc_* C API)
↕ TCP plano
```
- El "Server" del protocolo TCP es **i365**, NO Juphoon
- Juphoon Cloud **solo rutea audio/video** (media plane)
- La señalización (quién llama a quién) va por el backend
## Naming conventions (protocolo TCP)
| Tipo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + APP account | `p_abc10086` |
| Group room | `did` + `_group` | `0245423235_group` |
| Single room | `did` + `_` + APP account | `0245423235_abc10086` |
`@` y `.` se reemplazan por `_` en room numbers y userIDs.
## Documentación de referencia
- Quickstart V1.1: `~/Downloads/Video call API_ Juphoon Flutter SDK quickstart V1.1.pdf`
- TCP Protocol: `~/Downloads/Juphoon Video Call TCP Protocol.docx`
- Connection process: `~/Downloads/video call connection process Rev2.docx`
- Mutual dialing: `~/Downloads/video call mutual dialing process.docx`
- Schematics: `~/Downloads/schematics _2025.03.26 (2)/`
- pub.dev: https://pub.dev/packages/jc_sdk
- Consola Juphoon: https://developer.juphoon.com

View File

@@ -3,6 +3,11 @@ import java.io.FileInputStream
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
id("com.google.firebase.firebase-perf")
id("com.google.firebase.crashlytics")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
@@ -22,6 +27,9 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// Required by flutter_local_notifications (and any future libs that
// need Java 8+ APIs on older Android API levels).
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@@ -90,3 +98,13 @@ android {
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// Required by AntelopAwareMessagingService to forward FCM pushes to the
// Antelop SDK. The Antelop AAR (com.entrust.antelop:antelop) is brought
// in transitively through the flutter_treezor_entrust_sdk_bridge plugin.
implementation(platform("com.google.firebase:firebase-bom:33.4.0"))
implementation("com.google.firebase:firebase-messaging")
implementation("com.entrust.antelop:antelop:2.6.4")
}

View File

@@ -0,0 +1,48 @@
{
"project_info": {
"project_number": "535646668726",
"project_id": "sf-platform-pre",
"storage_bucket": "sf-platform-pre.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:535646668726:android:c3a09d6c26f0cdf95e6317",
"android_client_info": {
"package_name": "com.savefamily.app.dev"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:535646668726:android:b87245b807258e3e5e6317",
"android_client_info": {
"package_name": "com.savefamily.app.stag"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -21,3 +21,11 @@
-dontwarn com.huawei.hms.location.LocationServices
-dontwarn com.huawei.hms.push.RemoteMessage
-dontwarn com.huawei.hms.security.SecComponentInstallWizard
## Juphoon jc_sdk
-dontwarn com.juphoon.*
-keep class com.juphoon.**{*;}
-dontwarn com.justalk.*
-keep class com.justalk.**{*;}
-keepattributes InnerClasses
-keep class **.R$* {*;}

View File

@@ -1,9 +1,26 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<application
android:label="@string/app_name"
@@ -33,6 +50,29 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="sf_default_channel" />
<!-- Wrap FCM with Antelop SDK forwarding (see AntelopAwareMessagingService). -->
<service
android:name=".AntelopAwareMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Disable Antelop's stock FCM service so AntelopAwareMessagingService is the only handler. -->
<service
android:name="fr.antelop.exposed.DefaultAntelopFirebaseMessagingService"
tools:node="remove" />
<!-- Disable the firebase_messaging plugin's FCM service so AntelopAwareMessagingService is the only handler. -->
<service
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService"
tools:node="remove" />
</application>
<!-- Required to query activities that can process text, see:

View File

@@ -0,0 +1,33 @@
package com.savefamily.app
import com.google.firebase.messaging.RemoteMessage
import fr.antelop.sdk.firebase.AntelopFirebaseMessagingUtil
import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService
/**
* FCM service that gives the Antelop SDK first dibs on every push, then
* delegates the rest to the firebase_messaging Flutter plugin so Dart still
* receives the notifications it expects.
*
* Without this, only one FirebaseMessagingService can win the
* com.google.firebase.MESSAGING_EVENT intent — and once we added the
* firebase_messaging plugin, its FlutterFirebaseMessagingService started
* winning over Antelop's DefaultAntelopFirebaseMessagingService, leaving the
* SDK forever waiting for activation pushes that never reached it.
*/
class AntelopAwareMessagingService : FlutterFirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val handled = AntelopFirebaseMessagingUtil.onMessageReceived(
applicationContext,
remoteMessage,
)
if (!handled) {
super.onMessageReceived(remoteMessage)
}
}
override fun onNewToken(token: String) {
super.onNewToken(token)
AntelopFirebaseMessagingUtil.onTokenRefresh(applicationContext)
}
}

View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "950566980029",
"project_id": "sf-platform-pro",
"storage_bucket": "sf-platform-pro.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:950566980029:android:75a7c10b6259d09681aad4",
"android_client_info": {
"package_name": "com.savefamily.app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDkjNdOAK0ype7wgdgiC1BCKV_pP4s_mlA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# permission_handler: enable contacts permission
PERMISSION_CONTACTS=1

View File

@@ -21,6 +21,9 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
id("com.google.firebase.firebase-perf") version "1.4.2" apply false
id("com.google.firebase.crashlytics") version "3.0.2" apply false
}
include(":app")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,5 +0,0 @@
{
"env": "development",
"apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/",
"apiOrigin": "bde6ea73-d09c-475f-aabf-1d11137e4d0d"
}

View File

@@ -1,5 +0,0 @@
{
"env": "production",
"apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/",
"apiOrigin": "https://neki-b2b.neki.es"
}

View File

@@ -1,5 +0,0 @@
{
"env": "staging",
"apiBaseUrl": "https://api-platform.pre.savefamilygps.net/gateway/api/",
"apiOrigin": "https://platform.pre.savefamilygps.net"
}

View File

@@ -0,0 +1,637 @@
# Catálogo de Analíticas — SaveFamily (módulo legacy)
> Documento para el equipo de Marketing. Describe cada evento de Firebase
> Analytics que la app envía desde el módulo legacy: qué significa, cuándo se
> dispara, qué parámetros trae, y qué insight ofrece.
>
> **Ambiente:** Eventos visibles en vivo en Firebase Console Analytics
> **DebugView** (para builds debug/profile con el flag de debug activado).
> Los reportes históricos están en **Realtime**, **Engagement Events** y
> **Engagement Pages and screens**.
>
> **Parámetro común:** Cada evento incluye automáticamente un parámetro
> `consent_status` (`true` / `false`) para permitir filtrado por
> consentimiento GDPR cuando corresponda.
---
## Índice
1. [User Properties (propiedades del usuario)](#user-properties)
2. [Screen Views (vistas de pantalla automáticas)](#screen-views)
3. [Autenticación (`legacy_auth_*`)](#autenticación)
4. [Cuenta (`legacy_account_*`)](#cuenta)
5. [Dispositivo — Setup / alta (`legacy_device_setup_*`)](#dispositivo--setup)
6. [Dispositivo — Funciones (`legacy_device_*`)](#dispositivo--funciones)
7. [Contactos del dispositivo (`legacy_contacts_*`)](#contactos-del-dispositivo)
8. [Ajustes (`legacy_settings_*`)](#ajustes)
9. [Soporte (`legacy_support_*`)](#soporte)
10. [Onboarding (`legacy_onboarding_*`)](#onboarding)
11. [Panel principal (`legacy_control_panel_*`)](#panel-principal)
12. [Ubicación y mapa (`legacy_location_*`)](#ubicación-y-mapa)
---
## User Properties
Son propiedades que se setean una sola vez por usuario (al hacer login) y
sirven para **segmentar** a los usuarios en los reportes. Cualquier evento
puede cruzarse por estas dimensiones en Firebase Analytics.
| Propiedad | Descripción | Valores ejemplo | Cuándo se setea |
|---|---|---|---|
| `env` | Ambiente de la app | `development`, `staging`, `production` | Al arrancar la app |
| `user_id` (interna) | Identificador único del usuario | UUID del backend | Al confirmar el login (después del 2FA) |
| `user_role` | Rol del usuario en el backend | `client`, `admin`, etc. | Al login |
| `user_language` | Idioma preferido del usuario | `es`, `en`, `fr`, `de`, `it`, `pt` | Al login |
| `user_signup_date` | Fecha de creación de la cuenta (ISO 8601 UTC) | `2024-04-07T10:34:42.000Z` | Al login |
| `user_has_phone` | Si tiene teléfono registrado | `true` / `false` | Al login |
| `user_has_api_key` | Si tiene una API key asignada (usuario técnico) | `true` / `false` | Al login |
> **Nota futura:** Cuando se lance el plan premium, se agregará
> `user_plan` (`free` / `premium` / `family`) para segmentar la base por
> plan.
---
## Screen Views
Cada vez que el usuario navega a una pantalla, Firebase recibe un evento
automático `screen_view` con el parámetro `screen_name` igual al nombre
lógico de la ruta (no el nombre de clase Flutter).
**Esto se captura automáticamente**, sin instrumentación manual en cada
pantalla, mediante un listener del router. **También captura los cambios de
tab del bottom navigation** (home device functions mapa chat).
### Pantallas del módulo legacy que se trackean
| Screen name | Pantalla |
|---|---|
| `splash` | Pantalla de carga inicial |
| `legacy_onboarding` | Intro/onboarding |
| `legacy_login` | Pantalla de login |
| `legacy_signup` | Alta de cuenta |
| `legacy_recover_password` | Recuperación de contraseña |
| `legacy_device_setup` | Wizard de alta de reloj/dispositivo |
| `legacy_request_link_phone` | Inicio de vinculación de teléfono |
| `legacy_verify_link_phone_code` | Verificación del código OTP |
| `control_panel` | Dashboard principal (home del legacy) |
| `customer_service` | Pantalla de soporte |
| `account_settings` | Menú de cuenta |
| `personal_data` | Editar datos personales |
| `change_password` | Cambiar contraseña |
| `linked_devices` | Dispositivos vinculados a la cuenta |
| `app_users` | Sub-usuarios de la app |
| `delete_account` | Flujo de eliminación de cuenta |
| `device_management` | Menú de gestión del dispositivo |
| `scheduled_activities` | Actividades programadas |
| `contacts` | Contactos del dispositivo |
| `edit_contact` | Editar un contacto |
| `health` | Salud (ritmo cardíaco, SpO2) |
| `remote_connection` | Conexión remota (cámara, llamada) |
| `locate_device` | Localizar dispositivo |
| `rewards` | Recompensas |
| `activity_meter` | Medidor de actividad (pasos) |
| `apps_use` | Uso de apps |
| `volume_control` | Control de volumen |
| `call_history` | Historial de llamadas |
| `background_image` | Imagen de fondo del dispositivo |
| `legacy_location` | Mapa de ubicación |
| `legacy_chat` | Chat (placeholder) |
| `settings` | Menú de ajustes |
| `alarm` | Alarmas |
| `remote_management` | Gestión remota |
| `sos_agenda` | Contactos SOS |
| `sound` | Sonido del dispositivo |
| `sync_clock` | Sincronización de reloj |
| `app_store` | Gestión de apps instaladas |
| `battery` | Batería / modo nocturno |
| `block_phone` | Bloqueo de teléfono (whitelist) |
| `disable_functions` | Desactivar funciones (teclado, GPS) |
| `language` | Idioma del dispositivo |
| `legacy_notifications` | Notificaciones del dispositivo |
| `remote_on_off` | Encendido/apagado remoto |
| `alerts` | Alertas |
| `timezone` | Zona horaria |
| `wifi_settings` | Configuración WiFi |
**Insight para marketing:** Con estas screen_view podés construir funnels
(ej: `legacy_login control_panel device_management locate_device`) y
medir tiempos entre pantallas, rebotes y pantallas más visitadas.
---
## Autenticación
Prefijo `legacy_auth_*` — cubre login, 2FA, signup, recuperación de
contraseña, vinculación de teléfono y logout.
### Login
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_login_attempt` | El usuario pulsa "Iniciar sesión" después de validar el formulario en el cliente. | — | Tope del funnel de login. Usar como base del "100 %" del funnel. |
| `legacy_auth_login_success` | El backend aceptó email + contraseña. Aún falta el 2FA. | — | Credenciales válidas. Usar para medir la calidad de la contraseña/email. |
| `legacy_auth_login_failure` | El backend rechazó las credenciales o hubo un error de red. | `reason` (mensaje de error) | Fricción. Analizar los `reason` más frecuentes para detectar problemas. |
### 2FA (doble factor)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_2fa_requested` | El backend envió el código 2FA al usuario. | — | Usuario pasó el primer paso del login. |
| `legacy_auth_2fa_verified` | El código 2FA fue verificado y la sesión está activa. | — | Login exitoso. Fin del funnel de login. |
| `legacy_auth_2fa_failure` | El código 2FA fue rechazado (incorrecto, expirado). | `reason` | Fricción en el 2FA. Si es muy alto, puede indicar problemas con la entrega del código. |
| `legacy_auth_2fa_resend` | El usuario pidió reenviar el código. | — | Indica que no le llegó el primero. Útil para medir problemas de entrega. |
### Signup
El signup es un wizard de **2 pasos** (`step_index` 0, 1):
- **Paso 0 — Datos personales:** nombre, apellido, email, teléfono (con picker de país), aceptación de términos.
- **Paso 1 — Contraseña:** password y repeat password con validación de reglas.
El `language` se infiere del locale del dispositivo al momento del submit.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_signup_started` | El usuario envió el formulario final (submit del paso 1). | — | Top del funnel de request al backend. |
| `legacy_auth_signup_completed` | El backend creó la cuenta exitosamente. | — | Conversión de nueva cuenta. |
| `legacy_auth_signup_failed` | Error en el alta (email ya existe, datos inválidos, error de red). | `reason` | Drop-off del signup. Analizar `reason` para ver si hay patrones. |
| `legacy_auth_signup_step_completed` | El usuario avanzó a un paso siguiente (validación pasó). | `step_index` (0, 1) — el paso que JUSTO terminó | Funnel interno del signup. Permite ver cuántos completan paso 0 y llegan al 1. |
| `legacy_auth_signup_step_back` | El usuario tocó "atrás" dentro del wizard. | `step_index` — el paso del que vuelve | Indica que el usuario quiere corregir algo — fricción. |
| `legacy_auth_signup_step_validation_failed` | El usuario tocó "siguiente" pero la validación del formulario lo rechazó. | `step_index` | **Muy valioso:** dice en qué paso hay más problemas de validación. Si step 0 falla: nombre/apellido, email, teléfono o términos. Si step 1 falla: reglas de contraseña. |
### Recuperación de contraseña
Flujo **exclusivo por email** (se removió la opción de SMS/teléfono).
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_password_reset_requested` | El usuario inició el flujo de recuperar contraseña tipeando su email. | — | Fricción: alguien no recuerda su contraseña. |
| `legacy_auth_password_reset_email_sent` | El backend confirmó el envío del email de recuperación. | — | Confirma que el email salió. Cruzar con `reset_requested` para medir fallas. |
| `legacy_auth_password_reset_completed` | El usuario guardó la nueva contraseña exitosamente. | — | **Conversión final** del funnel de recuperación. |
| `legacy_auth_password_reset_failed` | Error al intentar guardar la nueva contraseña. | `reason` (`unequal_passwords`, `too_short`, `no_capitals`, `no_numbers`, `no_special_chars`, o mensaje del backend) | Permite ver qué reglas de validación molestan más a los usuarios. |
### Vinculación de teléfono
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_link_phone_code_requested` | El usuario envió su número y pidió el código OTP. | — | Inicio del flujo de linking. |
| `legacy_auth_link_phone_code_request_failed` | Falló el pedido del código al backend. | `reason` | Fricción inicial. |
| `legacy_auth_link_phone_code_verified` | El código OTP fue verificado con éxito. | — | Número vinculado. |
| `legacy_auth_link_phone_code_verification_failed` | Falló la verificación (código incorrecto o expirado). | `reason` | Fricción en el segundo paso. |
### Logout
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_logout` | El usuario cerró sesión y la app limpió la sesión local. | — | Señal de fin de sesión. Cruzar con duración de sesión para ver patrones de uso. |
---
## Cuenta
Prefijo `legacy_account_*` — cubre edición de perfil, contraseña,
dispositivos vinculados, usuarios de la app y **eliminación de cuenta
(señal crítica de churn)**.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_account_personal_data_edited` | El usuario guardó cambios en sus datos personales (nombre, apellido, teléfono). | — | Engagement con la cuenta. |
| `legacy_account_password_changed` | Cambio de contraseña exitoso. | — | Señal de buen hábito de seguridad. |
| `legacy_account_password_change_failed` | El cambio de contraseña falló. | `reason` | Fricción. |
| `legacy_account_linked_device_unlinked` | El usuario quitó un dispositivo vinculado de su cuenta. | — | Posible señal temprana de desuso del dispositivo. |
| `legacy_account_linked_device_renamed` | El usuario renombró un dispositivo vinculado (editó el carrier name). | — | Personalización / engagement con la gestión de dispositivos. |
| `legacy_account_app_user_delete_triggered` | El usuario tocó "eliminar" en la pantalla de app users. | — | Nota técnica: la implementación actual borra al usuario logueado (parece ser placeholder). El evento se mantiene para medir demanda del feature. |
| `legacy_account_deletion_initiated` | **CHURN SIGNAL** — El usuario entró al flujo "Eliminar cuenta". | — | Top del funnel de churn. |
| `legacy_account_deletion_confirmed` | El usuario confirmó la eliminación y la API call está en progreso. | — | El usuario quiere realmente irse. |
| `legacy_account_deletion_completed` | El backend confirmó la eliminación. | — | Usuario perdido. |
| `legacy_account_deletion_cancelled` | El usuario canceló antes de confirmar la eliminación. | — | Save: el usuario se arrepintió. Útil para medir efectividad de pantallas de retención. |
---
## Dispositivo — Setup
Prefijo `legacy_device_setup_*`**el momento aha del producto**: vincular
un reloj/dispositivo del hijo a la cuenta del padre/madre.
### Funnel del wizard
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_setup_started` | El usuario entró al wizard de alta de dispositivo. | — | Top del funnel de activación. |
| `legacy_device_setup_step_completed` | El usuario completó un paso del wizard. | `step` (`intro`, `link_info`, `scan_watch`, `profile`), `duration_seconds` (cuánto tardó en ese paso) | Permite ver dónde se abandona más el wizard **Y cuánto tiempo pasan los usuarios en cada paso** — fricción directa. |
| `legacy_device_setup_completed` | El dispositivo se creó exitosamente y está vinculado. | `child_gender` (M/F/other), `relation_type` (mother/father/etc), `child_age_years` | **Conversión de activación + demográficos del usuario final**. Marketing puede construir **personas reales** con estos 3 params: género, edad y relación con el adulto que compró. |
| `legacy_device_setup_failed` | Falló un paso del wizard. | `at_step` (en qué paso falló), `reason` (error) | Señal para el equipo técnico de dónde hay problemas. |
| `legacy_device_setup_cancelled` | El usuario volvió atrás y abandonó el wizard. | `at_step` | Drop-off del wizard. |
### Entrada del código del reloj (QR vs. manual)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_setup_qr_scanned` | El usuario escaneó exitosamente el código QR del reloj. | — | Método "rápido". Si su ratio baja, el QR scanner puede estar fallando. |
| `legacy_device_setup_manual_code_entered` | El usuario avanzó con el código tipeado manualmente (no escaneó). | — | Fallback. Si crece mucho el ratio vs QR, invertir en mejorar la UX del scanner. |
### Familias con múltiples hijos
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_setup_reset_for_new_kid` | Después de terminar un alta, el usuario tocó "agregar otro hijo". | — | **Señal de familia con múltiples hijos**. Estos usuarios típicamente tienen mayor retention y LTV — son el mejor segmento. |
---
## Dispositivo — Funciones
Prefijo `legacy_device_*` — acciones sobre el dispositivo ya vinculado.
Mide qué features del producto se usan más.
### Localización del dispositivo (comando "find")
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_locate_requested` | El usuario pulsó el botón de localizar (intento). | — | Uso del feature principal del producto. Top del mini-funnel de localización. |
| `legacy_device_locate_success` | El comando de localizar fue enviado con éxito al backend. | — | El dispositivo va a sonar. Conversión del mini-funnel. |
| `legacy_device_locate_failure` | El comando de localizar falló (error del backend o de red). | `reason` | Problema técnico al localizar. Drop-off del mini-funnel. |
### Conexión remota (cámara + llamadas)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_remote_connection_started` | El usuario entró a la pantalla de conexión remota. | — | Intención de interactuar remotamente. |
| `legacy_device_remote_connection_photo_taken` | El usuario pidió una foto de la cámara remota. | — | Feature avanzada. Permite medir uso de la cámara del reloj. |
| `legacy_device_remote_connection_call_initiated` | El usuario inició una llamada bidireccional. | — | Feature crítica: llamar al niño. |
| `legacy_device_remote_connection_picture_viewed` | El usuario navegó entre fotos de la cámara remota. | `direction` (`next`, `prev`, `direct`) | Engagement con la galería: cuántas fotos revisa el padre después de pedirlas. |
### Volumen del dispositivo
Cada envío del formulario dispara **un evento por tipo de volumen que
efectivamente cambió** (media, ringtone, alarm) — si el usuario movió solo
el media, solo se manda ese. Permite medir qué tipo de sonido configuran
más los padres.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_volume_control_changed` | El usuario guardó un cambio de volumen en el dispositivo, se emite 1 vez por cada tipo modificado. | `type` (`media`, `ringtone`, `alarm`), `level` (0-100) | Configuración. Cruzar `type` para ver cuál se ajusta más. |
### Imagen de fondo del reloj
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_background_image_changed` | El usuario seleccionó una imagen existente como fondo. | — | Personalización. |
| `legacy_device_background_image_uploaded` | El usuario subió una foto personal como fondo. | — | Alta personalización — indicador de engagement. |
### Actividades programadas (alarmas personalizadas del dispositivo)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_scheduled_activity_added` | El usuario agregó una actividad programada. | `week_day` (0-6, 0 = domingo), `period` (`HH:mm-HH:mm`) | **Dato muy útil:** permite ver qué horarios programan los padres (desayuno, colegio, deberes, etc) y qué días. |
| `legacy_device_scheduled_activity_updated` | El usuario editó una actividad programada. | `week_day`, `period` | Refinamiento de configuración. |
| `legacy_device_scheduled_activity_removed` | El usuario eliminó una actividad programada. | — | |
### Recompensas
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_rewards_granted` | El usuario asignó minutos de recompensa al dispositivo. | `amount` (cantidad de minutos) | Gamificación / recompensas de uso. |
### Podómetro (Activity Meter)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_activity_pedometer_toggled` | El usuario activó/desactivó el contador de pasos. | `enabled` (`true` / `false`) | |
| `legacy_device_activity_meter_time_range_changed` | El usuario cambió el rango de fechas en la pantalla de pasos. | `range` (`today`, `seven_days`, `thirty_days`, `custom`) | **Engagement profundo:** el padre no solo abre la pantalla, sino que investiga distintos períodos. |
### Salud (ritmo cardíaco / SpO2)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_health_heart_rate_frequency_changed` | El usuario cambió la frecuencia de medición del ritmo cardíaco. | `frequency_seconds` | Personalización de monitoreo de salud. |
| `legacy_device_health_measurement_started` | El usuario inició una medición manual de ritmo cardíaco. | — | Interés en datos de salud del niño. |
| `legacy_device_health_time_range_changed` | El usuario cambió el rango de fechas en la pantalla de salud. | `range` | Engagement profundo: padres revisando el historial de salud. |
### Uso de aplicaciones (Apps Use)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_apps_use_time_range_changed` | El usuario cambió el rango de fechas en la pantalla de uso de apps. | `range`, `total_duration_seconds` (total acumulado del período), `top_app_name` (app más usada en ese período) | **El evento más rico del módulo.** Permite a marketing segmentar directo: "padres cuyos hijos usan más TikTok que YouTube", "familias con uso > 4hs/día", etc. |
### Historial de llamadas
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_call_history_filter_changed` | El usuario cambió el filtro del historial. | `filter` (`all`, `incoming`, `outgoing`, `missed`) | **Cuando se filtra `missed` es señal de preocupación** del padre: busca llamadas perdidas del hijo. |
---
## Contactos del dispositivo
Prefijo `legacy_contacts_*` — contactos permitidos para llamadas desde el
dispositivo del niño.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_contacts_added` | El usuario agregó un contacto al dispositivo. | `total_count` (cantidad total de contactos DESPUÉS del add) | Configuración inicial o expansión de la agenda. El `total_count` permite segmentar "padres con agenda chica vs grande". |
| `legacy_contacts_edited` | El usuario editó un contacto existente. | — | |
| `legacy_contacts_deleted` | El usuario eliminó un contacto del dispositivo. | `total_count` (cantidad total DESPUÉS del delete) | |
---
## Ajustes
Prefijo `legacy_settings_*` — configuración general del dispositivo
(alarmas, SOS, bloqueos, idioma, red, etc).
### Alarmas
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_alarm_added` | Alarma nueva creada. | `time` (`HH:mm`) | Uso del feature de alarma. El `time` permite ver qué horarios son más populares (despertador matutino, hora del colegio, etc). |
| `legacy_settings_alarm_updated` | Alarma existente editada. | `time` (el NUEVO `HH:mm`) | Refinamiento. |
| `legacy_settings_alarm_removed` | Alarma eliminada. | — | |
### Contactos SOS
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_sos_contact_added` | Contacto SOS agregado. | `total_count` | Configuración de seguridad. Muy importante. |
| `legacy_settings_sos_contact_removed` | Contacto SOS removido. | `total_count` | |
### Whitelist del teléfono (bloqueo de llamadas)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_block_phone_contact_added` | Contacto agregado a la whitelist de llamadas permitidas. | `total_count` | Control parental. |
| `legacy_settings_block_phone_contact_removed` | Contacto removido de la whitelist. | `total_count` | |
### Control parental (funciones desactivadas)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_disable_functions_changed` | El usuario guardó cambios en la pantalla de funciones desactivadas. | — | Engagement con control parental (evento agregado). |
| `legacy_settings_disable_functions_keyboard_toggled` | Se guardó con el teclado habilitado/deshabilitado. | `enabled` | Control granular. |
| `legacy_settings_disable_functions_gps_toggled` | Se guardó con el GPS habilitado/deshabilitado. | `enabled` | Control granular. |
### Otros ajustes del dispositivo
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_language_changed` | Se cambió el idioma del dispositivo. | `language` (ej. `es`, `en`) | |
| `legacy_settings_alerts_configured` | El usuario guardó cambios en las alertas. | `alert_count` (cuántas alertas activas), `alerts_enabled` (lista separada por comas truncada a 100 chars) | Permite ver qué alertas son más populares y cuántas alertas promedio configuran los padres. |
| `legacy_settings_timezone_changed` | Se cambió la zona horaria. | `timezone` | |
| `legacy_settings_wifi_added` | Se agregó una red WiFi permitida. | `total_count` | |
| `legacy_settings_wifi_removed` | Se eliminó una red WiFi permitida. | `total_count` | |
| `legacy_settings_sound_changed` | Se cambió el modo de sonido del dispositivo. | `mode` (`normal` / `silent` / `vibrate`) | Preferencia de perfil sonoro del niño. |
| `legacy_settings_sync_clock_triggered` | El usuario disparó una sincronización manual del reloj del dispositivo. | — | |
| `legacy_settings_battery_night_mode_toggled` | El usuario activó/desactivó el modo nocturno (ahorro de batería). | `enabled` | |
### Gestión remota del dispositivo (comandos destructivos)
Estos eventos son **muy importantes** para churn analysis. Un
`factory_reset` típicamente precede a un desvinculado y potencial churn.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_remote_management_shutdown` | El usuario apagó el dispositivo remotamente. | — | Acción poco frecuente. |
| `legacy_settings_remote_management_restart` | El usuario reinició el dispositivo remotamente. | — | Típicamente usado cuando hay problemas técnicos. |
| `legacy_settings_remote_management_factory_reset` | **CHURN SIGNAL** — El usuario reseteó el dispositivo a fábrica. | — | Borra el dispositivo. Frecuentemente precede un `legacy_account_linked_device_unlinked` y luego `legacy_account_deletion_*`. Cruzar para medir correlación. |
---
## Soporte
Prefijo `legacy_support_*` — solo 1 evento hoy, medirá la demanda de
soporte.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_support_contact_initiated` | El usuario tocó el botón para contactar soporte (ej. abrir el cliente de email). | `channel` (`email` hoy; en el futuro también `phone`, `whatsapp`), `country` (país seleccionado en el formulario) | Demanda de soporte **por país**: permite ver dónde se originan más tickets. Nota: mide la **intención** de contactar, no confirma envío. |
---
## Onboarding
Prefijo `legacy_onboarding_*` — los slides de intro iniciales de la app.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_onboarding_step_changed` | El usuario pasó a un nuevo slide del intro. | `step_index` (número de slide, empieza en 0) | Medir cuántos slides el usuario ve antes de empezar. |
---
## Panel principal
Prefijo `legacy_control_panel_*` — acciones en el home del legacy.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_control_panel_device_selected` | El usuario cambió el dispositivo activo (útil cuando hay varios hijos). | `total_devices` (cuántos dispositivos tiene vinculados) | Qué dispositivo está monitoreando activamente. El `total_devices` permite **segmentar por tamaño de familia** (1 hijo, 2 hijos, 3+). |
| `legacy_control_panel_positions_refreshed` | El usuario tiró del pull-to-refresh o tocó "actualizar" en el dashboard. | — | Preocupación activa del usuario. Indicador de engagement alto. |
---
## Ubicación y mapa
Prefijo `legacy_location_*`**el feature más rico del producto**. Acá
capturamos toda la interacción del usuario con el mapa: ver el trayecto,
crear zonas seguras, ver lugares frecuentes, cambiar frecuencia de
actualización, etc.
### Geofences (zonas seguras) — CRUD básico
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_geofence_created` | Se creó una geofence (API confirmó). | — | **Conversión final** del funnel de creación. |
| `legacy_location_geofence_updated` | Se actualizó una geofence existente. | — | Refinamiento de configuración de zonas. |
| `legacy_location_geofence_deleted` | Se eliminó una geofence. | — | |
### Lugares frecuentes — CRUD básico
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_frequent_place_created` | Se creó un lugar frecuente (API confirmó). | — | **Conversión final** del funnel. |
| `legacy_location_frequent_place_updated` | Se actualizó un lugar frecuente. | — | |
| `legacy_location_frequent_place_deleted` | Se eliminó un lugar frecuente. | — | |
### Funnel de creación de lugares (geofences y frequent places)
Este es el funnel más valioso del módulo de ubicación. Permite medir
**cuánta gente empieza a crear una zona vs. cuánta termina**.
```
legacy_location_place_creation_started (top: 100 %)
legacy_location_point_confirmed (paso 1 completado)
legacy_location_radius_confirmed (solo geofences — paso 2)
legacy_location_geofence_created (bottom: API OK)
o legacy_location_frequent_place_created
```
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_place_creation_started` | El usuario tocó "agregar zona" o "agregar lugar frecuente". | `mode` (`geofence` / `frequent_place`) | Top del funnel. |
| `legacy_location_point_confirmed` | El usuario tocó el mapa para fijar el centro del lugar. | `mode` | Paso 1 del funnel completado. |
| `legacy_location_radius_confirmed` | El usuario confirmó el radio de la geofence (solo aplica a geofences). | `radius` (metros), `is_editing` (`true` si estaba editando una existente, `false` si es nueva) | Paso 2 del funnel completado. Permite también **analizar qué tamaños de zonas eligen los usuarios** (radios más comunes casa, escuela, etc.). |
| `legacy_location_place_creation_cancelled` | El usuario salió del flujo de creación/edición antes de terminar. | `mode`, `at_step` (`picking_point` o `adjusting_radius`) | **Drop-off del funnel**. El `at_step` dice exactamente dónde lo perdimos. |
### Exploración y edición
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_geofence_selected` | El usuario tocó una geofence del mapa para verla. | — | Engagement: el usuario está mirando sus zonas. |
| `legacy_location_geofence_dismissed` | El usuario cerró el popup de la geofence sin hacer nada. | — | "Miró pero no editó". Indicador de exploración. |
| `legacy_location_geofence_edit_started` | El usuario tocó "editar" en una geofence seleccionada. | — | Intención de editar. Mid-funnel de edición. |
| `legacy_location_frequent_place_selected` | El usuario tocó un lugar frecuente para verlo. | — | Engagement. |
| `legacy_location_frequent_place_dismissed` | El usuario cerró el popup del lugar frecuente. | — | |
| `legacy_location_history_position_selected` | El usuario tocó un punto del historial de ubicaciones en el mapa. | — | Inspección detallada del trayecto. |
| `legacy_location_history_position_dismissed` | El usuario cerró el detalle del punto de historial. | — | |
### Historial de ubicaciones
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_history_loaded` | El usuario cargó el historial para un rango de fechas. | — | Interés en el historial. |
| `legacy_location_history_cleared` | El usuario limpió el trayecto del mapa. | — | |
### Frecuencia de ubicación (privacidad vs. precisión)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_frequency_updated` | El usuario cambió cada cuánto el dispositivo manda su posición. | `frequency_seconds` (ej. `60`, `300`, `900`) | **Dato súper útil:** indica preferencia entre privacidad y precisión/batería. Correlacionar con retention. |
### Capas del mapa (toggles de visibilidad)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_map_geofences_toggled` | El usuario mostró/ocultó las geofences en el mapa. | `visible` (`true` / `false`) | |
| `legacy_location_map_frequent_places_toggled` | El usuario mostró/ocultó los lugares frecuentes. | `visible` | |
| `legacy_location_map_route_trail_toggled` | El usuario mostró/ocultó la línea del trayecto histórico. | `visible` | |
### Modo "seguir en vivo"
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_following_toggled` | El usuario activó/desactivó el modo "seguir dispositivo" (el mapa se re-centra automáticamente). | `enabled` (`true` / `false`) | **Engagement alto:** el usuario está viendo al hijo en tiempo real. Correlacionar con horarios (ej. entrada/salida del cole). |
### UI del mapa (chrome)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_map_actions_expanded` | El usuario abrió/cerró el drawer de acciones del mapa. | `expanded` (`true` / `false`) | Indica conocimiento de la UI. |
| `legacy_location_map_zoomed` | El usuario hizo zoom in/out y se quedó en ese nivel (con debounce de 1 segundo para no spamear). | `zoom` (nivel de zoom final) | Nivel de detalle con el que los usuarios miran el mapa. Un zoom alto indica "me importa ver dónde exactamente está". |
| `legacy_location_map_style_changed` | El usuario eligió otro estilo visual para el mapa desde el selector de capas. | `style` (`standard` / `voyager` / `light` / `dark` / `satellite`) | Personalización de la experiencia. **Satellite** es el más usado por padres que quieren ver edificios reales. |
| `legacy_location_map_center_tapped` | El usuario tocó el botón "centrar en el dispositivo" del mapa. | — | Acción de re-centrado manual. Indica que el mapa se desplazó y el usuario quiere volver al hijo. |
| `legacy_location_map_refresh_tapped` | El usuario tocó el botón de refresco dentro del mapa (distinto del pull-to-refresh del control panel). | — | **Engagement intenso:** el usuario quiere la posición más reciente AHORA. Suele dispararse en momentos de ansiedad. |
| `legacy_location_shared` | El usuario tocó "compartir ubicación" — abre el share sheet nativo para mandar la posición del hijo a otra app. | — | **Acción viral del producto.** Es la más importante para crecimiento orgánico: indica que el usuario está mandando data del producto a contactos fuera de la app (familia, pareja, abuelos). |
| `legacy_location_list_sheet_opened` | El usuario abrió el bottom sheet con la lista de geofences, lugares frecuentes e historial. | — | Quiere explorar todo lo que tiene configurado. Mid-funnel de gestión. |
| `legacy_location_history_type_filter_changed` | El usuario filtró el historial por tipo de posición. | `type` (`gps` / `wifi` / `sos` / `all` cuando limpia el filtro) | Indica interés en una fuente de datos específica. **`sos`** filtrado es señal de un evento crítico que el usuario está investigando. |
---
## Cómo usar este catálogo
### Para construir funnels
Tomá un evento "inicio" y uno "fin" en Firebase Analytics Engagement
**Funnels** y comparalos:
- **Signup:** `legacy_auth_signup_started _completed`
- **Login:** `legacy_auth_login_attempt _2fa_verified`
- **Activación (aha moment):** `legacy_device_setup_started _completed`
- **Creación de zona segura:** `legacy_location_place_creation_started _geofence_created`
- **Churn:** `legacy_account_deletion_initiated _deletion_completed`
### Para segmentar audiencias
En **Audiences** podés filtrar por user properties (`user_language`,
`user_has_phone`, etc.) y cruzarlo con cualquiera de estos eventos.
### Para detectar problemas
Filtrar por los eventos con `_failed` o `_failure` y mirar los `reason`
más frecuentes en la pestaña Events Parameter.
### Para medir engagement diario
Los eventos `legacy_control_panel_positions_refreshed`,
`legacy_location_following_toggled` y las screen_views del mapa son los
indicadores más fuertes de usuarios activos y preocupados.
---
## Eventos propuestos para el futuro (NO implementados aún)
Esta sección es la **wishlist** para cuando existan los features o lleguen
las decisiones pendientes.
### Cuando exista el plan premium/suscripción
- `purchase` / `purchase_subscription` (con `value`, `currency`, `transaction_id`)
- `action_click_gopremium` (botón de upgrade)
- `subscription_error_payment` / `subscription_canceled_payment`
- User property `user_plan` (`free` / `premium` / `family`)
### Limit popups / free-tier walls
- `legacy_limit_hit` con `limit_type` (max_devices, max_contacts, etc.)
- `legacy_limit_popup_shown`
- `legacy_limit_popup_upgrade_clicked`
### Referral / invitación
- `legacy_referral_screen_viewed`
- `legacy_referral_code_shared` (con `channel`)
- `legacy_referral_signup_completed`
### NPS / rating
- `legacy_nps_prompt_shown`
- `legacy_nps_score_submitted` (con `score` 010)
- `legacy_app_rating_submitted`
### Push notification engagement
- `legacy_notification_received` (background)
- `legacy_notification_opened` (tap app abre)
- `legacy_notification_dismissed`
### Aha moments
- `legacy_first_device_connected` (primera vez que el usuario vincula un dispositivo — requiere persistencia de "primera vez")
- `legacy_first_session_completed`
### A/B testing
- `ab_test_<experiment_name>` (cuando empecemos experimentos con Remote Config)
### Errores de API / health técnica
- `legacy_api_error` con `endpoint`, `status_code` (detectar endpoints flakey)
- `legacy_session_expired`
---
## Referencias técnicas
- **Proyecto Firebase:** `sf-platform-pre` (para dev+staging) / `sf-platform-prod` (pendiente de crear)
- **Package Dart:** `packages/sf_tracking/`
- **Mixins:** Cada grupo de eventos vive en un mixin aparte dentro del package (`auth_tracking.dart`, `location_tracking.dart`, etc).
- **GDPR:** Cada evento incluye automáticamente el parámetro `consent_status` para permitir filtrado post-hoc en BigQuery cuando se implemente el consent screen.
- **Ambiente:** `env` se setea como user property (`development` / `staging` / `production`), por lo que **todos los reportes pueden filtrarse por ambiente** y producción no se va a mezclar con testing.
---
## Changelog del catálogo
- **2026-04-07** — Creación inicial. 61 eventos del módulo legacy implementados y validados en device físico (iPhone 14 Pro iOS 18 + Samsung Galaxy A55 Android 15).
- **2026-04-07** — Se agregaron 16 eventos nuevos al módulo de ubicación (funnel de creación, exploración, edición, follow mode, map zoom debounced, history).
- **2026-04-07** — Se expandió el tracking de `device_management` con 8 eventos nuevos y 3 enriquecimientos de parámetros:
- NUEVOS: `legacy_device_locate_success/failure`, `legacy_device_remote_connection_picture_viewed`, `legacy_device_activity_meter_time_range_changed`, `legacy_device_health_time_range_changed`, `legacy_device_apps_use_time_range_changed` (con total_duration_seconds y top_app_name), `legacy_device_call_history_filter_changed`.
- ENRIQUECIDOS: `legacy_device_volume_control_changed` ahora dispara un evento por cada tipo (media/ringtone/alarm) que efectivamente cambió; `legacy_device_scheduled_activity_added/updated` ahora incluyen `week_day` y `period`; `legacy_contacts_added/deleted` ahora incluyen `total_count`.
- **2026-04-07** — Se expandió el tracking de `settings` con 3 eventos nuevos y 7 enriquecimientos de parámetros:
- NUEVOS: `legacy_settings_remote_management_shutdown/restart/factory_reset` (churn signal crítico).
- ENRIQUECIDOS: `legacy_settings_alarm_added/updated` ahora incluyen `time`; `legacy_settings_sos_contact_added/removed` incluyen `total_count`; `legacy_settings_block_phone_contact_added/removed` incluyen `total_count`; `legacy_settings_wifi_added/removed` incluyen `total_count`; `legacy_settings_sound_changed` incluye `mode`; `legacy_settings_alerts_configured` incluye `alert_count` y `alerts_enabled`.
- **2026-04-07** — Se expandió el tracking de `device_setup` con 3 eventos nuevos y 2 enriquecimientos críticos:
- NUEVOS: `legacy_device_setup_qr_scanned`, `legacy_device_setup_manual_code_entered`, `legacy_device_setup_reset_for_new_kid` (señal de familias con múltiples hijos).
- ENRIQUECIDOS: `legacy_device_setup_step_completed` ahora incluye `duration_seconds` (tiempo por paso — fricción directa); `legacy_device_setup_completed` ahora incluye `child_gender`, `relation_type`, `child_age_years` ( demográficos del usuario final para personas de marketing).
- **2026-04-07** — Se expandió el tracking de `legacy_auth` con 3 eventos nuevos para el funnel interno del signup:
- NUEVOS: `legacy_auth_signup_step_completed`, `legacy_auth_signup_step_back`, `legacy_auth_signup_step_validation_failed` (originalmente con `step_index` 0-2; reducido a 0-1 en abril 2026 al simplificar el signup).
- **2026-04-07** — Pasada final de cobertura en `legacy_auth`, `account`, `support`, `control_panel`: 6 eventos nuevos y 2 enriquecimientos.
- NUEVOS AUTH: `legacy_auth_password_reset_completed`, `legacy_auth_password_reset_failed` (con `reason` granular), `legacy_auth_link_phone_code_request_failed`, `legacy_auth_link_phone_code_verification_failed`.
- NUEVOS ACCOUNT: `legacy_account_linked_device_renamed`, `legacy_account_app_user_delete_triggered`.
- ENRIQUECIDOS: `legacy_support_contact_initiated` ahora incluye `country` además de `channel`; `legacy_control_panel_device_selected` ahora incluye `total_devices` (proxy de tamaño de familia).
- **2026-04-07** — Se expandió la cobertura de los widgets del módulo `location` con 6 eventos nuevos sobre acciones top-level del mapa:
- NUEVOS: `legacy_location_map_style_changed` (selector de capas), `legacy_location_map_center_tapped`, `legacy_location_map_refresh_tapped`, `legacy_location_shared` ( acción viral del producto), `legacy_location_list_sheet_opened`, `legacy_location_history_type_filter_changed` (con `type` para detectar interés en posiciones SOS).
- **2026-04-15** — Cambios de producto en `legacy_auth`:
- **Signup reducido a 2 pasos** (antes 3). Se quitaron los campos de documento, fecha de nacimiento, lugar de nacimiento, país de nacimiento, relación con el niño y dirección completa. El request al backend ahora solo incluye `firstName`, `lastName`, `email`, `phone` (E.164), `language` (del locale del dispositivo) y `password`. `step_index` de los eventos `legacy_auth_signup_step_*` pasa de 0-2 a 0-1.
- **Recover password solo por email**: se eliminó la UI de teléfono móvil en ambos screens del flujo (`request_recovery` y `new_password`). Los eventos del flujo se mantienen igual pero ahora siempre corresponden al canal email. Se eliminó del state `recoveryFormat` (ya no hay rama SMS).
- **User properties (Firebase Analytics)** ahora se sincronizan solo en shells autenticados (dashboards legacy y payment), no en rutas públicas. Los eventos en sí no cambian — solo se movió el disparador de la sync para evitar llamadas espurias a `/auth/me` en login/signup/recover_password.

View File

@@ -0,0 +1,353 @@
# Catálogo de Eventos — SaveFamily
> Documento para el equipo de Marketing. Lista todos los eventos que la app registra y describe el momento exacto en que se dispara cada uno.
---
## Índice
1. [Pantallas de la app](#pantallas-de-la-app)
2. [Autenticación](#autenticación)
3. [Cuenta](#cuenta)
4. [Alta de dispositivo (reloj/wearable del niño)](#alta-de-dispositivo)
5. [Funciones del dispositivo](#funciones-del-dispositivo)
6. [Contactos del dispositivo](#contactos-del-dispositivo)
7. [Ajustes del dispositivo](#ajustes-del-dispositivo)
8. [Soporte](#soporte)
9. [Onboarding](#onboarding)
10. [Panel principal (home)](#panel-principal)
11. [Ubicación y mapa](#ubicación-y-mapa)
---
## Pantallas de la app
Cada vez que el usuario navega a una pantalla, queda registrada
automáticamente. También se registran los cambios entre pestañas del menú
inferior.
Pantallas registradas:
- Pantalla de carga inicial
- Onboarding / intro
- Login
- Alta de cuenta (signup)
- Recuperación de contraseña
- Wizard de alta de reloj/dispositivo
- Inicio de vinculación de teléfono
- Verificación del código de vinculación
- Dashboard principal (home)
- Soporte / atención al cliente
- Menú de cuenta
- Editar datos personales
- Cambiar contraseña
- Dispositivos vinculados a la cuenta
- Sub-usuarios de la app
- Eliminación de cuenta
- Menú de gestión del dispositivo
- Actividades programadas
- Contactos
- Editar contacto
- Salud (ritmo cardíaco, oxígeno en sangre)
- Conexión remota (cámara y llamada)
- Localizar dispositivo
- Recompensas
- Medidor de actividad / pasos
- Uso de aplicaciones
- Control de volumen
- Historial de llamadas
- Imagen de fondo del dispositivo
- Mapa de ubicación
- Chat
- Menú de ajustes
- Alarmas
- Gestión remota
- Contactos SOS
- Sonido del dispositivo
- Sincronización del reloj
- Gestión de apps instaladas
- Batería / modo nocturno
- Bloqueo de teléfono (whitelist)
- Desactivar funciones (teclado, GPS)
- Idioma del dispositivo
- Notificaciones del dispositivo
- Encendido/apagado remoto
- Alertas
- Zona horaria
- Configuración WiFi
---
## Autenticación
### Login
- **legacy_auth_login_attempt** — El usuario pulsa "Iniciar sesión" después de completar el formulario.
- **legacy_auth_login_success** — Email y contraseña aceptados (todavía falta el segundo factor).
- **legacy_auth_login_failure** — El intento de login fue rechazado.
### Doble factor (2FA)
- **legacy_auth_2fa_requested** — Se le envió el código de verificación al usuario.
- **legacy_auth_2fa_verified** — El código fue aceptado y la sesión está activa (login completado).
- **legacy_auth_2fa_failure** — El código fue rechazado (incorrecto o expirado).
- **legacy_auth_2fa_resend** — El usuario pidió que le reenvíen el código.
### Alta de cuenta (signup)
El alta es un wizard de 2 pasos (datos personales → contraseña).
- **legacy_auth_signup_started** — El usuario envió el formulario final del alta.
- **legacy_auth_signup_completed** — La cuenta se creó exitosamente.
- **legacy_auth_signup_failed** — El alta falló.
- **legacy_auth_signup_step_completed** — El usuario completó un paso del wizard y avanzó al siguiente.
- **legacy_auth_signup_step_back** — El usuario volvió al paso anterior dentro del wizard.
- **legacy_auth_signup_step_validation_failed** — El usuario intentó avanzar pero el formulario tenía errores.
### Recuperación de contraseña
Flujo exclusivo por email (no hay opción de SMS).
- **legacy_auth_password_reset_requested** — El usuario inició el flujo de "olvidé mi contraseña" tipeando su email.
- **legacy_auth_password_reset_email_sent** — Se envió el email con el enlace de recuperación.
- **legacy_auth_password_reset_completed** — El usuario guardó exitosamente su nueva contraseña.
- **legacy_auth_password_reset_failed** — El intento de guardar la nueva contraseña falló.
### Vinculación de teléfono
- **legacy_auth_link_phone_code_requested** — El usuario envió su número y pidió el código.
- **legacy_auth_link_phone_code_request_failed** — Falló el envío del código.
- **legacy_auth_link_phone_code_verified** — El código fue verificado, número vinculado.
- **legacy_auth_link_phone_code_verification_failed** — El código no fue aceptado.
### Cierre de sesión
- **legacy_auth_logout** — El usuario cerró sesión.
---
## Cuenta
- **legacy_account_personal_data_edited** — El usuario guardó cambios en sus datos personales (nombre, apellido, teléfono).
- **legacy_account_password_changed** — El usuario cambió su contraseña exitosamente.
- **legacy_account_password_change_failed** — El cambio de contraseña falló.
- **legacy_account_linked_device_unlinked** — El usuario quitó un dispositivo vinculado de su cuenta.
- **legacy_account_linked_device_renamed** — El usuario renombró un dispositivo vinculado.
- **legacy_account_app_user_delete_triggered** — El usuario tocó "eliminar" en la pantalla de sub-usuarios.
- **legacy_account_deletion_initiated** — El usuario entró al flujo de "eliminar cuenta". Señal temprana de churn.
- **legacy_account_deletion_confirmed** — El usuario confirmó la eliminación.
- **legacy_account_deletion_completed** — La cuenta se eliminó.
- **legacy_account_deletion_cancelled** — El usuario canceló antes de confirmar la eliminación.
---
## Alta de dispositivo
Vincular el reloj/dispositivo del niño a la cuenta del adulto.
### Wizard de alta
- **legacy_device_setup_started** — El usuario entró al wizard de alta de dispositivo.
- **legacy_device_setup_step_completed** — El usuario completó un paso del wizard. Se registra cuánto tiempo tardó en ese paso.
- **legacy_device_setup_completed** — El dispositivo quedó vinculado. Se registra género y edad del niño y la relación con el adulto.
- **legacy_device_setup_failed** — Falló el alta del dispositivo.
- **legacy_device_setup_cancelled** — El usuario abandonó el wizard.
### Cómo se introdujo el código del reloj
- **legacy_device_setup_qr_scanned** — El usuario escaneó el código QR del reloj.
- **legacy_device_setup_manual_code_entered** — El usuario tipeó el código manualmente.
### Familias con varios hijos
- **legacy_device_setup_reset_for_new_kid** — Después de terminar un alta, el usuario eligió "agregar otro hijo".
---
## Funciones del dispositivo
### Localizar dispositivo
- **legacy_device_locate_requested** — El usuario pulsó el botón de localizar.
- **legacy_device_locate_success** — La orden de localizar se envió al dispositivo.
- **legacy_device_locate_failure** — La orden de localizar falló.
### Conexión remota (cámara y llamada)
- **legacy_device_remote_connection_started** — El usuario abrió la pantalla de conexión remota.
- **legacy_device_remote_connection_photo_taken** — El usuario pidió una foto desde la cámara remota.
- **legacy_device_remote_connection_call_initiated** — El usuario inició una llamada con el dispositivo.
- **legacy_device_remote_connection_picture_viewed** — El usuario navegó entre las fotos tomadas remotamente.
### Volumen del dispositivo
- **legacy_device_volume_control_changed** — El usuario guardó un cambio de volumen. Se dispara una vez por cada tipo modificado (multimedia, tono de llamada, alarma).
### Imagen de fondo del reloj
- **legacy_device_background_image_changed** — El usuario eligió una imagen existente como fondo.
- **legacy_device_background_image_uploaded** — El usuario subió una foto personal como fondo.
### Actividades programadas (rutinas en el dispositivo)
- **legacy_device_scheduled_activity_added** — El usuario creó una nueva actividad programada. Se registra el día de la semana y el horario.
- **legacy_device_scheduled_activity_updated** — El usuario editó una actividad programada.
- **legacy_device_scheduled_activity_removed** — El usuario eliminó una actividad programada.
### Recompensas
- **legacy_device_rewards_granted** — El usuario otorgó minutos de recompensa al dispositivo. Se registra la cantidad de minutos.
### Podómetro / pasos
- **legacy_device_activity_pedometer_toggled** — El usuario activó o desactivó el contador de pasos.
- **legacy_device_activity_meter_time_range_changed** — El usuario cambió el rango de fechas en la pantalla de pasos (hoy, 7 días, 30 días, personalizado).
### Salud (ritmo cardíaco / oxígeno en sangre)
- **legacy_device_health_heart_rate_frequency_changed** — El usuario cambió la frecuencia con la que se mide el ritmo cardíaco.
- **legacy_device_health_measurement_started** — El usuario inició una medición manual.
- **legacy_device_health_time_range_changed** — El usuario cambió el rango de fechas en la pantalla de salud.
### Uso de aplicaciones del dispositivo
- **legacy_device_apps_use_time_range_changed** — El usuario cambió el rango de fechas en la pantalla de uso de apps. Se registra el tiempo total acumulado y la app más usada del período.
### Historial de llamadas
- **legacy_device_call_history_filter_changed** — El usuario filtró el historial (todas, entrantes, salientes, perdidas). Filtrar perdidas suele ser señal de preocupación del adulto.
---
## Contactos del dispositivo
Contactos permitidos para llamar al/desde el dispositivo del niño.
- **legacy_contacts_added** — El usuario agregó un contacto. Se registra cuántos contactos tiene en total.
- **legacy_contacts_edited** — El usuario editó un contacto existente.
- **legacy_contacts_deleted** — El usuario eliminó un contacto. Se registra el total restante.
---
## Ajustes del dispositivo
### Alarmas
- **legacy_settings_alarm_added** — El usuario creó una alarma. Se registra la hora.
- **legacy_settings_alarm_updated** — El usuario editó una alarma.
- **legacy_settings_alarm_removed** — El usuario eliminó una alarma.
### Contactos SOS
- **legacy_settings_sos_contact_added** — El usuario agregó un contacto SOS.
- **legacy_settings_sos_contact_removed** — El usuario eliminó un contacto SOS.
### Whitelist de llamadas (bloqueo de teléfono)
- **legacy_settings_block_phone_contact_added** — El usuario agregó un contacto a la lista de llamadas permitidas.
- **legacy_settings_block_phone_contact_removed** — El usuario quitó un contacto de la lista de llamadas permitidas.
### Control parental (funciones desactivadas)
- **legacy_settings_disable_functions_changed** — El usuario guardó cambios en la pantalla de funciones desactivadas.
- **legacy_settings_disable_functions_keyboard_toggled** — El usuario activó o desactivó el teclado.
- **legacy_settings_disable_functions_gps_toggled** — El usuario activó o desactivó el GPS.
### Otros ajustes
- **legacy_settings_language_changed** — El usuario cambió el idioma del dispositivo.
- **legacy_settings_alerts_configured** — El usuario guardó cambios en las alertas. Se registra cuántas alertas activas tiene y cuáles están encendidas.
- **legacy_settings_timezone_changed** — El usuario cambió la zona horaria.
- **legacy_settings_wifi_added** — El usuario agregó una red WiFi permitida.
- **legacy_settings_wifi_removed** — El usuario eliminó una red WiFi permitida.
- **legacy_settings_sound_changed** — El usuario cambió el modo de sonido del dispositivo (normal / silencio / vibración).
- **legacy_settings_sync_clock_triggered** — El usuario disparó una sincronización manual del reloj.
- **legacy_settings_battery_night_mode_toggled** — El usuario activó o desactivó el modo nocturno (ahorro de batería).
### Gestión remota del dispositivo
- **legacy_settings_remote_management_shutdown** — El usuario apagó el dispositivo a distancia.
- **legacy_settings_remote_management_restart** — El usuario reinició el dispositivo a distancia.
- **legacy_settings_remote_management_factory_reset** — El usuario restauró el dispositivo a fábrica. Suele preceder al desvinculado y al churn.
---
## Soporte
- **legacy_support_contact_initiated** — El usuario tocó el botón para contactar a soporte. Se registra el canal (email) y el país seleccionado en el formulario.
---
## Onboarding
- **legacy_onboarding_step_changed** — El usuario pasó a un nuevo slide del intro inicial.
---
## Panel principal
- **legacy_control_panel_device_selected** — El usuario cambió el dispositivo activo (útil cuando hay varios hijos). Se registra cuántos dispositivos tiene vinculados.
- **legacy_control_panel_positions_refreshed** — El usuario refrescó manualmente el dashboard (pull-to-refresh o botón de actualizar).
---
## Ubicación y mapa
### Zonas seguras (geofences)
- **legacy_location_geofence_created** — Se creó una nueva zona segura.
- **legacy_location_geofence_updated** — Se editó una zona segura existente.
- **legacy_location_geofence_deleted** — Se eliminó una zona segura.
### Lugares frecuentes
- **legacy_location_frequent_place_created** — Se creó un nuevo lugar frecuente.
- **legacy_location_frequent_place_updated** — Se editó un lugar frecuente existente.
- **legacy_location_frequent_place_deleted** — Se eliminó un lugar frecuente.
### Funnel de creación de lugares (zonas y frecuentes)
- **legacy_location_place_creation_started** — El usuario tocó "agregar zona" o "agregar lugar frecuente".
- **legacy_location_point_confirmed** — El usuario fijó el centro del lugar tocando el mapa.
- **legacy_location_radius_confirmed** — El usuario confirmó el radio de la zona segura. Se registra el tamaño del radio.
- **legacy_location_place_creation_cancelled** — El usuario abandonó el flujo de creación o edición. Se registra en qué paso lo dejó.
### Exploración y edición
- **legacy_location_geofence_selected** — El usuario tocó una zona segura del mapa para verla.
- **legacy_location_geofence_dismissed** — El usuario cerró el detalle de la zona sin hacer cambios.
- **legacy_location_geofence_edit_started** — El usuario tocó "editar" en una zona seleccionada.
- **legacy_location_frequent_place_selected** — El usuario tocó un lugar frecuente para verlo.
- **legacy_location_frequent_place_dismissed** — El usuario cerró el detalle del lugar frecuente.
- **legacy_location_history_position_selected** — El usuario tocó un punto del historial de ubicaciones en el mapa.
- **legacy_location_history_position_dismissed** — El usuario cerró el detalle del punto del historial.
### Historial de ubicaciones
- **legacy_location_history_loaded** — El usuario cargó el historial de ubicaciones para un rango de fechas.
- **legacy_location_history_cleared** — El usuario limpió el trayecto del mapa.
### Frecuencia de ubicación
- **legacy_location_frequency_updated** — El usuario cambió cada cuánto el dispositivo manda su posición.
### Capas del mapa
- **legacy_location_map_geofences_toggled** — El usuario mostró u ocultó las zonas seguras en el mapa.
- **legacy_location_map_frequent_places_toggled** — El usuario mostró u ocultó los lugares frecuentes en el mapa.
- **legacy_location_map_route_trail_toggled** — El usuario mostró u ocultó la línea del trayecto histórico.
### Modo "seguir en vivo"
- **legacy_location_following_toggled** — El usuario activó o desactivó el modo "seguir dispositivo" (el mapa se re-centra automáticamente sobre el niño).
### Acciones del mapa
- **legacy_location_map_actions_expanded** — El usuario abrió o cerró el menú de acciones del mapa.
- **legacy_location_map_zoomed** — El usuario hizo zoom y se quedó en ese nivel.
- **legacy_location_map_style_changed** — El usuario eligió otro estilo de mapa (estándar, claro, oscuro, satélite, etc.).
- **legacy_location_map_center_tapped** — El usuario tocó "centrar en el dispositivo" para volver el mapa sobre el niño.
- **legacy_location_map_refresh_tapped** — El usuario tocó refrescar dentro del mapa para pedir la posición más reciente.
- **legacy_location_shared** — El usuario compartió la ubicación del niño hacia otra app (familia, pareja, abuelos). Acción viral del producto.
- **legacy_location_list_sheet_opened** — El usuario abrió la lista con todas sus zonas, lugares frecuentes e historial.
- **legacy_location_history_type_filter_changed** — El usuario filtró el historial por tipo de posición (GPS, WiFi, SOS). Filtrar SOS suele indicar que está investigando un evento crítico.

View File

@@ -0,0 +1,282 @@
# Integración Videollamadas — Juphoon jc_sdk
## Estado general
| Fase | Estado |
|---|---|
| 1. SDK wrapper (`videocall_sdk`) | ✅ Completado |
| 2. Configuración nativa (permisos) | ✅ Completado |
| 3. Configuración por entorno (AppKey) | ✅ Completado |
| 4. Feature videocall (UI + lógica) | ⏳ Pendiente |
| 5. Pruebas APP↔APP | ⏳ Pendiente |
| 6. Integración con backend (señalización) | ⏳ Pendiente |
| 7. Pruebas APP↔Reloj | ⏳ Pendiente |
| 8. Token auth (producción) | ⏳ Pendiente |
| 9. Push/background iOS | ⏳ Pendiente (sin doc del proveedor) |
| 10. Chat | ⏳ Pendiente (sin spec del proveedor) |
---
## Fase 1: SDK wrapper — ✅
Paquete `packages/videocall_sdk/` con wrap 100% de `jc_sdk` v2.16.5.
### Arquitectura (patrón sca_treezor)
- Constructor injection (no singletons estáticos)
- GetIt module (`videocallSdkModule(config)`)
- `VideocallSdkManager` orquestador de inicialización
- `VideocallSdkConfig` abstracto para config por entorno
- Riverpod providers + StreamProviders para UI reactiva
- Callbacks del SDK → Dart Streams
### Servicios (7 total, cobertura 100%)
- `VideocallClient` → JCClient (auth, login, logout, messaging)
- `VideocallCallService` → JCCall (llamadas 1-to-1)
- `VideocallDeviceService` → JCMediaDevice (cámara, mic, speaker)
- `VideocallChannelService` → JCMediaChannel (llamadas grupales)
- `VideocallPushService` → JCPush (push notifications)
- `VideocallNetService` → JCNet (estado de red)
- `VideocallLogService` → JCLog (logging)
### Estructura
```
packages/videocall_sdk/lib/src/
├── config/videocall_sdk_config.dart
├── di/videocall_sdk_module.dart
├── manager/videocall_sdk_manager.dart
├── models/ (call_state, call_direction, videocall_item, etc.)
├── services/ (7 servicios)
└── providers/videocall_providers.dart
```
---
## Fase 2: Permisos nativos — ✅
### Android (`AndroidManifest.xml`)
- [x] INTERNET (ya existía)
- [x] ACCESS_NETWORK_STATE (ya existía)
- [x] ACCESS_WIFI_STATE
- [x] CAMERA (ya existía)
- [x] RECORD_AUDIO
- [x] MODIFY_AUDIO_SETTINGS
- [x] BLUETOOTH
- [x] uses-feature: hardware.camera
- [x] uses-feature: hardware.camera.autofocus
- [x] uses-feature: hardware.bluetooth (optional)
### Android (`proguard-rules.pro`)
- [x] -keep com.juphoon.**
- [x] -keep com.justalk.**
- [x] -keepattributes InnerClasses
### iOS (`Info.plist`)
- [x] NSCameraUsageDescription (actualizado: QR + videollamadas)
- [x] NSMicrophoneUsageDescription
- [x] NSPhotoLibraryUsageDescription
### iOS (`Podfile`)
- [x] PERMISSION_CAMERA=1
- [x] PERMISSION_PHOTOS=1
- [x] PERMISSION_MICROPHONE=1
---
## Fase 3: Config por entorno — ✅
- [x] `juphoonAppKey` en development.json, staging.json, production.json
- [x] `Environment.juphoonAppKey` via `String.fromEnvironment()`
- [x] `SaveFamilyVideocallConfig` implementa `VideocallSdkConfig`
- [x] `videocallSdkModule(config)` llamado en `init_app.dart`
- [ ] AppKeys separadas por entorno (por ahora las 3 usan la misma clave de dev)
---
## Fase 4: Feature videocall — ⏳ SIGUIENTE
Feature en `modules/legacy/modules/device_management/lib/src/features/videocall/`
### Por hacer
- [ ] `videocall_builder.dart` — GoRouter builder
- [ ] `domain/entities/videocall_entity.dart` — Freezed entity
- [ ] `domain/entities/videocall_error.dart` — Error enum con i18n
- [ ] `presentation/state/videocall_view_model.dart` — Notifier
- [ ] `presentation/state/videocall_view_state.dart` — Freezed state
- [ ] `presentation/videocall_screen.dart` — Pantalla de llamada
- [ ] `presentation/widgets/local_video_view.dart` — Video local
- [ ] `presentation/widgets/remote_video_view.dart` — Video remoto
- [ ] `presentation/widgets/call_controls.dart` — Botones (colgar, mute, cámara)
- [ ] `presentation/widgets/incoming_call_dialog.dart` — Dialog llamada entrante
- [ ] Providers en `core/providers/`
- [ ] Ruta en GoRouter (`app_router.dart`)
- [ ] Runtime permissions (pedir cámara/mic en runtime)
---
## Fase 5: Pruebas APP↔APP — ⏳
- [ ] Login con 2 userIDs de prueba (`p_test1`, `p_test2`)
- [ ] Llamada de voz APP→APP
- [ ] Videollamada APP→APP
- [ ] Responder llamada entrante
- [ ] Rechazar llamada
- [ ] Colgar durante llamada
- [ ] Mute/unmute micrófono
- [ ] Cambiar cámara frontal/trasera
- [ ] Speaker on/off
- [ ] Llamada perdida (onMissedCallItem)
- [ ] Verificar desfase versión SDK (quickstart 1.0.2 vs pub.dev 2.16.5)
---
## Fase 6: Integración backend — ⏳
- [ ] Obtener documentación API REST del backend SaveFamily para señalización
- [ ] Endpoint para iniciar llamada → notificar al reloj
- [ ] Endpoint para recibir notificación de llamada entrante del reloj
- [ ] Formato userID definido con backend (`p_<cuenta>` vs `w_<IMEI>`)
- [ ] Sanitización de emails (@ → _ en userIDs)
---
## Fase 7: Pruebas APP↔Reloj — ⏳
- [ ] Llamada APP → Reloj
- [ ] Llamada Reloj → APP
- [ ] Videollamada grupal (JCMediaChannel)
- [ ] Límite 5 min de llamada
- [ ] Registro IMEI (protocolo RYIMEI) — lo hace el backend
---
## Fase 8: Token auth — ⏳
- [ ] Backend implementa generación de tokens Juphoon (usa AppSecret)
- [ ] App pide token al backend antes del login
- [ ] Token se pasa como `password` en `VideocallClient.login()`
- [ ] Activar Token鉴权 en consola Juphoon (ya está activo)
Nota: para dev/testing no es necesario — `autoCreateAccount = true` en LoginParam permite login con cualquier password.
---
## Fase 9: Push/Background iOS — ⏳ RIESGO
**Problema:** No hay documentación de cómo recibir llamadas con la app cerrada en iOS.
- [ ] Probar qué pasa cuando la app está cerrada y llega una llamada (fase 5)
- [ ] Si no funciona: investigar PushKit + CallKit
- [ ] Verificar si `JCPush` del SDK resuelve esto
- [ ] Consultar pestaña "消息通知服务" en la consola Juphoon
- [ ] Si es deal-breaker: escalar antes del pago ($8,835)
---
## Fase 10: Chat — ⏳ SIN SPEC
- [ ] Obtener especificación del módulo de chat del proveedor
- [ ] Determinar si usa JCMediaChannel (SDK) o API propia
- [ ] $2,950 pagados sin lista de features
---
## Credenciales Juphoon Cloud
| Campo | Valor | Quién lo usa |
|---|---|---|
| AppKey | `9efcf2d889dc8a0320925096` | App Flutter + Backend |
| AppSecret | `ui7pr73ggl5rr0gf01np` | Solo Backend |
| AES_KEY (IoT) | `8e3637pG7E9144E0` | Solo Backend |
| Consola | juphoon.com (+34 603675786) | Julián |
---
## Naming conventions (protocolo TCP)
| Tipo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + APP account | `p_abc10086` |
| Group room | `did` + `_group` | `0245423235_group` |
| Single room | `did` + `_` + APP account | `0245423235_abc10086` |
Nota: `@` y `.` se reemplazan por `_` en room numbers y userIDs.
---
## Documentación de referencia
- Quickstart V1.1: `~/Downloads/Video call API_ Juphoon Flutter SDK quickstart V1.1.pdf`
- TCP Protocol: `~/Downloads/Juphoon Video Call TCP Protocol.docx`
- Connection process: `~/Downloads/video call connection process Rev2.docx`
- Mutual dialing: `~/Downloads/video call mutual dialing process.docx`
- Schematics: `~/Downloads/schematics _2025.03.26 (2)/`
- pub.dev: https://pub.dev/packages/jc_sdk
- Consola: https://developer.juphoon.com
---
## Flujos de llamada (protocolo TCP + Juphoon SDK)
### APP → Reloj (outgoing)
1. App envía `VIDEO_CALL_REQUEST` al backend con `chatType`, `appAccount`, `roomNumber`, `sessionId`
2. Backend reenvía la notificación al reloj via TCP
3. App inicia audio/cámara y llama al watch account via SDK (`startCall(userId: "w_<IMEI>")`)
4. Reloj contesta → SDK notifica via `callItemUpdateStream` (estado `isTalking`)
5. App envía `VIDEO_CALL_ROOM_COUNT_REQUEST` con `type` (0/1), `count: 2`, `room_num`
### Reloj → APP (incoming)
1. Reloj envía notificación de llamada al backend
2. Backend notifica a la app (requiere app abierta con SDK inicializado, ver Fase 9)
3. SDK detecta llamada entrante via `callItemAddStream` con `CallDirection.incoming`
4. Usuario acepta → `answerCall()` → SDK conecta
5. Reloj reporta participantes al backend
### Colgar / Rechazar
- **Colgar (en llamada):** `hangUp()` en SDK + `VIDEO_CALL_CANCEL` al backend
- **Rechazar (incoming):** `hangUp()` en SDK + `VIDEO_CALL_REFUSE` al backend con `appAccount`, `roomNumber`
### Convenciones de nombres
| Campo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + email sanitizado | `p_user_example_com` |
| Room (single) | `deviceId` + `_` + appAccount | `0245423235_p_user_example_com` |
| Room (group) | `deviceId` + `_group` | `0245423235_group` |
| Session ID | `deviceId` + `_` + epoch en segundos | `0245423235_1714150800` |
Sanitización: `@` y `.` se reemplazan por `_` en userIDs y roomNumbers.
### Configuración del SDK por tipo de dispositivo
- RTOS watches: `MediaConfig.MODE_RTOS`
- Android watches: `MediaConfig.MODE_INTELLIGENT_HARDWARE`
- Se determina con `device.capabilities.system` (`isRtos` / `isAndroid`)
### Auto-login
- userId: `p_` + email sanitizado (ej: `p_julian_test_com`)
- password: `user.id` (UUID del usuario padre)
- En dev/testing `autoCreateAccount = true` permite login con cualquier password
---
## Limitaciones actuales
### Recepción de llamadas requiere app abierta
La app debe estar en primer plano con el SDK inicializado y el client logueado para recibir llamadas entrantes. Si el app está en background o cerrada, las llamadas no llegan. Esto se resuelve en Fase 9 (Push/Background).
### Sin timeout de llamada
El protocolo menciona un límite de 5 min por llamada, pero no está implementado en la app. El reloj podría manejar el corte por su lado.
---
## Pendientes por verificar
- **chatType**: El protocolo TCP usa `0` (single) y `1` (multi) como enteros. Nuestra app envía `"single"`/`"multi"` como strings en el JSON del comando. Verificar que el backend hace la conversión correctamente antes de enviar al reloj via TCP.

View File

@@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"sf-platform-pre","appId":"1:535646668726:android:b87245b807258e3e5e6317","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"sf-platform-pre","appId":"1:535646668726:ios:5172d626d02dfe215e6317","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options_dev.dart":{"projectId":"sf-platform-pre","configurations":{"android":"1:535646668726:android:c3a09d6c26f0cdf95e6317","ios":"1:535646668726:ios:524afa641f61d7cb5e6317"}},"lib/firebase_options_staging.dart":{"projectId":"sf-platform-pre","configurations":{"android":"1:535646668726:android:b87245b807258e3e5e6317","ios":"1:535646668726:ios:5172d626d02dfe215e6317"}},"lib/firebase_options_prod.dart":{"projectId":"sf-platform-pro","configurations":{"android":"1:950566980029:android:75a7c10b6259d09681aad4","ios":"1:950566980029:ios:987b4f0b9e9b897481aad4"}}}}}}

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '13.4'
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -45,5 +45,14 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
## Juphoon jc_sdk: enable camera, photos and microphone permissions
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_PHOTOS=1',
'PERMISSION_MICROPHONE=1'
]
end
end
end

View File

@@ -1,20 +1,250 @@
PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0)
- Firebase/Crashlytics (12.9.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.9.0)
- Firebase/Messaging (12.9.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.9.0)
- Firebase/Performance (12.9.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 12.9.0)
- Firebase/RemoteConfig (12.9.0):
- Firebase/CoreOnly
- FirebaseRemoteConfig (~> 12.9.0)
- firebase_analytics (12.2.0):
- firebase_core
- FirebaseAnalytics (= 12.9.0)
- Flutter
- firebase_core (4.6.0):
- Firebase/CoreOnly (= 12.9.0)
- Flutter
- firebase_crashlytics (5.1.0):
- Firebase/Crashlytics (= 12.9.0)
- firebase_core
- Flutter
- firebase_messaging (16.1.3):
- Firebase/Messaging (= 12.9.0)
- firebase_core
- Flutter
- firebase_performance (0.11.2):
- Firebase/Performance (= 12.9.0)
- firebase_core
- Flutter
- firebase_remote_config (6.3.0):
- Firebase/RemoteConfig (= 12.9.0)
- firebase_core
- Flutter
- FirebaseABTesting (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseAnalytics (12.9.0):
- FirebaseAnalytics/Default (= 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleAppMeasurement/Default (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreInternal (12.9.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- FirebaseRemoteConfigInterop (~> 12.9.0)
- FirebaseSessions (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.9.0):
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebasePerformance (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- FirebaseRemoteConfig (~> 12.9.0)
- FirebaseSessions (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (12.9.0):
- FirebaseABTesting (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- FirebaseRemoteConfigInterop (~> 12.9.0)
- FirebaseSharedSwift (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseRemoteConfigInterop (12.9.0)
- FirebaseSessions (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (12.9.0)
- Flutter (1.0.0)
- flutter_contacts (0.0.1):
- Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_ringtone_player (0.0.1):
- Flutter
- flutter_treezor_entrust_sdk_bridge (0.0.1):
- Flutter
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.9.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.9.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.9.0)
- GoogleAppMeasurement/IdentitySupport (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.9.0):
- GoogleAppMeasurement/Core (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- jc_sdk (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- record_ios (1.2.0):
- Flutter
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
@@ -22,47 +252,168 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`)
- Flutter (from `Flutter`)
- flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_ringtone_player (from `.symlinks/plugins/flutter_ringtone_player/ios`)
- flutter_treezor_entrust_sdk_bridge (from `.symlinks/plugins/flutter_treezor_entrust_sdk_bridge/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- jc_sdk (from `.symlinks/plugins/jc_sdk/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- Firebase
- FirebaseABTesting
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseMessaging
- FirebasePerformance
- FirebaseRemoteConfig
- FirebaseRemoteConfigInterop
- FirebaseSessions
- FirebaseSharedSwift
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- libwebp
- Mantle
- nanopb
- PromisesObjC
- PromisesSwift
- SDWebImage
- SDWebImageWebPCoder
EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
firebase_performance:
:path: ".symlinks/plugins/firebase_performance/ios"
firebase_remote_config:
:path: ".symlinks/plugins/firebase_remote_config/ios"
Flutter:
:path: Flutter
flutter_contacts:
:path: ".symlinks/plugins/flutter_contacts/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_ringtone_player:
:path: ".symlinks/plugins/flutter_ringtone_player/ios"
flutter_treezor_entrust_sdk_bridge:
:path: ".symlinks/plugins/flutter_treezor_entrust_sdk_bridge/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
jc_sdk:
:path: ".symlinks/plugins/jc_sdk/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
audioplayers_darwin: f15e209a3e856d1a7edcf98dc029f484fead2242
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_analytics: 42693ebf35c4d330b74abcb46ca80351703644e0
firebase_core: 98bcc1bd1a097bcb8b1ed6e091de3039802527c4
firebase_crashlytics: 2fd6c030ca2f91e8d3b13d2e6e9a08a282c9d259
firebase_messaging: e24e69d994d53e46fd794143544841877bd85a53
firebase_performance: 39d7f9632628c64cacd9e9808d4783cffd83eaa2
firebase_remote_config: 0d060eef0fdfb288ffc41903ba9a60bb963755ea
FirebaseABTesting: a399ffe546392a39b19a5c2fb28bd8ea178a6f47
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreExtension: e911052d59cd0da237a45d706fc0f81654f035c1
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseCrashlytics: 43913d587ef07beaf5db703baa61eacf9554658c
FirebaseInstallations: 7b64ffd006032b2b019a59b803858df5112d9eaa
FirebaseMessaging: 7d6cdbff969127c4151c824fe432f0e301210f15
FirebasePerformance: 94f614453614d8bb2a1a0177f3a1a6d2dbf4c504
FirebaseRemoteConfig: a2f6545e41551ffb520241d38b5d3d6776c9ebe8
FirebaseRemoteConfigInterop: 765ee19cd2bfa8e54937c8dae901eb634ad6787d
FirebaseSessions: a2d06fd980431fda934c7a543901aca05fc4edcc
FirebaseSharedSwift: 9d2fa84a46676302b89dbd5e6e62bce2fe376909
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_contacts: edb1c5ce76aa433e20e6cb14c615f4c0b66e0983
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_ringtone_player: 15eba85187230b87b2512f0e1b92225618bc03e7
flutter_treezor_entrust_sdk_bridge: 4c2c94fb74ab57576e8d49f5f2a4b214e41141fe
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
jc_sdk: 3c77f6d7e5e052e2960c47629f612127585779cf
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
record_ios: 26294aaa39e4bb7665b0fef78bdc23d723b432f2
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
PODFILE CHECKSUM: 02dccdf227cb9aef09ff0299e4898a8a19004223
PODFILE CHECKSUM: 88fd88ec59f7f53cf74c06ffd99479aec395968a
COCOAPODS: 1.16.2

View File

@@ -18,6 +18,7 @@
AA0000011234567800000001 /* AntelopRelease.plist in Resources */ = {isa = PBXBuildFile; fileRef = AA0000011234567800000002 /* AntelopRelease.plist */; };
AA5000010000000000000001 /* AntelopRelease-development.plist in Resources */ = {isa = PBXBuildFile; fileRef = AA5000010000000000000002 /* AntelopRelease-development.plist */; };
AA5000010000000000000003 /* AntelopRelease-staging.plist in Resources */ = {isa = PBXBuildFile; fileRef = AA5000010000000000000004 /* AntelopRelease-staging.plist */; };
D6B9158A899AF56C44180233 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B8D66015CBEA02CDD29EB55 /* GoogleService-Info.plist */; };
FB256274E508EC552E337980 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B56AB2467FA9548370ACF02 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
@@ -53,6 +54,7 @@
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B8D66015CBEA02CDD29EB55 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
401E1064C971570DADB8AA9B /* Pods-RunnerTests.profile-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-development.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-development.xcconfig"; sourceTree = "<group>"; };
4B56AB2467FA9548370ACF02 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4E688A593FA9E76BDD0DFBFB /* Pods-Runner.debug-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-staging.xcconfig"; sourceTree = "<group>"; };
@@ -144,6 +146,7 @@
331C8082294A63A400263BE5 /* RunnerTests */,
CB8808A12E373F2255B5FC16 /* Pods */,
BE496D7F3574271661ADBDCE /* Frameworks */,
3B8D66015CBEA02CDD29EB55 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -252,8 +255,11 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
F0758EB530B1A8787EB3F30B /* Copy GoogleService-Info */,
AA0000022345678900000001 /* Copy AntelopRelease */,
437F5EA1E5D92D7C421FD996 /* [CP] Embed Pods Frameworks */,
791C3CA41F1AAEE1267769C8 /* [CP] Copy Pods Resources */,
0F0F4E82D9AA0B3E11014E72 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
);
buildRules = (
);
@@ -322,12 +328,31 @@
AA0000011234567800000001 /* AntelopRelease.plist in Resources */,
AA5000010000000000000001 /* AntelopRelease-development.plist in Resources */,
AA5000010000000000000003 /* AntelopRelease-staging.plist in Resources */,
D6B9158A899AF56C44180233 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0F0F4E82D9AA0B3E11014E72 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -437,6 +462,42 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
F0758EB530B1A8787EB3F30B /* Copy GoogleService-Info */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Copy GoogleService-Info";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/scripts/copy-google-service-plist.sh\"";
};
AA0000022345678900000001 /* Copy AntelopRelease */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Copy AntelopRelease";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/scripts/copy-antelop-release-plist.sh\"";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -530,7 +591,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -543,7 +604,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-development";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-development.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -661,7 +722,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -712,7 +773,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -727,7 +788,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-development";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-development.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -751,7 +812,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-development";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-development.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -819,7 +880,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -876,7 +937,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -927,7 +988,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -981,7 +1042,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1035,7 +1096,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1087,7 +1148,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1100,7 +1161,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-staging";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-staging.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1124,7 +1185,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-production";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1148,7 +1209,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-staging";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-staging.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1171,7 +1232,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-production";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1194,7 +1255,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-staging";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-staging.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1217,7 +1278,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-production";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

View File

@@ -23,12 +23,16 @@ import AntelopSDK
override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
AntelopAppDelegate.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
// Forward to FlutterAppDelegate so Firebase Messaging can capture the APNs token via swizzling.
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard !AntelopAppDelegate.shared.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) else {
if AntelopAppDelegate.shared.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) {
return
}
// Forward to FlutterAppDelegate so Firebase Messaging can deliver the notification to Dart.
super.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
override func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ</string>
<key>GCM_SENDER_ID</key>
<string>535646668726</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app.dev</string>
<key>PROJECT_ID</key>
<string>sf-platform-pre</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pre.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:535646668726:ios:524afa641f61d7cb5e6317</string>
</dict>
</plist>

View File

@@ -47,8 +47,14 @@
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>NSContactsUsageDescription</key>
<string>Necesitamos acceso a tus contactos para seleccionar números de teléfono.</string>
<key>NSCameraUsageDescription</key>
<string>Necesitamos la cámara para escanear códigos QR</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Usamos tu ubicación para verificar la seguridad de las transacciones.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Usamos tu ubicación para verificar la seguridad de las transacciones.</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSFaceIDUsageDescription</key>

View File

@@ -47,8 +47,14 @@
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>NSContactsUsageDescription</key>
<string>Necesitamos acceso a tus contactos para seleccionar números de teléfono.</string>
<key>NSCameraUsageDescription</key>
<string>Necesitamos la cámara para escanear códigos QR</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Usamos tu ubicación para verificar la seguridad de las transacciones.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Usamos tu ubicación para verificar la seguridad de las transacciones.</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSFaceIDUsageDescription</key>

View File

@@ -47,8 +47,14 @@
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>NSContactsUsageDescription</key>
<string>Necesitamos acceso a tus contactos para seleccionar números de teléfono.</string>
<key>NSCameraUsageDescription</key>
<string>Necesitamos la cámara para escanear códigos QR</string>
<string>Necesitamos la cámara para escanear códigos QR y realizar videollamadas</string>
<key>NSMicrophoneUsageDescription</key>
<string>Necesitamos el micrófono para realizar videollamadas</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Necesitamos acceso a la galería de fotos para compartir imágenes</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Usamos tu ubicación para verificar la seguridad de las transacciones.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.savefamily.app.stag</string>

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.savefamily.app.prod</string>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ</string>
<key>GCM_SENDER_ID</key>
<string>535646668726</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app.dev</string>
<key>PROJECT_ID</key>
<string>sf-platform-pre</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pre.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:535646668726:ios:524afa641f61d7cb5e6317</string>
</dict>
</plist>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyC0_d7Z6uVHHKhaf7JHRROaY6g2mvvpOXU</string>
<key>GCM_SENDER_ID</key>
<string>950566980029</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app</string>
<key>PROJECT_ID</key>
<string>sf-platform-pro</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pro.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:950566980029:ios:987b4f0b9e9b897481aad4</string>
</dict>
</plist>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ</string>
<key>GCM_SENDER_ID</key>
<string>535646668726</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app.stag</string>
<key>PROJECT_ID</key>
<string>sf-platform-pre</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pre.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:535646668726:ios:5172d626d02dfe215e6317</string>
</dict>
</plist>

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env ruby
#
# Adds a "Copy GoogleService-Info" Run Script Build Phase to the Runner target.
# The script copies ios/flavors/{flavor}/GoogleService-Info.plist to the .app
# bundle based on the build CONFIGURATION (Debug-development, Release-staging, etc.).
#
# Idempotent: if the build phase already exists, does nothing.
#
# Usage:
# ruby ios/scripts/add-copy-google-service-build-phase.rb
require 'xcodeproj'
PROJECT_PATH = File.expand_path('../../Runner.xcodeproj', __FILE__)
TARGET_NAME = 'Runner'
PHASE_NAME = 'Copy GoogleService-Info'
SHELL_SCRIPT = '"${SRCROOT}/scripts/copy-google-service-plist.sh"'
project = Xcodeproj::Project.open(PROJECT_PATH)
target = project.targets.find { |t| t.name == TARGET_NAME }
unless target
abort "ERROR: Target '#{TARGET_NAME}' not found in project."
end
# Check if the build phase already exists (idempotency)
existing = target.build_phases.find do |phase|
phase.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && phase.name == PHASE_NAME
end
if existing
puts "OK: Build phase '#{PHASE_NAME}' already exists. No changes needed."
exit 0
end
# Create the new build phase
phase = target.new_shell_script_build_phase(PHASE_NAME)
phase.shell_path = '/bin/sh'
phase.shell_script = SHELL_SCRIPT
phase.input_paths = []
phase.output_paths = []
phase.run_only_for_deployment_postprocessing = '0'
# Move it before the embed frameworks phase (or at the end if no such phase)
# Order: Sources -> Frameworks -> Resources -> ... -> ThinBinary -> CopyGoogleService -> EmbedPodsFrameworks -> CopyPodsResources
build_phases = target.build_phases
# Find the index of "Thin Binary" if it exists
thin_binary_idx = build_phases.find_index do |p|
p.respond_to?(:name) && p.name == 'Thin Binary'
end
# Find the index of "[CP] Embed Pods Frameworks" if it exists
embed_pods_idx = build_phases.find_index do |p|
p.respond_to?(:name) && p.name && p.name.include?('Embed Pods Frameworks')
end
# Remove the just-added phase from its current position (it gets appended at the end)
build_phases.delete(phase)
# Insert at the right spot
target_idx = if thin_binary_idx && embed_pods_idx && thin_binary_idx < embed_pods_idx
# Place between Thin Binary and Embed Pods Frameworks
embed_pods_idx
elsif thin_binary_idx
# Place right after Thin Binary
thin_binary_idx + 1
elsif embed_pods_idx
# Place right before Embed Pods Frameworks
embed_pods_idx
else
# Append at the end
build_phases.length
end
build_phases.insert(target_idx, phase)
project.save
puts "OK: Added build phase '#{PHASE_NAME}' at position #{target_idx}."
puts "Build phases order:"
target.build_phases.each_with_index do |p, i|
name = p.respond_to?(:name) && p.name ? p.name : p.class.name
puts " #{i}: #{name}"
end

View File

@@ -0,0 +1,46 @@
#!/bin/bash
#
# Copies the correct AntelopRelease.plist into the .app bundle based on the
# active build CONFIGURATION (Debug-development, Release-staging, etc.). The
# Antelop SDK reads `AntelopRelease.plist` by a fixed name at runtime, so the
# per-flavor variants (AntelopRelease-development.plist,
# AntelopRelease-staging.plist) must be copied over that fixed name.
#
# Source layout: ios/Runner/AntelopRelease.plist (production),
# ios/Runner/AntelopRelease-development.plist,
# ios/Runner/AntelopRelease-staging.plist.
set -e
echo "Configuration: ${CONFIGURATION}"
if [[ $CONFIGURATION =~ \-([^-]*)$ ]]; then
flavor=${BASH_REMATCH[1]}
else
echo "warning: Could not extract flavor from CONFIGURATION='${CONFIGURATION}', defaulting to 'production'"
flavor="production"
fi
echo "Flavor: $flavor"
case "$flavor" in
development|staging)
SRC="${PROJECT_DIR}/Runner/AntelopRelease-${flavor}.plist"
;;
production)
SRC="${PROJECT_DIR}/Runner/AntelopRelease.plist"
;;
*)
echo "warning: Unknown flavor '${flavor}', falling back to AntelopRelease.plist"
SRC="${PROJECT_DIR}/Runner/AntelopRelease.plist"
;;
esac
if [ ! -f "$SRC" ]; then
echo "error: ${SRC} not found"
exit 1
fi
DEST="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/AntelopRelease.plist"
echo "Copying ${SRC} -> ${DEST}"
cp "${SRC}" "${DEST}"

View File

@@ -0,0 +1,35 @@
#!/bin/bash
#
# Copies the correct GoogleService-Info.plist into the .app bundle
# based on the active build CONFIGURATION (Debug-development,
# Release-staging, etc.). Reads from ios/flavors/{flavor}/GoogleService-Info.plist
# and writes to the final bundle.
#
# Add this as a Run Script Build Phase in Xcode AFTER "Thin Binary" and
# BEFORE "[CP] Embed Pods Frameworks" (or near the end of the phases).
set -e
echo "Configuration: ${CONFIGURATION}"
# Extract flavor from the build configuration name (everything after the last "-")
if [[ $CONFIGURATION =~ \-([^-]*)$ ]]; then
flavor=${BASH_REMATCH[1]}
else
echo "warning: Could not extract flavor from CONFIGURATION='${CONFIGURATION}', defaulting to 'development'"
flavor="development"
fi
echo "Flavor: $flavor"
GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist
GOOGLESERVICE_INFO_FILE="${PROJECT_DIR}/flavors/${flavor}/${GOOGLESERVICE_INFO_PLIST}"
if [ ! -f "$GOOGLESERVICE_INFO_FILE" ]; then
echo "error: ${GOOGLESERVICE_INFO_FILE} not found"
exit 1
fi
PLIST_DESTINATION="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app"
echo "Copying ${GOOGLESERVICE_INFO_FILE} -> ${PLIST_DESTINATION}/${GOOGLESERVICE_INFO_PLIST}"
cp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}/${GOOGLESERVICE_INFO_PLIST}"

View File

@@ -2,6 +2,8 @@ abstract class Environment {
static const env = String.fromEnvironment('env', defaultValue: 'development');
static const apiBaseUrl = String.fromEnvironment('apiBaseUrl');
static const apiOrigin = String.fromEnvironment('apiOrigin');
static const wsUrl = String.fromEnvironment('wsUrl');
static const juphoonAppKey = String.fromEnvironment('juphoonAppKey');
// --- Fase 2: Firebase & Sentry ---
// static const sentryDsn = String.fromEnvironment('sentryDsn');

View File

@@ -1,9 +1,11 @@
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'environment.dart';
class QuestiaEnvConfig implements EnvConfig {
class SaveFamilyEnvConfig implements EnvConfig {
@override
String get apiBaseUrl => Environment.apiBaseUrl;
@override
String get apiOrigin => Environment.apiOrigin;
@override
String get wsUrl => Environment.wsUrl;
}

View File

@@ -0,0 +1,14 @@
import 'package:videocall_sdk/videocall_sdk.dart';
import 'environment.dart';
class SaveFamilyVideocallConfig implements VideocallSdkConfig {
@override
String get appKey => Environment.juphoonAppKey;
@override
String get serverAddress => '';
@override
CreateParam? get createParam => null;
}

View File

@@ -0,0 +1,3 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
late ProviderContainer appProviderContainer;

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'app_version_check.dart';
Future<void> showAppUpdateDialog(
BuildContext context, {
required AvailableUpdate result,
VoidCallback? onDismiss,
VoidCallback? onUpdateTapped,
}) {
final isForce = result is ForceUpdate;
return showDialog<void>(
context: context,
barrierDismissible: !isForce,
builder: (dialogContext) {
return PopScope(
canPop: !isForce,
child: AlertDialog(
title: Text(
dialogContext.translate(
isForce
? I18n.appUpdateRequiredTitle
: I18n.appUpdateAvailableTitle,
),
),
content: Text(
result.message.isNotEmpty
? result.message
: dialogContext.translate(
isForce
? I18n.appUpdateRequiredMessage
: I18n.appUpdateAvailableMessage,
),
),
actions: [
if (!isForce)
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
onDismiss?.call();
},
child: Text(dialogContext.translate(I18n.appUpdateLater)),
),
TextButton(
onPressed: () => _launchStore(result.storeUrl, onUpdateTapped),
child: Text(dialogContext.translate(I18n.appUpdateNow)),
),
],
),
);
},
);
}
Future<void> _launchStore(String storeUrl, VoidCallback? onTapped) async {
onTapped?.call();
try {
final uri = Uri.tryParse(storeUrl);
if (uri == null) {
debugPrint('[AppUpdateDialog] invalid store URL: $storeUrl');
return;
}
final launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!launched) {
debugPrint('[AppUpdateDialog] launchUrl returned false for $storeUrl');
}
} catch (e) {
debugPrint('[AppUpdateDialog] launchUrl failed: $e');
}
}

View File

@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'app_update_dialog.dart';
import 'app_version_check.dart';
class AppUpdateGate extends ConsumerStatefulWidget {
const AppUpdateGate({super.key, required this.child});
final Widget child;
@override
ConsumerState<AppUpdateGate> createState() => _AppUpdateGateState();
}
class _AppUpdateGateState extends ConsumerState<AppUpdateGate> {
bool _dialogVisible = false;
VoidCallback? _pendingRouterListener;
@override
void dispose() {
_detachPendingRouterListener();
super.dispose();
}
void _detachPendingRouterListener() {
final listener = _pendingRouterListener;
if (listener != null) {
appRouter.routerDelegate.removeListener(listener);
_pendingRouterListener = null;
}
}
bool _isStableRoute() {
final path = appRouter.routerDelegate.currentConfiguration.uri.path;
return path.startsWith(AppRoutes.dashboard) ||
path.startsWith(AppRoutes.legacyDashboard);
}
void _onResultEmitted(AppVersionCheckResult result) {
if (result is! AvailableUpdate) {
_detachPendingRouterListener();
return;
}
if (_dialogVisible) return;
if (_isStableRoute()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_show(result);
});
return;
}
_detachPendingRouterListener();
void onChange() {
if (!_isStableRoute()) return;
_detachPendingRouterListener();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_show(result);
});
}
_pendingRouterListener = onChange;
appRouter.routerDelegate.addListener(onChange);
}
void _show(AvailableUpdate result) {
if (!mounted) return;
if (_dialogVisible) return;
final ctx = appRouter.routerDelegate.navigatorKey.currentContext;
if (ctx == null) return;
final tracking = ref.read(sfTrackingProvider);
final kind = _kindLabel(result);
tracking.appUpdateDialogShown(
kind: kind,
latestBuild: result.latestBuild,
currentBuild: result.currentBuild,
);
_dialogVisible = true;
showAppUpdateDialog(
ctx,
result: result,
onDismiss: () {
if (result is SoftUpdate) {
tracking.appUpdateDialogDismissed(latestBuild: result.latestBuild);
ref.read(appVersionCheckProvider.notifier).markSoftDismissed(result);
}
},
onUpdateTapped: () => tracking.appUpdateCtaTapped(
kind: kind,
latestBuild: result.latestBuild,
),
).whenComplete(() {
_dialogVisible = false;
});
}
String _kindLabel(AvailableUpdate result) {
return switch (result) {
SoftUpdate() => 'soft',
ForceUpdate() => 'force',
};
}
@override
Widget build(BuildContext context) {
ref.listen<AsyncValue<AppVersionCheckResult>>(
appVersionCheckProvider,
(previous, next) {
next.whenData(_onResultEmitted);
},
);
return widget.child;
}
}

View File

@@ -0,0 +1,61 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app_version_check_result.dart';
import 'app_version_check_service.dart';
export 'app_version_check_result.dart';
export 'app_version_check_service.dart';
export 'dismissed_build_store.dart';
export 'remote_config_reader.dart';
final appVersionCheckServiceProvider = Provider<AppVersionCheckService>((ref) {
return AppVersionCheckService();
});
class AppVersionCheck extends AsyncNotifier<AppVersionCheckResult> {
Future<void>? _inflight;
AppVersionCheckService get _service =>
ref.read(appVersionCheckServiceProvider);
@override
Future<AppVersionCheckResult> build() {
return _service.check();
}
Future<void> refresh() => _runSerialized(() async {
state = AsyncData(await _service.check());
});
Future<void> markSoftDismissed(SoftUpdate result) =>
_runSerialized(() async {
await _service.markSoftDismissed(result.latestBuild);
state = const AsyncData(NoUpdate());
});
Future<void> _runSerialized(Future<void> Function() op) async {
final previous = _inflight;
final completer = Completer<void>();
_inflight = completer.future;
try {
if (previous != null) {
try {
await previous;
} catch (_) {}
}
await op();
} finally {
completer.complete();
if (identical(_inflight, completer.future)) {
_inflight = null;
}
}
}
}
final appVersionCheckProvider =
AsyncNotifierProvider<AppVersionCheck, AppVersionCheckResult>(
AppVersionCheck.new,
);

View File

@@ -0,0 +1,39 @@
sealed class AppVersionCheckResult {
const AppVersionCheckResult();
}
class NoUpdate extends AppVersionCheckResult {
const NoUpdate();
}
sealed class AvailableUpdate extends AppVersionCheckResult {
const AvailableUpdate({
required this.message,
required this.storeUrl,
required this.latestBuild,
required this.currentBuild,
});
final String message;
final String storeUrl;
final int latestBuild;
final int currentBuild;
}
class SoftUpdate extends AvailableUpdate {
const SoftUpdate({
required super.message,
required super.storeUrl,
required super.latestBuild,
required super.currentBuild,
});
}
class ForceUpdate extends AvailableUpdate {
const ForceUpdate({
required super.message,
required super.storeUrl,
required super.latestBuild,
required super.currentBuild,
});
}

View File

@@ -0,0 +1,88 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'app_version_check_result.dart';
import 'dismissed_build_store.dart';
import 'remote_config_keys.dart';
import 'remote_config_reader.dart';
typedef CurrentBuildLoader = Future<int> Function();
Future<int> defaultCurrentBuildLoader() async {
final info = await PackageInfo.fromPlatform();
return int.tryParse(info.buildNumber) ?? 0;
}
class AppVersionCheckService {
AppVersionCheckService({
RemoteConfigReader? remoteConfig,
DismissedBuildStore? dismissedStore,
CurrentBuildLoader? currentBuildLoader,
bool? isIos,
}) : _remoteConfig = remoteConfig ?? FirebaseRemoteConfigReader(),
_dismissedStore = dismissedStore ?? SharedPrefsDismissedBuildStore(),
_currentBuildLoader = currentBuildLoader ?? defaultCurrentBuildLoader,
_isIos = isIos ?? Platform.isIOS;
final RemoteConfigReader _remoteConfig;
final DismissedBuildStore _dismissedStore;
final CurrentBuildLoader _currentBuildLoader;
final bool _isIos;
Future<AppVersionCheckResult> check() async {
try {
final currentBuild = await _currentBuildLoader();
try {
await _remoteConfig.fetchAndActivate();
} catch (e) {
debugPrint('[AppVersionCheck] RC fetch failed: $e');
}
final minRequired = _remoteConfig.getInt(RemoteConfigKeys.minRequiredBuild);
final latest = _remoteConfig.getInt(RemoteConfigKeys.latestBuild);
final forceFlag = _remoteConfig.getBool(RemoteConfigKeys.updateForce);
final message = _remoteConfig.getString(RemoteConfigKeys.updateMessage);
final storeUrl = _isIos
? _remoteConfig.getString(RemoteConfigKeys.updateUrlIos)
: _remoteConfig.getString(RemoteConfigKeys.updateUrlAndroid);
if (forceFlag || currentBuild < minRequired) {
return ForceUpdate(
message: message,
storeUrl: storeUrl,
latestBuild: latest,
currentBuild: currentBuild,
);
}
if (currentBuild < latest) {
final dismissedFor = await _dismissedStore.read();
if (latest <= dismissedFor) {
return const NoUpdate();
}
return SoftUpdate(
message: message,
storeUrl: storeUrl,
latestBuild: latest,
currentBuild: currentBuild,
);
}
return const NoUpdate();
} catch (e) {
debugPrint('[AppVersionCheck] check failed: $e');
return const NoUpdate();
}
}
Future<void> markSoftDismissed(int latestBuild) async {
try {
await _dismissedStore.write(latestBuild);
} catch (e) {
debugPrint('[AppVersionCheck] markSoftDismissed failed: $e');
}
}
}

View File

@@ -0,0 +1,22 @@
import 'package:shared_preferences/shared_preferences.dart';
abstract class DismissedBuildStore {
Future<int> read();
Future<void> write(int latestBuild);
}
class SharedPrefsDismissedBuildStore implements DismissedBuildStore {
static const _key = 'app_update_dismissed_for_latest_build';
@override
Future<int> read() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_key) ?? 0;
}
@override
Future<void> write(int latestBuild) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_key, latestBuild);
}
}

View File

@@ -0,0 +1,10 @@
class RemoteConfigKeys {
const RemoteConfigKeys._();
static const minRequiredBuild = 'min_required_build';
static const latestBuild = 'latest_build';
static const updateForce = 'update_force';
static const updateMessage = 'update_message';
static const updateUrlIos = 'update_url_ios';
static const updateUrlAndroid = 'update_url_android';
}

View File

@@ -0,0 +1,27 @@
import 'package:firebase_remote_config/firebase_remote_config.dart';
abstract class RemoteConfigReader {
Future<void> fetchAndActivate();
int getInt(String key);
bool getBool(String key);
String getString(String key);
}
class FirebaseRemoteConfigReader implements RemoteConfigReader {
FirebaseRemoteConfigReader([FirebaseRemoteConfig? rc])
: _rc = rc ?? FirebaseRemoteConfig.instance;
final FirebaseRemoteConfig _rc;
@override
Future<void> fetchAndActivate() => _rc.fetchAndActivate();
@override
int getInt(String key) => _rc.getInt(key);
@override
bool getBool(String key) => _rc.getBool(key);
@override
String getString(String key) => _rc.getString(key);
}

View File

@@ -0,0 +1,20 @@
/// Compile-time constant that controls which app the splash screen
/// navigates to when the app starts.
///
/// Set via `--dart-define=APP_MODE=payment` (or `legacy`) at launch time.
/// Defaults to `legacy` to preserve historical behavior when no flag is
/// passed (e.g. `flutter run` from CLI without arguments).
///
/// Used only for local development to switch between the legacy app
/// (watch/device control) and the payment app (Treezor wallet) without
/// needing separate flavors or entry points.
const String appMode = String.fromEnvironment(
'APP_MODE',
defaultValue: 'legacy',
);
/// Whether the app should boot into the payment (Treezor wallet) flow.
bool get isPaymentMode => appMode == 'payment';
/// Whether the app should boot into the legacy (watch/device) flow.
bool get isLegacyMode => appMode == 'legacy';

View File

@@ -0,0 +1,73 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/foundation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../config/env/environment_enum.dart';
import '../firebase_options_dev.dart' as dev_options;
import '../firebase_options_prod.dart' as prod_options;
import '../firebase_options_staging.dart' as staging_options;
import 'app_version_check/remote_config_keys.dart';
Future<void> setupFirebase(EnvironmentEnum env) async {
final FirebaseOptions options;
switch (env) {
case EnvironmentEnum.development:
options = dev_options.DefaultFirebaseOptions.currentPlatform;
case EnvironmentEnum.staging:
options = staging_options.DefaultFirebaseOptions.currentPlatform;
case EnvironmentEnum.production:
options = prod_options.DefaultFirebaseOptions.currentPlatform;
}
await Firebase.initializeApp(options: options);
// Report crashes in ALL builds (debug + release) so we catch issues during testing too.
// TODO(gdpr): wire `enabled` to real consent once the fix in backlog lands.
final crashlytics = FirebaseCrashlyticsService(enabled: true);
FlutterError.onError = (details) =>
crashlytics.recordFlutterError(details, fatal: true);
PlatformDispatcher.instance.onError = (error, stack) {
crashlytics.recordError(error, stack, fatal: true);
return true;
};
await FirebaseAnalytics.instance.setUserProperty(
name: 'env',
value: env.name,
);
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(
RemoteConfigSettings(
fetchTimeout: const Duration(minutes: 1),
minimumFetchInterval: kDebugMode
? const Duration(minutes: 1)
: const Duration(hours: 12),
),
);
await remoteConfig.setDefaults(<String, Object>{
RemoteConfigKeys.minRequiredBuild: 0,
RemoteConfigKeys.latestBuild: 0,
RemoteConfigKeys.updateForce: false,
RemoteConfigKeys.updateMessage: '',
RemoteConfigKeys.updateUrlIos: 'https://apps.apple.com/app/id6759875039',
RemoteConfigKeys.updateUrlAndroid:
'https://play.google.com/store/apps/details?id=com.savefamily.app',
BrandLinksKeys.privacyPolicyUrl:
'https://savefamilygps.com/pages/politica-de-privacidad-reloj-gps-infantil-localizador-savefamily',
BrandLinksKeys.corporateWebsiteUrl: 'https://www.savefamilygps.com/',
BrandLinksKeys.helpCenterUrl: 'https://savefamilygpshelp.zendesk.com/hc/es',
BrandLinksKeys.supportEmail: 'info@savefamilygps.com',
});
try {
await remoteConfig.fetchAndActivate();
} catch (e) {
debugPrint('[Firebase] RemoteConfig fetch failed: $e');
}
FirebasePerformance.instance.setPerformanceCollectionEnabled(true);
}

View File

@@ -0,0 +1,13 @@
class IncomingCallNotificationConfig {
const IncomingCallNotificationConfig._();
static const String channelId = 'sf_incoming_call_channel_v2';
static const String legacyChannelId = 'sf_incoming_call_channel';
static const String channelName = 'Videollamadas entrantes';
static const String channelDescription =
'Notificaciones tipo llamada para videollamadas entrantes desde el reloj.';
static const int notificationId = 1001;
static const String actionAccept = 'accept';
static const String actionReject = 'reject';
static const String systemRingtoneUri = 'content://settings/system/ringtone';
}

View File

@@ -0,0 +1,63 @@
import 'package:shared_preferences/shared_preferences.dart';
class IncomingCallStrings {
const IncomingCallStrings({
required this.title,
required this.body,
required this.acceptLabel,
required this.rejectLabel,
required this.channelName,
required this.channelDescription,
});
final String title;
final String body;
final String acceptLabel;
final String rejectLabel;
final String channelName;
final String channelDescription;
}
class IncomingCallStringsCache {
const IncomingCallStringsCache._();
static const _keyTitle = 'incoming_call.title';
static const _keyBody = 'incoming_call.body';
static const _keyAccept = 'incoming_call.accept';
static const _keyReject = 'incoming_call.reject';
static const _keyChannelName = 'incoming_call.channel_name';
static const _keyChannelDescription = 'incoming_call.channel_description';
static const _fallback = IncomingCallStrings(
title: 'Videollamada entrante',
body: 'El reloj te está llamando',
acceptLabel: 'Aceptar',
rejectLabel: 'Rechazar',
channelName: 'Videollamadas entrantes',
channelDescription:
'Notificaciones tipo llamada para videollamadas entrantes desde el reloj.',
);
static Future<void> save(IncomingCallStrings strings) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyTitle, strings.title);
await prefs.setString(_keyBody, strings.body);
await prefs.setString(_keyAccept, strings.acceptLabel);
await prefs.setString(_keyReject, strings.rejectLabel);
await prefs.setString(_keyChannelName, strings.channelName);
await prefs.setString(_keyChannelDescription, strings.channelDescription);
}
static Future<IncomingCallStrings> load() async {
final prefs = await SharedPreferences.getInstance();
return IncomingCallStrings(
title: prefs.getString(_keyTitle) ?? _fallback.title,
body: prefs.getString(_keyBody) ?? _fallback.body,
acceptLabel: prefs.getString(_keyAccept) ?? _fallback.acceptLabel,
rejectLabel: prefs.getString(_keyReject) ?? _fallback.rejectLabel,
channelName: prefs.getString(_keyChannelName) ?? _fallback.channelName,
channelDescription: prefs.getString(_keyChannelDescription) ??
_fallback.channelDescription,
);
}
}

View File

@@ -1,36 +1,54 @@
import 'package:get_it/get_it.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:design_system/design_system.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:sca_treezor/sca_treezor.dart';
import 'package:sf_app_platform/config/env/environment_enum.dart';
import 'package:sf_app_platform/config/env/questia_env_config.dart';
import 'package:sf_app_platform/config/env/save_family_env_config.dart';
import 'package:sf_app_platform/config/env/save_family_videocall_config.dart';
import 'package:sf_app_platform/core/app_provider_container.dart';
import 'package:sf_app_platform/core/config/app_mode.dart';
import 'package:sf_app_platform/core/firebase_init.dart';
import 'package:sf_app_platform/core/notifications_init.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:sf_app_platform/save_family_app.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:videocall_sdk/videocall_sdk.dart';
Future<void> initApp(EnvironmentEnum env) async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await initializeDateFormatting();
tz.initializeTimeZones();
final sharedPreferences = await SharedPreferences.getInstance();
navigationModule();
scaTreezorModule();
configureAppRouter();
videocallSdkModule(SaveFamilyVideocallConfig());
themePackages();
// --- Fase 2: Firebase ---
// await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await setupFirebase(env);
// --- Fase 2: Sentry ---
// await initSentry(env);
// TODO Fase 2: await initSentry(env);
configureAppRouter();
onRouterReady();
await configureDependencies(
QuestiaEnvConfig(),
SaveFamilyEnvConfig(),
log: env.isDevelopment || kDebugMode,
onTokenExpired: () => appRouter.go(AppRoutes.scaTreezor),
onTokenExpired: isPaymentMode
? () => appRouter.go(AppRoutes.scaTreezor)
: null,
onUnauthorized: () async {
final currentLocation =
appRouter.routerDelegate.currentConfiguration.uri.path;
@@ -39,9 +57,25 @@ Future<void> initApp(EnvironmentEnum env) async {
await GetIt.I<TreezorWalletConnectionService>().logout();
} catch (_) {}
await clearSessionData();
appRouter.go(AppRoutes.login);
WidgetsBinding.instance.addPostFrameCallback((_) {
appRouter.go(isPaymentMode ? AppRoutes.login : AppRoutes.legacyLogin);
});
},
);
runApp(const ProviderScope(child: SaveFamilyApp()));
appProviderContainer = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
);
await setupNotifications();
initSfTracking();
runApp(
UncontrolledProviderScope(
container: appProviderContainer,
child: const SaveFamilyApp(),
),
);
}

View File

@@ -0,0 +1,409 @@
import 'dart:async';
import 'dart:convert';
import 'package:chat/chat.dart';
import 'package:device_management/device_management.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/core/app_provider_container.dart';
import 'package:sf_app_platform/core/incoming_call_notification_config.dart';
import 'package:sf_app_platform/core/incoming_call_strings_cache.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
// iOS limitation: incoming-call UX requires PushKit + CallKit + a VoIP cert,
// which we don't have yet. On iOS the full-screen call UI and ring-while-killed
// behaviour will not work — only the standard notification banner.
// See TODO(videocall-ios-callkit) for the migration path.
//
// TODO(push-data-only): backend sends hybrid pushes (notification + data).
// In background/killed the SDK auto-shows the `notification` payload using
// `sf_default_channel`. For VIDEO_CALL_FROM that produces a duplicate notif
// alongside our custom incoming-call notif (ringtone + full-screen). Backend
// must drop the `notification` field at minimum for VIDEO_CALL_FROM pushes,
// ideally for all commands. When that happens, this handler should construct
// title/body locally with i18n for every command (CHAT_MESSAGE, ALERT, etc.)
// and call _localNotifications.show(...) directly.
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
debugPrint('[FCM-bg] messageId=${message.messageId}');
debugPrint('[FCM-bg] notification=${message.notification?.title} | ${message.notification?.body}');
debugPrint('[FCM-bg] data=${message.data}');
if (message.data['command'] == 'VIDEO_CALL_FROM') {
await _showIncomingCallNotification(message.data);
}
}
const String _localChannelId = 'sf_default_channel';
const String _localChannelName = 'General';
const String _localChannelDescription =
'General notifications shown while the app is in the foreground.';
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Map<String, dynamic>? _pendingNotificationData;
bool _routerReady = false;
ProviderSubscription<VideocallIncomingArgs?>? _incomingProviderSub;
Future<void> setupNotifications() async {
final messaging = FirebaseMessaging.instance;
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
await messaging.requestPermission(alert: true, badge: true, sound: true);
await messaging.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
await _initLocalNotifications();
_subscribeToIncomingProvider();
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
final initialMessage = await messaging.getInitialMessage();
if (initialMessage != null) {
_onMessageOpenedApp(initialMessage);
}
}
void onRouterReady() {
_routerReady = true;
final pending = _pendingNotificationData;
if (pending != null) {
_pendingNotificationData = null;
_handleNotificationNavigation(pending);
}
}
Future<void> _initLocalNotifications() async {
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const initSettings = InitializationSettings(
android: androidInit,
iOS: iosInit,
);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onLocalNotificationTapped,
);
const channel = AndroidNotificationChannel(
_localChannelId,
_localChannelName,
description: _localChannelDescription,
importance: Importance.high,
);
final strings = await IncomingCallStringsCache.load();
final callChannel = AndroidNotificationChannel(
IncomingCallNotificationConfig.channelId,
strings.channelName,
description: strings.channelDescription,
importance: Importance.max,
playSound: true,
sound: const UriAndroidNotificationSound(
IncomingCallNotificationConfig.systemRingtoneUri,
),
enableVibration: true,
enableLights: true,
);
final androidPlugin = _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
await androidPlugin?.deleteNotificationChannel(
IncomingCallNotificationConfig.legacyChannelId,
);
await androidPlugin?.createNotificationChannel(channel);
await androidPlugin?.createNotificationChannel(callChannel);
}
void _subscribeToIncomingProvider() {
_incomingProviderSub?.close();
_incomingProviderSub = appProviderContainer.listen<VideocallIncomingArgs?>(
videocallIncomingProvider,
(_, next) {
if (next == null) {
_dismissIncomingCallNotification();
}
},
);
}
Future<void> _showIncomingCallNotification(Map<String, dynamic> data) async {
final roomNumber = data['roomNumber'] as String?;
final appAccount = data['appAccount'] as String?;
final sessionId = data['sessionId'] as String?;
if (roomNumber == null || appAccount == null || sessionId == null) return;
final strings = await IncomingCallStringsCache.load();
final payload = jsonEncode(data);
await _localNotifications.show(
IncomingCallNotificationConfig.notificationId,
strings.title,
strings.body,
NotificationDetails(
android: AndroidNotificationDetails(
IncomingCallNotificationConfig.channelId,
strings.channelName,
channelDescription: strings.channelDescription,
importance: Importance.max,
priority: Priority.max,
category: AndroidNotificationCategory.call,
fullScreenIntent: true,
ongoing: true,
autoCancel: false,
playSound: true,
sound: const UriAndroidNotificationSound(
IncomingCallNotificationConfig.systemRingtoneUri,
),
enableVibration: true,
visibility: NotificationVisibility.public,
actions: <AndroidNotificationAction>[
AndroidNotificationAction(
IncomingCallNotificationConfig.actionAccept,
strings.acceptLabel,
showsUserInterface: true,
cancelNotification: true,
),
AndroidNotificationAction(
IncomingCallNotificationConfig.actionReject,
strings.rejectLabel,
showsUserInterface: false,
cancelNotification: true,
),
],
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
interruptionLevel: InterruptionLevel.timeSensitive,
),
),
payload: payload,
);
}
Future<void> _dismissIncomingCallNotification() async {
await _localNotifications.cancel(
IncomingCallNotificationConfig.notificationId,
);
}
// TODO(push-data-only): when backend stops sending the `notification` field,
// stop reading message.notification.title/body below. Instead, build title/body
// from message.data (e.g., data['carrierName']) plus i18n strings — same way
// _showIncomingCallNotification already does for VIDEO_CALL_FROM. Then we can
// drop the early-return when notification is null.
void _onForegroundMessage(RemoteMessage message) {
debugPrint('[FCM-fg] messageId=${message.messageId}');
debugPrint('[FCM-fg] notification=${message.notification?.title} | ${message.notification?.body}');
debugPrint('[FCM-fg] data=${message.data}');
if (message.data['command'] == 'VIDEO_CALL_FROM') {
_showIncomingCallNotification(message.data);
_handleNotificationNavigation(message.data);
return;
}
if (_shouldSuppressChatNotification(message.data)) {
debugPrint('[FCM-fg] chat notification suppressed by active context');
return;
}
final notification = message.notification;
if (notification == null) return;
final notificationId = message.messageId?.hashCode ?? 0;
_localNotifications.show(
notificationId,
notification.title,
notification.body,
const NotificationDetails(
android: AndroidNotificationDetails(
_localChannelId,
_localChannelName,
channelDescription: _localChannelDescription,
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode(message.data),
);
}
/// Suppresses chat notifications while the user is actively reading the chat
/// surface — same UX as WhatsApp/Telegram.
///
/// - Chat list → suppress (the list updates reactively via the WebSocket).
/// - Conversation matching the incoming chatId → suppress (the message arrives
/// through the reactive stream).
/// - Conversation viewing a different chat → show (cross-chat notification).
/// - Outside the chat surface → show.
bool _shouldSuppressChatNotification(Map<String, dynamic> data) {
if (data['command'] != 'CHAT_MESSAGE') return false;
final context = appProviderContainer.read(chatContextProvider);
if (context is ChatContextOutsideChat) return false;
if (context is ChatContextList) return true;
final deviceIdentificator = data['deviceIdentificator'] as String?;
if (deviceIdentificator == null || deviceIdentificator.isEmpty) return false;
final incomingChatId = appProviderContainer
.read(chatDeeplinkServiceProvider)
.resolveClientChatId(
chatId: data['chatId'] as String?,
deviceIdentificator: deviceIdentificator,
);
if (incomingChatId == null) return false;
return context is ChatContextConversation && context.chatId == incomingChatId;
}
void _onMessageOpenedApp(RemoteMessage message) {
debugPrint('[FCM-tap] messageId=${message.messageId}');
debugPrint('[FCM-tap] notification=${message.notification?.title} | ${message.notification?.body}');
debugPrint('[FCM-tap] data=${message.data}');
_handleNotificationNavigation(message.data);
}
// TODO(videocall-callkit-migration): tap "Accept" hoy abre el app y muestra
// IncomingView (doble-tap para entrar a la llamada). WhatsApp/Telegram entran
// directo porque usan CallKit/CallStyle nativos. Migración completa a
// flutter_callkit_incoming bloqueada por VoIP cert de Apple. Plan y opciones
// en docs/videocall-callkit-migration.md.
void _onLocalNotificationTapped(NotificationResponse response) {
final payload = response.payload;
if (payload == null || payload.isEmpty) return;
Map<String, dynamic> data;
try {
data = jsonDecode(payload) as Map<String, dynamic>;
} catch (_) {
return;
}
if (data['command'] == 'VIDEO_CALL_FROM' &&
response.actionId == IncomingCallNotificationConfig.actionReject) {
_rejectIncomingCallFromNotification(data);
return;
}
_handleNotificationNavigation(data);
}
Future<void> _rejectIncomingCallFromNotification(
Map<String, dynamic> data,
) async {
final roomNumber = data['roomNumber'] as String?;
final appAccount = data['appAccount'] as String?;
if (roomNumber == null || appAccount == null) {
appProviderContainer.read(videocallIncomingProvider.notifier).clear();
return;
}
final chatType = data['chatType'] == 'multi'
? VideocallChatType.multi
: VideocallChatType.single;
final deviceId = parseDeviceIdFromRoom(roomNumber);
try {
await appProviderContainer
.read(videocallSignalingRepositoryProvider)
.refuseCall(
deviceIdentificator: deviceId,
chatType: chatType,
appAccount: appAccount,
roomNumber: roomNumber,
);
} catch (error) {
debugPrint('[Notification] refuseCall from notif failed: $error');
}
appProviderContainer.read(videocallIncomingProvider.notifier).clear();
}
void _handleNotificationNavigation(Map<String, dynamic> data) {
if (!_routerReady) {
_pendingNotificationData = data;
return;
}
final currentLocation =
appRouter.routerDelegate.currentConfiguration.uri.path;
if (!currentLocation.startsWith(AppRoutes.legacyDashboard)) return;
Map<String, dynamic> resolved = data;
final pushData = data['pushData'];
if (pushData is String && pushData.isNotEmpty) {
try {
resolved = jsonDecode(pushData) as Map<String, dynamic>;
} catch (_) {}
}
final command = resolved['command'] as String? ?? data['command'] as String?;
switch (command) {
case 'ALERT':
appRouter.go(AppRoutes.deviceNotifications);
case 'CHAT_MESSAGE':
_openChatFromIncoming(resolved);
case 'VIDEO_CALL_FROM':
final chatType = resolved['chatType'] as String?;
final appAccount = resolved['appAccount'] as String?;
final roomNumber = resolved['roomNumber'] as String?;
final sessionId = resolved['sessionId'] as String?;
if (roomNumber == null || appAccount == null || sessionId == null) {
return;
}
appProviderContainer.read(videocallIncomingProvider.notifier).set(
VideocallIncomingArgs(
roomNumber: roomNumber,
appAccount: appAccount,
sessionId: sessionId,
chatType: chatType == 'multi'
? VideocallChatType.multi
: VideocallChatType.single,
),
);
appRouter.go(AppRoutes.videocall);
}
}
Future<void> _openChatFromIncoming(Map<String, dynamic> data) async {
final deviceIdentificator = data['deviceIdentificator'] as String?;
if (deviceIdentificator == null || deviceIdentificator.isEmpty) {
appRouter.go(AppRoutes.legacyChat);
return;
}
final outcome = await appProviderContainer
.read(chatDeeplinkServiceProvider)
.prepareIncomingChat(
chatId: data['chatId'] as String?,
deviceIdentificator: deviceIdentificator,
);
if (outcome == null) {
appRouter.go(AppRoutes.legacyChat);
return;
}
appRouter.go(AppRoutes.legacyChatConversationFor(outcome.chatId));
}

View File

@@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_dev.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY',
appId: '1:535646668726:android:c3a09d6c26f0cdf95e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ',
appId: '1:535646668726:ios:524afa641f61d7cb5e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
iosBundleId: 'com.savefamily.app.dev',
);
}

View File

@@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_prod.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDkjNdOAK0ype7wgdgiC1BCKV_pP4s_mlA',
appId: '1:950566980029:android:75a7c10b6259d09681aad4',
messagingSenderId: '950566980029',
projectId: 'sf-platform-pro',
storageBucket: 'sf-platform-pro.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyC0_d7Z6uVHHKhaf7JHRROaY6g2mvvpOXU',
appId: '1:950566980029:ios:987b4f0b9e9b897481aad4',
messagingSenderId: '950566980029',
projectId: 'sf-platform-pro',
storageBucket: 'sf-platform-pro.firebasestorage.app',
iosBundleId: 'com.savefamily.app',
);
}

View File

@@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_staging.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY',
appId: '1:535646668726:android:b87245b807258e3e5e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ',
appId: '1:535646668726:ios:5172d626d02dfe215e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
iosBundleId: 'com.savefamily.app.stag',
);
}

View File

@@ -7,6 +7,7 @@ import 'package:dashboard_shell/dashboard_builder.dart';
import 'package:device_management/device_management.dart';
import 'package:control_panel/control_panel.dart';
import 'package:legacy_dashboard_shell/legacy_dashboard_builder.dart';
import 'package:chat/chat.dart';
import 'package:location/location.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
@@ -16,29 +17,75 @@ import 'package:navigation/navigation.dart';
import 'package:notifications/notifications.dart';
import 'package:payments/payments.dart';
import 'package:profile/profile.dart';
import 'package:settings/settings.dart';
import 'package:sf_app_platform/core/config/app_mode.dart';
import 'package:sf_app_platform/widgets/user_identity_listener.dart';
import 'package:splash/splash.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final _legacyControlPanelNavKey =
GlobalKey<NavigatorState>(debugLabel: 'legacyControlPanel');
final _legacyDeviceMgmtNavKey =
GlobalKey<NavigatorState>(debugLabel: 'legacyDeviceMgmt');
final _legacyLocationNavKey =
GlobalKey<NavigatorState>(debugLabel: 'legacyLocation');
final _legacyChatNavKey = GlobalKey<NavigatorState>(debugLabel: 'legacyChat');
final _legacySettingsNavKey =
GlobalKey<NavigatorState>(debugLabel: 'legacySettings');
final _dashboardHomeNavKey =
GlobalKey<NavigatorState>(debugLabel: 'dashboardHome');
final _dashboardActivityNavKey =
GlobalKey<NavigatorState>(debugLabel: 'dashboardActivity');
final _dashboardNotificationsNavKey =
GlobalKey<NavigatorState>(debugLabel: 'dashboardNotifications');
final _dashboardProfileNavKey =
GlobalKey<NavigatorState>(debugLabel: 'dashboardProfile');
late final GoRouter appRouter;
/// Maps the splash's session check result to the destination route based
/// on the active [appMode]. Set `--dart-define=APP_MODE=payment` (or use
/// the `(Payment)` launch configurations) to boot into the payment app.
const _legacySplashRouteMap = <InitialRoute, String>{
InitialRoute.onboarding: AppRoutes.legacyOnboarding,
InitialRoute.login: AppRoutes.legacyLogin,
InitialRoute.home: AppRoutes.controlPanel,
InitialRoute.deviceSetup: AppRoutes.legacyDeviceSetup,
};
const _paymentSplashRouteMap = <InitialRoute, String>{
InitialRoute.onboarding: AppRoutes.onboarding,
InitialRoute.login: AppRoutes.login,
InitialRoute.home: AppRoutes.dashboardHome,
InitialRoute.deviceSetup: AppRoutes.deviceSetup,
};
void configureAppRouter() {
final splashRouteMap = isPaymentMode
? _paymentSplashRouteMap
: _legacySplashRouteMap;
appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: AppRoutes.controlPanel,
initialLocation: AppRoutes.splash,
debugLogDiagnostics: true,
routes: [
GoRoute(
path: AppRoutes.splash,
name: 'splash',
pageBuilder: SplashBuilder().buildPage,
pageBuilder: SplashBuilder(routeMap: splashRouteMap).buildPage,
),
StatefulShellRoute.indexedStack(
builder: (context, state, navShell) {
return LegacyDashboardBuilder().build(context, navShell);
return UserIdentityListener(
child: LegacyDashboardBuilder().build(context, navShell),
);
},
branches: [
StatefulShellBranch(
navigatorKey: _legacyControlPanelNavKey,
routes: [
GoRoute(
path: AppRoutes.controlPanel,
@@ -50,6 +97,11 @@ void configureAppRouter() {
name: 'customer_service',
pageBuilder: CustomerServiceBuilder().buildPage,
),
GoRoute(
path: 'notifications',
name: 'legacy_notifications',
pageBuilder: const DeviceNotificationsBuilder().buildPage,
),
GoRoute(
path: 'account_settings',
name: 'account_settings',
@@ -87,16 +139,34 @@ void configureAppRouter() {
],
),
StatefulShellBranch(
navigatorKey: _legacyDeviceMgmtNavKey,
routes: [
GoRoute(
path: AppRoutes.deviceManagement,
name: 'device_management',
pageBuilder: DeviceManagementBuilder().buildPage,
routes: [
GoRoute(
path: 'scheduled_activities',
name: 'scheduled_activities',
pageBuilder: const ScheduledActivitiesBuilder().buildPage,
),
GoRoute(
path: 'contacts',
name: 'contacts',
pageBuilder: ContactsBuilder().buildPage,
pageBuilder: const ContactsBuilder().buildPage,
routes: [
GoRoute(
path: 'edit/:contactId',
name: 'edit_contact',
pageBuilder: const EditContactBuilder().buildPage,
),
],
),
GoRoute(
path: 'health',
name: 'health',
pageBuilder: const HealthBuilder().buildPage,
),
GoRoute(
path: 'remote_connection',
@@ -113,11 +183,65 @@ void configureAppRouter() {
name: 'rewards',
pageBuilder: RewardsBuilder().buildPage,
),
GoRoute(
path: 'activity_meter',
name: 'activity_meter',
pageBuilder: const ActivityMeterBuilder().buildPage,
),
GoRoute(
path: 'apps_use',
name: 'apps_use',
pageBuilder: const AppsUseBuilder().buildPage,
),
GoRoute(
path: 'volume_control',
name: 'volume_control',
pageBuilder: const VolumeControlBuilder().buildPage,
),
GoRoute(
path: 'do_not_disturb',
name: 'do_not_disturb',
pageBuilder: const DoNotDisturbBuilder().buildPage,
),
GoRoute(
path: 'call_history',
name: 'call_history',
pageBuilder: const CallHistoryBuilder().buildPage,
),
GoRoute(
path: 'background_image',
name: 'background_image',
pageBuilder: const BackgroundImageBuilder().buildPage,
),
GoRoute(
path: 'installed_apps',
name: 'installed_apps',
pageBuilder: const InstalledAppsBuilder().buildPage,
routes: [
GoRoute(
path: 'schedules',
name: 'app_usage_schedules',
pageBuilder:
const AppUsageSchedulesBuilder().buildPage,
),
],
),
GoRoute(
path: 'videocall',
name: 'videocall',
pageBuilder: const VideocallBuilder().buildPage,
),
GoRoute(
path: 'friends',
name: 'friends',
pageBuilder: const FriendsBuilder().buildPage,
),
],
),
],
),
StatefulShellBranch(
navigatorKey: _legacyLocationNavKey,
routes: [
GoRoute(
path: AppRoutes.legacyLocation,
@@ -126,23 +250,113 @@ void configureAppRouter() {
),
],
),
// TODO: Añadir branch para Chat (tab 4)
StatefulShellBranch(
navigatorKey: _legacyChatNavKey,
routes: [
GoRoute(
path: '${ AppRoutes.legacyDashboard}/chat',
path: AppRoutes.legacyChat,
name: 'legacy_chat',
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const Scaffold(
body: Center(child: Text('Chat - Coming soon')),
pageBuilder: const ChatListBuilder().buildPage,
routes: [
GoRoute(
path: 'conversation/:chatId',
name: 'legacy_chat_conversation',
pageBuilder: const ChatConversationBuilder().buildPage,
),
),
],
),
],
),
StatefulShellBranch(
navigatorKey: _legacySettingsNavKey,
routes: [
GoRoute(
path: AppRoutes.settings,
name: 'settings',
pageBuilder: SettingsBuilder().buildPage,
routes: [
GoRoute(
path: 'appearance',
name: 'appearance',
pageBuilder: AppearanceBuilder().buildPage,
),
GoRoute(
path: 'alarm',
name: 'alarm',
pageBuilder: AlarmBuilder().buildPage,
),
GoRoute(
path: 'remote_management',
name: 'remote_management',
pageBuilder: RemoteManagementBuilder().buildPage,
),
GoRoute(
path: 'sos_agenda',
name: 'sos_agenda',
pageBuilder: SosContactsBuilder().buildPage,
),
GoRoute(
path: 'sound',
name: 'sound',
pageBuilder: SoundBuilder().buildPage,
),
GoRoute(
path: 'sync_clock',
name: 'sync_clock',
pageBuilder: SyncClockBuilder().buildPage,
),
GoRoute(
path: 'app_store',
name: 'app_store',
pageBuilder: AppStoreBuilder().buildPage,
),
GoRoute(
path: 'battery',
name: 'battery',
pageBuilder: BatteryBuilder().buildPage,
),
GoRoute(
path: 'block_phone',
name: 'block_phone',
pageBuilder: BlockPhoneBuilder().buildPage,
),
GoRoute(
path: 'disable_functions',
name: 'disable_functions',
pageBuilder: DisableFunctionsBuilder().buildPage,
),
GoRoute(
path: 'language',
name: 'language',
pageBuilder: LanguageBuilder().buildPage,
),
GoRoute(
path: 'remote_on_off',
name: 'remote_on_off',
pageBuilder: RemoteOnOffBuilder().buildPage,
),
GoRoute(
path: 'alerts',
name: 'alerts',
pageBuilder: AlertsBuilder().buildPage,
),
GoRoute(
path: 'timezone',
name: 'timezone',
pageBuilder: TimezoneBuilder().buildPage,
),
GoRoute(
path: 'wifi_settings',
name: 'wifi_settings',
pageBuilder: WifiSettingsBuilder().buildPage,
),
],
),
],
),
],
),
GoRoute(
path: AppRoutes.login,
name: 'login',
@@ -226,10 +440,13 @@ void configureAppRouter() {
),
StatefulShellRoute.indexedStack(
builder: (context, state, navShell) {
return DashboardBuilder().build(context, navShell);
return UserIdentityListener(
child: DashboardBuilder().build(context, navShell),
);
},
branches: [
StatefulShellBranch(
navigatorKey: _dashboardHomeNavKey,
routes: [
GoRoute(
path: AppRoutes.dashboardHome,
@@ -271,6 +488,26 @@ void configureAppRouter() {
name: 'home_extract',
pageBuilder: const ExtractBuilder().buildPage,
),
GoRoute(
path: 'edit',
name: 'home_edit_child_profile',
pageBuilder: const EditChildProfileBuilder().buildPage,
),
GoRoute(
path: 'set-pin',
name: 'home_set_card_pin',
pageBuilder: const SetCardPinBuilder().buildPage,
),
GoRoute(
path: 'change-pin',
name: 'home_change_card_pin',
pageBuilder: const ChangeCardPinBuilder().buildPage,
),
GoRoute(
path: 'renew-card',
name: 'home_renew_card',
pageBuilder: const RenewCardBuilder().buildPage,
),
],
),
],
@@ -278,6 +515,7 @@ void configureAppRouter() {
],
),
StatefulShellBranch(
navigatorKey: _dashboardActivityNavKey,
routes: [
GoRoute(
path: AppRoutes.dashboardActivity,
@@ -287,6 +525,7 @@ void configureAppRouter() {
],
),
StatefulShellBranch(
navigatorKey: _dashboardNotificationsNavKey,
routes: [
GoRoute(
path: AppRoutes.dashboardNotifications,
@@ -296,6 +535,7 @@ void configureAppRouter() {
],
),
StatefulShellBranch(
navigatorKey: _dashboardProfileNavKey,
routes: [
GoRoute(
path: AppRoutes.dashboardProfile,
@@ -312,6 +552,11 @@ void configureAppRouter() {
name: 'profile_settings',
pageBuilder: const ProfileSettingsBuilder().buildPage,
routes: [
GoRoute(
path: 'edit-personal-data',
name: 'profile_edit_personal_data',
pageBuilder: const EditPersonalDataBuilder().buildPage,
),
GoRoute(
path: 'payment-methods',
name: 'profile_payment_methods',

View File

@@ -0,0 +1,46 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart' show WidgetRef;
import 'package:sf_shared/sf_shared.dart';
class LegacyHeartbeatService {
LegacyHeartbeatService({
required WidgetRef ref,
required void Function() onUnauthorized,
}) : _ref = ref,
_onUnauthorized = onUnauthorized;
final WidgetRef _ref;
final void Function() _onUnauthorized;
Timer? _timer;
static const _interval = Duration(minutes: 2);
void start() {
if (_timer != null) return;
_beat();
_timer = Timer.periodic(_interval, (_) => _beat());
debugPrint('[LegacyHeartbeat] started');
}
void stop() {
_timer?.cancel();
_timer = null;
debugPrint('[LegacyHeartbeat] stopped');
}
Future<void> _beat() async {
try {
await _ref.read(legacyDevicesProvider.notifier).refresh();
debugPrint('[LegacyHeartbeat] devices refreshed');
} catch (e) {
debugPrint('[LegacyHeartbeat] error: $e');
if (e is DioException && e.response?.statusCode == 401) {
stop();
_onUnauthorized();
}
}
}
}

View File

@@ -1,17 +1,27 @@
import 'package:auth/auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:sf_app_platform/core/app_version_check/app_update_gate.dart';
import 'package:sf_app_platform/core/app_version_check/app_version_check.dart';
import 'package:sf_app_platform/core/config/app_mode.dart';
import 'package:sf_app_platform/core/incoming_call_strings_cache.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/providers/app_state_provider.dart';
import 'package:sf_app_platform/providers/permissions/permissions_provider.dart';
import 'package:sf_app_platform/providers/legacy_heartbeat_service.dart';
import 'package:sf_app_platform/providers/wallet_heartbeat_service.dart';
import 'package:get_it/get_it.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import 'package:fonts/fonts.dart';
import 'package:videocall_sdk/videocall_sdk.dart';
class SaveFamilyApp extends ConsumerStatefulWidget {
const SaveFamilyApp({super.key});
@@ -22,24 +32,82 @@ class SaveFamilyApp extends ConsumerStatefulWidget {
class SaveFamilyAppState extends ConsumerState<SaveFamilyApp>
with WidgetsBindingObserver {
late final WalletHeartbeatService walletHeartbeat;
WalletHeartbeatService? _walletHeartbeat;
LegacyHeartbeatService? _legacyHeartbeat;
SfRouterListener? _trackingRouterListener;
WebSocketService? _webSocket;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
walletHeartbeat = WalletHeartbeatService(
repository: ref.read(treezorRepositoryProvider),
sessionLocal: SessionLocalDatasourceImpl(),
onError: () => appRouter.go(AppRoutes.scaTreezor),
_trackingRouterListener = SfRouterListener(
listenable: appRouter.routerDelegate,
currentScreenName: () {
final config = appRouter.routerDelegate.currentConfiguration;
if (config.matches.isEmpty) return null;
return config.last.route.name;
},
tracking: sfTracking,
);
onBeforeSessionCleared = walletHeartbeat.stop;
// walletHeartbeat.start();
if (isPaymentMode) {
_walletHeartbeat = WalletHeartbeatService(
repository: ref.read(treezorRepositoryProvider),
sessionLocal: SessionLocalDatasourceImpl(),
onError: () => appRouter.go(AppRoutes.scaTreezor),
);
}
if (isLegacyMode) {
_legacyHeartbeat = LegacyHeartbeatService(
ref: ref,
onUnauthorized: () {
clearSessionData();
WidgetsBinding.instance.addPostFrameCallback((_) {
appRouter.go(AppRoutes.legacyLogin);
});
},
);
_webSocket = GetIt.I<WebSocketService>();
appRouter.routerDelegate.addListener(_onRouteChanged);
}
onBeforeSessionCleared = () {
_walletHeartbeat?.stop();
_legacyHeartbeat?.stop();
_webSocket?.disconnect();
GetIt.I<VideocallClient>().logout();
FirebaseMessaging.instance.deleteToken().catchError((Object e) {
debugPrint('[FCM] deleteToken on logout failed: $e');
});
};
}
void _onRouteChanged() {
final heartbeat = _legacyHeartbeat;
if (heartbeat == null) return;
final location = appRouter.routerDelegate.currentConfiguration.uri.path;
if (location.startsWith(AppRoutes.legacyDashboard)) {
heartbeat.start();
_webSocket?.connect();
} else {
heartbeat.stop();
_webSocket?.disconnect();
}
}
@override
void dispose() {
walletHeartbeat.stop();
if (isLegacyMode) {
appRouter.routerDelegate.removeListener(_onRouteChanged);
}
_trackingRouterListener?.dispose();
_walletHeartbeat?.stop();
_legacyHeartbeat?.stop();
_webSocket?.disconnect();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -49,10 +117,16 @@ class SaveFamilyAppState extends ConsumerState<SaveFamilyApp>
debugPrint('State: $state');
ref.read(appLifecycleStateProvider.notifier).setState(state);
if (state == AppLifecycleState.resumed) {
// walletHeartbeat.start();
_walletHeartbeat?.start();
if (isLegacyMode) {
_onRouteChanged();
}
ref.read(permissionsProvider.notifier).checkPermissions();
ref.read(appVersionCheckProvider.notifier).refresh();
} else if (state == AppLifecycleState.paused) {
// walletHeartbeat.stop();
_walletHeartbeat?.stop();
_legacyHeartbeat?.stop();
_webSocket?.disconnect();
}
super.didChangeAppLifecycleState(state);
}
@@ -60,32 +134,76 @@ class SaveFamilyAppState extends ConsumerState<SaveFamilyApp>
@override
Widget build(BuildContext context) {
SizeUtils.init(context: context);
ref.watch(pushTokenRefreshListenerProvider);
return MaterialApp.router(
title: 'SaveFamily',
theme: ThemeData(
// Theme wiring:
// - Legacy mode: new `legacy_theme` package (Material 3 + light/dark/system).
// - Payment mode: unchanged behaviour (seed-based ColorScheme, light only).
final ThemeData lightTheme;
final ThemeData? darkTheme;
final ThemeMode themeMode;
if (isLegacyMode) {
final legacyThemeState = ref.watch(legacyThemeNotifierProvider);
lightTheme = LegacyAppTheme.light;
darkTheme = LegacyAppTheme.dark;
themeMode = legacyThemeState.themeMode;
} else {
lightTheme = ThemeData(
fontFamily: AppFonts.stolzl,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF329E95)),
),
routerConfig: appRouter,
debugShowCheckedModeBanner: false,
localizationsDelegates: [
// CountryLocalizations.delegate,
SFLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [for (final lang in supportedLanguages) Locale(lang)],
localeResolutionCallback: (locale, supportedLocales) {
if (locale == null) return supportedLocales.first;
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return supportedLocale;
);
darkTheme = null;
themeMode = ThemeMode.light;
}
return AppUpdateGate(
child: MaterialApp.router(
title: 'SaveFamily',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
routerConfig: appRouter,
debugShowCheckedModeBanner: false,
localizationsDelegates: [
SFLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [for (final lang in supportedLanguages) Locale(lang)],
localeResolutionCallback: (locale, supportedLocales) {
if (locale == null) return supportedLocales.first;
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return supportedLocale;
}
}
}
return supportedLocales.first;
},
return supportedLocales.first;
},
builder: (context, child) {
_cacheIncomingCallStrings(context);
return child ?? const SizedBox.shrink();
},
),
);
}
String? _cachedIncomingCallLocale;
void _cacheIncomingCallStrings(BuildContext context) {
final localeCode = context.locale.languageCode;
if (_cachedIncomingCallLocale == localeCode) return;
_cachedIncomingCallLocale = localeCode;
IncomingCallStringsCache.save(
IncomingCallStrings(
title: context.translate(I18n.videocallIncomingVideo),
body: context.translate(I18n.videocallIncomingPushBody),
acceptLabel: context.translate(I18n.videocallAccept),
rejectLabel: context.translate(I18n.videocallReject),
channelName: context.translate(I18n.videocallNotificationChannelName),
channelDescription:
context.translate(I18n.videocallNotificationChannelDescription),
),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
class UserIdentityListener extends ConsumerWidget {
final Widget child;
const UserIdentityListener({super.key, required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<UserEntity>>(userInfoProvider, (_, next) {
next.whenData((user) {
UserInfoTrackingListener(ref.read(sfTrackingProvider)).onUserChanged(
userId: user.id,
role: user.role,
language: user.language,
createdAtMillis: user.createdAt,
hasPhone: user.phone.isNotEmpty,
hasApiKey: user.hasApiKey,
);
});
});
return child;
}
}

View File

@@ -1 +0,0 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/

View File

@@ -1 +0,0 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/

View File

@@ -1 +0,0 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/

View File

@@ -6,9 +6,21 @@
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,6 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux
record_linux
url_launcher_linux
)

Some files were not shown because too many files have changed in this diff Show More